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