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