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