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_lib::transaction::TransactionKernel;
7use miden_objects::account::{Account, AccountId, PartialAccount, StorageMapWitness, StorageSlot};
8use miden_objects::assembly::debuginfo::{SourceLanguage, Uri};
9use miden_objects::assembly::{SourceManager, SourceManagerSync};
10use miden_objects::asset::{AssetVaultKey, AssetWitness};
11use miden_objects::block::{AccountWitness, BlockHeader, BlockNumber};
12use miden_objects::note::{Note, NoteScript};
13use miden_objects::transaction::{
14 AccountInputs,
15 ExecutedTransaction,
16 InputNote,
17 InputNotes,
18 PartialBlockchain,
19 TransactionArgs,
20 TransactionInputs,
21};
22use miden_processor::fast::ExecutionOutput;
23use miden_processor::{ExecutionError, FutureMaybeSend, MastForest, MastForestStore, Word};
24use miden_tx::auth::{BasicAuthenticator, UnreachableAuth};
25use miden_tx::{
26 AccountProcedureIndexMap,
27 DataStore,
28 DataStoreError,
29 ScriptMastForestStore,
30 TransactionExecutor,
31 TransactionExecutorError,
32 TransactionExecutorHost,
33 TransactionMastStore,
34};
35
36use crate::executor::CodeExecutor;
37use crate::mock_host::MockHost;
38
39pub struct TransactionContext {
47 pub(super) account: Account,
48 pub(super) expected_output_notes: Vec<Note>,
49 pub(super) foreign_account_inputs: BTreeMap<AccountId, (Account, AccountWitness)>,
50 pub(super) tx_inputs: TransactionInputs,
51 pub(super) mast_store: TransactionMastStore,
52 pub(super) authenticator: Option<BasicAuthenticator>,
53 pub(super) source_manager: Arc<dyn SourceManagerSync>,
54 pub(super) is_lazy_loading_enabled: bool,
55 pub(super) note_scripts: BTreeMap<Word, NoteScript>,
56}
57
58impl TransactionContext {
59 pub async fn execute_code(&self, code: &str) -> Result<ExecutionOutput, ExecutionError> {
80 let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&self.tx_inputs)
81 .expect("error initializing transaction inputs");
82
83 let virtual_source_file = self.source_manager.load(
85 SourceLanguage::Masm,
86 Uri::new("_tx_context_code"),
87 code.to_owned(),
88 );
89
90 let assembler = TransactionKernel::with_mock_libraries(self.source_manager.clone())
91 .with_debug_mode(true);
92 let program = assembler
93 .with_debug_mode(true)
94 .assemble_program(virtual_source_file)
95 .expect("code was not well formed");
96
97 self.mast_store.insert(TransactionKernel::library().mast_forest().clone());
101 self.mast_store.insert(program.mast_forest().clone());
102
103 let account_procedure_idx_map = AccountProcedureIndexMap::new(
104 [self.tx_inputs().account().code()]
105 .into_iter()
106 .chain(self.foreign_account_inputs.values().map(|(account, _)| account.code())),
107 )
108 .expect("constructing account procedure index map should work");
109
110 let ref_block = self.tx_inputs().block_header().block_num();
112
113 let exec_host = TransactionExecutorHost::<'_, '_, _, UnreachableAuth>::new(
114 &PartialAccount::from(self.account()),
115 self.tx_inputs().input_notes().clone(),
116 self,
117 ScriptMastForestStore::default(),
118 account_procedure_idx_map,
119 None,
120 ref_block,
121 self.source_manager(),
122 );
123
124 let advice_inputs = advice_inputs.into_advice_inputs();
125
126 let mut mock_host = MockHost::new(exec_host);
127 if self.is_lazy_loading_enabled {
128 mock_host.enable_lazy_loading()
129 }
130
131 CodeExecutor::new(mock_host)
132 .stack_inputs(stack_inputs)
133 .extend_advice_inputs(advice_inputs)
134 .execute_program(program)
135 .await
136 }
137
138 pub async fn execute(self) -> Result<ExecutedTransaction, TransactionExecutorError> {
140 let account_id = self.account().id();
141 let block_num = self.tx_inputs().block_header().block_num();
142 let notes = self.tx_inputs().input_notes().clone();
143 let tx_args = self.tx_args().clone();
144
145 let mut tx_executor = TransactionExecutor::new(&self)
146 .with_source_manager(self.source_manager.clone())
147 .with_debug_mode();
148 if let Some(authenticator) = self.authenticator() {
149 tx_executor = tx_executor.with_authenticator(authenticator);
150 }
151
152 tx_executor.execute_transaction(account_id, block_num, notes, tx_args).await
153 }
154
155 pub fn account(&self) -> &Account {
156 &self.account
157 }
158
159 pub fn expected_output_notes(&self) -> &[Note] {
160 &self.expected_output_notes
161 }
162
163 pub fn tx_args(&self) -> &TransactionArgs {
164 self.tx_inputs.tx_args()
165 }
166
167 pub fn input_notes(&self) -> &InputNotes<InputNote> {
168 self.tx_inputs.input_notes()
169 }
170
171 pub fn set_tx_args(&mut self, tx_args: TransactionArgs) {
172 self.tx_inputs.set_tx_args(tx_args);
173 }
174
175 pub fn tx_inputs(&self) -> &TransactionInputs {
176 &self.tx_inputs
177 }
178
179 pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
180 self.authenticator.as_ref()
181 }
182
183 pub fn source_manager(&self) -> Arc<dyn SourceManagerSync> {
185 Arc::clone(&self.source_manager)
186 }
187}
188
189impl DataStore for TransactionContext {
190 fn get_transaction_inputs(
191 &self,
192 account_id: AccountId,
193 _ref_blocks: BTreeSet<BlockNumber>,
194 ) -> impl FutureMaybeSend<Result<(PartialAccount, BlockHeader, PartialBlockchain), DataStoreError>>
195 {
196 assert_eq!(account_id, self.account().id());
197 assert_eq!(account_id, self.tx_inputs.account().id());
198
199 let account = self.tx_inputs.account().clone();
200 let block_header = self.tx_inputs.block_header().clone();
201 let blockchain = self.tx_inputs.blockchain().clone();
202 async move { Ok((account, block_header, blockchain)) }
203 }
204
205 fn get_foreign_account_inputs(
206 &self,
207 foreign_account_id: AccountId,
208 _ref_block: BlockNumber,
209 ) -> impl FutureMaybeSend<Result<AccountInputs, DataStoreError>> {
210 async move {
213 let (foreign_account, account_witness) =
214 self.foreign_account_inputs.get(&foreign_account_id).ok_or_else(|| {
215 DataStoreError::other(format!(
216 "failed to find foreign account {foreign_account_id}"
217 ))
218 })?;
219
220 Ok(AccountInputs::new(
221 PartialAccount::from(foreign_account),
222 account_witness.clone(),
223 ))
224 }
225 }
226
227 fn get_vault_asset_witness(
228 &self,
229 account_id: AccountId,
230 vault_root: Word,
231 asset_key: AssetVaultKey,
232 ) -> impl FutureMaybeSend<Result<AssetWitness, DataStoreError>> {
233 async move {
234 if account_id == self.account().id() {
235 if self.account().vault().root() != vault_root {
236 return Err(DataStoreError::other(format!(
237 "native account {account_id} has vault root {} but {vault_root} was requested",
238 self.account().vault().root()
239 )));
240 }
241
242 Ok(self.account().vault().open(asset_key))
243 } else {
244 let (foreign_account, _witness) = self
245 .foreign_account_inputs
246 .iter()
247 .find_map(
248 |(id, account_inputs)| {
249 if account_id == *id { Some(account_inputs) } else { None }
250 },
251 )
252 .ok_or_else(|| {
253 DataStoreError::other(format!(
254 "failed to find foreign account {account_id} in foreign account inputs"
255 ))
256 })?;
257
258 if foreign_account.vault().root() != vault_root {
259 return Err(DataStoreError::other(format!(
260 "foreign account {account_id} has vault root {} but {vault_root} was requested",
261 foreign_account.vault().root()
262 )));
263 }
264
265 Ok(foreign_account.vault().open(asset_key))
266 }
267 }
268 }
269
270 fn get_storage_map_witness(
271 &self,
272 account_id: AccountId,
273 map_root: Word,
274 map_key: Word,
275 ) -> impl FutureMaybeSend<Result<StorageMapWitness, DataStoreError>> {
276 async move {
277 if account_id == self.account().id() {
278 let storage_map = self
280 .account()
281 .storage()
282 .slots()
283 .iter()
284 .find_map(|slot| match slot {
285 StorageSlot::Map(storage_map) if storage_map.root() == map_root => {
286 Some(storage_map)
287 },
288 _ => None,
289 })
290 .ok_or_else(|| {
291 DataStoreError::other(format!(
292 "failed to find storage map with root {map_root} in account storage"
293 ))
294 })?;
295
296 Ok(storage_map.open(&map_key))
297 } else {
298 let (foreign_account, _witness) = self
299 .foreign_account_inputs
300 .iter()
301 .find_map(
302 |(id, account_inputs)| {
303 if account_id == *id { Some(account_inputs) } else { None }
304 },
305 )
306 .ok_or_else(|| {
307 DataStoreError::other(format!(
308 "failed to find foreign account {account_id} in foreign account inputs"
309 ))
310 })?;
311
312 let map = foreign_account
313 .storage()
314 .slots()
315 .iter()
316 .find_map(|slot| match slot {
317 StorageSlot::Map(storage_map) if storage_map.root() == map_root => {Some(storage_map)},
318 _ => None,
319 })
320 .ok_or_else(|| {
321 DataStoreError::other(format!(
322 "failed to find storage map with root {map_root} in foreign account {account_id}"
323 ))
324 })?;
325
326 Ok(map.open(&map_key))
327 }
328 }
329 }
330
331 fn get_note_script(
332 &self,
333 script_root: Word,
334 ) -> impl FutureMaybeSend<Result<NoteScript, DataStoreError>> {
335 async move {
336 self.note_scripts
337 .get(&script_root)
338 .cloned()
339 .ok_or_else(|| DataStoreError::NoteScriptNotFound(script_root))
340 }
341 }
342}
343
344impl MastForestStore for TransactionContext {
345 fn get(&self, procedure_hash: &Word) -> Option<Arc<MastForest>> {
346 self.mast_store.get(procedure_hash)
347 }
348}
349
350#[cfg(test)]
354mod tests {
355 use miden_objects::Felt;
356 use miden_objects::assembly::Assembler;
357 use miden_objects::note::NoteScript;
358
359 use super::*;
360 use crate::TransactionContextBuilder;
361
362 #[tokio::test]
363 async fn test_get_note_scripts() {
364 let assembler1 = Assembler::default();
366 let script1_code = "begin push.1 end";
367 let program1 = assembler1
368 .assemble_program(script1_code)
369 .expect("Failed to assemble note script 1");
370 let note_script1 = NoteScript::new(program1);
371 let script_root1 = note_script1.root();
372
373 let assembler2 = Assembler::default();
374 let script2_code = "begin push.2 push.3 add end";
375 let program2 = assembler2
376 .assemble_program(script2_code)
377 .expect("Failed to assemble note script 2");
378 let note_script2 = NoteScript::new(program2);
379 let script_root2 = note_script2.root();
380
381 let tx_context = TransactionContextBuilder::with_existing_mock_account()
383 .add_note_script(note_script1.clone())
384 .add_note_script(note_script2.clone())
385 .build()
386 .expect("Failed to build transaction context");
387
388 let retrieved_script1 = tx_context
390 .get_note_script(script_root1)
391 .await
392 .expect("Failed to get note script 1");
393 assert_eq!(retrieved_script1, note_script1);
394
395 let retrieved_script2 = tx_context
396 .get_note_script(script_root2)
397 .await
398 .expect("Failed to get note script 2");
399 assert_eq!(retrieved_script2, note_script2);
400
401 let non_existent_root =
403 Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]);
404 let result = tx_context.get_note_script(non_existent_root).await;
405 assert!(matches!(result, Err(DataStoreError::NoteScriptNotFound(_))));
406 }
407}