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