use alloc::boxed::Box;
use alloc::vec::Vec;
use miden_processor::ProcessorState;
use miden_processor::advice::{AdviceMutation, AdviceProvider};
use miden_processor::trace::RowIndex;
use miden_protocol::account::auth::{PublicKeyCommitment, Signature};
use miden_protocol::account::delta::AssetDeltaOperation;
use miden_protocol::account::{
AccountId,
StorageMap,
StorageMapKey,
StorageSlotName,
StorageSlotType,
};
use miden_protocol::asset::{Asset, AssetId, AssetVault};
use miden_protocol::note::{
NoteAttachment,
NoteAttachmentContent,
NoteAttachmentScheme,
NoteId,
NoteRecipient,
NoteScript,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::transaction::memory::{NOTE_MEM_SIZE, OUTPUT_NOTE_SECTION_OFFSET};
use miden_protocol::transaction::{TransactionEventId, TransactionSummary};
use miden_protocol::vm::EventId;
use miden_protocol::{Felt, Hasher, WORD_SIZE, Word};
use crate::host::{TransactionBaseHost, TransactionKernelProcess};
use crate::{LinkMap, TransactionKernelError};
#[derive(Debug)]
pub(crate) enum TransactionProgressEvent {
PrologueStart(RowIndex),
PrologueEnd(RowIndex),
NotesProcessingStart(RowIndex),
NotesProcessingEnd(RowIndex),
NoteExecutionStart { note_id: NoteId, clk: RowIndex },
NoteExecutionEnd(RowIndex),
TxScriptProcessingStart(RowIndex),
TxScriptProcessingEnd(RowIndex),
EpilogueStart(RowIndex),
EpilogueEnd(RowIndex),
EpilogueAuthProcStart(RowIndex),
EpilogueAuthProcEnd(RowIndex),
}
#[derive(Debug)]
pub(crate) enum TransactionEvent {
AccountBeforeForeignLoad {
foreign_account_id: AccountId,
},
AccountVaultAfterAssetUpdate {
patch: AssetPatch,
},
AccountBeforeAssetDeltaComputation,
AccountOnAssetDeltaComputation {
delta: AssetDelta,
},
AccountStorageAfterSetItem {
slot_name: StorageSlotName,
new_value: Word,
},
AccountStorageAfterSetMapItem {
slot_name: StorageSlotName,
key: StorageMapKey,
old_value: Word,
new_value: Word,
},
AccountStorageBeforeMapItemAccess {
active_account_id: AccountId,
map_root: Word,
map_key: StorageMapKey,
},
AccountVaultBeforeAssetAccess {
active_account_id: AccountId,
vault_root: Word,
asset_id: AssetId,
},
AccountAfterIncrementNonce,
AccountPushProcedureIndex {
code_commitment: Word,
procedure_root: Word,
},
NoteBeforeCreated {
note_idx: usize,
metadata: PartialNoteMetadata,
recipient_data: RecipientData,
},
NoteBeforeAddAsset {
note_idx: usize,
asset: Asset,
},
NoteBeforeAddAttachment {
note_idx: usize,
attachment: NoteAttachment,
},
InputNoteIndexLookup {
note_id: NoteId,
},
AuthRequest {
pub_key_commitment: PublicKeyCommitment,
tx_summary_or_signature: TxSummaryOrSignature,
},
Unauthorized {
tx_summary: TransactionSummary,
},
LinkMapSet {
advice_mutation: Vec<AdviceMutation>,
},
LinkMapGet {
advice_mutation: Vec<AdviceMutation>,
},
Progress(TransactionProgressEvent),
}
impl TransactionEvent {
pub fn extract<'store, STORE>(
base_host: &TransactionBaseHost<'store, STORE>,
process: &ProcessorState,
) -> Result<Option<TransactionEvent>, TransactionKernelError> {
let event_id = EventId::from_felt(process.get_stack_item(0));
let tx_event_id = TransactionEventId::try_from(event_id).map_err(|err| {
TransactionKernelError::other_with_source(
"failed to convert event ID into transaction event ID",
err,
)
})?;
if tx_event_id.is_privileged() && !process.ctx().is_root() {
return Err(
TransactionKernelError::PrivilegedEventFromOutsideTransactionKernelContext(
tx_event_id,
),
);
}
let tx_event = match tx_event_id {
TransactionEventId::AccountBeforeForeignLoad => {
let account_id_suffix = process.get_stack_item(1);
let account_id_prefix = process.get_stack_item(2);
let account_id = AccountId::try_from_elements(account_id_suffix, account_id_prefix)
.map_err(|err| {
TransactionKernelError::other_with_source(
"failed to convert account ID word into account ID",
err,
)
})?;
Some(TransactionEvent::AccountBeforeForeignLoad { foreign_account_id: account_id })
},
TransactionEventId::AccountVaultBeforeAddAsset
| TransactionEventId::AccountVaultBeforeRemoveAsset
| TransactionEventId::AccountVaultBeforeMintAsset
| TransactionEventId::AccountVaultBeforeBurnAsset => {
let asset_id = process.get_stack_word(1);
let vault_root_ptr = process.get_stack_item(9);
let asset_id = AssetId::try_from(asset_id).map_err(|source| {
TransactionKernelError::MalformedAssetInEventHandler {
handler: "AccountVaultBefore{Add,Remove,Mint,Burn}Asset",
source,
}
})?;
let current_vault_root = process.get_vault_root(vault_root_ptr)?;
on_account_vault_asset_accessed(base_host, process, asset_id, current_vault_root)?
},
TransactionEventId::AccountVaultAfterRemoveAsset
| TransactionEventId::AccountVaultAfterAddAsset => {
let asset_id = process.get_stack_word(1);
let initial_vault_value = process.get_stack_word(5);
let final_vault_value = process.get_stack_word(9);
let asset_id = AssetId::try_from(asset_id).map_err(|source| {
TransactionKernelError::MalformedAssetInEventHandler {
handler: "AccountVaultAfterRemoveAsset",
source,
}
})?;
let patch = AssetPatch {
asset_id,
initial_vault_value,
final_vault_value,
};
Some(TransactionEvent::AccountVaultAfterAssetUpdate { patch })
},
TransactionEventId::AccountBeforeAssetDeltaComputation => {
Some(TransactionEvent::AccountBeforeAssetDeltaComputation)
},
TransactionEventId::AccountOnAssetDeltaComputation => Some({
let delta_op = process.get_stack_item(1);
let asset_id = process.get_stack_word(2);
let delta_asset_value = process.get_stack_word(6);
let asset_id = AssetId::try_from(asset_id).map_err(|source| {
TransactionKernelError::MalformedAssetInEventHandler {
handler: "AccountOnAssetDeltaComputation",
source,
}
})?;
let asset =
Asset::from_id_and_value(asset_id, delta_asset_value).map_err(|source| {
TransactionKernelError::MalformedAssetInEventHandler {
handler: "AccountOnAssetDeltaComputation",
source,
}
})?;
let delta_op = AssetDeltaOperation::try_from(
u8::try_from(delta_op.as_canonical_u64()).map_err(|_| {
TransactionKernelError::other("failed to convert asset delta op to u8")
})?,
)
.map_err(|source| {
TransactionKernelError::other_with_source(
"failed to decode asset delta op",
source,
)
})?;
TransactionEvent::AccountOnAssetDeltaComputation {
delta: AssetDelta { delta_op, asset },
}
}),
TransactionEventId::AccountVaultBeforeGetAsset => {
let asset_id = process.get_stack_word(1);
let vault_root_ptr = process.get_stack_item(5);
let asset_id = AssetId::try_from(asset_id).map_err(|source| {
TransactionKernelError::MalformedAssetInEventHandler {
handler: "AccountVaultBeforeGetAsset",
source,
}
})?;
let vault_root = process.get_vault_root(vault_root_ptr)?;
on_account_vault_asset_accessed(base_host, process, asset_id, vault_root)?
},
TransactionEventId::AccountStorageBeforeSetItem => None,
TransactionEventId::AccountStorageAfterSetItem => {
let slot_ptr = process.get_stack_item(1);
let new_value = process.get_stack_word(2);
let (slot_id, slot_type, _old_value) = process.get_storage_slot(slot_ptr)?;
let slot_header = base_host.initial_account_storage_slot(slot_id)?;
let slot_name = slot_header.name().clone();
if !slot_type.is_value() {
return Err(TransactionKernelError::other(format!(
"expected slot to be of type value, found {slot_type}"
)));
}
Some(TransactionEvent::AccountStorageAfterSetItem { slot_name, new_value })
},
TransactionEventId::AccountStorageBeforeGetMapItem => {
let slot_ptr = process.get_stack_item(1);
let map_key = process.get_stack_word(2);
let map_key = StorageMapKey::from_raw(map_key);
on_account_storage_map_item_accessed(base_host, process, slot_ptr, map_key)?
},
TransactionEventId::AccountStorageBeforeSetMapItem => {
let slot_ptr = process.get_stack_item(1);
let map_key = process.get_stack_word(2);
let map_key = StorageMapKey::from_raw(map_key);
on_account_storage_map_item_accessed(base_host, process, slot_ptr, map_key)?
},
TransactionEventId::AccountStorageAfterSetMapItem => {
let slot_ptr = process.get_stack_item(1);
let key = process.get_stack_word(2);
let old_value = process.get_stack_word(6);
let new_value = process.get_stack_word(10);
let key = StorageMapKey::from_raw(key);
let (slot_id, ..) = process.get_storage_slot(slot_ptr)?;
let slot_header = base_host.initial_account_storage_slot(slot_id)?;
let slot_name = slot_header.name().clone();
Some(TransactionEvent::AccountStorageAfterSetMapItem {
slot_name,
key,
old_value,
new_value,
})
},
TransactionEventId::AccountBeforeIncrementNonce => None,
TransactionEventId::AccountAfterIncrementNonce => {
Some(TransactionEvent::AccountAfterIncrementNonce)
},
TransactionEventId::AccountPushProcedureIndex => {
let procedure_root = process.get_stack_word(1);
let code_commitment = process.get_active_account_code_commitment()?;
Some(TransactionEvent::AccountPushProcedureIndex {
code_commitment,
procedure_root,
})
},
TransactionEventId::NoteBeforeCreated => {
let tag = process.get_stack_item(1);
let note_type = process.get_stack_item(2);
let recipient_digest = process.get_stack_word(3);
let sender = base_host.native_account_id();
let metadata = build_note_metadata(sender, note_type, tag)?;
let note_idx = process.get_num_output_notes() as usize;
let recipient_data = if process.has_advice_map_entry(recipient_digest) {
let (note_storage, script_root, serial_num) =
process.read_note_recipient_info_from_adv_map(recipient_digest)?;
let note_script = process
.advice_provider()
.get_mapped_values(&script_root)
.map(|script_data| {
NoteScript::try_from(script_data).map_err(|source| {
TransactionKernelError::MalformedNoteScript {
data: script_data.to_vec(),
source,
}
})
})
.transpose()?;
match note_script {
Some(note_script) => {
let recipient =
NoteRecipient::new(serial_num, note_script, note_storage);
if recipient.digest() != recipient_digest {
return Err(TransactionKernelError::other(format!(
"recipient digest is {recipient_digest}, but recipient constructed from raw inputs has digest {}",
recipient.digest()
)));
}
RecipientData::Recipient(recipient)
},
None => RecipientData::ScriptMissing {
recipient_digest,
serial_num,
script_root,
note_storage,
},
}
} else {
RecipientData::Digest(recipient_digest)
};
Some(TransactionEvent::NoteBeforeCreated { note_idx, metadata, recipient_data })
},
TransactionEventId::NoteAfterCreated => None,
TransactionEventId::NoteBeforeAddAsset => {
let asset_id = process.get_stack_word(1);
let asset_value = process.get_stack_word(5);
let note_idx = process.get_stack_item(9);
let asset =
Asset::from_id_and_value_words(asset_id, asset_value).map_err(|source| {
TransactionKernelError::MalformedAssetInEventHandler {
handler: "NoteBeforeAddAsset",
source,
}
})?;
let note_idx = note_idx.as_canonical_u64() as usize;
Some(TransactionEvent::NoteBeforeAddAsset { note_idx, asset })
},
TransactionEventId::NoteAfterAddAsset => None,
TransactionEventId::NoteBeforeAddAttachment => {
let note_ptr = process.get_stack_item(2);
let attachment_scheme = process.get_stack_item(3);
let attachment_commitment = process.get_stack_word(4);
let (note_idx, attachment) = extract_note_attachment(
attachment_scheme,
attachment_commitment,
note_ptr,
process.advice_provider(),
)?;
Some(TransactionEvent::NoteBeforeAddAttachment { note_idx, attachment })
},
TransactionEventId::InputNoteIndexLookup => {
let note_id = NoteId::from_raw(process.get_stack_word(1));
Some(TransactionEvent::InputNoteIndexLookup { note_id })
},
TransactionEventId::AuthRequest => {
let message = process.get_stack_word(1);
let pub_key_commitment = PublicKeyCommitment::from(process.get_stack_word(5));
let signature_key = Hasher::merge(&[pub_key_commitment.into(), message]);
let tx_summary_or_signature =
match process.advice_provider().get_mapped_values(&signature_key) {
Some(encoded_signature) => TxSummaryOrSignature::from_encoded_signature(
signature_key,
encoded_signature,
)?,
None => TxSummaryOrSignature::TxSummary(extract_tx_summary(
base_host, process, message,
)?),
};
Some(TransactionEvent::AuthRequest {
pub_key_commitment,
tx_summary_or_signature,
})
},
TransactionEventId::Unauthorized => {
let message = process.get_stack_word(1);
let tx_summary = extract_tx_summary(base_host, process, message)?;
Some(TransactionEvent::Unauthorized { tx_summary })
},
TransactionEventId::LinkMapSet => Some(TransactionEvent::LinkMapSet {
advice_mutation: LinkMap::handle_set_event(process),
}),
TransactionEventId::LinkMapGet => Some(TransactionEvent::LinkMapGet {
advice_mutation: LinkMap::handle_get_event(process),
}),
TransactionEventId::PrologueStart => Some(TransactionEvent::Progress(
TransactionProgressEvent::PrologueStart(process.clock()),
)),
TransactionEventId::PrologueEnd => Some(TransactionEvent::Progress(
TransactionProgressEvent::PrologueEnd(process.clock()),
)),
TransactionEventId::NotesProcessingStart => Some(TransactionEvent::Progress(
TransactionProgressEvent::NotesProcessingStart(process.clock()),
)),
TransactionEventId::NotesProcessingEnd => Some(TransactionEvent::Progress(
TransactionProgressEvent::NotesProcessingEnd(process.clock()),
)),
TransactionEventId::NoteExecutionStart => {
let note_id = process.get_active_note_id()?.ok_or_else(|| TransactionKernelError::other(
"note execution interval measurement is incorrect: check the placement of the start and the end of the interval",
))?;
Some(TransactionEvent::Progress(TransactionProgressEvent::NoteExecutionStart {
note_id,
clk: process.clock(),
}))
},
TransactionEventId::NoteExecutionEnd => Some(TransactionEvent::Progress(
TransactionProgressEvent::NoteExecutionEnd(process.clock()),
)),
TransactionEventId::TxScriptProcessingStart => Some(TransactionEvent::Progress(
TransactionProgressEvent::TxScriptProcessingStart(process.clock()),
)),
TransactionEventId::TxScriptProcessingEnd => Some(TransactionEvent::Progress(
TransactionProgressEvent::TxScriptProcessingEnd(process.clock()),
)),
TransactionEventId::EpilogueStart => Some(TransactionEvent::Progress(
TransactionProgressEvent::EpilogueStart(process.clock()),
)),
TransactionEventId::EpilogueEnd => Some(TransactionEvent::Progress(
TransactionProgressEvent::EpilogueEnd(process.clock()),
)),
TransactionEventId::EpilogueAuthProcStart => Some(TransactionEvent::Progress(
TransactionProgressEvent::EpilogueAuthProcStart(process.clock()),
)),
TransactionEventId::EpilogueAuthProcEnd => Some(TransactionEvent::Progress(
TransactionProgressEvent::EpilogueAuthProcEnd(process.clock()),
)),
};
Ok(tx_event)
}
}
#[expect(clippy::large_enum_variant)]
#[derive(Debug)]
pub(crate) enum TxSummaryOrSignature {
TxSummary(TransactionSummary),
Signature(Vec<Felt>),
}
impl TxSummaryOrSignature {
fn from_encoded_signature(
signature_key: Word,
encoded_signature: &[Felt],
) -> Result<Self, TransactionKernelError> {
if encoded_signature.is_empty()
|| encoded_signature.len() > Signature::MAX_NUM_ENCODED_SIGNATURE_FELTS
{
return Err(TransactionKernelError::InvalidEncodedSignatureLength {
signature_key,
actual: encoded_signature.len(),
});
}
Ok(TxSummaryOrSignature::Signature(encoded_signature.to_vec()))
}
}
#[derive(Debug)]
pub(crate) struct AssetPatch {
pub asset_id: AssetId,
pub initial_vault_value: Word,
pub final_vault_value: Word,
}
#[derive(Debug, Clone)]
pub(crate) struct AssetDelta {
pub delta_op: AssetDeltaOperation,
pub asset: Asset,
}
#[derive(Debug)]
pub(crate) enum RecipientData {
Digest(Word),
Recipient(NoteRecipient),
ScriptMissing {
recipient_digest: Word,
serial_num: Word,
script_root: Word,
note_storage: NoteStorage,
},
}
fn on_account_vault_asset_accessed<'store, STORE>(
base_host: &TransactionBaseHost<'store, STORE>,
process: &ProcessorState,
asset_id: AssetId,
vault_root: Word,
) -> Result<Option<TransactionEvent>, TransactionKernelError> {
let leaf_index = Felt::try_from(asset_id.hash().to_leaf_index().position())
.expect("expected key index to be a felt");
let active_account_id = process.get_active_account_id()?;
let vault_root = if active_account_id == base_host.native_account_id() {
base_host.initial_account_header().vault_root()
} else {
vault_root
};
if process.has_merkle_path::<{ AssetVault::DEPTH }>(vault_root, leaf_index)? {
Ok(None)
} else {
Ok(Some(TransactionEvent::AccountVaultBeforeAssetAccess {
active_account_id,
vault_root,
asset_id,
}))
}
}
fn on_account_storage_map_item_accessed<'store, STORE>(
base_host: &TransactionBaseHost<'store, STORE>,
process: &ProcessorState,
slot_ptr: Felt,
map_key: StorageMapKey,
) -> Result<Option<TransactionEvent>, TransactionKernelError> {
let (slot_id, slot_type, current_map_root) = process.get_storage_slot(slot_ptr)?;
if !slot_type.is_map() {
return Err(TransactionKernelError::other(format!(
"expected slot to be of type map, found {slot_type}"
)));
}
let active_account_id = process.get_active_account_id()?;
let leaf_index: Felt = map_key
.hash()
.to_leaf_index()
.position()
.try_into()
.expect("expected key index to be a felt");
let map_root = if active_account_id == base_host.native_account_id() {
let slot_header = base_host.initial_account_storage_slot(slot_id)?;
if slot_header.slot_type() != StorageSlotType::Map {
return Err(TransactionKernelError::other(format!(
"expected slot {slot_id} to be of type map"
)));
}
slot_header.value()
} else {
current_map_root
};
if process.has_merkle_path::<{ StorageMap::DEPTH }>(current_map_root, leaf_index)? {
Ok(None)
} else {
Ok(Some(TransactionEvent::AccountStorageBeforeMapItemAccess {
active_account_id,
map_root,
map_key,
}))
}
}
fn extract_tx_summary<'store, STORE>(
base_host: &TransactionBaseHost<'store, STORE>,
process: &ProcessorState,
message: Word,
) -> Result<TransactionSummary, TransactionKernelError> {
let Some(commitments) = process.advice_provider().get_mapped_values(&message) else {
return Err(TransactionKernelError::TransactionSummaryConstructionFailed(
"expected message to exist in advice provider".into(),
));
};
let (expiration_delta, user_params) = TransactionSummary::try_params_from_elements(commitments)
.map_err(|source| {
TransactionKernelError::TransactionSummaryConstructionFailed(Box::new(source))
})?;
let account_delta_commitment = extract_word(commitments, 0);
let input_notes_commitment = extract_word(commitments, 4);
let output_notes_commitment = extract_word(commitments, 8);
let block_commitment = extract_word(commitments, 12);
let expected_expiration_delta = process.get_expiration_block_delta()?;
if expiration_delta != expected_expiration_delta {
return Err(TransactionKernelError::TransactionSummaryExpirationDeltaMismatch {
expected: expected_expiration_delta,
actual: expiration_delta,
});
}
let tx_summary = base_host.build_tx_summary(
account_delta_commitment,
input_notes_commitment,
output_notes_commitment,
block_commitment,
expiration_delta,
user_params,
)?;
if tx_summary.to_commitment() != message {
return Err(TransactionKernelError::TransactionSummaryConstructionFailed(
"transaction summary doesn't commit to the expected message".into(),
));
}
Ok(tx_summary)
}
fn build_note_metadata(
sender: AccountId,
note_type: Felt,
tag: Felt,
) -> Result<PartialNoteMetadata, TransactionKernelError> {
let note_type = u8::try_from(note_type.as_canonical_u64())
.map_err(|_| TransactionKernelError::other("failed to decode note_type into u8"))
.and_then(|note_type_byte| {
NoteType::try_from(note_type_byte).map_err(|source| {
TransactionKernelError::other_with_source(
"failed to decode note_type from u8",
source,
)
})
})?;
let tag = u32::try_from(tag.as_canonical_u64())
.map_err(|_| TransactionKernelError::other("failed to decode note tag into u32"))
.map(NoteTag::new)?;
Ok(PartialNoteMetadata::new(sender, note_type).with_tag(tag))
}
fn extract_note_attachment(
attachment_scheme: Felt,
attachment_commitment: Word,
note_ptr: Felt,
advice_provider: &AdviceProvider,
) -> Result<(usize, NoteAttachment), TransactionKernelError> {
let note_idx = note_ptr_to_idx(note_ptr)?;
let attachment_scheme = u16::try_from(attachment_scheme.as_canonical_u64())
.map_err(|_| TransactionKernelError::other("failed to convert attachment scheme to u16"))
.and_then(|scheme| {
NoteAttachmentScheme::try_from(scheme).map_err(|source| {
TransactionKernelError::other_with_source(
"failed to convert u16 to attachment scheme",
source,
)
})
})?;
let elements = advice_provider.get_mapped_values(&attachment_commitment).ok_or_else(|| {
TransactionKernelError::other(
"elements of a note attachment commitment must be present in the advice provider",
)
})?;
if elements.is_empty() {
return Err(TransactionKernelError::other(
"num elements in attachment advice map value must not be empty",
));
}
if !elements.len().is_multiple_of(WORD_SIZE) {
return Err(TransactionKernelError::other(
"num elements in attachment advice map value must be multiple of word size",
));
}
let words: Vec<Word> = elements
.as_chunks::<WORD_SIZE>()
.0
.iter()
.map(|chunk| Word::from([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
let content = NoteAttachmentContent::new(words).map_err(|source| {
TransactionKernelError::other_with_source(
"failed to construct note attachment content",
source,
)
})?;
let attachment = NoteAttachment::new(attachment_scheme, content);
let actual_commitment = attachment.to_commitment();
if actual_commitment != attachment_commitment {
return Err(TransactionKernelError::NoteAttachmentCommitmentMismatch {
actual: actual_commitment,
provided: attachment_commitment,
});
}
Ok((note_idx as usize, attachment))
}
#[inline(always)]
fn extract_word(commitments: &[Felt], start: usize) -> Word {
Word::from([
commitments[start],
commitments[start + 1],
commitments[start + 2],
commitments[start + 3],
])
}
fn note_ptr_to_idx(note_ptr: Felt) -> Result<u32, TransactionKernelError> {
u32::try_from(note_ptr.as_canonical_u64())
.map_err(|_| TransactionKernelError::other("failed to convert note_ptr to u32"))
.and_then(|note_ptr| {
note_ptr
.checked_sub(OUTPUT_NOTE_SECTION_OFFSET)
.ok_or_else(|| {
TransactionKernelError::other("failed to calculate note_idx from note_ptr")
})
.map(|note_ptr| note_ptr / NOTE_MEM_SIZE)
})
}