Skip to main content

miden_testing/tx_context/
context.rs

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