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