miden_testing/tx_context/
context.rs1use 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
48pub 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 pub async fn execute_code(&self, code: &str) -> Result<ExecutionOutput, ExecError> {
84 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 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 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 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 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 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 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 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 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 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#[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 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 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 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 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 #[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 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}