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 let empty_script = [];
184
185 let res = ScriptType::detect(&empty_script, &[]).unwrap();
187
188 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 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 let script_type = ScriptType::detect(&script, &[]).unwrap();
208
209 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 #[test]
222 fn is_fine_with_handwritten_scripts() {
223 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 let script_type = ScriptType::detect(&handwritten_script, &[]).unwrap();
237
238 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 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 let err = ScriptType::detect(&script, ¬_enough_data).unwrap_err();
291
292 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 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 let script_type = ScriptType::detect(&script, &ok_data).unwrap();
327
328 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 let script = loader_instructions_w_data_section()
346 .iter()
347 .flat_map(|i| i.to_bytes())
348 .collect::<Vec<_>>();
349
350 let err = ScriptType::detect(&script, &[]).unwrap_err();
352
353 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 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 let script_type = ScriptType::detect(&script, &[]).unwrap();
379
380 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 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 let script_type = ScriptType::detect(&contract_call_script, &script_data).unwrap();
408
409 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 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 let err = ScriptType::detect(&together, &data_only_for_one_call).unwrap_err();
445
446 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}