fuels_programs/
debug.rs

1use fuel_asm::{Instruction, Opcode};
2use fuels_core::{error, types::errors::Result};
3use itertools::Itertools;
4
5use crate::assembly::script_and_predicate_loader::get_offset_for_section_containing_configurables;
6use crate::{
7    assembly::{
8        contract_call::{ContractCallData, ContractCallInstructions},
9        script_and_predicate_loader::LoaderCode,
10    },
11    utils::prepend_msg,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ScriptCallData {
16    pub code: Vec<u8>,
17    /// This will be renamed in next breaking release. For binary generated with sway 0.66.5 this will be data_offset
18    /// and for binary generated with sway 0.66.6 and above this will probably be data_section_offset and configurable_section_offset.
19    pub data_section_offset: Option<u64>,
20    pub data: Vec<u8>,
21}
22
23impl ScriptCallData {
24    pub fn data_section(&self) -> Option<&[u8]> {
25        self.data_section_offset.map(|offset| {
26            let offset = offset as usize;
27            &self.code[offset..]
28        })
29    }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum ScriptType {
34    ContractCall(Vec<ContractCallData>),
35    Loader {
36        script: ScriptCallData,
37        blob_id: [u8; 32],
38    },
39    Other(ScriptCallData),
40}
41
42fn parse_script_call(script: &[u8], script_data: &[u8]) -> Result<ScriptCallData> {
43    let data_section_offset = if script.len() >= 16 {
44        let offset = get_offset_for_section_containing_configurables(script)?;
45
46        if offset >= script.len() {
47            None
48        } else {
49            Some(offset as u64)
50        }
51    } else {
52        None
53    };
54
55    Ok(ScriptCallData {
56        data: script_data.to_vec(),
57        data_section_offset,
58        code: script.to_vec(),
59    })
60}
61
62fn parse_contract_calls(
63    script: &[u8],
64    script_data: &[u8],
65) -> Result<Option<Vec<ContractCallData>>> {
66    let instructions: std::result::Result<Vec<Instruction>, _> =
67        fuel_asm::from_bytes(script.to_vec()).try_collect();
68
69    let Ok(instructions) = instructions else {
70        return Ok(None);
71    };
72
73    let Some(call_instructions) = extract_call_instructions(&instructions) else {
74        return Ok(None);
75    };
76
77    let Some(minimum_call_offset) = call_instructions.iter().map(|i| i.call_data_offset()).min()
78    else {
79        return Ok(None);
80    };
81
82    let num_calls = call_instructions.len();
83
84    call_instructions.iter().enumerate().map(|(idx, current_call_instructions)| {
85            let data_start =
86                (current_call_instructions.call_data_offset() - minimum_call_offset) as usize;
87
88            let data_end = if idx + 1 < num_calls {
89                (call_instructions[idx + 1].call_data_offset()
90                    - current_call_instructions.call_data_offset()) as usize
91            } else {
92                script_data.len()
93            };
94
95            if data_start > script_data.len() || data_end > script_data.len() {
96                return Err(error!(
97                    Other,
98                    "call data offset requires data section of length {}, but data section is only {} bytes long",
99                    data_end,
100                    script_data.len()
101                ));
102            }
103
104            let contract_call_data = ContractCallData::decode(
105                &script_data[data_start..data_end],
106                current_call_instructions.is_gas_fwd_variant(),
107            )?;
108
109            Ok(contract_call_data)
110        }).collect::<Result<_>>().map(Some)
111}
112
113fn extract_call_instructions(
114    mut instructions: &[Instruction],
115) -> Option<Vec<ContractCallInstructions>> {
116    let mut call_instructions = vec![];
117
118    while let Some(extracted_instructions) = ContractCallInstructions::extract_from(instructions) {
119        let num_instructions = extracted_instructions.len();
120        debug_assert!(num_instructions > 0);
121
122        instructions = &instructions[num_instructions..];
123        call_instructions.push(extracted_instructions);
124    }
125
126    if !instructions.is_empty() {
127        match instructions {
128            [single_instruction] if single_instruction.opcode() == Opcode::RET => {}
129            _ => return None,
130        }
131    }
132
133    Some(call_instructions)
134}
135
136impl ScriptType {
137    pub fn detect(script: &[u8], data: &[u8]) -> Result<Self> {
138        if let Some(contract_calls) = parse_contract_calls(script, data)
139            .map_err(prepend_msg("while decoding contract call"))?
140        {
141            return Ok(Self::ContractCall(contract_calls));
142        }
143
144        if let Some((script, blob_id)) = parse_loader_script(script, data)? {
145            return Ok(Self::Loader { script, blob_id });
146        }
147
148        Ok(Self::Other(parse_script_call(script, data)?))
149    }
150}
151
152fn parse_loader_script(script: &[u8], data: &[u8]) -> Result<Option<(ScriptCallData, [u8; 32])>> {
153    let Some(loader_code) = LoaderCode::from_loader_binary(script)
154        .map_err(prepend_msg("while decoding loader script"))?
155    else {
156        return Ok(None);
157    };
158
159    Ok(Some((
160        ScriptCallData {
161            code: script.to_vec(),
162            data: data.to_vec(),
163            data_section_offset: Some(loader_code.configurables_section_offset() as u64),
164        },
165        loader_code.blob_id(),
166    )))
167}
168
169#[cfg(test)]
170mod tests {
171
172    use fuel_asm::RegId;
173    use fuels_core::types::errors::Error;
174    use rand::{RngCore, SeedableRng};
175    use test_case::test_case;
176
177    use crate::assembly::{
178        contract_call::{CallOpcodeParamsOffset, ContractCallInstructions},
179        script_and_predicate_loader::loader_instructions_w_configurables,
180    };
181
182    use super::*;
183
184    #[test]
185    fn can_handle_empty_scripts() {
186        // given
187        let empty_script = [];
188
189        // when
190        let res = ScriptType::detect(&empty_script, &[]).unwrap();
191
192        // then
193        assert_eq!(
194            res,
195            ScriptType::Other(ScriptCallData {
196                code: vec![],
197                data_section_offset: None,
198                data: vec![]
199            })
200        )
201    }
202
203    #[test]
204    fn is_fine_with_malformed_scripts() {
205        let mut script = vec![0; 100 * Instruction::SIZE];
206        let jmpf = fuel_asm::op::jmpf(0x0, 0x04).to_bytes();
207
208        let mut rng = rand::rngs::StdRng::from_seed([0; 32]);
209        rng.fill_bytes(&mut script);
210        script[4..8].copy_from_slice(&jmpf);
211
212        let script_type = ScriptType::detect(&script, &[]).unwrap();
213
214        assert_eq!(
215            script_type,
216            ScriptType::Other(ScriptCallData {
217                code: script,
218                data_section_offset: None,
219                data: vec![]
220            })
221        );
222    }
223
224    fn example_contract_call_data(has_args: bool, gas_fwd: bool) -> Vec<u8> {
225        let mut data = vec![];
226        data.extend_from_slice(&100u64.to_be_bytes());
227        data.extend_from_slice(&[0; 32]);
228        data.extend_from_slice(&[1; 32]);
229        data.extend_from_slice(&[0; 8]);
230        data.extend_from_slice(&[0; 8]);
231        data.extend_from_slice(&"test".len().to_be_bytes());
232        data.extend_from_slice("test".as_bytes());
233        if has_args {
234            data.extend_from_slice(&[0; 8]);
235        }
236        if gas_fwd {
237            data.extend_from_slice(&[0; 8]);
238        }
239        data
240    }
241
242    #[test_case(108, "amount")]
243    #[test_case(100, "asset id")]
244    #[test_case(68, "contract id")]
245    #[test_case(36, "function selector offset")]
246    #[test_case(28, "encoded args offset")]
247    #[test_case(20, "function selector length")]
248    #[test_case(12, "function selector")]
249    #[test_case(8, "forwarded gas")]
250    fn catches_missing_data(amount_of_data_to_steal: usize, expected_msg: &str) {
251        // given
252        let script = ContractCallInstructions::new(CallOpcodeParamsOffset {
253            call_data_offset: 0,
254            amount_offset: 0,
255            asset_id_offset: 0,
256            gas_forwarded_offset: Some(1),
257        })
258        .into_bytes()
259        .collect_vec();
260
261        let ok_data = example_contract_call_data(false, true);
262        let not_enough_data = ok_data[..ok_data.len() - amount_of_data_to_steal].to_vec();
263
264        // when
265        let err = ScriptType::detect(&script, &not_enough_data).unwrap_err();
266
267        // then
268        let Error::Other(mut msg) = err else {
269            panic!("expected Error::Other");
270        };
271
272        let expected_msg =
273            format!("while decoding contract call: while decoding {expected_msg}: not enough data");
274        msg.truncate(expected_msg.len());
275
276        assert_eq!(expected_msg, msg);
277    }
278
279    #[test]
280    fn handles_invalid_utf8_fn_selector() {
281        // given
282        let script = ContractCallInstructions::new(CallOpcodeParamsOffset {
283            call_data_offset: 0,
284            amount_offset: 0,
285            asset_id_offset: 0,
286            gas_forwarded_offset: Some(1),
287        })
288        .into_bytes()
289        .collect_vec();
290
291        let invalid_utf8 = {
292            let invalid_data = [0x80, 0xBF, 0xC0, 0xAF, 0xFF];
293            assert!(String::from_utf8(invalid_data.to_vec()).is_err());
294            invalid_data
295        };
296
297        let mut ok_data = example_contract_call_data(false, true);
298        ok_data[96..101].copy_from_slice(&invalid_utf8);
299
300        // when
301        let script_type = ScriptType::detect(&script, &ok_data).unwrap();
302
303        // then
304        let ScriptType::ContractCall(calls) = script_type else {
305            panic!("expected ScriptType::Other");
306        };
307        let Error::Codec(err) = calls[0].decode_fn_selector().unwrap_err() else {
308            panic!("expected Error::Codec");
309        };
310
311        assert_eq!(
312            err,
313            "cannot decode function selector: invalid utf-8 sequence of 1 bytes from index 0"
314        );
315    }
316
317    #[test]
318    fn loader_script_without_a_blob() {
319        // given
320        let script = loader_instructions_w_configurables()
321            .iter()
322            .flat_map(|i| i.to_bytes())
323            .collect::<Vec<_>>();
324
325        // when
326        let err = ScriptType::detect(&script, &[]).unwrap_err();
327
328        // then
329        let Error::Other(msg) = err else {
330            panic!("expected Error::Other");
331        };
332        assert_eq!(
333            "while decoding loader script: while decoding blob id: not enough data, available: 0, requested: 32",
334            msg
335        );
336    }
337
338    #[test]
339    fn loader_script_with_almost_matching_instructions() {
340        // given
341        let mut loader_instructions = loader_instructions_w_configurables().to_vec();
342
343        loader_instructions.insert(
344            loader_instructions.len() - 2,
345            fuel_asm::op::movi(RegId::ZERO, 0),
346        );
347        let script = loader_instructions
348            .iter()
349            .flat_map(|i| i.to_bytes())
350            .collect::<Vec<_>>();
351
352        // when
353        let script_type = ScriptType::detect(&script, &[]).unwrap();
354
355        // then
356        assert_eq!(
357            script_type,
358            ScriptType::Other(ScriptCallData {
359                code: script,
360                data_section_offset: None,
361                data: vec![]
362            })
363        );
364    }
365
366    #[test]
367    fn extra_instructions_in_contract_calling_scripts_not_tolerated() {
368        // given
369        let mut contract_call_script = ContractCallInstructions::new(CallOpcodeParamsOffset {
370            call_data_offset: 0,
371            amount_offset: 0,
372            asset_id_offset: 0,
373            gas_forwarded_offset: Some(1),
374        })
375        .into_bytes()
376        .collect_vec();
377
378        contract_call_script.extend(fuel_asm::op::movi(RegId::ZERO, 10).to_bytes());
379        let script_data = example_contract_call_data(false, true);
380
381        // when
382        let script_type = ScriptType::detect(&contract_call_script, &script_data).unwrap();
383
384        // then
385        assert_eq!(
386            script_type,
387            ScriptType::Other(ScriptCallData {
388                code: contract_call_script,
389                data_section_offset: None,
390                data: script_data
391            })
392        );
393    }
394
395    #[test]
396    fn handles_invalid_call_data_offset() {
397        // given
398        let contract_call_1 = ContractCallInstructions::new(CallOpcodeParamsOffset {
399            call_data_offset: 0,
400            amount_offset: 0,
401            asset_id_offset: 0,
402            gas_forwarded_offset: Some(1),
403        })
404        .into_bytes();
405
406        let contract_call_2 = ContractCallInstructions::new(CallOpcodeParamsOffset {
407            call_data_offset: u16::MAX as usize,
408            amount_offset: 0,
409            asset_id_offset: 0,
410            gas_forwarded_offset: Some(1),
411        })
412        .into_bytes();
413
414        let data_only_for_one_call = example_contract_call_data(false, true);
415
416        let together = contract_call_1.chain(contract_call_2).collect_vec();
417
418        // when
419        let err = ScriptType::detect(&together, &data_only_for_one_call).unwrap_err();
420
421        // then
422        let Error::Other(msg) = err else {
423            panic!("expected Error::Other");
424        };
425
426        assert_eq!(
427            "while decoding contract call: call data offset requires data section of length 65535, but data section is only 108 bytes long",
428            msg
429        );
430    }
431}