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