use alloc::collections::BTreeSet;
use alloc::sync::Arc;
use core::marker::PhantomData;
use miden_processor::advice::AdviceInputs;
use miden_processor::{ExecutionError, FastProcessor, StackInputs};
pub use miden_processor::{ExecutionOptions, MastForestStore};
use miden_protocol::account::AccountId;
use miden_protocol::assembly::DefaultSourceManager;
use miden_protocol::assembly::debuginfo::SourceManagerSync;
use miden_protocol::asset::{Asset, AssetVaultKey};
use miden_protocol::block::BlockNumber;
use miden_protocol::transaction::{
ExecutedTransaction,
InputNote,
InputNotes,
TransactionArgs,
TransactionInputs,
TransactionKernel,
TransactionScript,
};
use miden_protocol::vm::StackOutputs;
use miden_protocol::{Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES};
use super::TransactionExecutorError;
use crate::auth::TransactionAuthenticator;
use crate::errors::TransactionKernelError;
use crate::host::{AccountProcedureIndexMap, ScriptMastForestStore};
mod exec_host;
pub use exec_host::TransactionExecutorHost;
mod data_store;
pub use data_store::DataStore;
mod notes_checker;
pub use notes_checker::{
FailedNote,
MAX_NUM_CHECKER_NOTES,
NoteConsumptionChecker,
NoteConsumptionInfo,
};
mod program_executor;
pub use program_executor::ProgramExecutor;
pub struct TransactionExecutor<
'store,
'auth,
STORE: 'store,
AUTH: 'auth,
EXEC: ProgramExecutor = FastProcessor,
> {
data_store: &'store STORE,
authenticator: Option<&'auth AUTH>,
source_manager: Arc<dyn SourceManagerSync>,
exec_options: ExecutionOptions,
_executor: PhantomData<EXEC>,
}
impl<'store, 'auth, STORE, AUTH> TransactionExecutor<'store, 'auth, STORE, AUTH>
where
STORE: DataStore + 'store + Sync,
AUTH: TransactionAuthenticator + 'auth + Sync,
{
pub fn new(data_store: &'store STORE) -> Self {
const _: () = assert!(MIN_TX_EXECUTION_CYCLES <= MAX_TX_EXECUTION_CYCLES);
Self {
data_store,
authenticator: None,
source_manager: Arc::new(DefaultSourceManager::default()),
exec_options: ExecutionOptions::new(
Some(MAX_TX_EXECUTION_CYCLES),
MIN_TX_EXECUTION_CYCLES,
ExecutionOptions::DEFAULT_CORE_TRACE_FRAGMENT_SIZE,
false,
false,
)
.expect("Must not fail while max cycles is more than min trace length"),
_executor: PhantomData,
}
}
}
impl<'store, 'auth, STORE, AUTH, EXEC> TransactionExecutor<'store, 'auth, STORE, AUTH, EXEC>
where
STORE: DataStore + 'store + Sync,
AUTH: TransactionAuthenticator + 'auth + Sync,
EXEC: ProgramExecutor,
{
pub fn with_program_executor<EXEC2: ProgramExecutor>(
self,
) -> TransactionExecutor<'store, 'auth, STORE, AUTH, EXEC2> {
TransactionExecutor::<'store, 'auth, STORE, AUTH, EXEC2> {
data_store: self.data_store,
authenticator: self.authenticator,
source_manager: self.source_manager,
exec_options: self.exec_options,
_executor: PhantomData,
}
}
#[must_use]
pub fn with_authenticator(mut self, authenticator: &'auth AUTH) -> Self {
self.authenticator = Some(authenticator);
self
}
#[must_use]
pub fn with_source_manager(mut self, source_manager: Arc<dyn SourceManagerSync>) -> Self {
self.source_manager = source_manager;
self
}
pub fn with_options(
mut self,
exec_options: ExecutionOptions,
) -> Result<Self, TransactionExecutorError> {
validate_num_cycles(exec_options.max_cycles())?;
validate_num_cycles(exec_options.expected_cycles())?;
self.exec_options = exec_options;
Ok(self)
}
#[must_use]
pub fn with_debug_mode(mut self) -> Self {
self.exec_options = self.exec_options.with_debugging(true);
self
}
#[must_use]
pub fn with_tracing(mut self) -> Self {
self.exec_options = self.exec_options.with_tracing(true);
self
}
pub async fn execute_transaction(
&self,
account_id: AccountId,
block_ref: BlockNumber,
notes: InputNotes<InputNote>,
tx_args: TransactionArgs,
) -> Result<ExecutedTransaction, TransactionExecutorError> {
let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
let processor = EXEC::new(stack_inputs, advice_inputs, self.exec_options);
let output = processor
.execute(&TransactionKernel::main(), &mut host)
.await
.map_err(map_execution_error)?;
let stack_outputs = output.stack;
let advice_provider = output.advice;
let (_stack, advice_map, merkle_store, _pc_requests) = advice_provider.into_parts();
let advice_inputs = AdviceInputs {
map: advice_map,
store: merkle_store,
..Default::default()
};
build_executed_transaction(advice_inputs, tx_inputs, stack_outputs, host)
}
pub async fn execute_tx_view_script(
&self,
account_id: AccountId,
block_ref: BlockNumber,
tx_script: TransactionScript,
advice_inputs: AdviceInputs,
) -> Result<[Felt; 16], TransactionExecutorError> {
let mut tx_args = TransactionArgs::default().with_tx_script(tx_script);
tx_args.extend_advice_inputs(advice_inputs);
let notes = InputNotes::default();
let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
let processor = EXEC::new(stack_inputs, advice_inputs, self.exec_options);
let output = processor
.execute(&TransactionKernel::tx_script_main(), &mut host)
.await
.map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
let stack_outputs = output.stack;
Ok(*stack_outputs)
}
async fn prepare_tx_inputs(
&self,
account_id: AccountId,
block_ref: BlockNumber,
input_notes: InputNotes<InputNote>,
tx_args: TransactionArgs,
) -> Result<TransactionInputs, TransactionExecutorError> {
let (mut asset_vault_keys, mut ref_blocks) = validate_input_notes(&input_notes, block_ref)?;
ref_blocks.insert(block_ref);
let (account, block_header, blockchain) = self
.data_store
.get_transaction_inputs(account_id, ref_blocks)
.await
.map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
let native_account_vault_root = account.vault().root();
let fee_asset_vault_key =
AssetVaultKey::new_fungible(block_header.fee_parameters().native_asset_id())
.expect("fee asset should be a fungible asset");
let mut tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes)
.map_err(TransactionExecutorError::InvalidTransactionInputs)?
.with_tx_args(tx_args);
asset_vault_keys.insert(fee_asset_vault_key);
asset_vault_keys.retain(|asset_key| {
!tx_inputs.has_vault_asset_witness(native_account_vault_root, asset_key)
});
if !asset_vault_keys.is_empty() {
let asset_witnesses = self
.data_store
.get_vault_asset_witnesses(account_id, native_account_vault_root, asset_vault_keys)
.await
.map_err(TransactionExecutorError::FetchAssetWitnessFailed)?;
tx_inputs = tx_inputs.with_asset_witnesses(asset_witnesses);
}
Ok(tx_inputs)
}
async fn prepare_transaction(
&self,
tx_inputs: &TransactionInputs,
) -> Result<
(TransactionExecutorHost<'store, 'auth, STORE, AUTH>, StackInputs, AdviceInputs),
TransactionExecutorError,
> {
let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_inputs);
let input_notes = tx_inputs.input_notes();
let script_mast_store = ScriptMastForestStore::new(
tx_inputs.tx_script(),
input_notes.iter().map(|n| n.note().script()),
);
let account_procedure_index_map =
AccountProcedureIndexMap::new([tx_inputs.account().code()]);
let initial_fee_asset_balance = {
let vault_root = tx_inputs.account().vault().root();
let native_asset_id = tx_inputs.block_header().fee_parameters().native_asset_id();
let fee_asset_vault_key = AssetVaultKey::new_fungible(native_asset_id)
.expect("fee asset should be a fungible asset");
let fee_asset = tx_inputs
.read_vault_asset(vault_root, fee_asset_vault_key)
.map_err(TransactionExecutorError::FeeAssetRetrievalFailed)?;
match fee_asset {
Some(Asset::Fungible(fee_asset)) => fee_asset.amount(),
Some(Asset::NonFungible(_)) => {
return Err(TransactionExecutorError::FeeAssetMustBeFungible);
},
None => 0,
}
};
let host = TransactionExecutorHost::new(
tx_inputs.account(),
input_notes.clone(),
self.data_store,
script_mast_store,
account_procedure_index_map,
self.authenticator,
tx_inputs.block_header().block_num(),
initial_fee_asset_balance,
self.source_manager.clone(),
);
let advice_inputs = tx_advice_inputs.into_advice_inputs();
Ok((host, stack_inputs, advice_inputs))
}
}
fn build_executed_transaction<STORE: DataStore + Sync, AUTH: TransactionAuthenticator + Sync>(
mut advice_inputs: AdviceInputs,
tx_inputs: TransactionInputs,
stack_outputs: StackOutputs,
host: TransactionExecutorHost<STORE, AUTH>,
) -> Result<ExecutedTransaction, TransactionExecutorError> {
let (
pre_fee_account_delta,
_input_notes,
output_notes,
accessed_foreign_account_code,
generated_signatures,
tx_progress,
foreign_account_slot_names,
) = host.into_parts();
let tx_outputs =
TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes)
.map_err(TransactionExecutorError::TransactionOutputConstructionFailed)?;
let pre_fee_delta_commitment = pre_fee_account_delta.to_commitment();
if tx_outputs.account_delta_commitment() != pre_fee_delta_commitment {
return Err(TransactionExecutorError::InconsistentAccountDeltaCommitment {
in_kernel_commitment: tx_outputs.account_delta_commitment(),
host_commitment: pre_fee_delta_commitment,
});
}
let mut post_fee_account_delta = pre_fee_account_delta;
post_fee_account_delta
.vault_mut()
.remove_asset(Asset::from(tx_outputs.fee()))
.map_err(TransactionExecutorError::RemoveFeeAssetFromDelta)?;
let initial_account = tx_inputs.account();
let final_account = tx_outputs.account();
if initial_account.id() != final_account.id() {
return Err(TransactionExecutorError::InconsistentAccountId {
input_id: initial_account.id(),
output_id: final_account.id(),
});
}
let nonce_delta = final_account.nonce() - initial_account.nonce();
if nonce_delta != post_fee_account_delta.nonce_delta() {
return Err(TransactionExecutorError::InconsistentAccountNonceDelta {
expected: nonce_delta,
actual: post_fee_account_delta.nonce_delta(),
});
}
advice_inputs.map.extend(generated_signatures);
let tx_inputs = tx_inputs
.with_foreign_account_code(accessed_foreign_account_code)
.with_foreign_account_slot_names(foreign_account_slot_names)
.with_advice_inputs(advice_inputs);
Ok(ExecutedTransaction::new(
tx_inputs,
tx_outputs,
post_fee_account_delta,
tx_progress.into(),
))
}
fn validate_input_notes(
notes: &InputNotes<InputNote>,
block_ref: BlockNumber,
) -> Result<(BTreeSet<AssetVaultKey>, BTreeSet<BlockNumber>), TransactionExecutorError> {
let mut ref_blocks: BTreeSet<BlockNumber> = BTreeSet::new();
let mut asset_vault_keys: BTreeSet<AssetVaultKey> = BTreeSet::new();
for input_note in notes.iter() {
if let Some(location) = input_note.location() {
if location.block_num() > block_ref {
return Err(TransactionExecutorError::NoteBlockPastReferenceBlock(
input_note.id(),
block_ref,
));
}
ref_blocks.insert(location.block_num());
}
asset_vault_keys.extend(input_note.note().assets().iter().map(Asset::vault_key));
}
Ok((asset_vault_keys, ref_blocks))
}
fn validate_num_cycles(num_cycles: u32) -> Result<(), TransactionExecutorError> {
if !(MIN_TX_EXECUTION_CYCLES..=MAX_TX_EXECUTION_CYCLES).contains(&num_cycles) {
Err(TransactionExecutorError::InvalidExecutionOptionsCycles {
min_cycles: MIN_TX_EXECUTION_CYCLES,
max_cycles: MAX_TX_EXECUTION_CYCLES,
actual: num_cycles,
})
} else {
Ok(())
}
}
fn map_execution_error(exec_err: ExecutionError) -> TransactionExecutorError {
match exec_err {
ExecutionError::EventError { ref error, .. } => {
match error.downcast_ref::<TransactionKernelError>() {
Some(TransactionKernelError::Unauthorized(summary)) => {
TransactionExecutorError::Unauthorized(summary.clone())
},
Some(TransactionKernelError::InsufficientFee { account_balance, tx_fee }) => {
TransactionExecutorError::InsufficientFee {
account_balance: *account_balance,
tx_fee: *tx_fee,
}
},
Some(TransactionKernelError::MissingAuthenticator) => {
TransactionExecutorError::MissingAuthenticator
},
_ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
}
},
_ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
}
}