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 field::Script as ScriptField,
63 Script,
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 builder::TransactionBuilderExt,
97 Checked,
98 IntoChecked,
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 };
130 use fuel_asm::{
131 op,
132 GTFArgs,
133 Instruction,
134 PanicReason,
135 RegId,
136 };
137 use fuel_tx::{
138 field::{
139 Outputs,
140 ReceiptsRoot,
141 },
142 BlobBody,
143 BlobIdExt,
144 ConsensusParameters,
145 Contract,
146 ContractParameters,
147 Create,
148 FeeParameters,
149 Finalizable,
150 GasCosts,
151 Input,
152 Output,
153 PredicateParameters,
154 Receipt,
155 Script,
156 ScriptParameters,
157 StorageSlot,
158 Transaction,
159 TransactionBuilder,
160 TxParameters,
161 Witness,
162 };
163 use fuel_types::{
164 canonical::{
165 Deserialize,
166 Serialize,
167 },
168 Address,
169 AssetId,
170 BlobId,
171 BlockHeight,
172 ChainId,
173 ContractId,
174 Immediate12,
175 Salt,
176 Word,
177 };
178 use itertools::Itertools;
179 use rand::{
180 prelude::StdRng,
181 Rng,
182 SeedableRng,
183 };
184
185 pub struct CreatedContract {
186 pub tx: Create,
187 pub contract_id: ContractId,
188 pub salt: Salt,
189 }
190
191 pub struct TestBuilder {
192 pub rng: StdRng,
193 gas_price: Word,
194 max_fee_limit: Word,
195 script_gas_limit: Word,
196 builder: TransactionBuilder<Script>,
197 storage: MemoryStorage,
198 block_height: BlockHeight,
199 consensus_params: ConsensusParameters,
200 }
201
202 impl TestBuilder {
203 pub fn new(seed: u64) -> Self {
204 let bytecode = core::iter::once(op::ret(RegId::ONE)).collect();
205 TestBuilder {
206 rng: StdRng::seed_from_u64(seed),
207 gas_price: 0,
208 max_fee_limit: 0,
209 script_gas_limit: 100,
210 builder: TransactionBuilder::script(bytecode, vec![]),
211 storage: MemoryStorage::default(),
212 block_height: Default::default(),
213 consensus_params: ConsensusParameters::standard(),
214 }
215 }
216
217 pub fn get_block_height(&self) -> BlockHeight {
218 self.block_height
219 }
220
221 pub fn start_script_bytes(
222 &mut self,
223 script: Vec<u8>,
224 script_data: Vec<u8>,
225 ) -> &mut Self {
226 self.start_script_inner(script, script_data)
227 }
228
229 pub fn start_script(
230 &mut self,
231 script: Vec<Instruction>,
232 script_data: Vec<u8>,
233 ) -> &mut Self {
234 let script = script.into_iter().collect();
235 self.start_script_inner(script, script_data)
236 }
237
238 fn start_script_inner(
239 &mut self,
240 script: Vec<u8>,
241 script_data: Vec<u8>,
242 ) -> &mut Self {
243 self.builder = TransactionBuilder::script(script, script_data);
244 self.builder.script_gas_limit(self.script_gas_limit);
245 self
246 }
247
248 pub fn gas_price(&mut self, price: Word) -> &mut TestBuilder {
249 self.gas_price = price;
250 self
251 }
252
253 pub fn max_fee_limit(&mut self, max_fee_limit: Word) -> &mut TestBuilder {
254 self.max_fee_limit = max_fee_limit;
255 self
256 }
257
258 pub fn script_gas_limit(&mut self, limit: Word) -> &mut TestBuilder {
259 self.builder.script_gas_limit(limit);
260 self.script_gas_limit = limit;
261 self
262 }
263
264 pub fn change_output(&mut self, asset_id: AssetId) -> &mut TestBuilder {
265 self.builder
266 .add_output(Output::change(self.rng.gen(), 0, asset_id));
267 self
268 }
269
270 pub fn coin_output(
271 &mut self,
272 asset_id: AssetId,
273 amount: Word,
274 ) -> &mut TestBuilder {
275 self.builder
276 .add_output(Output::coin(self.rng.gen(), amount, asset_id));
277 self
278 }
279
280 pub fn variable_output(&mut self, asset_id: AssetId) -> &mut TestBuilder {
281 self.builder
282 .add_output(Output::variable(Address::zeroed(), 0, asset_id));
283 self
284 }
285
286 pub fn contract_output(&mut self, id: &ContractId) -> &mut TestBuilder {
287 let input_idx = self
288 .builder
289 .inputs()
290 .iter()
291 .find_position(|input| matches!(input, Input::Contract(contract) if &contract.contract_id == id))
292 .expect("expected contract input with matching contract id");
293
294 self.builder.add_output(Output::contract(
295 u16::try_from(input_idx.0).expect("The input index is more than allowed"),
296 self.rng.gen(),
297 self.rng.gen(),
298 ));
299
300 self
301 }
302
303 pub fn coin_input(
304 &mut self,
305 asset_id: AssetId,
306 amount: Word,
307 ) -> &mut TestBuilder {
308 self.builder.add_unsigned_coin_input(
309 fuel_crypto::SecretKey::random(&mut self.rng),
310 self.rng.gen(),
311 amount,
312 asset_id,
313 Default::default(),
314 );
315 self
316 }
317
318 pub fn fee_input(&mut self) -> &mut TestBuilder {
319 self.builder.add_fee_input();
320 self
321 }
322
323 pub fn contract_input(&mut self, contract_id: ContractId) -> &mut TestBuilder {
324 self.builder.add_input(Input::contract(
325 self.rng.gen(),
326 self.rng.gen(),
327 self.rng.gen(),
328 self.rng.gen(),
329 contract_id,
330 ));
331 self
332 }
333
334 pub fn witness(&mut self, witness: Witness) -> &mut TestBuilder {
335 self.builder.add_witness(witness);
336 self
337 }
338
339 pub fn storage(&mut self, storage: MemoryStorage) -> &mut TestBuilder {
340 self.storage = storage;
341 self
342 }
343
344 pub fn block_height(&mut self, block_height: BlockHeight) -> &mut TestBuilder {
345 self.block_height = block_height;
346 self
347 }
348
349 pub fn with_fee_params(&mut self, fee_params: FeeParameters) -> &mut TestBuilder {
350 self.consensus_params.set_fee_params(fee_params);
351 self
352 }
353
354 pub fn with_free_gas_costs(&mut self) -> &mut TestBuilder {
355 let gas_costs = GasCosts::free();
356 self.consensus_params.set_gas_costs(gas_costs);
357 self
358 }
359
360 pub fn base_asset_id(&mut self, base_asset_id: AssetId) -> &mut TestBuilder {
361 self.consensus_params.set_base_asset_id(base_asset_id);
362 self
363 }
364
365 pub fn build(&mut self) -> Checked<Script> {
366 self.builder.max_fee_limit(self.max_fee_limit);
367 self.builder.with_tx_params(*self.get_tx_params());
368 self.builder
369 .with_contract_params(*self.get_contract_params());
370 self.builder
371 .with_predicate_params(*self.get_predicate_params());
372 self.builder.with_script_params(*self.get_script_params());
373 self.builder.with_fee_params(*self.get_fee_params());
374 self.builder.with_base_asset_id(*self.get_base_asset_id());
375 self.builder
376 .finalize_checked_with_storage(self.block_height, &self.storage)
377 }
378
379 pub fn get_tx_params(&self) -> &TxParameters {
380 self.consensus_params.tx_params()
381 }
382
383 pub fn get_predicate_params(&self) -> &PredicateParameters {
384 self.consensus_params.predicate_params()
385 }
386
387 pub fn get_script_params(&self) -> &ScriptParameters {
388 self.consensus_params.script_params()
389 }
390
391 pub fn get_contract_params(&self) -> &ContractParameters {
392 self.consensus_params.contract_params()
393 }
394
395 pub fn get_fee_params(&self) -> &FeeParameters {
396 self.consensus_params.fee_params()
397 }
398
399 pub fn get_base_asset_id(&self) -> &AssetId {
400 self.consensus_params.base_asset_id()
401 }
402
403 pub fn get_block_gas_limit(&self) -> u64 {
404 self.consensus_params.block_gas_limit()
405 }
406
407 pub fn get_block_transaction_size_limit(&self) -> u64 {
408 self.consensus_params.block_transaction_size_limit()
409 }
410
411 pub fn get_privileged_address(&self) -> &Address {
412 self.consensus_params.privileged_address()
413 }
414
415 pub fn get_chain_id(&self) -> ChainId {
416 self.consensus_params.chain_id()
417 }
418
419 pub fn get_gas_costs(&self) -> &GasCosts {
420 self.consensus_params.gas_costs()
421 }
422
423 pub fn build_get_balance_tx(
424 contract_id: &ContractId,
425 asset_id: &AssetId,
426 tx_offset: usize,
427 ) -> Checked<Script> {
428 let (script, _) = script_with_data_offset!(
429 data_offset,
430 vec![
431 op::movi(0x11, data_offset),
432 op::addi(
433 0x12,
434 0x11,
435 Immediate12::try_from(AssetId::LEN)
436 .expect("`AssetId::LEN` is 32 bytes")
437 ),
438 op::bal(0x10, 0x11, 0x12),
439 op::log(0x10, RegId::ZERO, RegId::ZERO, RegId::ZERO),
440 op::ret(RegId::ONE),
441 ],
442 tx_offset
443 );
444
445 let script_data: Vec<u8> = [asset_id.as_ref(), contract_id.as_ref()]
446 .into_iter()
447 .flatten()
448 .copied()
449 .collect();
450
451 TestBuilder::new(2322u64)
452 .start_script(script, script_data)
453 .gas_price(0)
454 .script_gas_limit(1_000_000)
455 .contract_input(*contract_id)
456 .fee_input()
457 .contract_output(contract_id)
458 .build()
459 }
460
461 pub fn setup_contract_bytes(
462 &mut self,
463 contract: Vec<u8>,
464 initial_balance: Option<(AssetId, Word)>,
465 initial_state: Option<Vec<StorageSlot>>,
466 ) -> CreatedContract {
467 self.setup_contract_inner(contract, initial_balance, initial_state)
468 }
469
470 pub fn setup_contract(
471 &mut self,
472 contract: Vec<Instruction>,
473 initial_balance: Option<(AssetId, Word)>,
474 initial_state: Option<Vec<StorageSlot>>,
475 ) -> CreatedContract {
476 let contract = contract.into_iter().collect();
477
478 self.setup_contract_inner(contract, initial_balance, initial_state)
479 }
480
481 fn setup_contract_inner(
482 &mut self,
483 contract: Vec<u8>,
484 initial_balance: Option<(AssetId, Word)>,
485 initial_state: Option<Vec<StorageSlot>>,
486 ) -> CreatedContract {
487 let storage_slots = initial_state.unwrap_or_default();
488
489 let salt: Salt = self.rng.gen();
490 let program: Witness = contract.into();
491 let storage_root = Contract::initial_state_root(storage_slots.iter());
492 let contract = Contract::from(program.as_ref());
493 let contract_root = contract.root();
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)>
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>> {
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)> {
622 let interpreter_params =
623 InterpreterParams::new(self.gas_price, &self.consensus_params);
624 let mut transactor =
625 Transactor::<_, _, _, NotSupportedEcal, AttemptContinue>::new(
626 MemoryInstance::new(),
627 self.storage.clone(),
628 interpreter_params,
629 );
630
631 self.execute_tx_inner(&mut transactor, checked)
632 }
633
634 pub fn execute_tx(
635 &mut self,
636 checked: Checked<Script>,
637 ) -> anyhow::Result<StateTransition<Script>> {
638 let interpreter_params =
639 InterpreterParams::new(self.gas_price, &self.consensus_params);
640 let mut transactor = Transactor::<_, _, _>::new(
641 MemoryInstance::new(),
642 self.storage.clone(),
643 interpreter_params,
644 );
645
646 Ok(self.execute_tx_inner(&mut transactor, checked)?.0)
647 }
648
649 pub fn execute_tx_with_backtrace(
650 &mut self,
651 checked: Checked<Script>,
652 gas_price: u64,
653 ) -> anyhow::Result<(StateTransition<Script>, Option<Backtrace>)> {
654 let interpreter_params =
655 InterpreterParams::new(gas_price, &self.consensus_params);
656 let mut transactor = Transactor::<_, _, _>::new(
657 MemoryInstance::new(),
658 self.storage.clone(),
659 interpreter_params,
660 );
661
662 let state = self.execute_tx_inner(&mut transactor, checked)?.0;
663 let backtrace = transactor.backtrace();
664
665 Ok((state, backtrace))
666 }
667
668 pub fn attempt_execute(&mut self) -> (StateTransition<Script>, AttemptContinue) {
670 let tx = self.build();
671
672 self.attempt_execute_tx(tx)
673 .expect("expected successful vm execution")
674 }
675
676 pub fn execute(&mut self) -> StateTransition<Script> {
678 let tx = self.build();
679
680 self.execute_tx(tx)
681 .expect("expected successful vm execution")
682 }
683
684 pub fn get_storage(&self) -> &MemoryStorage {
685 &self.storage
686 }
687
688 pub fn execute_get_outputs(&mut self) -> Vec<Output> {
689 self.execute().tx().outputs().to_vec()
690 }
691
692 pub fn execute_get_change(&mut self, find_asset_id: AssetId) -> Word {
693 let outputs = self.execute_get_outputs();
694 find_change(outputs, find_asset_id)
695 }
696
697 pub fn get_contract_balance(
698 &mut self,
699 contract_id: &ContractId,
700 asset_id: &AssetId,
701 ) -> Word {
702 let tx = TestBuilder::build_get_balance_tx(
703 contract_id,
704 asset_id,
705 self.consensus_params.tx_params().tx_offset(),
706 );
707 let state = self
708 .execute_tx(tx)
709 .expect("expected successful vm execution in this context");
710 let receipts = state.receipts();
711 receipts[0].ra().expect("Balance expected")
712 }
713 }
714
715 pub fn check_expected_reason_for_instructions(
716 instructions: Vec<Instruction>,
717 expected_reason: PanicReason,
718 ) {
719 let client = MemoryClient::default();
720
721 check_expected_reason_for_instructions_with_client(
722 client,
723 instructions,
724 expected_reason,
725 );
726 }
727
728 pub fn check_expected_reason_for_instructions_with_client<M>(
729 mut client: MemoryClient<M>,
730 instructions: Vec<Instruction>,
731 expected_reason: PanicReason,
732 ) where
733 M: Memory,
734 {
735 let tx_params = TxParameters::default().with_max_gas_per_tx(Word::MAX / 2);
736 let gas_limit = tx_params.max_gas_per_tx() / 2;
739 let maturity = Default::default();
740 let height = Default::default();
741 let zero_fee_limit = 0;
742
743 let contract: Witness = instructions.into_iter().collect::<Vec<u8>>().into();
745 let salt = Default::default();
746 let code_root = Contract::root_from_code(contract.as_ref());
747 let storage_slots = vec![];
748 let state_root = Contract::initial_state_root(storage_slots.iter());
749 let contract_id =
750 Contract::from(contract.as_ref()).id(&salt, &code_root, &state_root);
751
752 let contract_deployer = TransactionBuilder::create(contract, salt, storage_slots)
753 .max_fee_limit(zero_fee_limit)
754 .with_tx_params(tx_params)
755 .add_fee_input()
756 .add_contract_created()
757 .finalize_checked(height);
758
759 client
760 .deploy(contract_deployer)
761 .expect("valid contract deployment");
762
763 let script = [
765 op::gtf(0x10, 0x0, Immediate12::from(GTFArgs::ScriptData)),
767 op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS),
769 op::ret(RegId::ONE),
770 ]
771 .into_iter()
772 .collect();
773 let script_data: Vec<u8> = [Call::new(contract_id, 0, 0).to_bytes().as_slice()]
774 .into_iter()
775 .flatten()
776 .copied()
777 .collect();
778
779 let tx_deploy_loader = TransactionBuilder::script(script, script_data)
780 .max_fee_limit(zero_fee_limit)
781 .script_gas_limit(gas_limit)
782 .maturity(maturity)
783 .with_tx_params(tx_params)
784 .add_input(Input::contract(
785 Default::default(),
786 Default::default(),
787 Default::default(),
788 Default::default(),
789 contract_id,
790 ))
791 .add_fee_input()
792 .add_output(Output::contract(0, Default::default(), Default::default()))
793 .finalize_checked(height);
794
795 check_reason_for_transaction(client, tx_deploy_loader, expected_reason);
796 }
797
798 pub fn check_reason_for_transaction<M>(
799 mut client: MemoryClient<M>,
800 checked_tx: Checked<Script>,
801 expected_reason: PanicReason,
802 ) where
803 M: Memory,
804 {
805 let receipts = client.transact(checked_tx);
806
807 let panic_found = receipts.iter().any(|receipt| {
808 if let Receipt::Panic { id: _, reason, .. } = receipt {
809 assert_eq!(
810 &expected_reason,
811 reason.reason(),
812 "Expected {}, found {}",
813 expected_reason,
814 reason.reason()
815 );
816 true
817 } else {
818 false
819 }
820 });
821
822 if !panic_found {
823 panic!("Script should have panicked");
824 }
825 }
826
827 pub fn find_change(outputs: Vec<Output>, find_asset_id: AssetId) -> Word {
828 let change = outputs.into_iter().find_map(|output| {
829 if let Output::Change {
830 amount, asset_id, ..
831 } = output
832 {
833 if asset_id == find_asset_id {
834 Some(amount)
835 } else {
836 None
837 }
838 } else {
839 None
840 }
841 });
842 change.unwrap_or_else(|| {
843 panic!("no change matching asset ID {:x} was found", &find_asset_id)
844 })
845 }
846}