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                        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        // 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            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            // 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)>
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>> {
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        /// Build test tx and execute it with error collection
669        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        /// Build test tx and execute it
677        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        // The gas should be huge enough to cover the execution but still much less than
737        // `MAX_GAS_PER_TX`.
738        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        // setup contract with state tests
744        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        // call deployed contract
764        let script = [
765            // load call data to 0x10
766            op::gtf(0x10, 0x0, Immediate12::from(GTFArgs::ScriptData)),
767            // call the transfer contract
768            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}