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 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 let empty_script = [];
188
189 let res = ScriptType::detect(&empty_script, &[]).unwrap();
191
192 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 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 let err = ScriptType::detect(&script, ¬_enough_data).unwrap_err();
266
267 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 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 let script_type = ScriptType::detect(&script, &ok_data).unwrap();
302
303 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 let script = loader_instructions_w_configurables()
321 .iter()
322 .flat_map(|i| i.to_bytes())
323 .collect::<Vec<_>>();
324
325 let err = ScriptType::detect(&script, &[]).unwrap_err();
327
328 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 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 let script_type = ScriptType::detect(&script, &[]).unwrap();
354
355 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 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 let script_type = ScriptType::detect(&contract_call_script, &script_data).unwrap();
383
384 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 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 let err = ScriptType::detect(&together, &data_only_for_one_call).unwrap_err();
420
421 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}