miden_testing/mock_transaction/
transaction.rs1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4
5#[cfg(test)]
6use miden_processor::{ExecutionOutput, Felt};
7use miden_processor::{FutureMaybeSend, LoadedMastForest, MastForestStore, Word};
8use miden_protocol::account::{
9 Account,
10 AccountId,
11 PartialAccount,
12 StorageMapKey,
13 StorageMapWitness,
14 StorageSlotContent,
15};
16use miden_protocol::assembly::SourceManagerSync;
17use miden_protocol::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};
30use miden_tx::auth::BasicAuthenticator;
31use miden_tx::{
32 DataStore,
33 DataStoreError,
34 TransactionExecutor,
35 TransactionExecutorError,
36 TransactionMastStore,
37};
38
39#[cfg(test)]
40use crate::mock_transaction::ExecError;
41
42pub struct MockTransaction {
50 pub(super) account: Account,
51 pub(super) expected_output_notes: Vec<Note>,
52 pub(super) foreign_account_inputs: BTreeMap<AccountId, (Account, AccountWitness)>,
53 pub(super) tx_inputs: TransactionInputs,
54 pub(super) mast_store: TransactionMastStore,
55 pub(super) authenticator: Option<BasicAuthenticator>,
56 pub(super) source_manager: Arc<dyn SourceManagerSync>,
57 pub(super) note_scripts: BTreeMap<NoteScriptRoot, NoteScript>,
58}
59
60impl MockTransaction {
61 pub async fn execute(self) -> Result<ExecutedTransaction, TransactionExecutorError> {
63 let account_id = self.account().id();
64 let block_num = self.tx_inputs().block_header().block_num();
65 let notes = self.tx_inputs().input_notes().clone();
66 let tx_args = self.tx_args().clone();
67
68 let mut tx_executor =
69 TransactionExecutor::new(&self).with_source_manager(self.source_manager.clone());
70
71 if let Some(authenticator) = self.authenticator() {
72 tx_executor = tx_executor.with_authenticator(authenticator);
73 }
74
75 tx_executor.execute_transaction(account_id, block_num, notes, tx_args).await
76 }
77
78 pub fn account(&self) -> &Account {
79 &self.account
80 }
81
82 pub fn expected_output_notes(&self) -> &[Note] {
83 &self.expected_output_notes
84 }
85
86 pub fn tx_args(&self) -> &TransactionArgs {
87 self.tx_inputs.tx_args()
88 }
89
90 pub fn input_notes(&self) -> &InputNotes<InputNote> {
91 self.tx_inputs.input_notes()
92 }
93
94 pub fn set_tx_args(&mut self, tx_args: TransactionArgs) {
95 self.tx_inputs.set_tx_args(tx_args);
96 }
97
98 pub fn tx_inputs(&self) -> &TransactionInputs {
99 &self.tx_inputs
100 }
101
102 pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
103 self.authenticator.as_ref()
104 }
105
106 pub fn source_manager(&self) -> Arc<dyn SourceManagerSync> {
108 Arc::clone(&self.source_manager)
109 }
110}
111
112#[cfg(test)]
116impl MockTransaction {
117 pub(crate) async fn execute_code(&self, code: &str) -> Result<ExecutionOutput, ExecError> {
138 self.execute_code_inner(code, true, None).await
139 }
140
141 pub(crate) async fn execute_code_without_lazy_loading(
144 &self,
145 code: &str,
146 ) -> Result<ExecutionOutput, ExecError> {
147 self.execute_code_inner(code, false, None).await
148 }
149
150 pub(crate) async fn execute_code_with_input_note_index_response(
153 &self,
154 code: &str,
155 response: [Felt; 2],
156 ) -> Result<ExecutionOutput, ExecError> {
157 self.execute_code_inner(code, true, Some(response)).await
158 }
159
160 async fn execute_code_inner(
161 &self,
162 code: &str,
163 is_lazy_loading_enabled: bool,
164 input_note_index_response: Option<[Felt; 2]>,
165 ) -> Result<ExecutionOutput, ExecError> {
166 use alloc::borrow::ToOwned;
167
168 use miden_protocol::assembly::debuginfo::{SourceLanguage, Uri};
169 use miden_protocol::assembly::{Assembler, SourceManager};
170 use miden_protocol::asset::Asset;
171 use miden_protocol::transaction::TransactionKernel;
172 use miden_standards::code_builder::CodeBuilder;
173 use miden_tx::auth::UnreachableAuth;
174 use miden_tx::{AccountProcedureIndexMap, ScriptMastForestStore, TransactionExecutorHost};
175
176 use crate::executor::CodeExecutor;
177 use crate::mock_host::MockHost;
178
179 let asset_ids = self
181 .tx_inputs
182 .input_notes()
183 .iter()
184 .flat_map(|note| note.note().assets().iter().map(Asset::id))
185 .collect::<BTreeSet<_>>();
186
187 let (account, _block_header, _blockchain) = self
188 .get_transaction_inputs(
189 self.tx_inputs.account().id(),
190 BTreeSet::from_iter([self.tx_inputs.block_header().block_num()]),
191 )
192 .await
193 .expect("failed to fetch transaction inputs");
194
195 let asset_witnesses = self
197 .get_vault_asset_witnesses(account.id(), account.vault().root(), asset_ids)
198 .await
199 .expect("failed to fetch asset witnesses");
200
201 let tx_inputs = self.tx_inputs.clone().with_asset_witnesses(asset_witnesses);
202 let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&tx_inputs);
203
204 let virtual_source_file = self.source_manager.load(
206 SourceLanguage::Masm,
207 Uri::new("_mock_tx_code"),
208 code.to_owned(),
209 );
210
211 let assembler: Assembler =
212 CodeBuilder::with_mock_packages_with_source_manager(self.source_manager.clone()).into();
213
214 let program = assembler
215 .assemble_program("mock-tx-code", virtual_source_file)
216 .expect("code was not well formed");
217
218 self.mast_store.insert_package(&TransactionKernel::core_package());
222 self.mast_store.insert_package(&program);
223
224 let account_procedure_idx_map = AccountProcedureIndexMap::new(
225 [tx_inputs.account().code()]
226 .into_iter()
227 .chain(self.foreign_account_inputs.values().map(|(account, _)| account.code())),
228 );
229
230 let ref_block = tx_inputs.block_header().block_num();
233 let ref_block_commitment = tx_inputs.block_header().commitment();
234
235 let exec_host = TransactionExecutorHost::<'_, '_, _, UnreachableAuth>::new(
236 &PartialAccount::from(self.account()),
237 tx_inputs.input_notes().clone(),
238 self,
239 ScriptMastForestStore::default(),
240 account_procedure_idx_map,
241 None,
242 ref_block,
243 ref_block_commitment,
244 self.source_manager(),
245 );
246
247 let advice_inputs = advice_inputs.into_advice_inputs();
248
249 let mut mock_host = MockHost::new(exec_host);
250 if is_lazy_loading_enabled {
251 mock_host.enable_lazy_loading()
252 }
253 if let Some(response) = input_note_index_response {
254 mock_host.set_input_note_index_response(response);
255 }
256
257 CodeExecutor::new(mock_host)
258 .stack_inputs(stack_inputs)
259 .extend_advice_inputs(advice_inputs)
260 .execute_package(program)
261 .await
262 }
263}
264
265impl DataStore for MockTransaction {
266 fn get_transaction_inputs(
267 &self,
268 account_id: AccountId,
269 ref_blocks: BTreeSet<BlockNumber>,
270 ) -> impl FutureMaybeSend<Result<(PartialAccount, BlockHeader, PartialBlockchain), DataStoreError>>
271 {
272 assert_eq!(account_id, self.account().id());
274 assert_eq!(account_id, self.tx_inputs.account().id());
275 assert_eq!(
276 ref_blocks
277 .last()
278 .copied()
279 .expect("at least the tx ref block should be provided"),
280 self.tx_inputs().blockchain().chain_length(),
281 "tx reference block should match partial blockchain length"
282 );
283
284 let account = self.tx_inputs.account().clone();
285 let block_header = self.tx_inputs.block_header().clone();
286 let blockchain = self.tx_inputs.blockchain().clone();
287
288 async move { Ok((account, block_header, blockchain)) }
289 }
290
291 fn get_foreign_account_inputs(
292 &self,
293 foreign_account_id: AccountId,
294 _ref_block: BlockNumber,
295 ) -> impl FutureMaybeSend<Result<AccountInputs, DataStoreError>> {
296 async move {
299 let (foreign_account, account_witness) =
300 self.foreign_account_inputs.get(&foreign_account_id).ok_or_else(|| {
301 DataStoreError::other(format!(
302 "failed to find foreign account {foreign_account_id}"
303 ))
304 })?;
305
306 Ok(AccountInputs::new(
307 PartialAccount::from(foreign_account),
308 account_witness.clone(),
309 ))
310 }
311 }
312
313 fn get_vault_asset_witnesses(
314 &self,
315 account_id: AccountId,
316 vault_root: Word,
317 asset_ids: BTreeSet<AssetId>,
318 ) -> impl FutureMaybeSend<Result<Vec<AssetWitness>, DataStoreError>> {
319 async move {
320 let asset_vault = if account_id == self.account().id() {
321 if self.account().vault().root() != vault_root {
322 return Err(DataStoreError::other(format!(
323 "native account {account_id} has vault root {} but {vault_root} was requested",
324 self.account().vault().root()
325 )));
326 }
327 self.account().vault()
328 } else {
329 let (foreign_account, _witness) = self
330 .foreign_account_inputs
331 .iter()
332 .find_map(
333 |(id, account_inputs)| {
334 if account_id == *id { Some(account_inputs) } else { None }
335 },
336 )
337 .ok_or_else(|| {
338 DataStoreError::other(format!(
339 "failed to find foreign account {account_id} in foreign account inputs"
340 ))
341 })?;
342
343 if foreign_account.vault().root() != vault_root {
344 return Err(DataStoreError::other(format!(
345 "foreign account {account_id} has vault root {} but {vault_root} was requested",
346 foreign_account.vault().root()
347 )));
348 }
349 foreign_account.vault()
350 };
351
352 Ok(asset_ids.into_iter().map(|asset_id| asset_vault.open(asset_id)).collect())
353 }
354 }
355
356 fn get_storage_map_witness(
357 &self,
358 account_id: AccountId,
359 map_root: Word,
360 map_key: StorageMapKey,
361 ) -> impl FutureMaybeSend<Result<StorageMapWitness, DataStoreError>> {
362 async move {
363 if account_id == self.account().id() {
364 let storage_map = self
366 .account()
367 .storage()
368 .slots()
369 .iter()
370 .find_map(|slot| match slot.content() {
371 StorageSlotContent::Map(storage_map) if storage_map.root() == map_root => {
372 Some(storage_map)
373 },
374 _ => None,
375 })
376 .ok_or_else(|| {
377 DataStoreError::other(format!(
378 "failed to find storage map with root {map_root} in account storage"
379 ))
380 })?;
381
382 Ok(storage_map.open(&map_key))
383 } else {
384 let (foreign_account, _witness) = self
385 .foreign_account_inputs
386 .iter()
387 .find_map(
388 |(id, account_inputs)| {
389 if account_id == *id { Some(account_inputs) } else { None }
390 },
391 )
392 .ok_or_else(|| {
393 DataStoreError::other(format!(
394 "failed to find foreign account {account_id} in foreign account inputs"
395 ))
396 })?;
397
398 let map = foreign_account
399 .storage()
400 .slots()
401 .iter()
402 .find_map(|slot| match slot.content() {
403 StorageSlotContent::Map(storage_map) if storage_map.root() == map_root => {Some(storage_map)},
404 _ => None,
405 })
406 .ok_or_else(|| {
407 DataStoreError::other(format!(
408 "failed to find storage map with root {map_root} in foreign account {account_id}"
409 ))
410 })?;
411
412 Ok(map.open(&map_key))
413 }
414 }
415 }
416
417 fn get_note_script(
418 &self,
419 script_root: NoteScriptRoot,
420 ) -> impl FutureMaybeSend<Result<Option<NoteScript>, DataStoreError>> {
421 async move { Ok(self.note_scripts.get(&script_root).cloned()) }
422 }
423}
424
425impl MastForestStore for MockTransaction {
426 fn get(&self, procedure_hash: &Word) -> Option<LoadedMastForest> {
427 self.mast_store.get(procedure_hash)
428 }
429}
430
431#[cfg(test)]
435mod tests {
436 use std::string::ToString;
437
438 use miden_protocol::errors::tx_kernel::ERR_TX_COMPUTE_FEE_EXTRA_CYCLES_NOT_U32;
439 use miden_standards::code_builder::CodeBuilder;
440
441 use super::*;
442 use crate::{Auth, MockChain, TestTransactionBuilder};
443
444 #[tokio::test]
445 async fn test_get_note_scripts() {
446 let script1_code = "@note_script\npub proc main\n push.1\nend";
448 let note_script1 = CodeBuilder::default()
449 .compile_note_script(script1_code)
450 .expect("failed to assemble note script 1");
451 let script_root1 = note_script1.root();
452
453 let script2_code = "@note_script\npub proc main\n push.2 push.3 add\nend";
454 let note_script2 = CodeBuilder::default()
455 .compile_note_script(script2_code)
456 .expect("failed to assemble note script 2");
457 let script_root2 = note_script2.root();
458
459 let mock_tx = TestTransactionBuilder::with_existing_mock_account()
461 .add_note_script(note_script1.clone())
462 .add_note_script(note_script2.clone())
463 .build()
464 .expect("failed to build mock transaction");
465
466 let retrieved_script1 = mock_tx
468 .get_note_script(script_root1)
469 .await
470 .expect("failed to get note script 1")
471 .expect("note script 1 should exist");
472 assert_eq!(retrieved_script1, note_script1);
473
474 let retrieved_script2 = mock_tx
475 .get_note_script(script_root2)
476 .await
477 .expect("failed to get note script 2")
478 .expect("note script 2 should exist");
479 assert_eq!(retrieved_script2, note_script2);
480
481 let non_existent_root = NoteScriptRoot::from_array([1, 2, 3, 4]);
483 let result = mock_tx.get_note_script(non_existent_root).await;
484 assert!(matches!(result, Ok(None)));
485 }
486
487 #[tokio::test]
491 async fn execute_code_renders_masm_error_message() -> anyhow::Result<()> {
492 let mut builder = MockChain::builder();
493 let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
494 let mock_chain = builder.build()?;
495
496 let mock_tx = mock_chain.build_transaction(account).build()?;
497
498 let code = format!(
500 r#"
501 use miden::tx_kernel_core::prologue
502 use miden::protocol::tx
503
504 begin
505 exec.prologue::prepare_transaction
506
507 padw
508 push.{num_extra_cycles}
509 exec.tx::compute_fee
510 end"#,
511 num_extra_cycles = u64::from(u32::MAX) + 1
512 );
513
514 let Err(error) = mock_tx.execute_code(&code).await else {
515 anyhow::bail!("execution should fail on non-u32 extra cycles");
516 };
517
518 let rendered = error.to_string();
519 let expected_error = ERR_TX_COMPUTE_FEE_EXTRA_CYCLES_NOT_U32;
520 assert!(
521 rendered.contains(expected_error.message()),
522 "rendered error should contain the masm error message",
523 );
524
525 Ok(())
526 }
527}