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 base_asset_id(&mut self, base_asset_id: AssetId) -> &mut TestBuilder {
362 self.consensus_params.set_base_asset_id(base_asset_id);
363 self
364 }
365
366 pub fn build(&mut self) -> Checked<Script> {
367 self.builder.max_fee_limit(self.max_fee_limit);
368 self.builder.with_tx_params(*self.get_tx_params());
369 self.builder
370 .with_contract_params(*self.get_contract_params());
371 self.builder
372 .with_predicate_params(*self.get_predicate_params());
373 self.builder.with_script_params(*self.get_script_params());
374 self.builder.with_fee_params(*self.get_fee_params());
375 self.builder.with_base_asset_id(*self.get_base_asset_id());
376 self.builder
377 .finalize_checked_with_storage(self.block_height, &self.storage)
378 }
379
380 pub fn get_tx_params(&self) -> &TxParameters {
381 self.consensus_params.tx_params()
382 }
383
384 pub fn get_predicate_params(&self) -> &PredicateParameters {
385 self.consensus_params.predicate_params()
386 }
387
388 pub fn get_script_params(&self) -> &ScriptParameters {
389 self.consensus_params.script_params()
390 }
391
392 pub fn get_contract_params(&self) -> &ContractParameters {
393 self.consensus_params.contract_params()
394 }
395
396 pub fn get_fee_params(&self) -> &FeeParameters {
397 self.consensus_params.fee_params()
398 }
399
400 pub fn get_base_asset_id(&self) -> &AssetId {
401 self.consensus_params.base_asset_id()
402 }
403
404 pub fn get_block_gas_limit(&self) -> u64 {
405 self.consensus_params.block_gas_limit()
406 }
407
408 pub fn get_block_transaction_size_limit(&self) -> u64 {
409 self.consensus_params.block_transaction_size_limit()
410 }
411
412 pub fn get_privileged_address(&self) -> &Address {
413 self.consensus_params.privileged_address()
414 }
415
416 pub fn get_chain_id(&self) -> ChainId {
417 self.consensus_params.chain_id()
418 }
419
420 pub fn get_gas_costs(&self) -> &GasCosts {
421 self.consensus_params.gas_costs()
422 }
423
424 pub fn build_get_balance_tx(
425 contract_id: &ContractId,
426 asset_id: &AssetId,
427 tx_offset: usize,
428 ) -> Checked<Script> {
429 let (script, _) = script_with_data_offset!(
430 data_offset,
431 vec![
432 op::movi(0x11, data_offset),
433 op::addi(
434 0x12,
435 0x11,
436 Immediate12::try_from(AssetId::LEN)
437 .expect("`AssetId::LEN` is 32 bytes")
438 ),
439 op::bal(0x10, 0x11, 0x12),
440 op::log(0x10, RegId::ZERO, RegId::ZERO, RegId::ZERO),
441 op::ret(RegId::ONE),
442 ],
443 tx_offset
444 );
445
446 let script_data: Vec<u8> = [asset_id.as_ref(), contract_id.as_ref()]
447 .into_iter()
448 .flatten()
449 .copied()
450 .collect();
451
452 TestBuilder::new(2322u64)
453 .start_script(script, script_data)
454 .gas_price(0)
455 .script_gas_limit(1_000_000)
456 .contract_input(*contract_id)
457 .fee_input()
458 .contract_output(contract_id)
459 .build()
460 }
461
462 pub fn setup_contract_bytes(
463 &mut self,
464 contract: Vec<u8>,
465 initial_balance: Option<(AssetId, Word)>,
466 initial_state: Option<Vec<StorageSlot>>,
467 ) -> CreatedContract {
468 self.setup_contract_inner(contract, initial_balance, initial_state)
469 }
470
471 pub fn setup_contract(
472 &mut self,
473 contract: Vec<Instruction>,
474 initial_balance: Option<(AssetId, Word)>,
475 initial_state: Option<Vec<StorageSlot>>,
476 ) -> CreatedContract {
477 let contract = contract.into_iter().collect();
478
479 self.setup_contract_inner(contract, initial_balance, initial_state)
480 }
481
482 fn setup_contract_inner(
483 &mut self,
484 contract: Vec<u8>,
485 initial_balance: Option<(AssetId, Word)>,
486 initial_state: Option<Vec<StorageSlot>>,
487 ) -> CreatedContract {
488 let storage_slots = initial_state.unwrap_or_default();
489
490 let salt: Salt = self.rng.r#gen();
491 let program: Witness = contract.into();
492 let storage_root = Contract::initial_state_root(storage_slots.iter());
493 let contract_root = Contract::root_from_code(program.as_ref());
494 let contract_id = Contract::id(&salt, &contract_root, &storage_root);
495
496 let tx = TransactionBuilder::create(program, salt, storage_slots)
497 .max_fee_limit(self.max_fee_limit)
498 .maturity(Default::default())
499 .add_fee_input()
500 .add_contract_created()
501 .finalize()
502 .into_checked(self.block_height, &self.consensus_params)
503 .expect("failed to check tx");
504
505 let state = self
507 .deploy(tx)
508 .expect("Expected vm execution to be successful");
509
510 if let Some((asset_id, amount)) = initial_balance {
512 self.storage
513 .contract_asset_id_balance_insert(&contract_id, &asset_id, amount)
514 .unwrap();
515 }
516
517 CreatedContract {
518 tx: state.tx().clone(),
519 contract_id,
520 salt,
521 }
522 }
523
524 pub fn setup_blob(&mut self, data: Vec<u8>) {
525 let id = BlobId::compute(data.as_slice());
526
527 let tx = TransactionBuilder::blob(BlobBody {
528 id,
529 witness_index: 0,
530 })
531 .add_witness(data.into())
532 .max_fee_limit(self.max_fee_limit)
533 .maturity(Default::default())
534 .add_fee_input()
535 .finalize()
536 .into_checked(self.block_height, &self.consensus_params)
537 .expect("failed to check tx");
538
539 let interpreter_params =
540 InterpreterParams::new(self.gas_price, &self.consensus_params);
541 let mut transactor = Transactor::<_, _, _>::new(
542 MemoryInstance::new(),
543 self.storage.clone(),
544 interpreter_params,
545 );
546
547 self.execute_tx_inner(&mut transactor, tx)
548 .expect("Expected vm execution to be successful");
549 }
550
551 fn execute_tx_inner<M, Tx, Ecal, V>(
552 &mut self,
553 transactor: &mut Transactor<M, MemoryStorage, Tx, Ecal, V>,
554 checked: Checked<Tx>,
555 ) -> anyhow::Result<(StateTransition<Tx, V>, V)>
556 where
557 M: Memory,
558 Tx: ExecutableTransaction,
559 <Tx as IntoChecked>::Metadata: CheckedMetadata,
560 Ecal: crate::interpreter::EcalHandler,
561 V: Verifier + Clone,
562 {
563 self.storage.set_block_height(self.block_height);
564
565 transactor.transact(checked);
566
567 let storage = transactor.as_mut().clone();
568
569 if let Some(e) = transactor.error() {
570 return Err(anyhow!("{:?}", e));
571 }
572 let is_reverted = transactor.is_reverted();
573
574 let state = transactor.to_owned_state_transition().unwrap();
575
576 let interpreter = transactor.interpreter();
577
578 let verifier = interpreter.verifier().clone();
579
580 let transaction: Transaction = interpreter.transaction().clone().into();
582 let tx_offset = self.get_tx_params().tx_offset();
583 let mut tx_mem = interpreter
584 .memory()
585 .read(tx_offset, transaction.size())
586 .unwrap();
587 let mut deser_tx = Transaction::decode(&mut tx_mem).unwrap();
588
589 if let Transaction::Script(ref mut s) = deser_tx {
591 *s.receipts_root_mut() = interpreter.compute_receipts_root();
592 }
593
594 assert_eq!(deser_tx, transaction);
595 if !is_reverted {
596 self.storage = storage;
598 }
599
600 Ok((state, verifier))
601 }
602
603 pub fn deploy(
604 &mut self,
605 checked: Checked<Create>,
606 ) -> anyhow::Result<StateTransition<Create, Normal>> {
607 let interpreter_params =
608 InterpreterParams::new(self.gas_price, &self.consensus_params);
609 let mut transactor = Transactor::<_, _, _>::new(
610 MemoryInstance::new(),
611 self.storage.clone(),
612 interpreter_params,
613 );
614
615 Ok(self.execute_tx_inner(&mut transactor, checked)?.0)
616 }
617
618 pub fn attempt_execute_tx(
619 &mut self,
620 checked: Checked<Script>,
621 ) -> anyhow::Result<(StateTransition<Script, AttemptContinue>, AttemptContinue)>
622 {
623 let interpreter_params =
624 InterpreterParams::new(self.gas_price, &self.consensus_params);
625 let mut transactor =
626 Transactor::<_, _, _, NotSupportedEcal, AttemptContinue>::new(
627 MemoryInstance::new(),
628 self.storage.clone(),
629 interpreter_params,
630 );
631
632 self.execute_tx_inner(&mut transactor, checked)
633 }
634
635 pub fn execute_tx(
636 &mut self,
637 checked: Checked<Script>,
638 ) -> anyhow::Result<StateTransition<Script, Normal>> {
639 let interpreter_params =
640 InterpreterParams::new(self.gas_price, &self.consensus_params);
641 let mut transactor = Transactor::<_, _, _>::new(
642 MemoryInstance::new(),
643 self.storage.clone(),
644 interpreter_params,
645 );
646
647 Ok(self.execute_tx_inner(&mut transactor, checked)?.0)
648 }
649
650 pub fn execute_tx_with_backtrace(
651 &mut self,
652 checked: Checked<Script>,
653 gas_price: u64,
654 ) -> anyhow::Result<(StateTransition<Script, Normal>, Option<Backtrace>)>
655 {
656 let interpreter_params =
657 InterpreterParams::new(gas_price, &self.consensus_params);
658 let mut transactor = Transactor::<_, _, _>::new(
659 MemoryInstance::new(),
660 self.storage.clone(),
661 interpreter_params,
662 );
663
664 let state = self.execute_tx_inner(&mut transactor, checked)?.0;
665 let backtrace = transactor.backtrace();
666
667 Ok((state, backtrace))
668 }
669
670 pub fn attempt_execute(
672 &mut self,
673 ) -> (StateTransition<Script, AttemptContinue>, AttemptContinue) {
674 let tx = self.build();
675
676 self.attempt_execute_tx(tx)
677 .expect("expected successful vm execution")
678 }
679
680 pub fn execute(&mut self) -> StateTransition<Script, Normal> {
682 let tx = self.build();
683
684 self.execute_tx(tx)
685 .expect("expected successful vm execution")
686 }
687
688 pub fn get_storage(&self) -> &MemoryStorage {
689 &self.storage
690 }
691
692 pub fn execute_get_outputs(&mut self) -> Vec<Output> {
693 self.execute().tx().outputs().to_vec()
694 }
695
696 pub fn execute_get_change(&mut self, find_asset_id: AssetId) -> Word {
697 let outputs = self.execute_get_outputs();
698 find_change(outputs, find_asset_id)
699 }
700
701 pub fn get_contract_balance(
702 &mut self,
703 contract_id: &ContractId,
704 asset_id: &AssetId,
705 ) -> Word {
706 let tx = TestBuilder::build_get_balance_tx(
707 contract_id,
708 asset_id,
709 self.consensus_params.tx_params().tx_offset(),
710 );
711 let state = self
712 .execute_tx(tx)
713 .expect("expected successful vm execution in this context");
714 let receipts = state.receipts();
715 receipts[0].ra().expect("Balance expected")
716 }
717 }
718
719 pub fn check_expected_reason_for_instructions(
720 instructions: Vec<Instruction>,
721 expected_reason: PanicReason,
722 ) {
723 let client = MemoryClient::default();
724
725 check_expected_reason_for_instructions_with_client(
726 client,
727 instructions,
728 expected_reason,
729 );
730 }
731
732 pub fn check_expected_reason_for_instructions_with_client<M>(
733 mut client: MemoryClient<M>,
734 instructions: Vec<Instruction>,
735 expected_reason: PanicReason,
736 ) where
737 M: Memory,
738 {
739 let tx_params = TxParameters::default().with_max_gas_per_tx(Word::MAX / 2);
740 let gas_limit = tx_params.max_gas_per_tx() / 2;
743 let maturity = Default::default();
744 let height = Default::default();
745 let zero_fee_limit = 0;
746
747 let contract: Witness = instructions.into_iter().collect::<Vec<u8>>().into();
749 let salt = Default::default();
750 let code_root = Contract::root_from_code(contract.as_ref());
751 let storage_slots = vec![];
752 let state_root = Contract::initial_state_root(storage_slots.iter());
753 let contract_id = Contract::id(&salt, &code_root, &state_root);
754
755 let contract_deployer = TransactionBuilder::create(contract, salt, storage_slots)
756 .max_fee_limit(zero_fee_limit)
757 .with_tx_params(tx_params)
758 .add_fee_input()
759 .add_contract_created()
760 .finalize_checked(height);
761
762 client
763 .deploy(contract_deployer)
764 .expect("valid contract deployment");
765
766 let script = [
768 op::gtf(0x10, 0x0, Immediate12::from(GTFArgs::ScriptData)),
770 op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS),
772 op::ret(RegId::ONE),
773 ]
774 .into_iter()
775 .collect();
776 let script_data: Vec<u8> = [Call::new(contract_id, 0, 0).to_bytes().as_slice()]
777 .into_iter()
778 .flatten()
779 .copied()
780 .collect();
781
782 let tx_deploy_loader = TransactionBuilder::script(script, script_data)
783 .max_fee_limit(zero_fee_limit)
784 .script_gas_limit(gas_limit)
785 .maturity(maturity)
786 .with_tx_params(tx_params)
787 .add_input(Input::contract(
788 Default::default(),
789 Default::default(),
790 Default::default(),
791 Default::default(),
792 contract_id,
793 ))
794 .add_fee_input()
795 .add_output(Output::contract(0, Default::default(), Default::default()))
796 .finalize_checked(height);
797
798 check_reason_for_transaction(client, tx_deploy_loader, expected_reason);
799 }
800
801 pub fn check_reason_for_transaction<M>(
802 mut client: MemoryClient<M>,
803 checked_tx: Checked<Script>,
804 expected_reason: PanicReason,
805 ) where
806 M: Memory,
807 {
808 let receipts = client.transact(checked_tx);
809
810 let panic_found = receipts.iter().any(|receipt| {
811 if let Receipt::Panic { id: _, reason, .. } = receipt {
812 assert_eq!(
813 &expected_reason,
814 reason.reason(),
815 "Expected {}, found {}",
816 expected_reason,
817 reason.reason()
818 );
819 true
820 } else {
821 false
822 }
823 });
824
825 if !panic_found {
826 panic!("Script should have panicked");
827 }
828 }
829
830 pub fn find_change(outputs: Vec<Output>, find_asset_id: AssetId) -> Word {
831 let change = outputs.into_iter().find_map(|output| {
832 if let Output::Change {
833 amount, asset_id, ..
834 } = output
835 {
836 if asset_id == find_asset_id {
837 Some(amount)
838 } else {
839 None
840 }
841 } else {
842 None
843 }
844 });
845 change.unwrap_or_else(|| {
846 panic!("no change matching asset ID {:x} was found", &find_asset_id)
847 })
848 }
849}