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 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            // setup a contract in current test state
506            let state = self
507                .deploy(tx)
508                .expect("Expected vm execution to be successful");
509
510            // set initial contract balance
511            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            // verify serialized tx == referenced tx
581            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            // Patch the tx with correct receipts root
590            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                // save storage between client instances
597                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        /// Build test tx and execute it with error collection
671        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        /// Build test tx and execute it
681        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        // The gas should be huge enough to cover the execution but still much less than
741        // `MAX_GAS_PER_TX`.
742        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        // setup contract with state tests
748        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        // call deployed contract
767        let script = [
768            // load call data to 0x10
769            op::gtf(0x10, 0x0, Immediate12::from(GTFArgs::ScriptData)),
770            // call the transfer contract
771            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}