1use alloc::collections::BTreeSet;
2use alloc::sync::Arc;
3
4use miden_processor::advice::AdviceInputs;
5use miden_processor::{ExecutionError, FastProcessor, 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 ExecutionOptions::DEFAULT_CORE_TRACE_FRAGMENT_SIZE,
84 false,
85 false,
86 )
87 .expect("Must not fail while max cycles is more than min trace length"),
88 }
89 }
90
91 #[must_use]
96 pub fn with_authenticator(mut self, authenticator: &'auth AUTH) -> Self {
97 self.authenticator = Some(authenticator);
98 self
99 }
100
101 #[must_use]
110 pub fn with_source_manager(mut self, source_manager: Arc<dyn SourceManagerSync>) -> Self {
111 self.source_manager = source_manager;
112 self
113 }
114
115 pub fn with_options(
123 mut self,
124 exec_options: ExecutionOptions,
125 ) -> Result<Self, TransactionExecutorError> {
126 validate_num_cycles(exec_options.max_cycles())?;
127 validate_num_cycles(exec_options.expected_cycles())?;
128
129 self.exec_options = exec_options;
130 Ok(self)
131 }
132
133 #[must_use]
139 pub fn with_debug_mode(mut self) -> Self {
140 self.exec_options = self.exec_options.with_debugging(true);
141 self
142 }
143
144 #[must_use]
151 pub fn with_tracing(mut self) -> Self {
152 self.exec_options = self.exec_options.with_tracing(true);
153 self
154 }
155
156 pub async fn execute_transaction(
177 &self,
178 account_id: AccountId,
179 block_ref: BlockNumber,
180 notes: InputNotes<InputNote>,
181 tx_args: TransactionArgs,
182 ) -> Result<ExecutedTransaction, TransactionExecutorError> {
183 let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
184
185 let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
186
187 let processor =
190 FastProcessor::new_with_options(stack_inputs, advice_inputs, self.exec_options);
191
192 let output = processor
193 .execute(&TransactionKernel::main(), &mut host)
194 .await
195 .map_err(map_execution_error)?;
196 let stack_outputs = output.stack;
197 let advice_provider = output.advice;
198
199 let (_stack, advice_map, merkle_store, _pc_requests) = advice_provider.into_parts();
201 let advice_inputs = AdviceInputs {
202 map: advice_map,
203 store: merkle_store,
204 ..Default::default()
205 };
206
207 build_executed_transaction(advice_inputs, tx_inputs, stack_outputs, host)
208 }
209
210 pub async fn execute_tx_view_script(
222 &self,
223 account_id: AccountId,
224 block_ref: BlockNumber,
225 tx_script: TransactionScript,
226 advice_inputs: AdviceInputs,
227 ) -> Result<[Felt; 16], TransactionExecutorError> {
228 let mut tx_args = TransactionArgs::default().with_tx_script(tx_script);
229 tx_args.extend_advice_inputs(advice_inputs);
230
231 let notes = InputNotes::default();
232 let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
233
234 let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
235
236 let processor = FastProcessor::new(stack_inputs).with_advice(advice_inputs);
237 let output = processor
238 .execute(&TransactionKernel::tx_script_main(), &mut host)
239 .await
240 .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
241 let stack_outputs = output.stack;
242
243 Ok(*stack_outputs)
244 }
245
246 async fn prepare_tx_inputs(
255 &self,
256 account_id: AccountId,
257 block_ref: BlockNumber,
258 input_notes: InputNotes<InputNote>,
259 tx_args: TransactionArgs,
260 ) -> Result<TransactionInputs, TransactionExecutorError> {
261 let (mut asset_vault_keys, mut ref_blocks) = validate_input_notes(&input_notes, block_ref)?;
262 ref_blocks.insert(block_ref);
263
264 let (account, block_header, blockchain) = self
265 .data_store
266 .get_transaction_inputs(account_id, ref_blocks)
267 .await
268 .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
269
270 let native_account_vault_root = account.vault().root();
271 let fee_asset_vault_key =
272 AssetVaultKey::new_fungible(block_header.fee_parameters().native_asset_id())
273 .expect("fee asset should be a fungible asset");
274
275 let mut tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes)
276 .map_err(TransactionExecutorError::InvalidTransactionInputs)?
277 .with_tx_args(tx_args);
278
279 asset_vault_keys.insert(fee_asset_vault_key);
282
283 asset_vault_keys.retain(|asset_key| {
285 !tx_inputs.has_vault_asset_witness(native_account_vault_root, asset_key)
286 });
287
288 if !asset_vault_keys.is_empty() {
290 let asset_witnesses = self
291 .data_store
292 .get_vault_asset_witnesses(account_id, native_account_vault_root, asset_vault_keys)
293 .await
294 .map_err(TransactionExecutorError::FetchAssetWitnessFailed)?;
295
296 tx_inputs = tx_inputs.with_asset_witnesses(asset_witnesses);
297 }
298
299 Ok(tx_inputs)
300 }
301
302 async fn prepare_transaction(
307 &self,
308 tx_inputs: &TransactionInputs,
309 ) -> Result<
310 (TransactionExecutorHost<'store, 'auth, STORE, AUTH>, StackInputs, AdviceInputs),
311 TransactionExecutorError,
312 > {
313 let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_inputs);
314 let input_notes = tx_inputs.input_notes();
315
316 let script_mast_store = ScriptMastForestStore::new(
317 tx_inputs.tx_script(),
318 input_notes.iter().map(|n| n.note().script()),
319 );
320
321 let account_procedure_index_map =
324 AccountProcedureIndexMap::new([tx_inputs.account().code()]);
325
326 let initial_fee_asset_balance = {
327 let vault_root = tx_inputs.account().vault().root();
328 let native_asset_id = tx_inputs.block_header().fee_parameters().native_asset_id();
329 let fee_asset_vault_key = AssetVaultKey::new_fungible(native_asset_id)
330 .expect("fee asset should be a fungible asset");
331
332 let fee_asset = tx_inputs
333 .read_vault_asset(vault_root, fee_asset_vault_key)
334 .map_err(TransactionExecutorError::FeeAssetRetrievalFailed)?;
335 match fee_asset {
336 Some(Asset::Fungible(fee_asset)) => fee_asset.amount(),
337 Some(Asset::NonFungible(_)) => {
338 return Err(TransactionExecutorError::FeeAssetMustBeFungible);
339 },
340 None => 0,
342 }
343 };
344 let host = TransactionExecutorHost::new(
345 tx_inputs.account(),
346 input_notes.clone(),
347 self.data_store,
348 script_mast_store,
349 account_procedure_index_map,
350 self.authenticator,
351 tx_inputs.block_header().block_num(),
352 initial_fee_asset_balance,
353 self.source_manager.clone(),
354 );
355
356 let advice_inputs = tx_advice_inputs.into_advice_inputs();
357
358 Ok((host, stack_inputs, advice_inputs))
359 }
360}
361
362fn build_executed_transaction<STORE: DataStore + Sync, AUTH: TransactionAuthenticator + Sync>(
367 mut advice_inputs: AdviceInputs,
368 tx_inputs: TransactionInputs,
369 stack_outputs: StackOutputs,
370 host: TransactionExecutorHost<STORE, AUTH>,
371) -> Result<ExecutedTransaction, TransactionExecutorError> {
372 let (
376 pre_fee_account_delta,
377 _input_notes,
378 output_notes,
379 accessed_foreign_account_code,
380 generated_signatures,
381 tx_progress,
382 foreign_account_slot_names,
383 ) = host.into_parts();
384
385 let tx_outputs =
386 TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes)
387 .map_err(TransactionExecutorError::TransactionOutputConstructionFailed)?;
388
389 let pre_fee_delta_commitment = pre_fee_account_delta.to_commitment();
390 if tx_outputs.account_delta_commitment != pre_fee_delta_commitment {
391 return Err(TransactionExecutorError::InconsistentAccountDeltaCommitment {
392 in_kernel_commitment: tx_outputs.account_delta_commitment,
393 host_commitment: pre_fee_delta_commitment,
394 });
395 }
396
397 let mut post_fee_account_delta = pre_fee_account_delta;
399 post_fee_account_delta
400 .vault_mut()
401 .remove_asset(Asset::from(tx_outputs.fee))
402 .map_err(TransactionExecutorError::RemoveFeeAssetFromDelta)?;
403
404 let initial_account = tx_inputs.account();
405 let final_account = &tx_outputs.account;
406
407 if initial_account.id() != final_account.id() {
408 return Err(TransactionExecutorError::InconsistentAccountId {
409 input_id: initial_account.id(),
410 output_id: final_account.id(),
411 });
412 }
413
414 let nonce_delta = final_account.nonce() - initial_account.nonce();
416 if nonce_delta != post_fee_account_delta.nonce_delta() {
417 return Err(TransactionExecutorError::InconsistentAccountNonceDelta {
418 expected: nonce_delta,
419 actual: post_fee_account_delta.nonce_delta(),
420 });
421 }
422
423 advice_inputs.map.extend(generated_signatures);
425
426 let tx_inputs = tx_inputs
429 .with_foreign_account_code(accessed_foreign_account_code)
430 .with_foreign_account_slot_names(foreign_account_slot_names)
431 .with_advice_inputs(advice_inputs);
432
433 Ok(ExecutedTransaction::new(
434 tx_inputs,
435 tx_outputs,
436 post_fee_account_delta,
437 tx_progress.into(),
438 ))
439}
440
441fn validate_input_notes(
450 notes: &InputNotes<InputNote>,
451 block_ref: BlockNumber,
452) -> Result<(BTreeSet<AssetVaultKey>, BTreeSet<BlockNumber>), TransactionExecutorError> {
453 let mut ref_blocks: BTreeSet<BlockNumber> = BTreeSet::new();
454 let mut asset_vault_keys: BTreeSet<AssetVaultKey> = BTreeSet::new();
455
456 for input_note in notes.iter() {
457 if let Some(location) = input_note.location() {
460 if location.block_num() > block_ref {
461 return Err(TransactionExecutorError::NoteBlockPastReferenceBlock(
462 input_note.id(),
463 block_ref,
464 ));
465 }
466 ref_blocks.insert(location.block_num());
467 }
468
469 asset_vault_keys.extend(input_note.note().assets().iter().map(Asset::vault_key));
470 }
471
472 Ok((asset_vault_keys, ref_blocks))
473}
474
475fn validate_num_cycles(num_cycles: u32) -> Result<(), TransactionExecutorError> {
477 if !(MIN_TX_EXECUTION_CYCLES..=MAX_TX_EXECUTION_CYCLES).contains(&num_cycles) {
478 Err(TransactionExecutorError::InvalidExecutionOptionsCycles {
479 min_cycles: MIN_TX_EXECUTION_CYCLES,
480 max_cycles: MAX_TX_EXECUTION_CYCLES,
481 actual: num_cycles,
482 })
483 } else {
484 Ok(())
485 }
486}
487
488fn map_execution_error(exec_err: ExecutionError) -> TransactionExecutorError {
495 match exec_err {
496 ExecutionError::EventError { ref error, .. } => {
497 match error.downcast_ref::<TransactionKernelError>() {
498 Some(TransactionKernelError::Unauthorized(summary)) => {
499 TransactionExecutorError::Unauthorized(summary.clone())
500 },
501 Some(TransactionKernelError::InsufficientFee { account_balance, tx_fee }) => {
502 TransactionExecutorError::InsufficientFee {
503 account_balance: *account_balance,
504 tx_fee: *tx_fee,
505 }
506 },
507 Some(TransactionKernelError::MissingAuthenticator) => {
508 TransactionExecutorError::MissingAuthenticator
509 },
510 _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
511 }
512 },
513 _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
514 }
515}