fuels_programs/
debug.rs

1use fuel_asm::{Instruction, Opcode};
2use fuels_core::{error, types::errors::Result};
3use itertools::Itertools;
4
5use crate::{
6    assembly::{
7        contract_call::{ContractCallData, ContractCallInstructions},
8        script_and_predicate_loader::{
9            LoaderCode, get_offset_for_section_containing_configurables,
10        },
11    },
12    utils::prepend_msg,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ScriptCallData {
17    pub code: Vec<u8>,
18    /// This will be renamed in next breaking release. For binary generated with sway 0.66.5 this will be data_offset
19    /// and for binary generated with sway 0.66.6 and above this will probably be data_section_offset and configurable_section_offset.
20    pub data_section_offset: Option<u64>,
21    pub data: Vec<u8>,
22}
23
24impl ScriptCallData {
25    pub fn data_section(&self) -> Option<&[u8]> {
26        self.data_section_offset.map(|offset| {
27            let offset = offset as usize;
28            &self.code[offset..]
29        })
30    }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum ScriptType {
35    ContractCall(Vec<ContractCallData>),
36    Loader {
37        script: ScriptCallData,
38        blob_id: [u8; 32],
39    },
40    Other(ScriptCallData),
41}
42
43fn parse_script_call(script: &[u8], script_data: &[u8]) -> Result<ScriptCallData> {
44    let data_section_offset = if script.len() >= 16 {
45        let offset = get_offset_for_section_containing_configurables(script)?;
46
47        if offset >= script.len() {
48            None
49        } else {
50            Some(offset as u64)
51        }
52    } else {
53        None
54    };
55
56    Ok(ScriptCallData {
57        data: script_data.to_vec(),
58        data_section_offset,
59        code: script.to_vec(),
60    })
61}
62
63fn parse_contract_calls(
64    script: &[u8],
65    script_data: &[u8],
66) -> Result<Option<Vec<ContractCallData>>> {
67    let instructions: std::result::Result<Vec<Instruction>, _> =
68        fuel_asm::from_bytes(script.to_vec()).try_collect();
69
70    let Ok(instructions) = instructions else {
71        return Ok(None);
72    };
73
74    let Some(call_instructions) = extract_call_instructions(&instructions) else {
75        return Ok(None);
76    };
77
78    let Some(minimum_call_offset) = call_instructions.iter().map(|i| i.call_data_offset()).min()
79    else {
80        return Ok(None);
81    };
82
83    let num_calls = call_instructions.len();
84
85    call_instructions.iter().enumerate().map(|(idx, current_call_instructions)| {
86            let data_start =
87                (current_call_instructions.call_data_offset() - minimum_call_offset) as usize;
88
89            let data_end = if idx + 1 < num_calls {
90                (call_instructions[idx + 1].call_data_offset()
91                    - current_call_instructions.call_data_offset()) as usize
92            } else {
93                script_data.len()
94            };
95
96            if data_start > script_data.len() || data_end > script_data.len() {
97                return Err(error!(
98                    Other,
99                    "call data offset requires data section of length {}, but data section is only {} bytes long",
100                    data_end,
101                    script_data.len()
102                ));
103            }
104
105            let contract_call_data = ContractCallData::decode(
106                &script_data[data_start..data_end],
107                current_call_instructions.is_gas_fwd_variant(),
108            )?;
109
110            Ok(contract_call_data)
111        }).collect::<Result<_>>().map(Some)
112}
113
114fn extract_call_instructions(
115    mut instructions: &[Instruction],
116) -> Option<Vec<ContractCallInstructions>> {
117    let mut call_instructions = vec![];
118
119    while let Some(extracted_instructions) = ContractCallInstructions::extract_from(instructions) {
120        let num_instructions = extracted_instructions.len();
121        debug_assert!(num_instructions > 0);
122
123        instructions = &instructions[num_instructions..];
124        call_instructions.push(extracted_instructions);
125    }
126
127    if !instructions.is_empty() {
128        match instructions {
129            [single_instruction] if single_instruction.opcode() == Opcode::RET => {}
130            _ => return None,
131        }
132    }
133
134    Some(call_instructions)
135}
136
137impl ScriptType {
138    pub fn detect(script: &[u8], data: &[u8]) -> Result<Self> {
139        if let Some(contract_calls) = parse_contract_calls(script, data)
140            .map_err(prepend_msg("while decoding contract call"))?
141        {
142            return Ok(Self::ContractCall(contract_calls));
143        }
144
145        if let Some((script, blob_id)) = parse_loader_script(script, data)? {
146            return Ok(Self::Loader { script, blob_id });
147        }
148
149        Ok(Self::Other(parse_script_call(script, data)?))
150    }
151}
152
153fn parse_loader_script(script: &[u8], data: &[u8]) -> Result<Option<(ScriptCallData, [u8; 32])>> {
154    let Some(loader_code) = LoaderCode::from_loader_binary(script)
155        .map_err(prepend_msg("while decoding loader script"))?
156    else {
157        return Ok(None);
158    };
159
160    Ok(Some((
161        ScriptCallData {
162            code: script.to_vec(),
163            data: data.to_vec(),
164            data_section_offset: Some(loader_code.configurables_section_offset() as u64),
165        },
166        loader_code.blob_id(),
167    )))
168}
169
170#[cfg(test)]
171mod tests {
172
173    use fuel_asm::RegId;
174    use fuels_core::types::errors::Error;
175    use rand::{RngCore, SeedableRng};
176    use test_case::test_case;
177
178    use super::*;
179    use crate::assembly::{
180        contract_call::{CallOpcodeParamsOffset, ContractCallInstructions},
181        script_and_predicate_loader::loader_instructions_w_configurables,
182    };
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}