Skip to main content

miden_testing/mock_transaction/
transaction.rs

1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4
5#[cfg(test)]
6use miden_processor::{ExecutionOutput, Felt};
7use miden_processor::{FutureMaybeSend, LoadedMastForest, MastForestStore, Word};
8use miden_protocol::account::{
9    Account,
10    AccountId,
11    PartialAccount,
12    StorageMapKey,
13    StorageMapWitness,
14    StorageSlotContent,
15};
16use miden_protocol::assembly::SourceManagerSync;
17use miden_protocol::asset::{AssetId, AssetWitness};
18use miden_protocol::block::account_tree::AccountWitness;
19use miden_protocol::block::{BlockHeader, BlockNumber};
20use miden_protocol::note::{Note, NoteScript, NoteScriptRoot};
21use miden_protocol::protocol_config::ProtocolConfig;
22use miden_protocol::transaction::{
23    AccountInputs,
24    ExecutedTransaction,
25    InputNote,
26    InputNotes,
27    PartialBlockchain,
28    TransactionArgs,
29    TransactionInputs,
30};
31use miden_tx::auth::BasicAuthenticator;
32use miden_tx::{
33    DataStore,
34    DataStoreError,
35    TransactionExecutor,
36    TransactionExecutorError,
37    TransactionMastStore,
38};
39
40#[cfg(test)]
41use crate::mock_transaction::ExecError;
42
43// MOCK TRANSACTION
44// ================================================================================================
45
46/// Represents all needed data for executing a transaction, or arbitrary code.
47///
48/// It implements [`DataStore`], so transactions may be executed with
49/// [TransactionExecutor](miden_tx::TransactionExecutor)
50pub struct MockTransaction {
51    pub(super) account: Account,
52    pub(super) expected_output_notes: Vec<Note>,
53    pub(super) foreign_account_inputs: BTreeMap<AccountId, (Account, AccountWitness)>,
54    pub(super) tx_inputs: TransactionInputs,
55    pub(super) mast_store: TransactionMastStore,
56    pub(super) authenticator: Option<BasicAuthenticator>,
57    pub(super) source_manager: Arc<dyn SourceManagerSync>,
58    pub(super) note_scripts: BTreeMap<NoteScriptRoot, NoteScript>,
59}
60
61impl MockTransaction {
62    /// Executes the transaction through a [TransactionExecutor]
63    pub async fn execute(self) -> Result<ExecutedTransaction, TransactionExecutorError> {
64        let account_id = self.account().id();
65        let block_num = self.tx_inputs().block_header().block_num();
66        let notes = self.tx_inputs().input_notes().clone();
67        let tx_args = self.tx_args().clone();
68
69        let mut tx_executor =
70            TransactionExecutor::new(&self).with_source_manager(self.source_manager.clone());
71
72        if let Some(authenticator) = self.authenticator() {
73            tx_executor = tx_executor.with_authenticator(authenticator);
74        }
75
76        tx_executor.execute_transaction(account_id, block_num, notes, tx_args).await
77    }
78
79    pub fn account(&self) -> &Account {
80        &self.account
81    }
82
83    pub fn expected_output_notes(&self) -> &[Note] {
84        &self.expected_output_notes
85    }
86
87    pub fn tx_args(&self) -> &TransactionArgs {
88        self.tx_inputs.tx_args()
89    }
90
91    pub fn input_notes(&self) -> &InputNotes<InputNote> {
92        self.tx_inputs.input_notes()
93    }
94
95    pub fn set_tx_args(&mut self, tx_args: TransactionArgs) {
96        self.tx_inputs.set_tx_args(tx_args);
97    }
98
99    pub fn tx_inputs(&self) -> &TransactionInputs {
100        &self.tx_inputs
101    }
102
103    pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
104        self.authenticator.as_ref()
105    }
106
107    /// Returns the source manager used in the assembler of the mock transaction.
108    pub fn source_manager(&self) -> Arc<dyn SourceManagerSync> {
109        Arc::clone(&self.source_manager)
110    }
111}
112
113// CODE EXECUTION
114// ================================================================================================
115
116#[cfg(test)]
117impl MockTransaction {
118    /// Executes arbitrary code within the context of a mocked transaction environment and returns
119    /// the resulting [`ExecutionOutput`].
120    ///
121    /// The code is compiled with the assembler built by
122    /// [`CodeBuilder::with_mock_packages_with_source_manager`] and executed with advice inputs
123    /// constructed from the data stored in the context. The program
124    /// is run on a modified [`TransactionExecutorHost`] which is loaded with the procedures exposed
125    /// by the transaction kernel, and also individual kernel functions (not normally exposed).
126    ///
127    /// This executes in the memory context of the transaction kernel and requires invoking internal
128    /// kernel APIs to be used correctly, so it is restricted to this crate's tests. Use
129    /// [`MockTransaction::execute`] to execute a transaction.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if the assembly or execution of the provided code fails.
134    ///
135    /// # Panics
136    ///
137    /// - If the provided `code` is not a valid program.
138    pub(crate) async fn execute_code(&self, code: &str) -> Result<ExecutionOutput, ExecError> {
139        self.execute_code_inner(code, true, None).await
140    }
141
142    /// Same as [`MockTransaction::execute_code`], except that the host does _not_ handle lazy
143    /// loading events, which lets a kernel procedure be tested in isolation from them.
144    pub(crate) async fn execute_code_without_lazy_loading(
145        &self,
146        code: &str,
147    ) -> Result<ExecutionOutput, ExecError> {
148        self.execute_code_inner(code, false, None).await
149    }
150
151    /// Same as [`MockTransaction::execute_code`], but overrides the host response to an input-note
152    /// index request with `[note_idx, is_found]`.
153    pub(crate) async fn execute_code_with_input_note_index_response(
154        &self,
155        code: &str,
156        response: [Felt; 2],
157    ) -> Result<ExecutionOutput, ExecError> {
158        self.execute_code_inner(code, true, Some(response)).await
159    }
160
161    async fn execute_code_inner(
162        &self,
163        code: &str,
164        is_lazy_loading_enabled: bool,
165        input_note_index_response: Option<[Felt; 2]>,
166    ) -> Result<ExecutionOutput, ExecError> {
167        use alloc::borrow::ToOwned;
168
169        use miden_protocol::assembly::debuginfo::{SourceLanguage, Uri};
170        use miden_protocol::assembly::{Assembler, SourceManager};
171        use miden_protocol::asset::Asset;
172        use miden_protocol::transaction::TransactionKernel;
173        use miden_standards::code_builder::CodeBuilder;
174        use miden_tx::auth::UnreachableAuth;
175        use miden_tx::{AccountProcedureIndexMap, ScriptMastForestStore, TransactionExecutorHost};
176
177        use crate::executor::CodeExecutor;
178        use crate::mock_host::MockHost;
179
180        // Fetch all witnesses for note assets.
181        let asset_ids = self
182            .tx_inputs
183            .input_notes()
184            .iter()
185            .flat_map(|note| note.note().assets().iter().map(Asset::id))
186            .collect::<BTreeSet<_>>();
187
188        let (account, _block_header, _protocol_config, _blockchain) = self
189            .get_transaction_inputs(
190                self.tx_inputs.account().id(),
191                BTreeSet::from_iter([self.tx_inputs.block_header().block_num()]),
192            )
193            .await
194            .expect("failed to fetch transaction inputs");
195
196        // Fetch the witnesses for all asset IDs.
197        let asset_witnesses = self
198            .get_vault_asset_witnesses(account.id(), account.vault().root(), asset_ids)
199            .await
200            .expect("failed to fetch asset witnesses");
201
202        let tx_inputs = self.tx_inputs.clone().with_asset_witnesses(asset_witnesses);
203        let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&tx_inputs);
204
205        // Virtual file name should be unique.
206        let virtual_source_file = self.source_manager.load(
207            SourceLanguage::Masm,
208            Uri::new("_mock_tx_code"),
209            code.to_owned(),
210        );
211
212        let assembler: Assembler =
213            CodeBuilder::with_mock_packages_with_source_manager(self.source_manager.clone()).into();
214
215        let program = assembler
216            .assemble_program("mock-tx-code", virtual_source_file)
217            .expect("code was not well formed");
218
219        // Load transaction kernel and the program into the mast forest in self.
220        // Note that native and foreign account's code are already loaded by the
221        // MockTransactionBuilder.
222        self.mast_store.insert_package(&TransactionKernel::core_package());
223        self.mast_store.insert_package(&program);
224
225        let account_procedure_idx_map = AccountProcedureIndexMap::new(
226            [tx_inputs.account().code()]
227                .into_iter()
228                .chain(self.foreign_account_inputs.values().map(|(account, _)| account.code())),
229        );
230
231        // The host validates the tx summary's block commitment against these values, so they
232        // must come from the tx inputs.
233        let ref_block = tx_inputs.block_header().block_num();
234        let block_commitments = tx_inputs.collect_block_commitments();
235
236        let exec_host = TransactionExecutorHost::<'_, '_, _, UnreachableAuth>::new(
237            &PartialAccount::from(self.account()),
238            tx_inputs.input_notes().clone(),
239            self,
240            ScriptMastForestStore::default(),
241            account_procedure_idx_map,
242            None,
243            ref_block,
244            block_commitments,
245            self.source_manager(),
246        )
247        .expect("partial account from a full account should contain its complete storage");
248
249        let advice_inputs = advice_inputs.into_advice_inputs();
250
251        let mut mock_host = MockHost::new(exec_host);
252        if is_lazy_loading_enabled {
253            mock_host.enable_lazy_loading()
254        }
255        if let Some(response) = input_note_index_response {
256            mock_host.set_input_note_index_response(response);
257        }
258
259        CodeExecutor::new(mock_host)
260            .stack_inputs(stack_inputs)
261            .extend_advice_inputs(advice_inputs)
262            .execute_package(program)
263            .await
264    }
265}
266
267impl DataStore for MockTransaction {
268    fn get_transaction_inputs(
269        &self,
270        account_id: AccountId,
271        ref_blocks: BTreeSet<BlockNumber>,
272    ) -> impl FutureMaybeSend<
273        Result<(PartialAccount, BlockHeader, ProtocolConfig, PartialBlockchain), DataStoreError>,
274    > {
275        // Sanity checks
276        assert_eq!(account_id, self.account().id());
277        assert_eq!(account_id, self.tx_inputs.account().id());
278        assert_eq!(
279            ref_blocks
280                .last()
281                .copied()
282                .expect("at least the tx ref block should be provided"),
283            self.tx_inputs().blockchain().chain_length(),
284            "tx reference block should match partial blockchain length"
285        );
286
287        let account = self.tx_inputs.account().clone();
288        let block_header = self.tx_inputs.block_header().clone();
289        let protocol_config = self.tx_inputs.protocol_config().clone();
290        let blockchain = self.tx_inputs.blockchain().clone();
291
292        async move { Ok((account, block_header, protocol_config, blockchain)) }
293    }
294
295    fn get_foreign_account_inputs(
296        &self,
297        foreign_account_id: AccountId,
298        _ref_block: BlockNumber,
299    ) -> impl FutureMaybeSend<Result<AccountInputs, DataStoreError>> {
300        // Note that we cannot validate that the foreign account inputs are valid for the
301        // transaction's reference block.
302        async move {
303            let (foreign_account, account_witness) =
304                self.foreign_account_inputs.get(&foreign_account_id).ok_or_else(|| {
305                    DataStoreError::other(format!(
306                        "failed to find foreign account {foreign_account_id}"
307                    ))
308                })?;
309
310            Ok(AccountInputs::new(
311                PartialAccount::from(foreign_account),
312                account_witness.clone(),
313            ))
314        }
315    }
316
317    fn get_vault_asset_witnesses(
318        &self,
319        account_id: AccountId,
320        vault_root: Word,
321        asset_ids: BTreeSet<AssetId>,
322    ) -> impl FutureMaybeSend<Result<Vec<AssetWitness>, DataStoreError>> {
323        async move {
324            let asset_vault = if account_id == self.account().id() {
325                if self.account().vault().root() != vault_root {
326                    return Err(DataStoreError::other(format!(
327                        "native account {account_id} has vault root {} but {vault_root} was requested",
328                        self.account().vault().root()
329                    )));
330                }
331                self.account().vault()
332            } else {
333                let (foreign_account, _witness) = self
334                    .foreign_account_inputs
335                    .iter()
336                    .find_map(
337                        |(id, account_inputs)| {
338                            if account_id == *id { Some(account_inputs) } else { None }
339                        },
340                    )
341                    .ok_or_else(|| {
342                        DataStoreError::other(format!(
343                            "failed to find foreign account {account_id} in foreign account inputs"
344                        ))
345                    })?;
346
347                if foreign_account.vault().root() != vault_root {
348                    return Err(DataStoreError::other(format!(
349                        "foreign account {account_id} has vault root {} but {vault_root} was requested",
350                        foreign_account.vault().root()
351                    )));
352                }
353                foreign_account.vault()
354            };
355
356            Ok(asset_ids.into_iter().map(|asset_id| asset_vault.open(asset_id)).collect())
357        }
358    }
359
360    fn get_storage_map_witness(
361        &self,
362        account_id: AccountId,
363        map_root: Word,
364        map_key: StorageMapKey,
365    ) -> impl FutureMaybeSend<Result<StorageMapWitness, DataStoreError>> {
366        async move {
367            if account_id == self.account().id() {
368                // Iterate the account storage to find the map with the requested root.
369                let storage_map = self
370                    .account()
371                    .storage()
372                    .slots()
373                    .iter()
374                    .find_map(|slot| match slot.content() {
375                        StorageSlotContent::Map(storage_map) if storage_map.root() == map_root => {
376                            Some(storage_map)
377                        },
378                        _ => None,
379                    })
380                    .ok_or_else(|| {
381                        DataStoreError::other(format!(
382                            "failed to find storage map with root {map_root} in account storage"
383                        ))
384                    })?;
385
386                Ok(storage_map.open(&map_key))
387            } else {
388                let (foreign_account, _witness) = self
389                    .foreign_account_inputs
390                    .iter()
391                    .find_map(
392                        |(id, account_inputs)| {
393                            if account_id == *id { Some(account_inputs) } else { None }
394                        },
395                    )
396                    .ok_or_else(|| {
397                        DataStoreError::other(format!(
398                            "failed to find foreign account {account_id} in foreign account inputs"
399                        ))
400                    })?;
401
402                let map = foreign_account
403                    .storage()
404                    .slots()
405                    .iter()
406                    .find_map(|slot| match slot.content() {
407                        StorageSlotContent::Map(storage_map) if storage_map.root() == map_root => {Some(storage_map)},
408                        _ => None,
409                    })
410                    .ok_or_else(|| {
411                        DataStoreError::other(format!(
412                            "failed to find storage map with root {map_root} in foreign account {account_id}"
413                        ))
414                    })?;
415
416                Ok(map.open(&map_key))
417            }
418        }
419    }
420
421    fn get_note_script(
422        &self,
423        script_root: NoteScriptRoot,
424    ) -> impl FutureMaybeSend<Result<Option<NoteScript>, DataStoreError>> {
425        async move { Ok(self.note_scripts.get(&script_root).cloned()) }
426    }
427}
428
429impl MastForestStore for MockTransaction {
430    fn get(&self, procedure_hash: &Word) -> Option<LoadedMastForest> {
431        self.mast_store.get(procedure_hash)
432    }
433}
434
435// TESTS
436// ================================================================================================
437
438#[cfg(test)]
439mod tests {
440    use std::string::ToString;
441
442    use miden_protocol::errors::tx_kernel::ERR_TX_COMPUTE_FEE_EXTRA_CYCLES_NOT_U32;
443    use miden_standards::code_builder::CodeBuilder;
444
445    use super::*;
446    use crate::{Auth, MockChain, TestTransactionBuilder};
447
448    #[tokio::test]
449    async fn test_get_note_scripts() {
450        // Create two note scripts
451        let script1_code = "@note_script\npub proc main\n    push.1\nend";
452        let note_script1 = CodeBuilder::default()
453            .compile_note_script(script1_code)
454            .expect("failed to assemble note script 1");
455        let script_root1 = note_script1.root();
456
457        let script2_code = "@note_script\npub proc main\n    push.2 push.3 add\nend";
458        let note_script2 = CodeBuilder::default()
459            .compile_note_script(script2_code)
460            .expect("failed to assemble note script 2");
461        let script_root2 = note_script2.root();
462
463        // Build a mock transaction with both note scripts
464        let mock_tx = TestTransactionBuilder::with_existing_mock_account()
465            .add_note_script(note_script1.clone())
466            .add_note_script(note_script2.clone())
467            .build()
468            .expect("failed to build mock transaction");
469
470        // Assert that fetching both note scripts works
471        let retrieved_script1 = mock_tx
472            .get_note_script(script_root1)
473            .await
474            .expect("failed to get note script 1")
475            .expect("note script 1 should exist");
476        assert_eq!(retrieved_script1, note_script1);
477
478        let retrieved_script2 = mock_tx
479            .get_note_script(script_root2)
480            .await
481            .expect("failed to get note script 2")
482            .expect("note script 2 should exist");
483        assert_eq!(retrieved_script2, note_script2);
484
485        // Fetching a non-existent one returns None
486        let non_existent_root = NoteScriptRoot::from_array([1, 2, 3, 4]);
487        let result = mock_tx.get_note_script(non_existent_root).await;
488        assert!(matches!(result, Ok(None)));
489    }
490
491    /// Regression test: an error raised in a dynamically-linked library (here `compute_fee` from
492    /// the protocol library) must render its human-readable message, not just an opaque error
493    /// code. This relies on the assembled packages retaining their debug info.
494    #[tokio::test]
495    async fn execute_code_renders_masm_error_message() -> anyhow::Result<()> {
496        let mut builder = MockChain::builder();
497        let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
498        let mock_chain = builder.build()?;
499
500        let mock_tx = mock_chain.build_transaction(account).build()?;
501
502        // A value that exceeds u32::MAX triggers the `u32assert` inside `compute_fee`.
503        let code = format!(
504            r#"
505        use miden::tx_kernel_core::prologue
506        use miden::protocol::tx
507
508        begin
509            exec.prologue::prepare_transaction
510
511            padw
512            push.{num_extra_cycles}
513            exec.tx::compute_fee
514        end"#,
515            num_extra_cycles = u64::from(u32::MAX) + 1
516        );
517
518        let Err(error) = mock_tx.execute_code(&code).await else {
519            anyhow::bail!("execution should fail on non-u32 extra cycles");
520        };
521
522        let rendered = error.to_string();
523        let expected_error = ERR_TX_COMPUTE_FEE_EXTRA_CYCLES_NOT_U32;
524        assert!(
525            rendered.contains(expected_error.message()),
526            "rendered error should contain the masm error message",
527        );
528
529        Ok(())
530    }
531}