1use alloc::collections::BTreeSet;
2use alloc::sync::Arc;
3
4use miden_processor::fast::FastProcessor;
5use miden_processor::{AdviceInputs, ExecutionError, StackInputs};
6pub use miden_processor::{ExecutionOptions, MastForestStore};
7use miden_protocol::account::AccountId;
8use miden_protocol::assembly::DefaultSourceManager;
9use miden_protocol::assembly::debuginfo::SourceManagerSync;
10use miden_protocol::asset::{Asset, AssetVaultKey};
11use miden_protocol::block::BlockNumber;
12use miden_protocol::transaction::{
13 ExecutedTransaction,
14 InputNote,
15 InputNotes,
16 TransactionArgs,
17 TransactionInputs,
18 TransactionKernel,
19 TransactionScript,
20};
21use miden_protocol::vm::StackOutputs;
22use miden_protocol::{Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES};
23
24use super::TransactionExecutorError;
25use crate::auth::TransactionAuthenticator;
26use crate::errors::TransactionKernelError;
27use crate::host::{AccountProcedureIndexMap, ScriptMastForestStore};
28
29mod exec_host;
30pub use exec_host::TransactionExecutorHost;
31
32mod data_store;
33pub use data_store::DataStore;
34
35mod notes_checker;
36pub use notes_checker::{
37 FailedNote,
38 MAX_NUM_CHECKER_NOTES,
39 NoteConsumptionChecker,
40 NoteConsumptionInfo,
41};
42
43pub struct TransactionExecutor<'store, 'auth, STORE: 'store, AUTH: 'auth> {
56 data_store: &'store STORE,
57 authenticator: Option<&'auth AUTH>,
58 source_manager: Arc<dyn SourceManagerSync>,
59 exec_options: ExecutionOptions,
60}
61
62impl<'store, 'auth, STORE, AUTH> TransactionExecutor<'store, 'auth, STORE, AUTH>
63where
64 STORE: DataStore + 'store + Sync,
65 AUTH: TransactionAuthenticator + 'auth + Sync,
66{
67 pub fn new(data_store: &'store STORE) -> Self {
75 const _: () = assert!(MIN_TX_EXECUTION_CYCLES <= MAX_TX_EXECUTION_CYCLES);
76 TransactionExecutor {
77 data_store,
78 authenticator: None,
79 source_manager: Arc::new(DefaultSourceManager::default()),
80 exec_options: ExecutionOptions::new(
81 Some(MAX_TX_EXECUTION_CYCLES),
82 MIN_TX_EXECUTION_CYCLES,
83 false,
84 false,
85 )
86 .expect("Must not fail while max cycles is more than min trace length"),
87 }
88 }
89
90 #[must_use]
95 pub fn with_authenticator(mut self, authenticator: &'auth AUTH) -> Self {
96 self.authenticator = Some(authenticator);
97 self
98 }
99
100 #[must_use]
109 pub fn with_source_manager(mut self, source_manager: Arc<dyn SourceManagerSync>) -> Self {
110 self.source_manager = source_manager;
111 self
112 }
113
114 pub fn with_options(
122 mut self,
123 exec_options: ExecutionOptions,
124 ) -> Result<Self, TransactionExecutorError> {
125 validate_num_cycles(exec_options.max_cycles())?;
126 validate_num_cycles(exec_options.expected_cycles())?;
127
128 self.exec_options = exec_options;
129 Ok(self)
130 }
131
132 #[must_use]
138 pub fn with_debug_mode(mut self) -> Self {
139 self.exec_options = self.exec_options.with_debugging(true);
140 self
141 }
142
143 #[must_use]
150 pub fn with_tracing(mut self) -> Self {
151 self.exec_options = self.exec_options.with_tracing();
152 self
153 }
154
155 pub async fn execute_transaction(
176 &self,
177 account_id: AccountId,
178 block_ref: BlockNumber,
179 notes: InputNotes<InputNote>,
180 tx_args: TransactionArgs,
181 ) -> Result<ExecutedTransaction, TransactionExecutorError> {
182 let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
183
184 let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
185
186 let processor = if self.exec_options.enable_debugging() {
191 FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs)
192 } else {
193 FastProcessor::new_with_advice_inputs(stack_inputs.as_slice(), advice_inputs)
194 };
195
196 let output = processor
197 .execute(&TransactionKernel::main(), &mut host)
198 .await
199 .map_err(map_execution_error)?;
200 let stack_outputs = output.stack;
201 let advice_provider = output.advice;
202
203 let (_stack, advice_map, merkle_store, _pc_requests) = advice_provider.into_parts();
205 let advice_inputs = AdviceInputs {
206 map: advice_map,
207 store: merkle_store,
208 ..Default::default()
209 };
210
211 build_executed_transaction(advice_inputs, tx_inputs, stack_outputs, host)
212 }
213
214 pub async fn execute_tx_view_script(
226 &self,
227 account_id: AccountId,
228 block_ref: BlockNumber,
229 tx_script: TransactionScript,
230 advice_inputs: AdviceInputs,
231 ) -> Result<[Felt; 16], TransactionExecutorError> {
232 let mut tx_args = TransactionArgs::default().with_tx_script(tx_script);
233 tx_args.extend_advice_inputs(advice_inputs);
234
235 let notes = InputNotes::default();
236 let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
237
238 let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
239
240 let processor =
241 FastProcessor::new_with_advice_inputs(stack_inputs.as_slice(), advice_inputs);
242 let output = processor
243 .execute(&TransactionKernel::tx_script_main(), &mut host)
244 .await
245 .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
246 let stack_outputs = output.stack;
247
248 Ok(*stack_outputs)
249 }
250
251 async fn prepare_tx_inputs(
260 &self,
261 account_id: AccountId,
262 block_ref: BlockNumber,
263 input_notes: InputNotes<InputNote>,
264 tx_args: TransactionArgs,
265 ) -> Result<TransactionInputs, TransactionExecutorError> {
266 let (mut asset_vault_keys, mut ref_blocks) = validate_input_notes(&input_notes, block_ref)?;
267 ref_blocks.insert(block_ref);
268
269 let (account, block_header, blockchain) = self
270 .data_store
271 .get_transaction_inputs(account_id, ref_blocks)
272 .await
273 .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
274
275 let native_account_vault_root = account.vault().root();
276 let fee_asset_vault_key =
277 AssetVaultKey::from_account_id(block_header.fee_parameters().native_asset_id())
278 .expect("fee asset should be a fungible asset");
279
280 let mut tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes)
281 .map_err(TransactionExecutorError::InvalidTransactionInputs)?
282 .with_tx_args(tx_args);
283
284 asset_vault_keys.insert(fee_asset_vault_key);
287
288 asset_vault_keys.retain(|asset_key| {
290 !tx_inputs.has_vault_asset_witness(native_account_vault_root, asset_key)
291 });
292
293 if !asset_vault_keys.is_empty() {
295 let asset_witnesses = self
296 .data_store
297 .get_vault_asset_witnesses(account_id, native_account_vault_root, asset_vault_keys)
298 .await
299 .map_err(TransactionExecutorError::FetchAssetWitnessFailed)?;
300
301 tx_inputs = tx_inputs.with_asset_witnesses(asset_witnesses);
302 }
303
304 Ok(tx_inputs)
305 }
306
307 async fn prepare_transaction(
312 &self,
313 tx_inputs: &TransactionInputs,
314 ) -> Result<
315 (TransactionExecutorHost<'store, 'auth, STORE, AUTH>, StackInputs, AdviceInputs),
316 TransactionExecutorError,
317 > {
318 let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_inputs);
319
320 let stack_inputs = StackInputs::new(stack_inputs.iter().copied().collect()).unwrap();
326
327 let input_notes = tx_inputs.input_notes();
328
329 let script_mast_store = ScriptMastForestStore::new(
330 tx_inputs.tx_script(),
331 input_notes.iter().map(|n| n.note().script()),
332 );
333
334 let account_procedure_index_map =
337 AccountProcedureIndexMap::new([tx_inputs.account().code()]);
338
339 let initial_fee_asset_balance = {
340 let vault_root = tx_inputs.account().vault().root();
341 let native_asset_id = tx_inputs.block_header().fee_parameters().native_asset_id();
342 let fee_asset_vault_key = AssetVaultKey::from_account_id(native_asset_id)
343 .expect("fee asset should be a fungible asset");
344
345 let fee_asset = tx_inputs
346 .read_vault_asset(vault_root, fee_asset_vault_key)
347 .map_err(TransactionExecutorError::FeeAssetRetrievalFailed)?;
348 match fee_asset {
349 Some(Asset::Fungible(fee_asset)) => fee_asset.amount(),
350 Some(Asset::NonFungible(_)) => {
351 return Err(TransactionExecutorError::FeeAssetMustBeFungible);
352 },
353 None => 0,
355 }
356 };
357 let host = TransactionExecutorHost::new(
358 tx_inputs.account(),
359 input_notes.clone(),
360 self.data_store,
361 script_mast_store,
362 account_procedure_index_map,
363 self.authenticator,
364 tx_inputs.block_header().block_num(),
365 initial_fee_asset_balance,
366 self.source_manager.clone(),
367 );
368
369 let advice_inputs = tx_advice_inputs.into_advice_inputs();
370
371 Ok((host, stack_inputs, advice_inputs))
372 }
373}
374
375fn build_executed_transaction<STORE: DataStore + Sync, AUTH: TransactionAuthenticator + Sync>(
380 mut advice_inputs: AdviceInputs,
381 tx_inputs: TransactionInputs,
382 stack_outputs: StackOutputs,
383 host: TransactionExecutorHost<STORE, AUTH>,
384) -> Result<ExecutedTransaction, TransactionExecutorError> {
385 let (
389 pre_fee_account_delta,
390 _input_notes,
391 output_notes,
392 accessed_foreign_account_code,
393 generated_signatures,
394 tx_progress,
395 foreign_account_slot_names,
396 ) = host.into_parts();
397
398 let tx_outputs =
399 TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes)
400 .map_err(TransactionExecutorError::TransactionOutputConstructionFailed)?;
401
402 let pre_fee_delta_commitment = pre_fee_account_delta.to_commitment();
403 if tx_outputs.account_delta_commitment != pre_fee_delta_commitment {
404 return Err(TransactionExecutorError::InconsistentAccountDeltaCommitment {
405 in_kernel_commitment: tx_outputs.account_delta_commitment,
406 host_commitment: pre_fee_delta_commitment,
407 });
408 }
409
410 let mut post_fee_account_delta = pre_fee_account_delta;
412 post_fee_account_delta
413 .vault_mut()
414 .remove_asset(Asset::from(tx_outputs.fee))
415 .map_err(TransactionExecutorError::RemoveFeeAssetFromDelta)?;
416
417 let initial_account = tx_inputs.account();
418 let final_account = &tx_outputs.account;
419
420 if initial_account.id() != final_account.id() {
421 return Err(TransactionExecutorError::InconsistentAccountId {
422 input_id: initial_account.id(),
423 output_id: final_account.id(),
424 });
425 }
426
427 let nonce_delta = final_account.nonce() - initial_account.nonce();
429 if nonce_delta != post_fee_account_delta.nonce_delta() {
430 return Err(TransactionExecutorError::InconsistentAccountNonceDelta {
431 expected: nonce_delta,
432 actual: post_fee_account_delta.nonce_delta(),
433 });
434 }
435
436 advice_inputs.map.extend(generated_signatures);
438
439 let tx_inputs = tx_inputs
442 .with_foreign_account_code(accessed_foreign_account_code)
443 .with_foreign_account_slot_names(foreign_account_slot_names)
444 .with_advice_inputs(advice_inputs);
445
446 Ok(ExecutedTransaction::new(
447 tx_inputs,
448 tx_outputs,
449 post_fee_account_delta,
450 tx_progress.into(),
451 ))
452}
453
454fn validate_input_notes(
463 notes: &InputNotes<InputNote>,
464 block_ref: BlockNumber,
465) -> Result<(BTreeSet<AssetVaultKey>, BTreeSet<BlockNumber>), TransactionExecutorError> {
466 let mut ref_blocks: BTreeSet<BlockNumber> = BTreeSet::new();
467 let mut asset_vault_keys: BTreeSet<AssetVaultKey> = BTreeSet::new();
468
469 for input_note in notes.iter() {
470 if let Some(location) = input_note.location() {
473 if location.block_num() > block_ref {
474 return Err(TransactionExecutorError::NoteBlockPastReferenceBlock(
475 input_note.id(),
476 block_ref,
477 ));
478 }
479 ref_blocks.insert(location.block_num());
480 }
481
482 asset_vault_keys.extend(input_note.note().assets().iter().map(Asset::vault_key));
483 }
484
485 Ok((asset_vault_keys, ref_blocks))
486}
487
488fn validate_num_cycles(num_cycles: u32) -> Result<(), TransactionExecutorError> {
490 if !(MIN_TX_EXECUTION_CYCLES..=MAX_TX_EXECUTION_CYCLES).contains(&num_cycles) {
491 Err(TransactionExecutorError::InvalidExecutionOptionsCycles {
492 min_cycles: MIN_TX_EXECUTION_CYCLES,
493 max_cycles: MAX_TX_EXECUTION_CYCLES,
494 actual: num_cycles,
495 })
496 } else {
497 Ok(())
498 }
499}
500
501fn map_execution_error(exec_err: ExecutionError) -> TransactionExecutorError {
508 match exec_err {
509 ExecutionError::EventError { ref error, .. } => {
510 match error.downcast_ref::<TransactionKernelError>() {
511 Some(TransactionKernelError::Unauthorized(summary)) => {
512 TransactionExecutorError::Unauthorized(summary.clone())
513 },
514 Some(TransactionKernelError::InsufficientFee { account_balance, tx_fee }) => {
515 TransactionExecutorError::InsufficientFee {
516 account_balance: *account_balance,
517 tx_fee: *tx_fee,
518 }
519 },
520 Some(TransactionKernelError::MissingAuthenticator) => {
521 TransactionExecutorError::MissingAuthenticator
522 },
523 _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
524 }
525 },
526 _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
527 }
528}