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 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 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}