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::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
49pub 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 pub async fn execute_code(&self, code: &str) -> Result<ExecutionOutput, ExecError> {
86 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 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 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 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 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 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 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 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 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 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 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 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#[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 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 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 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 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}