1use alloc::collections::BTreeSet;
2use alloc::sync::Arc;
3use core::marker::PhantomData;
4
5use miden_processor::advice::AdviceInputs;
6use miden_processor::{ExecutionError, FastProcessor, StackInputs};
7pub use miden_processor::{ExecutionOptions, MastForestStore};
8use miden_protocol::account::AccountId;
9use miden_protocol::assembly::DefaultSourceManager;
10use miden_protocol::assembly::debuginfo::SourceManagerSync;
11use miden_protocol::asset::{Asset, AssetVaultKey};
12use miden_protocol::block::BlockNumber;
13use miden_protocol::transaction::{
14 ExecutedTransaction,
15 InputNote,
16 InputNotes,
17 TransactionArgs,
18 TransactionInputs,
19 TransactionKernel,
20 TransactionScript,
21};
22use miden_protocol::vm::StackOutputs;
23use miden_protocol::{Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES};
24
25use super::TransactionExecutorError;
26use crate::auth::TransactionAuthenticator;
27use crate::errors::TransactionKernelError;
28use crate::host::{AccountProcedureIndexMap, ScriptMastForestStore};
29
30mod exec_host;
31pub use exec_host::TransactionExecutorHost;
32
33mod data_store;
34pub use data_store::DataStore;
35
36mod notes_checker;
37pub use notes_checker::{
38 FailedNote,
39 MAX_NUM_CHECKER_NOTES,
40 NoteConsumptionChecker,
41 NoteConsumptionInfo,
42};
43
44mod program_executor;
45pub use program_executor::ProgramExecutor;
46
47pub struct TransactionExecutor<
60 'store,
61 'auth,
62 STORE: 'store,
63 AUTH: 'auth,
64 EXEC: ProgramExecutor = FastProcessor,
65> {
66 data_store: &'store STORE,
67 authenticator: Option<&'auth AUTH>,
68 source_manager: Arc<dyn SourceManagerSync>,
69 exec_options: ExecutionOptions,
70 _executor: PhantomData<EXEC>,
71}
72
73impl<'store, 'auth, STORE, AUTH> TransactionExecutor<'store, 'auth, STORE, AUTH>
74where
75 STORE: DataStore + 'store + Sync,
76 AUTH: TransactionAuthenticator + 'auth + Sync,
77{
78 pub fn new(data_store: &'store STORE) -> Self {
90 const _: () = assert!(MIN_TX_EXECUTION_CYCLES <= MAX_TX_EXECUTION_CYCLES);
91 Self {
92 data_store,
93 authenticator: None,
94 source_manager: Arc::new(DefaultSourceManager::default()),
95 exec_options: ExecutionOptions::new(
96 Some(MAX_TX_EXECUTION_CYCLES),
97 MIN_TX_EXECUTION_CYCLES,
98 ExecutionOptions::DEFAULT_CORE_TRACE_FRAGMENT_SIZE,
99 false,
100 false,
101 )
102 .expect("Must not fail while max cycles is more than min trace length"),
103 _executor: PhantomData,
104 }
105 }
106}
107
108impl<'store, 'auth, STORE, AUTH, EXEC> TransactionExecutor<'store, 'auth, STORE, AUTH, EXEC>
109where
110 STORE: DataStore + 'store + Sync,
111 AUTH: TransactionAuthenticator + 'auth + Sync,
112 EXEC: ProgramExecutor,
113{
114 pub fn with_program_executor<EXEC2: ProgramExecutor>(
119 self,
120 ) -> TransactionExecutor<'store, 'auth, STORE, AUTH, EXEC2> {
121 TransactionExecutor::<'store, 'auth, STORE, AUTH, EXEC2> {
122 data_store: self.data_store,
123 authenticator: self.authenticator,
124 source_manager: self.source_manager,
125 exec_options: self.exec_options,
126 _executor: PhantomData,
127 }
128 }
129
130 #[must_use]
135 pub fn with_authenticator(mut self, authenticator: &'auth AUTH) -> Self {
136 self.authenticator = Some(authenticator);
137 self
138 }
139
140 #[must_use]
149 pub fn with_source_manager(mut self, source_manager: Arc<dyn SourceManagerSync>) -> Self {
150 self.source_manager = source_manager;
151 self
152 }
153
154 pub fn with_options(
162 mut self,
163 exec_options: ExecutionOptions,
164 ) -> Result<Self, TransactionExecutorError> {
165 validate_num_cycles(exec_options.max_cycles())?;
166 validate_num_cycles(exec_options.expected_cycles())?;
167
168 self.exec_options = exec_options;
169 Ok(self)
170 }
171
172 #[must_use]
178 pub fn with_debug_mode(mut self) -> Self {
179 self.exec_options = self.exec_options.with_debugging(true);
180 self
181 }
182
183 #[must_use]
190 pub fn with_tracing(mut self) -> Self {
191 self.exec_options = self.exec_options.with_tracing(true);
192 self
193 }
194
195 pub async fn execute_transaction(
216 &self,
217 account_id: AccountId,
218 block_ref: BlockNumber,
219 notes: InputNotes<InputNote>,
220 tx_args: TransactionArgs,
221 ) -> Result<ExecutedTransaction, TransactionExecutorError> {
222 let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
223
224 let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
225
226 let processor = EXEC::new(stack_inputs, advice_inputs, self.exec_options);
229
230 let output = processor
231 .execute(&TransactionKernel::main(), &mut host)
232 .await
233 .map_err(map_execution_error)?;
234 let stack_outputs = output.stack;
235 let advice_provider = output.advice;
236
237 let (_stack, advice_map, merkle_store, _pc_requests) = advice_provider.into_parts();
239 let advice_inputs = AdviceInputs {
240 map: advice_map,
241 store: merkle_store,
242 ..Default::default()
243 };
244
245 build_executed_transaction(advice_inputs, tx_inputs, stack_outputs, host)
246 }
247
248 pub async fn execute_tx_view_script(
260 &self,
261 account_id: AccountId,
262 block_ref: BlockNumber,
263 tx_script: TransactionScript,
264 advice_inputs: AdviceInputs,
265 ) -> Result<[Felt; 16], TransactionExecutorError> {
266 let mut tx_args = TransactionArgs::default().with_tx_script(tx_script);
267 tx_args.extend_advice_inputs(advice_inputs);
268
269 let notes = InputNotes::default();
270 let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
271
272 let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
273
274 let processor = EXEC::new(stack_inputs, advice_inputs, self.exec_options);
275 let output = processor
276 .execute(&TransactionKernel::tx_script_main(), &mut host)
277 .await
278 .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
279 let stack_outputs = output.stack;
280
281 Ok(*stack_outputs)
282 }
283
284 async fn prepare_tx_inputs(
293 &self,
294 account_id: AccountId,
295 block_ref: BlockNumber,
296 input_notes: InputNotes<InputNote>,
297 tx_args: TransactionArgs,
298 ) -> Result<TransactionInputs, TransactionExecutorError> {
299 let (mut asset_vault_keys, mut ref_blocks) = validate_input_notes(&input_notes, block_ref)?;
300 ref_blocks.insert(block_ref);
301
302 let (account, block_header, blockchain) = self
303 .data_store
304 .get_transaction_inputs(account_id, ref_blocks)
305 .await
306 .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
307
308 let native_account_vault_root = account.vault().root();
309 let fee_asset_vault_key =
310 AssetVaultKey::new_fungible(block_header.fee_parameters().native_asset_id())
311 .expect("fee asset should be a fungible asset");
312
313 let mut tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes)
314 .map_err(TransactionExecutorError::InvalidTransactionInputs)?
315 .with_tx_args(tx_args);
316
317 asset_vault_keys.insert(fee_asset_vault_key);
320
321 asset_vault_keys.retain(|asset_key| {
323 !tx_inputs.has_vault_asset_witness(native_account_vault_root, asset_key)
324 });
325
326 if !asset_vault_keys.is_empty() {
328 let asset_witnesses = self
329 .data_store
330 .get_vault_asset_witnesses(account_id, native_account_vault_root, asset_vault_keys)
331 .await
332 .map_err(TransactionExecutorError::FetchAssetWitnessFailed)?;
333
334 tx_inputs = tx_inputs.with_asset_witnesses(asset_witnesses);
335 }
336
337 Ok(tx_inputs)
338 }
339
340 async fn prepare_transaction(
345 &self,
346 tx_inputs: &TransactionInputs,
347 ) -> Result<
348 (TransactionExecutorHost<'store, 'auth, STORE, AUTH>, StackInputs, AdviceInputs),
349 TransactionExecutorError,
350 > {
351 let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_inputs);
352 let input_notes = tx_inputs.input_notes();
353
354 let script_mast_store = ScriptMastForestStore::new(
355 tx_inputs.tx_script(),
356 input_notes.iter().map(|n| n.note().script()),
357 );
358
359 let account_procedure_index_map =
362 AccountProcedureIndexMap::new([tx_inputs.account().code()]);
363
364 let initial_fee_asset_balance = {
365 let vault_root = tx_inputs.account().vault().root();
366 let native_asset_id = tx_inputs.block_header().fee_parameters().native_asset_id();
367 let fee_asset_vault_key = AssetVaultKey::new_fungible(native_asset_id)
368 .expect("fee asset should be a fungible asset");
369
370 let fee_asset = tx_inputs
371 .read_vault_asset(vault_root, fee_asset_vault_key)
372 .map_err(TransactionExecutorError::FeeAssetRetrievalFailed)?;
373 match fee_asset {
374 Some(Asset::Fungible(fee_asset)) => fee_asset.amount(),
375 Some(Asset::NonFungible(_)) => {
376 return Err(TransactionExecutorError::FeeAssetMustBeFungible);
377 },
378 None => 0,
380 }
381 };
382 let host = TransactionExecutorHost::new(
383 tx_inputs.account(),
384 input_notes.clone(),
385 self.data_store,
386 script_mast_store,
387 account_procedure_index_map,
388 self.authenticator,
389 tx_inputs.block_header().block_num(),
390 initial_fee_asset_balance,
391 self.source_manager.clone(),
392 );
393
394 let advice_inputs = tx_advice_inputs.into_advice_inputs();
395
396 Ok((host, stack_inputs, advice_inputs))
397 }
398}
399
400fn build_executed_transaction<STORE: DataStore + Sync, AUTH: TransactionAuthenticator + Sync>(
405 mut advice_inputs: AdviceInputs,
406 tx_inputs: TransactionInputs,
407 stack_outputs: StackOutputs,
408 host: TransactionExecutorHost<STORE, AUTH>,
409) -> Result<ExecutedTransaction, TransactionExecutorError> {
410 let (
414 pre_fee_account_delta,
415 _input_notes,
416 output_notes,
417 accessed_foreign_account_code,
418 generated_signatures,
419 tx_progress,
420 foreign_account_slot_names,
421 ) = host.into_parts();
422
423 let tx_outputs =
424 TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes)
425 .map_err(TransactionExecutorError::TransactionOutputConstructionFailed)?;
426
427 let pre_fee_delta_commitment = pre_fee_account_delta.to_commitment();
428 if tx_outputs.account_delta_commitment() != pre_fee_delta_commitment {
429 return Err(TransactionExecutorError::InconsistentAccountDeltaCommitment {
430 in_kernel_commitment: tx_outputs.account_delta_commitment(),
431 host_commitment: pre_fee_delta_commitment,
432 });
433 }
434
435 let mut post_fee_account_delta = pre_fee_account_delta;
437 post_fee_account_delta
438 .vault_mut()
439 .remove_asset(Asset::from(tx_outputs.fee()))
440 .map_err(TransactionExecutorError::RemoveFeeAssetFromDelta)?;
441
442 let initial_account = tx_inputs.account();
443 let final_account = tx_outputs.account();
444
445 if initial_account.id() != final_account.id() {
446 return Err(TransactionExecutorError::InconsistentAccountId {
447 input_id: initial_account.id(),
448 output_id: final_account.id(),
449 });
450 }
451
452 let nonce_delta = final_account.nonce() - initial_account.nonce();
454 if nonce_delta != post_fee_account_delta.nonce_delta() {
455 return Err(TransactionExecutorError::InconsistentAccountNonceDelta {
456 expected: nonce_delta,
457 actual: post_fee_account_delta.nonce_delta(),
458 });
459 }
460
461 advice_inputs.map.extend(generated_signatures);
463
464 let tx_inputs = tx_inputs
467 .with_foreign_account_code(accessed_foreign_account_code)
468 .with_foreign_account_slot_names(foreign_account_slot_names)
469 .with_advice_inputs(advice_inputs);
470
471 Ok(ExecutedTransaction::new(
472 tx_inputs,
473 tx_outputs,
474 post_fee_account_delta,
475 tx_progress.into(),
476 ))
477}
478
479fn validate_input_notes(
488 notes: &InputNotes<InputNote>,
489 block_ref: BlockNumber,
490) -> Result<(BTreeSet<AssetVaultKey>, BTreeSet<BlockNumber>), TransactionExecutorError> {
491 let mut ref_blocks: BTreeSet<BlockNumber> = BTreeSet::new();
492 let mut asset_vault_keys: BTreeSet<AssetVaultKey> = BTreeSet::new();
493
494 for input_note in notes.iter() {
495 if let Some(location) = input_note.location() {
498 if location.block_num() > block_ref {
499 return Err(TransactionExecutorError::NoteBlockPastReferenceBlock(
500 input_note.id(),
501 block_ref,
502 ));
503 }
504 ref_blocks.insert(location.block_num());
505 }
506
507 asset_vault_keys.extend(input_note.note().assets().iter().map(Asset::vault_key));
508 }
509
510 Ok((asset_vault_keys, ref_blocks))
511}
512
513fn validate_num_cycles(num_cycles: u32) -> Result<(), TransactionExecutorError> {
515 if !(MIN_TX_EXECUTION_CYCLES..=MAX_TX_EXECUTION_CYCLES).contains(&num_cycles) {
516 Err(TransactionExecutorError::InvalidExecutionOptionsCycles {
517 min_cycles: MIN_TX_EXECUTION_CYCLES,
518 max_cycles: MAX_TX_EXECUTION_CYCLES,
519 actual: num_cycles,
520 })
521 } else {
522 Ok(())
523 }
524}
525
526fn map_execution_error(exec_err: ExecutionError) -> TransactionExecutorError {
533 match exec_err {
534 ExecutionError::EventError { ref error, .. } => {
535 match error.downcast_ref::<TransactionKernelError>() {
536 Some(TransactionKernelError::Unauthorized(summary)) => {
537 TransactionExecutorError::Unauthorized(summary.clone())
538 },
539 Some(TransactionKernelError::InsufficientFee { account_balance, tx_fee }) => {
540 TransactionExecutorError::InsufficientFee {
541 account_balance: *account_balance,
542 tx_fee: *tx_fee,
543 }
544 },
545 Some(TransactionKernelError::MissingAuthenticator) => {
546 TransactionExecutorError::MissingAuthenticator
547 },
548 _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
549 }
550 },
551 _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
552 }
553}