Skip to main content

fuel_vm/
util.rs

1//! FuelVM utilities
2
3/// A utility macro for writing scripts with the data offset included. Since the
4/// script data offset depends on the length of the script, this macro will
5/// evaluate the length and then rewrite the resultant script output with the
6/// correct offset (using the offset parameter).
7///
8/// # Example
9///
10/// ```
11/// use fuel_asm::{op, RegId};
12/// use fuel_types::{Immediate18, Word, canonical::Serialize};
13/// use fuel_vm::prelude::{Call, TxParameters, ContractId, Opcode};
14/// use fuel_vm::script_with_data_offset;
15/// use itertools::Itertools;
16///
17/// // Example of making a contract call script using script_data for the call info and asset id.
18/// let contract_id = ContractId::from([0x11; 32]);
19/// let call = Call::new(contract_id, 0, 0).to_bytes();
20/// let asset_id = [0x00; 32];
21/// let transfer_amount: Word = 100;
22/// let gas_to_forward = 100_000;
23/// let script_data = [call.as_ref(), asset_id.as_ref()]
24///     .into_iter()
25///     .flatten()
26///     .copied()
27///     .collect_vec();
28///
29/// // Use the macro since we don't know the exact offset for script_data.
30/// let (script, data_offset) = script_with_data_offset!(
31///     data_offset,
32///     vec![
33///         // use data_offset to reference the location of the call bytes inside script_data
34///         op::movi(0x10, data_offset),
35///         op::movi(0x11, transfer_amount as Immediate18),
36///         // use data_offset again to reference the location of the asset id inside of script data
37///         op::movi(0x12, data_offset + call.len() as Immediate18),
38///         op::movi(0x13, gas_to_forward as Immediate18),
39///         op::call(0x10, 0x11, 0x12, 0x13),
40///         op::ret(RegId::ONE),
41///     ],
42///     TxParameters::DEFAULT.tx_offset()
43/// );
44/// ```
45#[cfg(feature = "alloc")]
46#[macro_export]
47macro_rules! script_with_data_offset {
48    ($offset:ident, $script:expr, $tx_offset:expr) => {{
49        let $offset = {
50            // first set offset to 0 before evaluating script expression
51            let $offset = {
52                use $crate::prelude::Immediate18;
53                0 as Immediate18
54            };
55            // evaluate script expression with zeroed data offset to get the script length
56            let script_bytes: $crate::alloc::vec::Vec<u8> =
57                ::core::iter::IntoIterator::into_iter({ $script }).collect();
58            // compute the script data offset within the VM memory given the script length
59            {
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        // re-evaluate and return the finalized script with the correct data offset length
79        // set.
80        ($script, $offset)
81    }};
82}
83
84#[allow(missing_docs)]
85#[cfg(feature = "random")]
86#[cfg(any(test, feature = "test-helpers"))]
87/// Testing utilities
88pub 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            // setup a contract in current test state
511            let state = self
512                .deploy(tx)
513                .expect("Expected vm execution to be successful");
514
515            // set initial contract balance
516            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            // verify serialized tx == referenced tx
586            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            // Patch the tx with correct receipts root
595            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                // save storage between client instances
602                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        /// Build test tx and execute it with error collection
676        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        /// Build test tx and execute it
686        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        // The gas should be huge enough to cover the execution but still much less than
746        // `MAX_GAS_PER_TX`.
747        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        // setup contract with state tests
753        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        // call deployed contract
772        let script = [
773            // load call data to 0x10
774            op::gtf(0x10, 0x0, Immediate12::from(GTFArgs::ScriptData)),
775            // call the transfer contract
776            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}