use alloc::vec::Vec;
use miden_processor::ProcessorState;
use miden_processor::advice::{AdviceMutation, AdviceProvider};
use miden_processor::trace::RowIndex;
use miden_protocol::account::{
AccountId,
StorageMap,
StorageMapKey,
StorageSlotName,
StorageSlotType,
};
use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset};
use miden_protocol::note::{
NoteAttachment,
NoteAttachmentArray,
NoteAttachmentContent,
NoteAttachmentKind,
NoteAttachmentScheme,
NoteId,
NoteMetadata,
NoteRecipient,
NoteScript,
NoteStorage,
NoteTag,
NoteType,
};
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};
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),
EpilogueAfterTxCyclesObtained(RowIndex),
}
#[derive(Debug)]
pub(crate) enum TransactionEvent {
AccountBeforeForeignLoad {
foreign_account_id: AccountId,
},
AccountVaultAfterRemoveAsset {
asset: Asset,
},
AccountVaultAfterAddAsset {
asset: Asset,
},
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_key: AssetVaultKey,
},
AccountAfterIncrementNonce,
AccountPushProcedureIndex {
code_commitment: Word,
procedure_root: Word,
},
NoteBeforeCreated {
note_idx: usize,
metadata: NoteMetadata,
recipient_data: RecipientData,
},
NoteBeforeAddAsset {
note_idx: usize,
asset: Asset,
},
NoteBeforeSetAttachment {
note_idx: usize,
attachment: NoteAttachment,
},
AuthRequest {
pub_key_hash: Word,
tx_summary: TransactionSummary,
signature: Option<Vec<Felt>>,
},
Unauthorized {
tx_summary: TransactionSummary,
},
EpilogueBeforeTxFeeRemovedFromAccount {
fee_asset: FungibleAsset,
},
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,
)
})?;
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 => {
let asset_vault_key = process.get_stack_word(1);
let vault_root_ptr = process.get_stack_item(9);
let asset_vault_key =
AssetVaultKey::try_from(asset_vault_key).map_err(|source| {
TransactionKernelError::MalformedAssetInEventHandler {
handler: "AccountVaultBefore{Add,Remove}Asset",
source,
}
})?;
let current_vault_root = process.get_vault_root(vault_root_ptr)?;
on_account_vault_asset_accessed(
base_host,
process,
asset_vault_key,
current_vault_root,
)?
},
TransactionEventId::AccountVaultAfterRemoveAsset => {
let asset_key = process.get_stack_word(1);
let asset_value = process.get_stack_word(5);
let asset =
Asset::from_key_value_words(asset_key, asset_value).map_err(|source| {
TransactionKernelError::MalformedAssetInEventHandler {
handler: "AccountVaultAfterRemoveAsset",
source,
}
})?;
Some(TransactionEvent::AccountVaultAfterRemoveAsset { asset })
},
TransactionEventId::AccountVaultAfterAddAsset => {
let asset_key = process.get_stack_word(1);
let asset_value = process.get_stack_word(5);
let asset =
Asset::from_key_value_words(asset_key, asset_value).map_err(|source| {
TransactionKernelError::MalformedAssetInEventHandler {
handler: "AccountVaultAfterAddAsset",
source,
}
})?;
Some(TransactionEvent::AccountVaultAfterAddAsset { asset })
},
TransactionEventId::AccountVaultBeforeGetAsset => {
let asset_key = process.get_stack_word(1);
let vault_root_ptr = process.get_stack_item(5);
let asset_key = AssetVaultKey::try_from(asset_key).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_key, 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_key = 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_key_value_words(asset_key, 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::NoteBeforeSetAttachment => {
let attachment_scheme = process.get_stack_item(1);
let attachment_kind = process.get_stack_item(2);
let note_ptr = process.get_stack_item(3);
let attachment = process.get_stack_word(5);
let (note_idx, attachment) = extract_note_attachment(
attachment_scheme,
attachment_kind,
attachment,
note_ptr,
process.advice_provider(),
)?;
Some(TransactionEvent::NoteBeforeSetAttachment { note_idx, attachment })
},
TransactionEventId::AuthRequest => {
let message = process.get_stack_word(1);
let pub_key_hash = process.get_stack_word(5);
let signature_key = Hasher::merge(&[pub_key_hash, message]);
let signature = process
.advice_provider()
.get_mapped_values(&signature_key)
.map(|slice| slice.to_vec());
let tx_summary = extract_tx_summary(base_host, process, message)?;
Some(TransactionEvent::AuthRequest { pub_key_hash, tx_summary, 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::EpilogueBeforeTxFeeRemovedFromAccount => {
let fee_asset_key = process.get_stack_word(1);
let fee_asset_value = process.get_stack_word(5);
let fee_asset = FungibleAsset::from_key_value_words(fee_asset_key, fee_asset_value)
.map_err(TransactionKernelError::FailedToConvertFeeAsset)?;
Some(TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount { fee_asset })
},
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()),
)),
TransactionEventId::EpilogueAfterTxCyclesObtained => Some(TransactionEvent::Progress(
TransactionProgressEvent::EpilogueAfterTxCyclesObtained(process.clock()),
)),
};
Ok(tx_event)
}
}
#[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,
vault_key: AssetVaultKey,
vault_root: Word,
) -> Result<Option<TransactionEvent>, TransactionKernelError> {
let leaf_index = Felt::try_from(vault_key.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_key: vault_key,
}))
}
}
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(),
));
};
if commitments.len() != 16 {
return Err(TransactionKernelError::TransactionSummaryConstructionFailed(
"expected 4 words for transaction summary commitments".into(),
));
}
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 salt = extract_word(commitments, 12);
let tx_summary = base_host.build_tx_summary(
account_delta_commitment,
input_notes_commitment,
output_notes_commitment,
salt,
)?;
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<NoteMetadata, 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(NoteMetadata::new(sender, note_type).with_tag(tag))
}
fn extract_note_attachment(
attachment_scheme: Felt,
attachment_kind: Felt,
attachment: Word,
note_ptr: Felt,
advice_provider: &AdviceProvider,
) -> Result<(usize, NoteAttachment), TransactionKernelError> {
let note_idx = note_ptr_to_idx(note_ptr)?;
let attachment_kind = u8::try_from(attachment_kind.as_canonical_u64())
.map_err(|_| TransactionKernelError::other("failed to convert attachment kind to u8"))
.and_then(|attachment_kind| {
NoteAttachmentKind::try_from(attachment_kind).map_err(|source| {
TransactionKernelError::other_with_source(
"failed to convert u8 to attachment kind",
source,
)
})
})?;
let attachment_scheme = u32::try_from(attachment_scheme.as_canonical_u64())
.map_err(|_| TransactionKernelError::other("failed to convert attachment scheme to u32"))
.map(NoteAttachmentScheme::new)?;
let attachment_content = match attachment_kind {
NoteAttachmentKind::None => {
if !attachment.is_empty() {
return Err(TransactionKernelError::NoteAttachmentNoneIsNotEmpty);
}
NoteAttachmentContent::None
},
NoteAttachmentKind::Word => NoteAttachmentContent::Word(attachment),
NoteAttachmentKind::Array => {
let elements = advice_provider.get_mapped_values(&attachment).ok_or_else(|| {
TransactionKernelError::other(
"elements of a note attachment commitment must be present in the advice provider",
)
})?;
let commitment_attachment =
NoteAttachmentArray::new(elements.to_vec()).map_err(|source| {
TransactionKernelError::other_with_source(
"failed to construct note attachment commitment",
source,
)
})?;
if commitment_attachment.commitment() != attachment {
return Err(TransactionKernelError::NoteAttachmentArrayMismatch {
actual: commitment_attachment.commitment(),
provided: attachment,
});
}
NoteAttachmentContent::Array(commitment_attachment)
},
};
let attachment =
NoteAttachment::new(attachment_scheme, attachment_content).map_err(|source| {
TransactionKernelError::other_with_source("failed to extract note attachment", source)
})?;
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)
})
}