1#[cfg(feature = "alloc")]
46#[macro_export]
47macro_rules! script_with_data_offset {
48 ($offset:ident, $script:expr, $tx_offset:expr) => {{
49 let $offset = {
50 let $offset = {
52 use $crate::prelude::Immediate18;
53 0 as Immediate18
54 };
55 let script_bytes: $crate::alloc::vec::Vec<u8> =
57 ::core::iter::IntoIterator::into_iter({ $script }).collect();
58 {
60 use $crate::{
61 fuel_tx::{
62 Script,
63 field::Script as ScriptField,
64 },
65 fuel_types::bytes::padded_len,
66 prelude::Immediate18,
67 };
68 let value: Immediate18 = $tx_offset
69 .saturating_add(Script::script_offset_static())
70 .saturating_add(
71 padded_len(script_bytes.as_slice()).unwrap_or(usize::MAX),
72 )
73 .try_into()
74 .expect("script data offset is too large");
75 value
76 }
77 };
78 ($script, $offset)
81 }};
82}
83
84#[allow(missing_docs)]
85#[cfg(feature = "random")]
86#[cfg(any(test, feature = "test-helpers"))]
87pub mod test_helpers {
89 use alloc::{
90 vec,
91 vec::Vec,
92 };
93
94 use crate::{
95 checked_transaction::{
96 Checked,
97 IntoChecked,
98 builder::TransactionBuilderExt,
99 },
100 interpreter::{
101 Memory,
102 NotSupportedEcal,
103 },
104 memory_client::MemoryClient,
105 state::StateTransition,
106 storage::{
107 ContractsAssetsStorage,
108 MemoryStorage,
109 },
110 transactor::Transactor,
111 verification::{
112 AttemptContinue,
113 Verifier,
114 },
115 };
116 use anyhow::anyhow;
117
118 use crate::{
119 interpreter::{
120 CheckedMetadata,
121 ExecutableTransaction,
122 InterpreterParams,
123 MemoryInstance,
124 },
125 prelude::{
126 Backtrace,
127 Call,
128 },
129 verification::Normal,
130 };
131 use fuel_asm::{
132 GTFArgs,
133 Instruction,
134 PanicReason,
135 RegId,
136 op,
137 };
138 use fuel_tx::{
139 BlobBody,
140 BlobIdExt,
141 ConsensusParameters,
142 Contract,
143 ContractParameters,
144 Create,
145 FeeParameters,
146 Finalizable,
147 GasCosts,
148 Input,
149 Output,
150 PredicateParameters,
151 Receipt,
152 Script,
153 ScriptParameters,
154 StorageSlot,
155 Transaction,
156 TransactionBuilder,
157 TxParameters,
158 Witness,
159 field::{
160 Outputs,
161 ReceiptsRoot,
162 },
163 };
164 use fuel_types::{
165 Address,
166 AssetId,
167 BlobId,
168 BlockHeight,
169 ChainId,
170 ContractId,
171 Immediate12,
172 Salt,
173 Word,
174 canonical::{
175 Deserialize,
176 Serialize,
177 },
178 };
179 use itertools::Itertools;
180 use rand::{
181 Rng,
182 SeedableRng,
183 prelude::StdRng,
184 };
185
186 pub struct CreatedContract {
187 pub tx: Create,
188 pub contract_id: ContractId,
189 pub salt: Salt,
190 }
191
192 pub struct TestBuilder {
193 pub rng: StdRng,
194 gas_price: Word,
195 max_fee_limit: Word,
196 script_gas_limit: Word,
197 builder: TransactionBuilder<Script>,
198 storage: MemoryStorage,
199 block_height: BlockHeight,
200 consensus_params: ConsensusParameters,
201 }
202
203 impl TestBuilder {
204 pub fn new(seed: u64) -> Self {
205 let bytecode = core::iter::once(op::ret(RegId::ONE)).collect();
206 TestBuilder {
207 rng: StdRng::seed_from_u64(seed),
208 gas_price: 0,
209 max_fee_limit: 0,
210 script_gas_limit: 100,
211 builder: TransactionBuilder::script(bytecode, vec![]),
212 storage: MemoryStorage::default(),
213 block_height: Default::default(),
214 consensus_params: ConsensusParameters::standard(),
215 }
216 }
217
218 pub fn get_block_height(&self) -> BlockHeight {
219 self.block_height
220 }
221
222 pub fn start_script_bytes(
223 &mut self,
224 script: Vec<u8>,
225 script_data: Vec<u8>,
226 ) -> &mut Self {
227 self.start_script_inner(script, script_data)
228 }
229
230 pub fn start_script(
231 &mut self,
232 script: Vec<Instruction>,
233 script_data: Vec<u8>,
234 ) -> &mut Self {
235 let script = script.into_iter().collect();
236 self.start_script_inner(script, script_data)
237 }
238
239 fn start_script_inner(
240 &mut self,
241 script: Vec<u8>,
242 script_data: Vec<u8>,
243 ) -> &mut Self {
244 self.builder = TransactionBuilder::script(script, script_data);
245 self.builder.script_gas_limit(self.script_gas_limit);
246 self
247 }
248
249 pub fn gas_price(&mut self, price: Word) -> &mut TestBuilder {
250 self.gas_price = price;
251 self
252 }
253
254 pub fn max_fee_limit(&mut self, max_fee_limit: Word) -> &mut TestBuilder {
255 self.max_fee_limit = max_fee_limit;
256 self
257 }
258
259 pub fn script_gas_limit(&mut self, limit: Word) -> &mut TestBuilder {
260 self.builder.script_gas_limit(limit);
261 self.script_gas_limit = limit;
262 self
263 }
264
265 pub fn change_output(&mut self, asset_id: AssetId) -> &mut TestBuilder {
266 self.builder
267 .add_output(Output::change(self.rng.r#gen(), 0, asset_id));
268 self
269 }
270
271 pub fn coin_output(
272 &mut self,
273 asset_id: AssetId,
274 amount: Word,
275 ) -> &mut TestBuilder {
276 self.builder
277 .add_output(Output::coin(self.rng.r#gen(), amount, asset_id));
278 self
279 }
280
281 pub fn variable_output(&mut self, asset_id: AssetId) -> &mut TestBuilder {
282 self.builder
283 .add_output(Output::variable(Address::zeroed(), 0, asset_id));
284 self
285 }
286
287 pub fn contract_output(&mut self, id: &ContractId) -> &mut TestBuilder {
288 let input_idx = self
289 .builder
290 .inputs()
291 .iter()
292 .find_position(|input| matches!(input, Input::Contract(contract) if &contract.contract_id == id))
293 .expect("expected contract input with matching contract id");
294
295 self.builder.add_output(Output::contract(
296 u16::try_from(input_idx.0).expect("The input index is more than allowed"),
297 self.rng.r#gen(),
298 self.rng.r#gen(),
299 ));
300
301 self
302 }
303
304 pub fn coin_input(
305 &mut self,
306 asset_id: AssetId,
307 amount: Word,
308 ) -> &mut TestBuilder {
309 self.builder.add_unsigned_coin_input(
310 fuel_crypto::SecretKey::random(&mut self.rng),
311 self.rng.r#gen(),
312 amount,
313 asset_id,
314 Default::default(),
315 );
316 self
317 }
318
319 pub fn fee_input(&mut self) -> &mut TestBuilder {
320 self.builder.add_fee_input();
321 self
322 }
323
324 pub fn contract_input(&mut self, contract_id: ContractId) -> &mut TestBuilder {
325 self.builder.add_input(Input::contract(
326 self.rng.r#gen(),
327 self.rng.r#gen(),
328 self.rng.r#gen(),
329 self.rng.r#gen(),
330 contract_id,
331 ));
332 self
333 }
334
335 pub fn witness(&mut self, witness: Witness) -> &mut TestBuilder {
336 self.builder.add_witness(witness);
337 self
338 }
339
340 pub fn storage(&mut self, storage: MemoryStorage) -> &mut TestBuilder {
341 self.storage = storage;
342 self
343 }
344
345 pub fn block_height(&mut self, block_height: BlockHeight) -> &mut TestBuilder {
346 self.block_height = block_height;
347 self
348 }
349
350 pub fn with_fee_params(&mut self, fee_params: FeeParameters) -> &mut TestBuilder {
351 self.consensus_params.set_fee_params(fee_params);
352 self
353 }
354
355 pub fn with_free_gas_costs(&mut self) -> &mut TestBuilder {
356 let gas_costs = GasCosts::free();
357 self.consensus_params.set_gas_costs(gas_costs);
358 self
359 }
360
361 pub fn with_gas_costs(&mut self, gas_costs: GasCosts) -> &mut TestBuilder {
362 self.consensus_params.set_gas_costs(gas_costs);
363 self
364 }
365
366 pub fn base_asset_id(&mut self, base_asset_id: AssetId) -> &mut TestBuilder {
367 self.consensus_params.set_base_asset_id(base_asset_id);
368 self
369 }
370
371 pub fn build(&mut self) -> Checked<Script> {
372 self.builder.max_fee_limit(self.max_fee_limit);
373 self.builder.with_tx_params(*self.get_tx_params());
374 self.builder
375 .with_contract_params(*self.get_contract_params());
376 self.builder
377 .with_predicate_params(*self.get_predicate_params());
378 self.builder.with_script_params(*self.get_script_params());
379 self.builder.with_fee_params(*self.get_fee_params());
380 self.builder.with_base_asset_id(*self.get_base_asset_id());
381 self.builder
382 .finalize_checked_with_storage(self.block_height, &self.storage)
383 }
384
385 pub fn get_tx_params(&self) -> &TxParameters {
386 self.consensus_params.tx_params()
387 }
388
389 pub fn get_predicate_params(&self) -> &PredicateParameters {
390 self.consensus_params.predicate_params()
391 }
392
393 pub fn get_script_params(&self) -> &ScriptParameters {
394 self.consensus_params.script_params()
395 }
396
397 pub fn get_contract_params(&self) -> &ContractParameters {
398 self.consensus_params.contract_params()
399 }
400
401 pub fn get_fee_params(&self) -> &FeeParameters {
402 self.consensus_params.fee_params()
403 }
404
405 pub fn get_base_asset_id(&self) -> &AssetId {
406 self.consensus_params.base_asset_id()
407 }
408
409 pub fn get_block_gas_limit(&self) -> u64 {
410 self.consensus_params.block_gas_limit()
411 }
412
413 pub fn get_block_transaction_size_limit(&self) -> u64 {
414 self.consensus_params.block_transaction_size_limit()
415 }
416
417 pub fn get_privileged_address(&self) -> &Address {
418 self.consensus_params.privileged_address()
419 }
420
421 pub fn get_chain_id(&self) -> ChainId {
422 self.consensus_params.chain_id()
423 }
424
425 pub fn get_gas_costs(&self) -> &GasCosts {
426 self.consensus_params.gas_costs()
427 }
428
429 pub fn build_get_balance_tx(
430 contract_id: &ContractId,
431 asset_id: &AssetId,
432 tx_offset: usize,
433 ) -> Checked<Script> {
434 let (script, _) = script_with_data_offset!(
435 data_offset,
436 vec![
437 op::movi(0x11, data_offset),
438 op::addi(
439 0x12,
440 0x11,
441 Immediate12::try_from(AssetId::LEN)
442 .expect("`AssetId::LEN` is 32 bytes")
443 ),
444 op::bal(0x10, 0x11, 0x12),
445 op::log(0x10, RegId::ZERO, RegId::ZERO, RegId::ZERO),
446 op::ret(RegId::ONE),
447 ],
448 tx_offset
449 );
450
451 let script_data: Vec<u8> = [asset_id.as_ref(), contract_id.as_ref()]
452 .into_iter()
453 .flatten()
454 .copied()
455 .collect();
456
457 TestBuilder::new(2322u64)
458 .start_script(script, script_data)
459 .gas_price(0)
460 .script_gas_limit(1_000_000)
461 .contract_input(*contract_id)
462 .fee_input()
463 .contract_output(contract_id)
464 .build()
465 }
466
467 pub fn setup_contract_bytes(
468 &mut self,
469 contract: Vec<u8>,
470 initial_balance: Option<(AssetId, Word)>,
471 initial_state: Option<Vec<StorageSlot>>,
472 ) -> CreatedContract {
473 self.setup_contract_inner(contract, initial_balance, initial_state)
474 }
475
476 pub fn setup_contract(
477 &mut self,
478 contract: Vec<Instruction>,
479 initial_balance: Option<(AssetId, Word)>,
480 initial_state: Option<Vec<StorageSlot>>,
481 ) -> CreatedContract {
482 let contract = contract.into_iter().collect();
483
484 self.setup_contract_inner(contract, initial_balance, initial_state)
485 }
486
487 fn setup_contract_inner(
488 &mut self,
489 contract: Vec<u8>,
490 initial_balance: Option<(AssetId, Word)>,
491 initial_state: Option<Vec<StorageSlot>>,
492 ) -> CreatedContract {
493 let storage_slots = initial_state.unwrap_or_default();
494
495 let salt: Salt = self.rng.r#gen();
496 let program: Witness = contract.into();
497 let storage_root = Contract::initial_state_root(storage_slots.iter());
498 let contract_root = Contract::root_from_code(program.as_ref());
499 let contract_id = Contract::id(&salt, &contract_root, &storage_root);
500
501 let tx = TransactionBuilder::create(program, salt, storage_slots)
502 .max_fee_limit(self.max_fee_limit)
503 .maturity(Default::default())
504 .add_fee_input()
505 .add_contract_created()
506 .finalize()
507 .into_checked(self.block_height, &self.consensus_params)
508 .expect("failed to check tx");
509
510 let state = self
512 .deploy(tx)
513 .expect("Expected vm execution to be successful");
514
515 if let Some((asset_id, amount)) = initial_balance {
517 self.storage
518 .contract_asset_id_balance_insert(&contract_id, &asset_id, amount)
519 .unwrap();
520 }
521
522 CreatedContract {
523 tx: state.tx().clone(),
524 contract_id,
525 salt,
526 }
527 }
528
529 pub fn setup_blob(&mut self, data: Vec<u8>) {
530 let id = BlobId::compute(data.as_slice());
531
532 let tx = TransactionBuilder::blob(BlobBody {
533 id,
534 witness_index: 0,
535 })
536 .add_witness(data.into())
537 .max_fee_limit(self.max_fee_limit)
538 .maturity(Default::default())
539 .add_fee_input()
540 .finalize()
541 .into_checked(self.block_height, &self.consensus_params)
542 .expect("failed to check tx");
543
544 let interpreter_params =
545 InterpreterParams::new(self.gas_price, &self.consensus_params);
546 let mut transactor = Transactor::<_, _, _>::new(
547 MemoryInstance::new(),
548 self.storage.clone(),
549 interpreter_params,
550 );
551
552 self.execute_tx_inner(&mut transactor, tx)
553 .expect("Expected vm execution to be successful");
554 }
555
556 fn execute_tx_inner<M, Tx, Ecal, V>(
557 &mut self,
558 transactor: &mut Transactor<M, MemoryStorage, Tx, Ecal, V>,
559 checked: Checked<Tx>,
560 ) -> anyhow::Result<(StateTransition<Tx, V>, V)>
561 where
562 M: Memory,
563 Tx: ExecutableTransaction,
564 <Tx as IntoChecked>::Metadata: CheckedMetadata,
565 Ecal: crate::interpreter::EcalHandler,
566 V: Verifier + Clone,
567 {
568 self.storage.set_block_height(self.block_height);
569
570 transactor.transact(checked);
571
572 let storage = transactor.as_mut().clone();
573
574 if let Some(e) = transactor.error() {
575 return Err(anyhow!("{:?}", e));
576 }
577 let is_reverted = transactor.is_reverted();
578
579 let state = transactor.to_owned_state_transition().unwrap();
580
581 let interpreter = transactor.interpreter();
582
583 let verifier = interpreter.verifier().clone();
584
585 let transaction: Transaction = interpreter.transaction().clone().into();
587 let tx_offset = self.get_tx_params().tx_offset();
588 let mut tx_mem = interpreter
589 .memory()
590 .read(tx_offset, transaction.size())
591 .unwrap();
592 let mut deser_tx = Transaction::decode(&mut tx_mem).unwrap();
593
594 if let Transaction::Script(ref mut s) = deser_tx {
596 *s.receipts_root_mut() = interpreter.compute_receipts_root();
597 }
598
599 assert_eq!(deser_tx, transaction);
600 if !is_reverted {
601 self.storage = storage;
603 }
604
605 Ok((state, verifier))
606 }
607
608 pub fn deploy(
609 &mut self,
610 checked: Checked<Create>,
611 ) -> anyhow::Result<StateTransition<Create, Normal>> {
612 let interpreter_params =
613 InterpreterParams::new(self.gas_price, &self.consensus_params);
614 let mut transactor = Transactor::<_, _, _>::new(
615 MemoryInstance::new(),
616 self.storage.clone(),
617 interpreter_params,
618 );
619
620 Ok(self.execute_tx_inner(&mut transactor, checked)?.0)
621 }
622
623 pub fn attempt_execute_tx(
624 &mut self,
625 checked: Checked<Script>,
626 ) -> anyhow::Result<(StateTransition<Script, AttemptContinue>, AttemptContinue)>
627 {
628 let interpreter_params =
629 InterpreterParams::new(self.gas_price, &self.consensus_params);
630 let mut transactor =
631 Transactor::<_, _, _, NotSupportedEcal, AttemptContinue>::new(
632 MemoryInstance::new(),
633 self.storage.clone(),
634 interpreter_params,
635 );
636
637 self.execute_tx_inner(&mut transactor, checked)
638 }
639
640 pub fn execute_tx(
641 &mut self,
642 checked: Checked<Script>,
643 ) -> anyhow::Result<StateTransition<Script, Normal>> {
644 let interpreter_params =
645 InterpreterParams::new(self.gas_price, &self.consensus_params);
646 let mut transactor = Transactor::<_, _, _>::new(
647 MemoryInstance::new(),
648 self.storage.clone(),
649 interpreter_params,
650 );
651
652 Ok(self.execute_tx_inner(&mut transactor, checked)?.0)
653 }
654
655 pub fn execute_tx_with_backtrace(
656 &mut self,
657 checked: Checked<Script>,
658 gas_price: u64,
659 ) -> anyhow::Result<(StateTransition<Script, Normal>, Option<Backtrace>)>
660 {
661 let interpreter_params =
662 InterpreterParams::new(gas_price, &self.consensus_params);
663 let mut transactor = Transactor::<_, _, _>::new(
664 MemoryInstance::new(),
665 self.storage.clone(),
666 interpreter_params,
667 );
668
669 let state = self.execute_tx_inner(&mut transactor, checked)?.0;
670 let backtrace = transactor.backtrace();
671
672 Ok((state, backtrace))
673 }
674
675 pub fn attempt_execute(
677 &mut self,
678 ) -> (StateTransition<Script, AttemptContinue>, AttemptContinue) {
679 let tx = self.build();
680
681 self.attempt_execute_tx(tx)
682 .expect("expected successful vm execution")
683 }
684
685 pub fn execute(&mut self) -> StateTransition<Script, Normal> {
687 let tx = self.build();
688
689 self.execute_tx(tx)
690 .expect("expected successful vm execution")
691 }
692
693 pub fn get_storage(&self) -> &MemoryStorage {
694 &self.storage
695 }
696
697 pub fn execute_get_outputs(&mut self) -> Vec<Output> {
698 self.execute().tx().outputs().to_vec()
699 }
700
701 pub fn execute_get_change(&mut self, find_asset_id: AssetId) -> Word {
702 let outputs = self.execute_get_outputs();
703 find_change(outputs, find_asset_id)
704 }
705
706 pub fn get_contract_balance(
707 &mut self,
708 contract_id: &ContractId,
709 asset_id: &AssetId,
710 ) -> Word {
711 let tx = TestBuilder::build_get_balance_tx(
712 contract_id,
713 asset_id,
714 self.consensus_params.tx_params().tx_offset(),
715 );
716 let state = self
717 .execute_tx(tx)
718 .expect("expected successful vm execution in this context");
719 let receipts = state.receipts();
720 receipts[0].ra().expect("Balance expected")
721 }
722 }
723
724 pub fn check_expected_reason_for_instructions(
725 instructions: Vec<Instruction>,
726 expected_reason: PanicReason,
727 ) {
728 let client = MemoryClient::default();
729
730 check_expected_reason_for_instructions_with_client(
731 client,
732 instructions,
733 expected_reason,
734 );
735 }
736
737 pub fn check_expected_reason_for_instructions_with_client<M>(
738 mut client: MemoryClient<M>,
739 instructions: Vec<Instruction>,
740 expected_reason: PanicReason,
741 ) where
742 M: Memory,
743 {
744 let tx_params = TxParameters::default().with_max_gas_per_tx(Word::MAX / 2);
745 let gas_limit = tx_params.max_gas_per_tx() / 2;
748 let maturity = Default::default();
749 let height = Default::default();
750 let zero_fee_limit = 0;
751
752 let contract: Witness = instructions.into_iter().collect::<Vec<u8>>().into();
754 let salt = Default::default();
755 let code_root = Contract::root_from_code(contract.as_ref());
756 let storage_slots = vec![];
757 let state_root = Contract::initial_state_root(storage_slots.iter());
758 let contract_id = Contract::id(&salt, &code_root, &state_root);
759
760 let contract_deployer = TransactionBuilder::create(contract, salt, storage_slots)
761 .max_fee_limit(zero_fee_limit)
762 .with_tx_params(tx_params)
763 .add_fee_input()
764 .add_contract_created()
765 .finalize_checked(height);
766
767 client
768 .deploy(contract_deployer)
769 .expect("valid contract deployment");
770
771 let script = [
773 op::gtf(0x10, 0x0, Immediate12::from(GTFArgs::ScriptData)),
775 op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS),
777 op::ret(RegId::ONE),
778 ]
779 .into_iter()
780 .collect();
781 let script_data: Vec<u8> = [Call::new(contract_id, 0, 0).to_bytes().as_slice()]
782 .into_iter()
783 .flatten()
784 .copied()
785 .collect();
786
787 let tx_deploy_loader = TransactionBuilder::script(script, script_data)
788 .max_fee_limit(zero_fee_limit)
789 .script_gas_limit(gas_limit)
790 .maturity(maturity)
791 .with_tx_params(tx_params)
792 .add_input(Input::contract(
793 Default::default(),
794 Default::default(),
795 Default::default(),
796 Default::default(),
797 contract_id,
798 ))
799 .add_fee_input()
800 .add_output(Output::contract(0, Default::default(), Default::default()))
801 .finalize_checked(height);
802
803 check_reason_for_transaction(client, tx_deploy_loader, expected_reason);
804 }
805
806 pub fn check_reason_for_transaction<M>(
807 mut client: MemoryClient<M>,
808 checked_tx: Checked<Script>,
809 expected_reason: PanicReason,
810 ) where
811 M: Memory,
812 {
813 let receipts = client.transact(checked_tx);
814
815 let panic_found = receipts.iter().any(|receipt| {
816 if let Receipt::Panic { id: _, reason, .. } = receipt {
817 assert_eq!(
818 &expected_reason,
819 reason.reason(),
820 "Expected {}, found {}",
821 expected_reason,
822 reason.reason()
823 );
824 true
825 } else {
826 false
827 }
828 });
829
830 if !panic_found {
831 panic!("Script should have panicked");
832 }
833 }
834
835 pub fn find_change(outputs: Vec<Output>, find_asset_id: AssetId) -> Word {
836 let change = outputs.into_iter().find_map(|output| {
837 if let Output::Change {
838 amount, asset_id, ..
839 } = output
840 {
841 if asset_id == find_asset_id {
842 Some(amount)
843 } else {
844 None
845 }
846 } else {
847 None
848 }
849 });
850 change.unwrap_or_else(|| {
851 panic!("no change matching asset ID {:x} was found", &find_asset_id)
852 })
853 }
854}