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::fast::ExecutionOutput;
7use miden_processor::{ExecutionError, FutureMaybeSend, MastForest, MastForestStore, Word};
8use miden_protocol::account::{
9    Account,
10    AccountId,
11    PartialAccount,
12    StorageMapWitness,
13    StorageSlotContent,
14};
15use miden_protocol::assembly::debuginfo::{SourceLanguage, Uri};
16use miden_protocol::assembly::{Assembler, SourceManager, SourceManagerSync};
17use miden_protocol::asset::{Asset, AssetVaultKey, AssetWitness};
18use miden_protocol::block::account_tree::AccountWitness;
19use miden_protocol::block::{BlockHeader, BlockNumber};
20use miden_protocol::note::{Note, NoteScript};
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;
46
47// TRANSACTION CONTEXT
48// ================================================================================================
49
50/// Represents all needed data for executing a transaction, or arbitrary code.
51///
52/// It implements [`DataStore`], so transactions may be executed with
53/// [TransactionExecutor](miden_tx::TransactionExecutor)
54pub struct TransactionContext {
55    pub(super) account: Account,
56    pub(super) expected_output_notes: Vec<Note>,
57    pub(super) foreign_account_inputs: BTreeMap<AccountId, (Account, AccountWitness)>,
58    pub(super) tx_inputs: TransactionInputs,
59    pub(super) mast_store: TransactionMastStore,
60    pub(super) authenticator: Option<BasicAuthenticator>,
61    pub(super) source_manager: Arc<dyn SourceManagerSync>,
62    pub(super) note_scripts: BTreeMap<Word, NoteScript>,
63    pub(super) is_lazy_loading_enabled: bool,
64    pub(super) is_debug_mode_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    /// To improve the error message quality, convert the returned [`ExecutionError`] into a
77    /// [`Report`](miden_protocol::assembly::diagnostics::Report) or use `?` with
78    /// [`miden_protocol::assembly::diagnostics::Result`].
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if the assembly or execution of the provided code fails.
83    ///
84    /// # Panics
85    ///
86    /// - If the provided `code` is not a valid program.
87    pub async fn execute_code(&self, code: &str) -> Result<ExecutionOutput, ExecutionError> {
88        // Fetch all witnesses for note assets and the fee asset.
89        let mut asset_vault_keys = self
90            .tx_inputs
91            .input_notes()
92            .iter()
93            .flat_map(|note| note.note().assets().iter().map(Asset::vault_key))
94            .collect::<BTreeSet<_>>();
95        let fee_asset_vault_key = AssetVaultKey::from_account_id(
96            self.tx_inputs().block_header().fee_parameters().native_asset_id(),
97        )
98        .expect("fee asset should be a fungible asset");
99        asset_vault_keys.extend([fee_asset_vault_key]);
100
101        let (account, block_header, _blockchain) = self
102            .get_transaction_inputs(
103                self.tx_inputs.account().id(),
104                BTreeSet::from_iter([self.tx_inputs.block_header().block_num()]),
105            )
106            .await
107            .expect("failed to fetch transaction inputs");
108
109        // Add the vault key for the fee asset to the list of asset vault keys which may need to be
110        // accessed at the end of the transaction.
111        let fee_asset_vault_key =
112            AssetVaultKey::from_account_id(block_header.fee_parameters().native_asset_id())
113                .expect("fee asset should be a fungible asset");
114        asset_vault_keys.insert(fee_asset_vault_key);
115
116        // Fetch the witnesses for all asset vault keys.
117        let asset_witnesses = self
118            .get_vault_asset_witnesses(account.id(), account.vault().root(), asset_vault_keys)
119            .await
120            .expect("failed to fetch asset witnesses");
121
122        let tx_inputs = self.tx_inputs.clone().with_asset_witnesses(asset_witnesses);
123        let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&tx_inputs);
124
125        // Virtual file name should be unique.
126        let virtual_source_file = self.source_manager.load(
127            SourceLanguage::Masm,
128            Uri::new("_tx_context_code"),
129            code.to_owned(),
130        );
131
132        let assembler: Assembler =
133            CodeBuilder::with_mock_libraries_with_source_manager(self.source_manager.clone())
134                .into();
135
136        let program = assembler
137            .assemble_program(virtual_source_file)
138            .expect("code was not well formed");
139
140        // Load transaction kernel and the program into the mast forest in self.
141        // Note that native and foreign account's code are already loaded by the
142        // TransactionContextBuilder.
143        self.mast_store.insert(TransactionKernel::library().mast_forest().clone());
144        self.mast_store.insert(program.mast_forest().clone());
145
146        let account_procedure_idx_map = AccountProcedureIndexMap::new(
147            [tx_inputs.account().code()]
148                .into_iter()
149                .chain(self.foreign_account_inputs.values().map(|(account, _)| account.code())),
150        );
151
152        // The ref block is unimportant when using execute_code so we can set it to any value.
153        let ref_block = tx_inputs.block_header().block_num();
154
155        let exec_host = TransactionExecutorHost::<'_, '_, _, UnreachableAuth>::new(
156            &PartialAccount::from(self.account()),
157            tx_inputs.input_notes().clone(),
158            self,
159            ScriptMastForestStore::default(),
160            account_procedure_idx_map,
161            None,
162            ref_block,
163            // We don't need to set the initial balance in this context under the assumption that
164            // fees are zero.
165            0u64,
166            self.source_manager(),
167        );
168
169        let advice_inputs = advice_inputs.into_advice_inputs();
170
171        let mut mock_host = MockHost::new(exec_host);
172        if self.is_lazy_loading_enabled {
173            mock_host.enable_lazy_loading()
174        }
175
176        CodeExecutor::new(mock_host)
177            .stack_inputs(stack_inputs)
178            .extend_advice_inputs(advice_inputs)
179            .execute_program(program)
180            .await
181    }
182
183    /// Executes the transaction through a [TransactionExecutor]
184    pub async fn execute(self) -> Result<ExecutedTransaction, TransactionExecutorError> {
185        let account_id = self.account().id();
186        let block_num = self.tx_inputs().block_header().block_num();
187        let notes = self.tx_inputs().input_notes().clone();
188        let tx_args = self.tx_args().clone();
189
190        let mut tx_executor =
191            TransactionExecutor::new(&self).with_source_manager(self.source_manager.clone());
192
193        if self.is_debug_mode_enabled {
194            tx_executor = tx_executor.with_debug_mode();
195        }
196
197        if let Some(authenticator) = self.authenticator() {
198            tx_executor = tx_executor.with_authenticator(authenticator);
199        }
200
201        tx_executor.execute_transaction(account_id, block_num, notes, tx_args).await
202    }
203
204    pub fn account(&self) -> &Account {
205        &self.account
206    }
207
208    pub fn expected_output_notes(&self) -> &[Note] {
209        &self.expected_output_notes
210    }
211
212    pub fn tx_args(&self) -> &TransactionArgs {
213        self.tx_inputs.tx_args()
214    }
215
216    pub fn input_notes(&self) -> &InputNotes<InputNote> {
217        self.tx_inputs.input_notes()
218    }
219
220    pub fn set_tx_args(&mut self, tx_args: TransactionArgs) {
221        self.tx_inputs.set_tx_args(tx_args);
222    }
223
224    pub fn tx_inputs(&self) -> &TransactionInputs {
225        &self.tx_inputs
226    }
227
228    pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
229        self.authenticator.as_ref()
230    }
231
232    /// Returns the source manager used in the assembler of the transaction context builder.
233    pub fn source_manager(&self) -> Arc<dyn SourceManagerSync> {
234        Arc::clone(&self.source_manager)
235    }
236}
237
238impl DataStore for TransactionContext {
239    fn get_transaction_inputs(
240        &self,
241        account_id: AccountId,
242        ref_blocks: BTreeSet<BlockNumber>,
243    ) -> impl FutureMaybeSend<Result<(PartialAccount, BlockHeader, PartialBlockchain), DataStoreError>>
244    {
245        // Sanity checks
246        assert_eq!(account_id, self.account().id());
247        assert_eq!(account_id, self.tx_inputs.account().id());
248        assert_eq!(
249            ref_blocks
250                .last()
251                .copied()
252                .expect("at least the tx ref block should be provided"),
253            self.tx_inputs().blockchain().chain_length(),
254            "tx reference block should match partial blockchain length"
255        );
256
257        let account = self.tx_inputs.account().clone();
258        let block_header = self.tx_inputs.block_header().clone();
259        let blockchain = self.tx_inputs.blockchain().clone();
260
261        async move { Ok((account, block_header, blockchain)) }
262    }
263
264    fn get_foreign_account_inputs(
265        &self,
266        foreign_account_id: AccountId,
267        _ref_block: BlockNumber,
268    ) -> impl FutureMaybeSend<Result<AccountInputs, DataStoreError>> {
269        // Note that we cannot validate that the foreign account inputs are valid for the
270        // transaction's reference block.
271        async move {
272            let (foreign_account, account_witness) =
273                self.foreign_account_inputs.get(&foreign_account_id).ok_or_else(|| {
274                    DataStoreError::other(format!(
275                        "failed to find foreign account {foreign_account_id}"
276                    ))
277                })?;
278
279            Ok(AccountInputs::new(
280                PartialAccount::from(foreign_account),
281                account_witness.clone(),
282            ))
283        }
284    }
285
286    fn get_vault_asset_witnesses(
287        &self,
288        account_id: AccountId,
289        vault_root: Word,
290        vault_keys: BTreeSet<AssetVaultKey>,
291    ) -> impl FutureMaybeSend<Result<Vec<AssetWitness>, DataStoreError>> {
292        async move {
293            let asset_vault = if account_id == self.account().id() {
294                if self.account().vault().root() != vault_root {
295                    return Err(DataStoreError::other(format!(
296                        "native account {account_id} has vault root {} but {vault_root} was requested",
297                        self.account().vault().root()
298                    )));
299                }
300                self.account().vault()
301            } else {
302                let (foreign_account, _witness) = self
303                    .foreign_account_inputs
304                    .iter()
305                    .find_map(
306                        |(id, account_inputs)| {
307                            if account_id == *id { Some(account_inputs) } else { None }
308                        },
309                    )
310                    .ok_or_else(|| {
311                        DataStoreError::other(format!(
312                            "failed to find foreign account {account_id} in foreign account inputs"
313                        ))
314                    })?;
315
316                if foreign_account.vault().root() != vault_root {
317                    return Err(DataStoreError::other(format!(
318                        "foreign account {account_id} has vault root {} but {vault_root} was requested",
319                        foreign_account.vault().root()
320                    )));
321                }
322                foreign_account.vault()
323            };
324
325            Ok(vault_keys.into_iter().map(|vault_key| asset_vault.open(vault_key)).collect())
326        }
327    }
328
329    fn get_storage_map_witness(
330        &self,
331        account_id: AccountId,
332        map_root: Word,
333        map_key: Word,
334    ) -> impl FutureMaybeSend<Result<StorageMapWitness, DataStoreError>> {
335        async move {
336            if account_id == self.account().id() {
337                // Iterate the account storage to find the map with the requested root.
338                let storage_map = self
339                    .account()
340                    .storage()
341                    .slots()
342                    .iter()
343                    .find_map(|slot| match slot.content() {
344                        StorageSlotContent::Map(storage_map) if storage_map.root() == map_root => {
345                            Some(storage_map)
346                        },
347                        _ => None,
348                    })
349                    .ok_or_else(|| {
350                        DataStoreError::other(format!(
351                            "failed to find storage map with root {map_root} in account storage"
352                        ))
353                    })?;
354
355                Ok(storage_map.open(&map_key))
356            } else {
357                let (foreign_account, _witness) = self
358                    .foreign_account_inputs
359                    .iter()
360                    .find_map(
361                        |(id, account_inputs)| {
362                            if account_id == *id { Some(account_inputs) } else { None }
363                        },
364                    )
365                    .ok_or_else(|| {
366                        DataStoreError::other(format!(
367                            "failed to find foreign account {account_id} in foreign account inputs"
368                        ))
369                    })?;
370
371                let map = foreign_account
372                    .storage()
373                    .slots()
374                    .iter()
375                    .find_map(|slot| match slot.content() {
376                        StorageSlotContent::Map(storage_map) if storage_map.root() == map_root => {Some(storage_map)},
377                        _ => None,
378                    })
379                    .ok_or_else(|| {
380                        DataStoreError::other(format!(
381                            "failed to find storage map with root {map_root} in foreign account {account_id}"
382                        ))
383                    })?;
384
385                Ok(map.open(&map_key))
386            }
387        }
388    }
389
390    fn get_note_script(
391        &self,
392        script_root: Word,
393    ) -> impl FutureMaybeSend<Result<Option<NoteScript>, DataStoreError>> {
394        async move { Ok(self.note_scripts.get(&script_root).cloned()) }
395    }
396}
397
398impl MastForestStore for TransactionContext {
399    fn get(&self, procedure_hash: &Word) -> Option<Arc<MastForest>> {
400        self.mast_store.get(procedure_hash)
401    }
402}
403
404// TESTS
405// ================================================================================================
406
407#[cfg(test)]
408mod tests {
409    use miden_protocol::Felt;
410    use miden_protocol::assembly::Assembler;
411    use miden_protocol::note::NoteScript;
412
413    use super::*;
414    use crate::TransactionContextBuilder;
415
416    #[tokio::test]
417    async fn test_get_note_scripts() {
418        // Create two note scripts
419        let assembler1 = Assembler::default();
420        let script1_code = "begin push.1 end";
421        let program1 = assembler1
422            .assemble_program(script1_code)
423            .expect("failed to assemble note script 1");
424        let note_script1 = NoteScript::new(program1);
425        let script_root1 = note_script1.root();
426
427        let assembler2 = Assembler::default();
428        let script2_code = "begin push.2 push.3 add end";
429        let program2 = assembler2
430            .assemble_program(script2_code)
431            .expect("failed to assemble note script 2");
432        let note_script2 = NoteScript::new(program2);
433        let script_root2 = note_script2.root();
434
435        // Build a transaction context with both note scripts
436        let tx_context = TransactionContextBuilder::with_existing_mock_account()
437            .add_note_script(note_script1.clone())
438            .add_note_script(note_script2.clone())
439            .build()
440            .expect("failed to build transaction context");
441
442        // Assert that fetching both note scripts works
443        let retrieved_script1 = tx_context
444            .get_note_script(script_root1)
445            .await
446            .expect("failed to get note script 1")
447            .expect("note script 1 should exist");
448        assert_eq!(retrieved_script1, note_script1);
449
450        let retrieved_script2 = tx_context
451            .get_note_script(script_root2)
452            .await
453            .expect("failed to get note script 2")
454            .expect("note script 2 should exist");
455        assert_eq!(retrieved_script2, note_script2);
456
457        // Fetching a non-existent one returns None
458        let non_existent_root =
459            Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]);
460        let result = tx_context.get_note_script(non_existent_root).await;
461        assert!(matches!(result, Ok(None)));
462    }
463}