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::fast::ExecutionOutput;
7use miden_processor::{ExecutionError, FutureMaybeSend, MastForest, MastForestStore, Word};
8use miden_protocol::account::{
9 Account,
10 AccountId,
11 PartialAccount,
12 StorageMapWitness,
13 StorageSlotContent,
14};
15use miden_protocol::assembly::debuginfo::{SourceLanguage, Uri};
16use miden_protocol::assembly::{Assembler, SourceManager, SourceManagerSync};
17use miden_protocol::asset::{Asset, AssetVaultKey, AssetWitness};
18use miden_protocol::block::account_tree::AccountWitness;
19use miden_protocol::block::{BlockHeader, BlockNumber};
20use miden_protocol::note::{Note, NoteScript};
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;
46
47pub struct TransactionContext {
55 pub(super) account: Account,
56 pub(super) expected_output_notes: Vec<Note>,
57 pub(super) foreign_account_inputs: BTreeMap<AccountId, (Account, AccountWitness)>,
58 pub(super) tx_inputs: TransactionInputs,
59 pub(super) mast_store: TransactionMastStore,
60 pub(super) authenticator: Option<BasicAuthenticator>,
61 pub(super) source_manager: Arc<dyn SourceManagerSync>,
62 pub(super) is_lazy_loading_enabled: bool,
63 pub(super) note_scripts: BTreeMap<Word, NoteScript>,
64}
65
66impl TransactionContext {
67 pub async fn execute_code(&self, code: &str) -> Result<ExecutionOutput, ExecutionError> {
87 let mut asset_vault_keys = self
89 .tx_inputs
90 .input_notes()
91 .iter()
92 .flat_map(|note| note.note().assets().iter().map(Asset::vault_key))
93 .collect::<BTreeSet<_>>();
94 let fee_asset_vault_key = AssetVaultKey::from_account_id(
95 self.tx_inputs().block_header().fee_parameters().native_asset_id(),
96 )
97 .expect("fee asset should be a fungible asset");
98 asset_vault_keys.extend([fee_asset_vault_key]);
99
100 let (account, block_header, _blockchain) = self
101 .get_transaction_inputs(
102 self.tx_inputs.account().id(),
103 BTreeSet::from_iter([self.tx_inputs.block_header().block_num()]),
104 )
105 .await
106 .expect("failed to fetch transaction inputs");
107
108 let fee_asset_vault_key =
111 AssetVaultKey::from_account_id(block_header.fee_parameters().native_asset_id())
112 .expect("fee asset should be a fungible asset");
113 asset_vault_keys.insert(fee_asset_vault_key);
114
115 let asset_witnesses = self
117 .get_vault_asset_witnesses(account.id(), account.vault().root(), asset_vault_keys)
118 .await
119 .expect("failed to fetch asset witnesses");
120
121 let tx_inputs = self.tx_inputs.clone().with_asset_witnesses(asset_witnesses);
122 let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&tx_inputs);
123
124 let virtual_source_file = self.source_manager.load(
126 SourceLanguage::Masm,
127 Uri::new("_tx_context_code"),
128 code.to_owned(),
129 );
130
131 let assembler: Assembler =
132 CodeBuilder::with_mock_libraries_with_source_manager(self.source_manager.clone())
133 .into();
134
135 let program = assembler
136 .assemble_program(virtual_source_file)
137 .expect("code was not well formed");
138
139 self.mast_store.insert(TransactionKernel::library().mast_forest().clone());
143 self.mast_store.insert(program.mast_forest().clone());
144
145 let account_procedure_idx_map = AccountProcedureIndexMap::new(
146 [tx_inputs.account().code()]
147 .into_iter()
148 .chain(self.foreign_account_inputs.values().map(|(account, _)| account.code())),
149 );
150
151 let ref_block = tx_inputs.block_header().block_num();
153
154 let exec_host = TransactionExecutorHost::<'_, '_, _, UnreachableAuth>::new(
155 &PartialAccount::from(self.account()),
156 tx_inputs.input_notes().clone(),
157 self,
158 ScriptMastForestStore::default(),
159 account_procedure_idx_map,
160 None,
161 ref_block,
162 0u64,
165 self.source_manager(),
166 );
167
168 let advice_inputs = advice_inputs.into_advice_inputs();
169
170 let mut mock_host = MockHost::new(exec_host);
171 if self.is_lazy_loading_enabled {
172 mock_host.enable_lazy_loading()
173 }
174
175 CodeExecutor::new(mock_host)
176 .stack_inputs(stack_inputs)
177 .extend_advice_inputs(advice_inputs)
178 .execute_program(program)
179 .await
180 }
181
182 pub async fn execute(self) -> Result<ExecutedTransaction, TransactionExecutorError> {
184 let account_id = self.account().id();
185 let block_num = self.tx_inputs().block_header().block_num();
186 let notes = self.tx_inputs().input_notes().clone();
187 let tx_args = self.tx_args().clone();
188
189 let mut tx_executor = TransactionExecutor::new(&self)
190 .with_source_manager(self.source_manager.clone())
191 .with_debug_mode();
192 if let Some(authenticator) = self.authenticator() {
193 tx_executor = tx_executor.with_authenticator(authenticator);
194 }
195
196 tx_executor.execute_transaction(account_id, block_num, notes, tx_args).await
197 }
198
199 pub fn account(&self) -> &Account {
200 &self.account
201 }
202
203 pub fn expected_output_notes(&self) -> &[Note] {
204 &self.expected_output_notes
205 }
206
207 pub fn tx_args(&self) -> &TransactionArgs {
208 self.tx_inputs.tx_args()
209 }
210
211 pub fn input_notes(&self) -> &InputNotes<InputNote> {
212 self.tx_inputs.input_notes()
213 }
214
215 pub fn set_tx_args(&mut self, tx_args: TransactionArgs) {
216 self.tx_inputs.set_tx_args(tx_args);
217 }
218
219 pub fn tx_inputs(&self) -> &TransactionInputs {
220 &self.tx_inputs
221 }
222
223 pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
224 self.authenticator.as_ref()
225 }
226
227 pub fn source_manager(&self) -> Arc<dyn SourceManagerSync> {
229 Arc::clone(&self.source_manager)
230 }
231}
232
233impl DataStore for TransactionContext {
234 fn get_transaction_inputs(
235 &self,
236 account_id: AccountId,
237 ref_blocks: BTreeSet<BlockNumber>,
238 ) -> impl FutureMaybeSend<Result<(PartialAccount, BlockHeader, PartialBlockchain), DataStoreError>>
239 {
240 assert_eq!(account_id, self.account().id());
242 assert_eq!(account_id, self.tx_inputs.account().id());
243 assert_eq!(
244 ref_blocks
245 .last()
246 .copied()
247 .expect("at least the tx ref block should be provided"),
248 self.tx_inputs().blockchain().chain_length(),
249 "tx reference block should match partial blockchain length"
250 );
251
252 let account = self.tx_inputs.account().clone();
253 let block_header = self.tx_inputs.block_header().clone();
254 let blockchain = self.tx_inputs.blockchain().clone();
255
256 async move { Ok((account, block_header, blockchain)) }
257 }
258
259 fn get_foreign_account_inputs(
260 &self,
261 foreign_account_id: AccountId,
262 _ref_block: BlockNumber,
263 ) -> impl FutureMaybeSend<Result<AccountInputs, DataStoreError>> {
264 async move {
267 let (foreign_account, account_witness) =
268 self.foreign_account_inputs.get(&foreign_account_id).ok_or_else(|| {
269 DataStoreError::other(format!(
270 "failed to find foreign account {foreign_account_id}"
271 ))
272 })?;
273
274 Ok(AccountInputs::new(
275 PartialAccount::from(foreign_account),
276 account_witness.clone(),
277 ))
278 }
279 }
280
281 fn get_vault_asset_witnesses(
282 &self,
283 account_id: AccountId,
284 vault_root: Word,
285 vault_keys: BTreeSet<AssetVaultKey>,
286 ) -> impl FutureMaybeSend<Result<Vec<AssetWitness>, DataStoreError>> {
287 async move {
288 let asset_vault = if account_id == self.account().id() {
289 if self.account().vault().root() != vault_root {
290 return Err(DataStoreError::other(format!(
291 "native account {account_id} has vault root {} but {vault_root} was requested",
292 self.account().vault().root()
293 )));
294 }
295 self.account().vault()
296 } else {
297 let (foreign_account, _witness) = self
298 .foreign_account_inputs
299 .iter()
300 .find_map(
301 |(id, account_inputs)| {
302 if account_id == *id { Some(account_inputs) } else { None }
303 },
304 )
305 .ok_or_else(|| {
306 DataStoreError::other(format!(
307 "failed to find foreign account {account_id} in foreign account inputs"
308 ))
309 })?;
310
311 if foreign_account.vault().root() != vault_root {
312 return Err(DataStoreError::other(format!(
313 "foreign account {account_id} has vault root {} but {vault_root} was requested",
314 foreign_account.vault().root()
315 )));
316 }
317 foreign_account.vault()
318 };
319
320 Ok(vault_keys.into_iter().map(|vault_key| asset_vault.open(vault_key)).collect())
321 }
322 }
323
324 fn get_storage_map_witness(
325 &self,
326 account_id: AccountId,
327 map_root: Word,
328 map_key: Word,
329 ) -> impl FutureMaybeSend<Result<StorageMapWitness, DataStoreError>> {
330 async move {
331 if account_id == self.account().id() {
332 let storage_map = self
334 .account()
335 .storage()
336 .slots()
337 .iter()
338 .find_map(|slot| match slot.content() {
339 StorageSlotContent::Map(storage_map) if storage_map.root() == map_root => {
340 Some(storage_map)
341 },
342 _ => None,
343 })
344 .ok_or_else(|| {
345 DataStoreError::other(format!(
346 "failed to find storage map with root {map_root} in account storage"
347 ))
348 })?;
349
350 Ok(storage_map.open(&map_key))
351 } else {
352 let (foreign_account, _witness) = self
353 .foreign_account_inputs
354 .iter()
355 .find_map(
356 |(id, account_inputs)| {
357 if account_id == *id { Some(account_inputs) } else { None }
358 },
359 )
360 .ok_or_else(|| {
361 DataStoreError::other(format!(
362 "failed to find foreign account {account_id} in foreign account inputs"
363 ))
364 })?;
365
366 let map = foreign_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 => {Some(storage_map)},
372 _ => None,
373 })
374 .ok_or_else(|| {
375 DataStoreError::other(format!(
376 "failed to find storage map with root {map_root} in foreign account {account_id}"
377 ))
378 })?;
379
380 Ok(map.open(&map_key))
381 }
382 }
383 }
384
385 fn get_note_script(
386 &self,
387 script_root: Word,
388 ) -> impl FutureMaybeSend<Result<Option<NoteScript>, DataStoreError>> {
389 async move { Ok(self.note_scripts.get(&script_root).cloned()) }
390 }
391}
392
393impl MastForestStore for TransactionContext {
394 fn get(&self, procedure_hash: &Word) -> Option<Arc<MastForest>> {
395 self.mast_store.get(procedure_hash)
396 }
397}
398
399#[cfg(test)]
403mod tests {
404 use miden_protocol::Felt;
405 use miden_protocol::assembly::Assembler;
406 use miden_protocol::note::NoteScript;
407
408 use super::*;
409 use crate::TransactionContextBuilder;
410
411 #[tokio::test]
412 async fn test_get_note_scripts() {
413 let assembler1 = Assembler::default();
415 let script1_code = "begin push.1 end";
416 let program1 = assembler1
417 .assemble_program(script1_code)
418 .expect("failed to assemble note script 1");
419 let note_script1 = NoteScript::new(program1);
420 let script_root1 = note_script1.root();
421
422 let assembler2 = Assembler::default();
423 let script2_code = "begin push.2 push.3 add end";
424 let program2 = assembler2
425 .assemble_program(script2_code)
426 .expect("failed to assemble note script 2");
427 let note_script2 = NoteScript::new(program2);
428 let script_root2 = note_script2.root();
429
430 let tx_context = TransactionContextBuilder::with_existing_mock_account()
432 .add_note_script(note_script1.clone())
433 .add_note_script(note_script2.clone())
434 .build()
435 .expect("failed to build transaction context");
436
437 let retrieved_script1 = tx_context
439 .get_note_script(script_root1)
440 .await
441 .expect("failed to get note script 1")
442 .expect("note script 1 should exist");
443 assert_eq!(retrieved_script1, note_script1);
444
445 let retrieved_script2 = tx_context
446 .get_note_script(script_root2)
447 .await
448 .expect("failed to get note script 2")
449 .expect("note script 2 should exist");
450 assert_eq!(retrieved_script2, note_script2);
451
452 let non_existent_root =
454 Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]);
455 let result = tx_context.get_note_script(non_existent_root).await;
456 assert!(matches!(result, Ok(None)));
457 }
458}