diff --git a/CHANGELOG.md b/CHANGELOG.md index 1edd194eba..2ef5f4d4f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [BREAKING] Allowed account components to share identical account code procedures ([#2164](https://github.com/0xMiden/miden-base/pull/2164)). - Add `From<&ExecutedTransaction> for TransactionHeader` implementation ([#2178](https://github.com/0xMiden/miden-base/pull/2178)). - Add `AccountId::parse()` helper function to parse both hex and bech32 formats ([#2223](https://github.com/0xMiden/miden-base/pull/2223)). +- Add `read_foreign_account_inputs()`, `read_vault_asset_witnesses()`, and `read_storage_map_witness()` for `TransactionInputs` ([#2246](https://github.com/0xMiden/miden-base/pull/2246)). ### Changes diff --git a/crates/miden-protocol/src/account/storage/header.rs b/crates/miden-protocol/src/account/storage/header.rs index 8f48bfe646..e6cb794ebf 100644 --- a/crates/miden-protocol/src/account/storage/header.rs +++ b/crates/miden-protocol/src/account/storage/header.rs @@ -1,3 +1,5 @@ +use alloc::collections::BTreeMap; +use alloc::format; use alloc::string::ToString; use alloc::vec::Vec; @@ -150,6 +152,49 @@ impl AccountStorageHeader { ::to_elements(self) } + /// Reconstructs an [`AccountStorageHeader`] from field elements with provided slot names. + /// + /// The elements are expected to be groups of 8 elements per slot: + /// `[[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE]` + pub fn try_from_elements( + elements: &[Felt], + slot_names: &BTreeMap, + ) -> Result { + if !elements.len().is_multiple_of(StorageSlot::NUM_ELEMENTS) { + return Err(AccountError::other( + "storage header elements length must be divisible by 8", + )); + } + + let mut slots = Vec::new(); + for chunk in elements.chunks_exact(StorageSlot::NUM_ELEMENTS) { + // Parse slot type from second element. + let slot_type_felt = chunk[1]; + let slot_type = slot_type_felt.try_into()?; + + // Parse slot ID from third and fourth elements. + let slot_id_suffix = chunk[2]; + let slot_id_prefix = chunk[3]; + let parsed_slot_id = StorageSlotId::new(slot_id_suffix, slot_id_prefix); + + // Retrieve slot name from the map. + let slot_name = slot_names.get(&parsed_slot_id).cloned().ok_or(AccountError::other( + format!("slot name not found for slot ID {}", parsed_slot_id), + ))?; + + // Parse slot value from last 4 elements. + let slot_value = Word::new([chunk[4], chunk[5], chunk[6], chunk[7]]); + + let slot_header = StorageSlotHeader::new(slot_name, slot_type, slot_value); + slots.push(slot_header); + } + + // Sort slots by ID. + slots.sort_by_key(|slot| slot.id()); + + Self::new(slots) + } + /// Returns the commitment to the [`AccountStorage`] this header represents. pub fn to_commitment(&self) -> Word { ::to_commitment(self) @@ -301,12 +346,15 @@ impl Deserializable for StorageSlotHeader { #[cfg(test)] mod tests { + use alloc::collections::BTreeMap; + use alloc::string::ToString; + use miden_core::Felt; use miden_core::utils::{Deserializable, Serializable}; use super::AccountStorageHeader; use crate::Word; - use crate::account::{AccountStorage, StorageSlotType}; + use crate::account::{AccountStorage, StorageSlotHeader, StorageSlotName, StorageSlotType}; use crate::testing::storage::{MOCK_MAP_SLOT, MOCK_VALUE_SLOT0, MOCK_VALUE_SLOT1}; #[test] @@ -344,4 +392,121 @@ mod tests { // assert deserialized == storage header assert_eq!(storage_header, deserialized); } + + #[test] + fn test_to_elements_from_elements_empty() { + // Construct empty header. + let empty_header = AccountStorageHeader::new(vec![]).unwrap(); + let empty_elements = empty_header.to_elements(); + + // Call from_elements. + let empty_slot_names = BTreeMap::new(); + let reconstructed_empty = + AccountStorageHeader::try_from_elements(&empty_elements, &empty_slot_names).unwrap(); + assert_eq!(empty_header, reconstructed_empty); + } + + #[test] + fn test_to_elements_from_elements_single_slot() { + // Construct single slot header. + let slot_name1 = StorageSlotName::new("test::value::slot1".to_string()).unwrap(); + let slot1 = StorageSlotHeader::new( + slot_name1, + StorageSlotType::Value, + Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + ); + + let single_slot_header = AccountStorageHeader::new(vec![slot1.clone()]).unwrap(); + let single_elements = single_slot_header.to_elements(); + + // Call from_elements. + let slot_names = BTreeMap::from([(slot1.id(), slot1.name().clone())]); + let reconstructed_single = + AccountStorageHeader::try_from_elements(&single_elements, &slot_names).unwrap(); + + assert_eq!(single_slot_header, reconstructed_single); + } + + #[test] + fn test_to_elements_from_elements_multiple_slot() { + // Construct multi slot header. + let slot_name2 = StorageSlotName::new("test::map::slot2".to_string()).unwrap(); + let slot_name3 = StorageSlotName::new("test::value::slot3".to_string()).unwrap(); + + let slot2 = StorageSlotHeader::new( + slot_name2, + StorageSlotType::Map, + Word::new([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]), + ); + let slot3 = StorageSlotHeader::new( + slot_name3, + StorageSlotType::Value, + Word::new([Felt::new(9), Felt::new(10), Felt::new(11), Felt::new(12)]), + ); + + let mut slots = vec![slot2, slot3]; + slots.sort_by_key(|slot| slot.id()); + let multi_slot_header = AccountStorageHeader::new(slots.clone()).unwrap(); + let multi_elements = multi_slot_header.to_elements(); + + // Call from_elements. + let slot_names = BTreeMap::from([ + (slots[0].id(), slots[0].name.clone()), + (slots[1].id(), slots[1].name.clone()), + ]); + let reconstructed_multi = + AccountStorageHeader::try_from_elements(&multi_elements, &slot_names).unwrap(); + + assert_eq!(multi_slot_header, reconstructed_multi); + } + + #[test] + fn test_from_elements_errors() { + // Test with invalid length (not divisible by 8). + let invalid_elements = vec![Felt::new(1), Felt::new(2), Felt::new(3)]; + let empty_slot_names = BTreeMap::new(); + assert!( + AccountStorageHeader::try_from_elements(&invalid_elements, &empty_slot_names).is_err() + ); + + // Test with invalid slot type. + let mut invalid_type_elements = vec![crate::ZERO; 8]; + invalid_type_elements[1] = Felt::new(5); // Invalid slot type. + assert!( + AccountStorageHeader::try_from_elements(&invalid_type_elements, &empty_slot_names) + .is_err() + ); + } + + #[test] + fn test_from_elements_with_slot_names() { + use alloc::collections::BTreeMap; + + // Create original slot with known name. + let slot_name1 = StorageSlotName::new("test::value::slot1".to_string()).unwrap(); + let slot1 = StorageSlotHeader::new( + slot_name1.clone(), + StorageSlotType::Value, + Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + ); + + // Serialize the single slot to elements + let elements = slot1.to_elements(); + + // Create slot names map using the slot's ID + let mut slot_names = BTreeMap::new(); + slot_names.insert(slot1.id(), slot_name1.clone()); + + // Test from_elements with provided slot names on raw slot elements. + let reconstructed_header = + AccountStorageHeader::try_from_elements(&elements, &slot_names).unwrap(); + + // Verify that the original slot names are preserved. + assert_eq!(reconstructed_header.slots().count(), 1); + let reconstructed_slot = reconstructed_header.slots().next().unwrap(); + + assert_eq!(slot_name1.as_str(), reconstructed_slot.name().as_str()); + assert_eq!(slot1.slot_type(), reconstructed_slot.slot_type()); + assert_eq!(slot1.value(), reconstructed_slot.value()); + } } diff --git a/crates/miden-protocol/src/account/storage/map/mod.rs b/crates/miden-protocol/src/account/storage/map/mod.rs index 2882496190..31bb7fa355 100644 --- a/crates/miden-protocol/src/account/storage/map/mod.rs +++ b/crates/miden-protocol/src/account/storage/map/mod.rs @@ -8,7 +8,7 @@ use crate::account::StorageMapDelta; use crate::crypto::merkle::InnerNodeInfo; use crate::crypto::merkle::smt::{LeafIndex, SMT_DEPTH, Smt, SmtLeaf}; use crate::errors::StorageMapError; -use crate::{AccountError, Felt, Hasher}; +use crate::{AccountError, Hasher}; mod partial; pub use partial::PartialStorageMap; @@ -204,16 +204,22 @@ impl StorageMap { self.entries } + // UTILITY FUNCTIONS + // -------------------------------------------------------------------------------------------- + /// Hashes the given key to get the key of the SMT. pub fn hash_key(raw_key: Word) -> Word { Hasher::hash_elements(raw_key.as_elements()) } - // TODO: Replace with https://github.com/0xMiden/crypto/issues/515 once implemented. + /// Returns leaf index of a raw map key. + pub fn map_key_to_leaf_index(raw_key: Word) -> LeafIndex { + Self::hash_key(raw_key).into() + } + /// Returns the leaf index of a map key. - pub fn hashed_map_key_to_leaf_index(hashed_map_key: Word) -> Felt { - // The third element in an SMT key is the index. - hashed_map_key[3] + pub fn hashed_map_key_to_leaf_index(hashed_map_key: Word) -> LeafIndex { + hashed_map_key.into() } } diff --git a/crates/miden-protocol/src/account/storage/slot/slot_id.rs b/crates/miden-protocol/src/account/storage/slot/slot_id.rs index 1fcaf52e4e..b22b46b2dd 100644 --- a/crates/miden-protocol/src/account/storage/slot/slot_id.rs +++ b/crates/miden-protocol/src/account/storage/slot/slot_id.rs @@ -5,6 +5,13 @@ use core::hash::Hash; use miden_core::utils::hash_string_to_word; use crate::Felt; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; /// The partial hash of a [`StorageSlotName`](super::StorageSlotName). /// @@ -91,6 +98,21 @@ impl Display for StorageSlotId { } } +impl Serializable for StorageSlotId { + fn write_into(&self, target: &mut W) { + self.suffix.write_into(target); + self.prefix.write_into(target); + } +} + +impl Deserializable for StorageSlotId { + fn read_from(source: &mut R) -> Result { + let suffix = Felt::read_from(source)?; + let prefix = Felt::read_from(source)?; + Ok(StorageSlotId::new(suffix, prefix)) + } +} + // TESTS // ================================================================================================ diff --git a/crates/miden-protocol/src/account/storage/slot/type.rs b/crates/miden-protocol/src/account/storage/slot/type.rs index 9fac8cada1..32da9c7e27 100644 --- a/crates/miden-protocol/src/account/storage/slot/type.rs +++ b/crates/miden-protocol/src/account/storage/slot/type.rs @@ -1,6 +1,8 @@ use alloc::string::ToString; use core::fmt::Display; +use miden_core::{ONE, ZERO}; + use crate::utils::serde::{ ByteReader, ByteWriter, @@ -54,6 +56,20 @@ impl TryFrom for StorageSlotType { } } +impl TryFrom for StorageSlotType { + type Error = AccountError; + + fn try_from(value: Felt) -> Result { + if value == ZERO { + Ok(StorageSlotType::Value) + } else if value == ONE { + Ok(StorageSlotType::Map) + } else { + Err(AccountError::other("invalid storage slot type")) + } + } +} + impl Display for StorageSlotType { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -100,6 +116,7 @@ mod tests { use miden_core::utils::{Deserializable, Serializable}; use crate::account::StorageSlotType; + use crate::{Felt, FieldElement}; #[test] fn test_serde_account_storage_slot_type() { @@ -112,4 +129,15 @@ mod tests { assert_eq!(type_0, deserialized_0); assert_eq!(type_1, deserialized_1); } + + #[test] + fn test_storage_slot_type_from_felt() { + let felt = Felt::ZERO; + let slot_type = StorageSlotType::try_from(felt).unwrap(); + assert_eq!(slot_type, StorageSlotType::Value); + + let felt = Felt::ONE; + let slot_type = StorageSlotType::try_from(felt).unwrap(); + assert_eq!(slot_type, StorageSlotType::Map); + } } diff --git a/crates/miden-protocol/src/block/account_tree/mod.rs b/crates/miden-protocol/src/block/account_tree/mod.rs index f48ce04425..2bff2b5f44 100644 --- a/crates/miden-protocol/src/block/account_tree/mod.rs +++ b/crates/miden-protocol/src/block/account_tree/mod.rs @@ -1,6 +1,8 @@ use alloc::string::ToString; use alloc::vec::Vec; +use miden_crypto::merkle::smt::LeafIndex; + use crate::Word; use crate::account::{AccountId, AccountIdPrefix}; use crate::crypto::merkle::MerkleError; @@ -46,6 +48,11 @@ pub fn smt_key_to_account_id(key: Word) -> AccountId { .expect("account tree should only contain valid IDs") } +/// Converts an AccountId to an SMT leaf index for use with MerkleStore operations. +pub fn account_id_to_smt_index(account_id: AccountId) -> LeafIndex { + account_id_to_smt_key(account_id).into() +} + // ACCOUNT TREE // ================================================================================================ diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 2dfd04eaa2..73c2b0688a 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -8,6 +8,7 @@ use miden_assembly::diagnostics::reporting::PrintDiagnostic; use miden_core::mast::MastForestError; use miden_core::{EventId, Felt}; use miden_crypto::merkle::mmr::MmrError; +use miden_crypto::merkle::smt::{SmtLeafError, SmtProofError}; use miden_crypto::utils::HexParseError; use miden_processor::DeserializationError; use thiserror::Error; @@ -694,6 +695,41 @@ pub enum TransactionInputError { TooManyInputNotes(usize), } +// TRANSACTION INPUTS EXTRACTION ERROR +// =============================================================================================== + +#[derive(Debug, Error)] +pub enum TransactionInputsExtractionError { + #[error("specified foreign account id matches the transaction input's account id")] + AccountNotForeign, + #[error("foreign account data not found in advice map for account {0}")] + ForeignAccountNotFound(AccountId), + #[error("foreign account code not found for account {0}")] + ForeignAccountCodeNotFound(AccountId), + #[error("storage header data not found in advice map for account {0}")] + StorageHeaderNotFound(AccountId), + #[error("failed to handle account data")] + AccountError(#[from] AccountError), + #[error("failed to handle merkle data")] + MerkleError(#[from] MerkleError), + #[error("failed to handle account tree data")] + AccountTreeError(#[from] AccountTreeError), + #[error("missing vault root from Merkle store")] + MissingVaultRoot, + #[error("missing storage map root from Merkle store")] + MissingMapRoot, + #[error("failed to construct SMT proof")] + SmtProofError(#[from] SmtProofError), + #[error("failed to construct asset witness")] + AssetError(#[from] AssetError), + #[error("failed to handle storage map data")] + StorageMapError(#[from] StorageMapError), + #[error("failed to convert elements to leaf index: {0}")] + LeafConversionError(String), + #[error("failed to construct SMT leaf")] + SmtLeafError(#[from] SmtLeafError), +} + // TRANSACTION OUTPUT ERROR // =============================================================================================== diff --git a/crates/miden-protocol/src/transaction/inputs/mod.rs b/crates/miden-protocol/src/transaction/inputs/mod.rs index 7e131e360a..ef3d6b367d 100644 --- a/crates/miden-protocol/src/transaction/inputs/mod.rs +++ b/crates/miden-protocol/src/transaction/inputs/mod.rs @@ -1,21 +1,41 @@ +use alloc::collections::{BTreeMap, BTreeSet}; use alloc::vec::Vec; use core::fmt::Debug; use miden_core::utils::{Deserializable, Serializable}; +use miden_crypto::merkle::NodeIndex; +use miden_crypto::merkle::smt::{LeafIndex, SmtLeaf, SmtProof}; use super::PartialBlockchain; -use crate::TransactionInputError; -use crate::account::{AccountCode, PartialAccount}; -use crate::asset::AssetWitness; +use crate::account::{ + AccountCode, + AccountHeader, + AccountId, + AccountStorageHeader, + PartialAccount, + PartialStorage, + StorageMap, + StorageMapWitness, + StorageSlotId, + StorageSlotName, +}; +use crate::asset::{AssetVaultKey, AssetWitness, PartialVault}; +use crate::block::account_tree::{AccountWitness, account_id_to_smt_index}; use crate::block::{BlockHeader, BlockNumber}; +use crate::crypto::merkle::SparseMerklePath; +use crate::errors::TransactionInputsExtractionError; use crate::note::{Note, NoteInclusionProof}; -use crate::transaction::{TransactionArgs, TransactionScript}; +use crate::transaction::{TransactionAdviceInputs, TransactionArgs, TransactionScript}; +use crate::{Felt, TransactionInputError, Word}; + +#[cfg(test)] +mod tests; mod account; pub use account::AccountInputs; mod notes; -use miden_processor::AdviceInputs; +use miden_processor::{AdviceInputs, SMT_DEPTH}; pub use notes::{InputNote, InputNotes, ToInputNoteCommitments}; // TRANSACTION INPUTS @@ -33,6 +53,8 @@ pub struct TransactionInputs { foreign_account_code: Vec, /// Pre-fetched asset witnesses for note assets and the fee asset. asset_witnesses: Vec, + /// Storage slot names for foreign accounts. + foreign_account_slot_names: BTreeMap, } impl TransactionInputs { @@ -89,6 +111,7 @@ impl TransactionInputs { advice_inputs: AdviceInputs::default(), foreign_account_code: Vec::new(), asset_witnesses: Vec::new(), + foreign_account_slot_names: BTreeMap::new(), }) } @@ -110,6 +133,15 @@ impl TransactionInputs { self } + /// Replaces the transaction inputs and assigns the given foreign account slot names. + pub fn with_foreign_account_slot_names( + mut self, + foreign_account_slot_names: BTreeMap, + ) -> Self { + self.foreign_account_slot_names = foreign_account_slot_names; + self + } + /// Replaces the transaction inputs and assigns the given advice inputs. pub fn with_advice_inputs(mut self, advice_inputs: AdviceInputs) -> Self { self.set_advice_inputs(advice_inputs); @@ -183,6 +215,11 @@ impl TransactionInputs { &self.asset_witnesses } + /// Returns the foreign account storage slot names. + pub fn foreign_account_slot_names(&self) -> &BTreeMap { + &self.foreign_account_slot_names + } + /// Returns the advice inputs to be consumed in the transaction. pub fn advice_inputs(&self) -> &AdviceInputs { &self.advice_inputs @@ -193,6 +230,167 @@ impl TransactionInputs { &self.tx_args } + // DATA EXTRACTORS + // -------------------------------------------------------------------------------------------- + + /// Reads the storage map witness for the given account and map key. + pub fn read_storage_map_witness( + &self, + map_root: Word, + map_key: Word, + ) -> Result { + // Convert map key into the index at which the key-value pair for this key is stored + let leaf_index = StorageMap::map_key_to_leaf_index(map_key); + + // Construct sparse Merkle path. + let merkle_path = self.advice_inputs.store.get_path(map_root, leaf_index.into())?; + let sparse_path = SparseMerklePath::from_sized_iter(merkle_path.path)?; + + // Construct SMT leaf. + let merkle_node = self.advice_inputs.store.get_node(map_root, leaf_index.into())?; + let smt_leaf_elements = self + .advice_inputs + .map + .get(&merkle_node) + .ok_or(TransactionInputsExtractionError::MissingVaultRoot)?; + let smt_leaf = smt_leaf_from_elements(smt_leaf_elements, leaf_index)?; + + // Construct SMT proof and witness. + let smt_proof = SmtProof::new(sparse_path, smt_leaf)?; + let storage_witness = StorageMapWitness::new(smt_proof, [map_key])?; + + Ok(storage_witness) + } + + /// Reads the vault asset witnesses for the given account and vault keys. + pub fn read_vault_asset_witnesses( + &self, + vault_root: Word, + vault_keys: BTreeSet, + ) -> Result, TransactionInputsExtractionError> { + let mut asset_witnesses = Vec::new(); + for vault_key in vault_keys { + let smt_index = vault_key.to_leaf_index(); + // Construct sparse Merkle path. + let merkle_path = self.advice_inputs.store.get_path(vault_root, smt_index.into())?; + let sparse_path = SparseMerklePath::from_sized_iter(merkle_path.path)?; + + // Construct SMT leaf. + let merkle_node = self.advice_inputs.store.get_node(vault_root, smt_index.into())?; + let smt_leaf_elements = self + .advice_inputs + .map + .get(&merkle_node) + .ok_or(TransactionInputsExtractionError::MissingVaultRoot)?; + let smt_leaf = smt_leaf_from_elements(smt_leaf_elements, smt_index)?; + + // Construct SMT proof and witness. + let smt_proof = SmtProof::new(sparse_path, smt_leaf)?; + let asset_witness = AssetWitness::new(smt_proof)?; + asset_witnesses.push(asset_witness); + } + Ok(asset_witnesses) + } + + /// Reads AccountInputs for a foreign account from the advice inputs. + /// + /// This function reverses the process of [`TransactionAdviceInputs::add_foreign_accounts`] by: + /// 1. Reading the account header from the advice map using the account_id_key. + /// 2. Building a PartialAccount from the header and foreign account code. + /// 3. Creating an AccountWitness. + pub fn read_foreign_account_inputs( + &self, + account_id: AccountId, + ) -> Result { + if account_id == self.account().id() { + return Err(TransactionInputsExtractionError::AccountNotForeign); + } + + // Read the account header elements from the advice map. + let account_id_key = TransactionAdviceInputs::account_id_map_key(account_id); + let header_elements = self + .advice_inputs + .map + .get(&account_id_key) + .ok_or(TransactionInputsExtractionError::ForeignAccountNotFound(account_id))?; + + // Parse the header from elements. + let header = AccountHeader::try_from_elements(header_elements)?; + + // Construct and return account inputs. + let partial_account = self.read_foreign_partial_account(&header)?; + let witness = self.read_foreign_account_witness(&header)?; + Ok(AccountInputs::new(partial_account, witness)) + } + + /// Reads a foreign partial account from the advice inputs based on the account ID corresponding + /// to the provided header. + fn read_foreign_partial_account( + &self, + header: &AccountHeader, + ) -> Result { + // Derive the partial vault from the header. + let partial_vault = PartialVault::new(header.vault_root()); + + // Find the corresponding foreign account code. + let account_code = self + .foreign_account_code + .iter() + .find(|code| code.commitment() == header.code_commitment()) + .ok_or(TransactionInputsExtractionError::ForeignAccountCodeNotFound(header.id()))? + .clone(); + + // Try to get storage header from advice map using storage commitment as key. + let storage_header_elements = self + .advice_inputs + .map + .get(&header.storage_commitment()) + .ok_or(TransactionInputsExtractionError::StorageHeaderNotFound(header.id()))?; + + // Get slot names for this foreign account, or use empty map if not available. + let storage_header = AccountStorageHeader::try_from_elements( + storage_header_elements, + self.foreign_account_slot_names(), + )?; + + // Build partial storage. + let partial_storage = PartialStorage::new(storage_header, [])?; + + // Create the partial account. + let partial_account = PartialAccount::new( + header.id(), + header.nonce(), + account_code, + partial_storage, + partial_vault, + None, // We know that foreign accounts are existing accounts so a seed is not required. + )?; + + Ok(partial_account) + } + + /// Reads a foreign account witness from the advice inputs based on the account ID corresponding + /// to the provided header. + fn read_foreign_account_witness( + &self, + header: &AccountHeader, + ) -> Result { + // Get the account tree root from the block header. + let account_tree_root = self.block_header.account_root(); + let leaf_index: NodeIndex = account_id_to_smt_index(header.id()).into(); + + // Get the Merkle path from the merkle store. + let merkle_path = self.advice_inputs.store.get_path(account_tree_root, leaf_index)?; + + // Convert the Merkle path to SparseMerklePath. + let sparse_path = SparseMerklePath::from_sized_iter(merkle_path.path)?; + + // Create the account witness. + let witness = AccountWitness::new(header.id(), header.commitment(), sparse_path)?; + + Ok(witness) + } + // CONVERSIONS // -------------------------------------------------------------------------------------------- @@ -222,6 +420,9 @@ impl TransactionInputs { } } +// SERIALIZATION / DESERIALIZATION +// ================================================================================================ + impl Serializable for TransactionInputs { fn write_into(&self, target: &mut W) { self.account.write_into(target); @@ -232,6 +433,7 @@ impl Serializable for TransactionInputs { self.advice_inputs.write_into(target); self.foreign_account_code.write_into(target); self.asset_witnesses.write_into(target); + self.foreign_account_slot_names.write_into(target); } } @@ -247,6 +449,8 @@ impl Deserializable for TransactionInputs { let advice_inputs = AdviceInputs::read_from(source)?; let foreign_account_code = Vec::::read_from(source)?; let asset_witnesses = Vec::::read_from(source)?; + let foreign_account_slot_names = + BTreeMap::::read_from(source)?; Ok(TransactionInputs { account, @@ -257,6 +461,7 @@ impl Deserializable for TransactionInputs { advice_inputs, foreign_account_code, asset_witnesses, + foreign_account_slot_names, }) } } @@ -264,6 +469,58 @@ impl Deserializable for TransactionInputs { // HELPER FUNCTIONS // ================================================================================================ +// TODO(sergerad): Move this fn to crypto SmtLeaf::try_from_elements. +pub fn smt_leaf_from_elements( + elements: &[Felt], + leaf_index: LeafIndex, +) -> Result { + use miden_crypto::merkle::smt::SmtLeaf; + + // Based on the miden-crypto SMT leaf serialization format. + + if elements.is_empty() { + return Ok(SmtLeaf::new_empty(leaf_index)); + } + + // Elements should be organized into a contiguous array of K/V Words (4 Felts each). + if !elements.len().is_multiple_of(8) { + return Err(TransactionInputsExtractionError::LeafConversionError( + "invalid SMT leaf format: elements length must be divisible by 8".into(), + )); + } + + let num_entries = elements.len() / 8; + + if num_entries == 1 { + // Single entry. + let key = Word::new([elements[0], elements[1], elements[2], elements[3]]); + let value = Word::new([elements[4], elements[5], elements[6], elements[7]]); + Ok(SmtLeaf::new_single(key, value)) + } else { + // Multiple entries. + let mut entries = Vec::with_capacity(num_entries); + // Read k/v pairs from each entry. + for i in 0..num_entries { + let base_idx = i * 8; + let key = Word::new([ + elements[base_idx], + elements[base_idx + 1], + elements[base_idx + 2], + elements[base_idx + 3], + ]); + let value = Word::new([ + elements[base_idx + 4], + elements[base_idx + 5], + elements[base_idx + 6], + elements[base_idx + 7], + ]); + entries.push((key, value)); + } + let leaf = SmtLeaf::new_multiple(entries)?; + Ok(leaf) + } +} + /// Validates whether the provided note belongs to the note tree of the specified block. fn validate_is_in_block( note: &Note, diff --git a/crates/miden-protocol/src/transaction/inputs/tests.rs b/crates/miden-protocol/src/transaction/inputs/tests.rs new file mode 100644 index 0000000000..09500ef25b --- /dev/null +++ b/crates/miden-protocol/src/transaction/inputs/tests.rs @@ -0,0 +1,375 @@ +use alloc::string::ToString; +use alloc::vec; +use std::collections::BTreeMap; +use std::vec::Vec; + +use miden_core::utils::{Deserializable, Serializable}; + +use crate::account::{ + AccountCode, + AccountHeader, + AccountId, + AccountStorageHeader, + PartialAccount, + PartialStorage, + StorageSlotHeader, + StorageSlotName, + StorageSlotType, +}; +use crate::asset::PartialVault; +use crate::errors::TransactionInputsExtractionError; +use crate::testing::account_id::{ + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, +}; +use crate::transaction::TransactionInputs; +use crate::{Felt, Word}; + +#[test] +fn test_read_foreign_account_inputs_missing_data() { + let native_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + let foreign_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(); + + // Create minimal transaction inputs with empty advice map. + let code = AccountCode::mock(); + let storage_header = AccountStorageHeader::new(vec![]).unwrap(); + let partial_storage = PartialStorage::new(storage_header, []).unwrap(); + let partial_vault = PartialVault::new(Word::default()); + let partial_account = PartialAccount::new( + native_account_id, + Felt::new(10), + code, + partial_storage, + partial_vault, + None, + ) + .unwrap(); + + let tx_inputs = TransactionInputs { + account: partial_account, + block_header: crate::block::BlockHeader::mock(0, None, None, &[], Word::default()), + blockchain: crate::transaction::PartialBlockchain::default(), + input_notes: crate::transaction::InputNotes::new(vec![]).unwrap(), + tx_args: crate::transaction::TransactionArgs::default(), + advice_inputs: crate::vm::AdviceInputs::default(), + foreign_account_code: Vec::new(), + asset_witnesses: Vec::new(), + foreign_account_slot_names: BTreeMap::new(), + }; + + // Try to read foreign account that doesn't exist in advice map. + let result = tx_inputs.read_foreign_account_inputs(foreign_account_id); + + assert!( + matches!(result, Err(TransactionInputsExtractionError::ForeignAccountNotFound(id)) if id == foreign_account_id) + ); +} + +#[test] +fn test_read_foreign_account_inputs_with_storage_data() { + use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2; + + let native_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + let foreign_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(); + + // Create minimal transaction inputs with proper advice map. + let code = AccountCode::mock(); + let storage_header = AccountStorageHeader::new(vec![]).unwrap(); + let partial_storage = PartialStorage::new(storage_header, []).unwrap(); + let partial_vault = PartialVault::new(Word::default()); + let partial_account = PartialAccount::new( + native_account_id, + Felt::new(10), + code.clone(), + partial_storage, + partial_vault, + None, + ) + .unwrap(); + + // Create foreign account header and storage data. + let foreign_header = AccountHeader::new( + foreign_account_id, + Felt::new(5), + Word::default(), + Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + code.commitment(), + ); + + // Create storage slots with test data. + let slot_name1 = StorageSlotName::new("test::slot1::value".to_string()).unwrap(); + let slot_name2 = StorageSlotName::new("test::slot2::value".to_string()).unwrap(); + let slot1 = StorageSlotHeader::new( + slot_name1, + StorageSlotType::Value, + Word::new([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]), + ); + let slot2 = StorageSlotHeader::new( + slot_name2, + StorageSlotType::Map, + Word::new([Felt::new(50), Felt::new(60), Felt::new(70), Felt::new(80)]), + ); + + let mut slots = vec![slot1, slot2]; + slots.sort_by_key(|slot| slot.id()); + let foreign_storage_header = AccountStorageHeader::new(slots.clone()).unwrap(); + + // Create advice inputs with both account header and storage header. + let mut advice_inputs = crate::vm::AdviceInputs::default(); + let account_id_key = + crate::transaction::TransactionAdviceInputs::account_id_map_key(foreign_account_id); + advice_inputs.map.insert(account_id_key, foreign_header.as_elements().to_vec()); + advice_inputs + .map + .insert(foreign_header.storage_commitment(), foreign_storage_header.to_elements()); + + let foreign_account_slot_names = BTreeMap::from([ + (slots[0].id(), slots[0].name().clone()), + (slots[1].id(), slots[1].name().clone()), + ]); + let tx_inputs = TransactionInputs { + account: partial_account, + block_header: crate::block::BlockHeader::mock(0, None, None, &[], Word::default()), + blockchain: crate::transaction::PartialBlockchain::default(), + input_notes: crate::transaction::InputNotes::new(vec![]).unwrap(), + tx_args: crate::transaction::TransactionArgs::default(), + advice_inputs, + foreign_account_code: vec![code], + asset_witnesses: Vec::new(), + foreign_account_slot_names, + }; + + // Try to read foreign account with storage data. + // Should succeed and create partial account with proper storage. + let account_inputs = tx_inputs.read_foreign_account_inputs(foreign_account_id).unwrap(); + assert_eq!(account_inputs.id(), foreign_account_id); + assert_eq!(account_inputs.account().nonce(), Felt::new(5)); + + // Verify storage was properly reconstructed. + let storage = account_inputs.account().storage(); + assert_eq!(storage.header().slots().count(), 2); + + // Verify witness data is valid. + let witness = account_inputs.witness(); + assert_eq!(witness.id(), foreign_account_id); + + // Verify the witness can compute a valid account root. + let computed_root = account_inputs.compute_account_root(); + assert!( + computed_root.is_ok(), + "Failed to compute account root from witness: {:?}", + computed_root.err() + ); + + // Test that the witness path has the expected depth for SMT. + assert_eq!(witness.path().depth(), 64, "Witness path should have SMT depth of 64"); +} + +#[test] +fn test_read_foreign_account_inputs_with_proper_witness() { + use crate::block::account_tree::AccountTree; + use crate::crypto::merkle::smt::Smt; + use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2; + + let native_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + let foreign_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(); + + // Create a native account. + let code = AccountCode::mock(); + let storage_header = AccountStorageHeader::new(vec![]).unwrap(); + let partial_storage = PartialStorage::new(storage_header, []).unwrap(); + let partial_vault = PartialVault::new(Word::default()); + let native_account = PartialAccount::new( + native_account_id, + Felt::new(10), + code.clone(), + partial_storage, + partial_vault, + None, + ) + .unwrap(); + + // Create a foreign account with proper commitment. + let foreign_header = AccountHeader::new( + foreign_account_id, + Felt::new(5), + Word::default(), + Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + code.commitment(), + ); + + // Create storage header for the foreign account. + let foreign_storage_header = AccountStorageHeader::new(vec![]).unwrap(); + + // Create an account tree and insert both accounts to get proper Merkle paths. + let mut account_tree = AccountTree::::default(); + + // Insert native account. + let native_commitment = AccountHeader::from(&native_account).commitment(); + account_tree.insert(native_account_id, native_commitment).unwrap(); + + // Insert foreign account. + let _foreign_partial_account = PartialAccount::new( + foreign_account_id, + Felt::new(5), + code.clone(), + PartialStorage::new(foreign_storage_header.clone(), []).unwrap(), + PartialVault::new(Word::default()), + None, + ) + .unwrap(); + account_tree.insert(foreign_account_id, foreign_header.commitment()).unwrap(); + + // Get the account tree root and create witness. + let account_tree_root = account_tree.root(); + let foreign_witness = account_tree.open(foreign_account_id); + + // Create advice inputs with proper Merkle store data. + let mut advice_inputs = crate::vm::AdviceInputs::default(); + + // Add account header to advice map. + let account_id_key = + crate::transaction::TransactionAdviceInputs::account_id_map_key(foreign_account_id); + advice_inputs.map.insert(account_id_key, foreign_header.as_elements().to_vec()); + + // Add storage header to advice map. + advice_inputs + .map + .insert(foreign_header.storage_commitment(), foreign_storage_header.to_elements()); + + // Add authenticated nodes from the witness to the Merkle store. + advice_inputs.store.extend(foreign_witness.authenticated_nodes()); + + // Add the account leaf to the advice map (needed for witness verification). + let leaf = foreign_witness.leaf(); + advice_inputs.map.insert(leaf.hash(), leaf.to_elements()); + + // Create block header with the account tree root. + let block_header = crate::block::BlockHeader::mock(0, None, None, &[], account_tree_root); + + let tx_inputs = TransactionInputs { + account: native_account, + block_header, + blockchain: crate::transaction::PartialBlockchain::default(), + input_notes: crate::transaction::InputNotes::new(vec![]).unwrap(), + tx_args: crate::transaction::TransactionArgs::default(), + advice_inputs, + foreign_account_code: vec![code], + asset_witnesses: Vec::new(), + foreign_account_slot_names: BTreeMap::new(), + }; + + // Test reading foreign account inputs. + // Should succeed and create proper witness. + let account_inputs = tx_inputs.read_foreign_account_inputs(foreign_account_id).unwrap(); + assert_eq!(account_inputs.id(), foreign_account_id); + assert_eq!(account_inputs.account().nonce(), Felt::new(5)); + + // Verify witness data. + let witness = account_inputs.witness(); + assert_eq!(witness.id(), foreign_account_id); + + // Verify the witness contains the expected account ID and can compute a root. + let computed_root = account_inputs.compute_account_root(); + assert!( + computed_root.is_ok(), + "Failed to compute account root from witness: {:?}", + computed_root.err() + ); + + // The computed root should be consistent - we're mainly testing that + // the witness was properly reconstructed from the Merkle store data. + let _computed_root_value = computed_root.unwrap(); + + // Test that the witness path has the expected depth (64 for SMT). + assert_eq!(witness.path().depth(), 64, "Witness path should have SMT depth of 64"); +} + +#[test] +fn test_transaction_inputs_serialization_with_foreign_slot_names() { + use miden_core::Felt; + + use crate::account::{ + AccountCode, + AccountId, + AccountStorageHeader, + PartialAccount, + PartialStorage, + StorageSlotName, + }; + use crate::asset::PartialVault; + use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; + + // Create test account IDs + let native_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + + // Create some slot names and IDs. + let slot_name_1 = StorageSlotName::new("test::slot1::value".to_string()).unwrap(); + let slot_name_2 = StorageSlotName::new("test::slot2::map".to_string()).unwrap(); + let slot_name_3 = StorageSlotName::new("another::slot::value".to_string()).unwrap(); + + let slot_id_1 = slot_name_1.id(); + let slot_id_2 = slot_name_2.id(); + let slot_id_3 = slot_name_3.id(); + + // Create foreign account slot names map. + let mut foreign_account_slot_names = BTreeMap::new(); + foreign_account_slot_names.insert(slot_id_1, slot_name_1.clone()); + foreign_account_slot_names.insert(slot_id_2, slot_name_2.clone()); + foreign_account_slot_names.insert(slot_id_3, slot_name_3.clone()); + + // Create a basic TransactionInputs with foreign slot names. + let code = AccountCode::mock(); + let storage_header = AccountStorageHeader::new(vec![]).unwrap(); + let partial_storage = PartialStorage::new(storage_header, []).unwrap(); + let partial_vault = PartialVault::new(Word::default()); + let partial_account = PartialAccount::new( + native_account_id, + Felt::new(10), + code, + partial_storage, + partial_vault, + None, + ) + .unwrap(); + + let original_tx_inputs = TransactionInputs { + account: partial_account, + block_header: crate::block::BlockHeader::mock(0, None, None, &[], Word::default()), + blockchain: crate::transaction::PartialBlockchain::default(), + input_notes: crate::transaction::InputNotes::new(vec![]).unwrap(), + tx_args: crate::transaction::TransactionArgs::default(), + advice_inputs: crate::vm::AdviceInputs::default(), + foreign_account_code: Vec::new(), + asset_witnesses: Vec::new(), + foreign_account_slot_names, + }; + + // Test serialization roundtrip. + let serialized = original_tx_inputs.to_bytes(); + let deserialized = TransactionInputs::read_from_bytes(&serialized).unwrap(); + + // Verify that foreign account slot names are preserved. + assert_eq!( + original_tx_inputs.foreign_account_slot_names(), + deserialized.foreign_account_slot_names() + ); + + // Verify specific slot names. + let deserialized_slots = deserialized.foreign_account_slot_names(); + + // Check slots. + assert_eq!(deserialized_slots.get(&slot_id_1).unwrap(), &slot_name_1); + assert_eq!(deserialized_slots.get(&slot_id_2).unwrap(), &slot_name_2); + assert_eq!(deserialized_slots.get(&slot_id_3).unwrap(), &slot_name_3); + + // Verify the entire structure is identical. + assert_eq!(original_tx_inputs, deserialized); +} diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index c536f6e188..6f1423e37a 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -106,6 +106,16 @@ impl TransactionAdviceInputs { .into_iter() } + // PUBLIC UTILITIES + // -------------------------------------------------------------------------------------------- + + /// Returns the advice map key where: + /// - the seed for native accounts is stored. + /// - the account header for foreign accounts is stored. + pub fn account_id_map_key(id: AccountId) -> Word { + Word::from([id.suffix(), id.prefix().as_felt(), ZERO, ZERO]) + } + // MUTATORS // -------------------------------------------------------------------------------------------- @@ -410,13 +420,6 @@ impl TransactionAdviceInputs { fn extend_merkle_store(&mut self, iter: impl Iterator) { self.0.store.extend(iter); } - - /// Returns the advice map key where: - /// - the seed for native accounts is stored. - /// - the account header for foreign accounts is stored. - fn account_id_map_key(id: AccountId) -> Word { - Word::from([id.suffix(), id.prefix().as_felt(), ZERO, ZERO]) - } } // CONVERSIONS diff --git a/crates/miden-tx/src/executor/exec_host.rs b/crates/miden-tx/src/executor/exec_host.rs index b24aa39e5a..7f4fa78114 100644 --- a/crates/miden-tx/src/executor/exec_host.rs +++ b/crates/miden-tx/src/executor/exec_host.rs @@ -13,7 +13,14 @@ use miden_processor::{ ProcessState, }; use miden_protocol::account::auth::PublicKeyCommitment; -use miden_protocol::account::{AccountCode, AccountDelta, AccountId, PartialAccount}; +use miden_protocol::account::{ + AccountCode, + AccountDelta, + AccountId, + PartialAccount, + StorageSlotId, + StorageSlotName, +}; use miden_protocol::assembly::debuginfo::Location; use miden_protocol::assembly::{SourceFile, SourceManagerSync, SourceSpan}; use miden_protocol::asset::{AssetVaultKey, AssetWitness, FungibleAsset}; @@ -77,6 +84,9 @@ where /// This is required for re-executing the transaction, e.g. as part of transaction proving. accessed_foreign_account_code: Vec, + /// Storage slot names for foreign accounts accessed during transaction execution. + foreign_account_slot_names: BTreeMap, + /// Contains generated signatures (as a message |-> signature map) required for transaction /// execution. Once a signature was created for a given message, it is inserted into this map. /// After transaction execution, these can be inserted into the advice inputs to re-execute the @@ -127,6 +137,7 @@ where authenticator, ref_block, accessed_foreign_account_code: Vec::new(), + foreign_account_slot_names: BTreeMap::new(), generated_signatures: BTreeMap::new(), initial_fee_asset_balance, source_manager, @@ -141,6 +152,11 @@ where &self.tx_progress } + /// Returns a reference to the foreign account slot names collected during execution. + pub fn foreign_account_slot_names(&self) -> &BTreeMap { + &self.foreign_account_slot_names + } + // EVENT HANDLERS // -------------------------------------------------------------------------------------------- @@ -163,6 +179,11 @@ where let mut tx_advice_inputs = TransactionAdviceInputs::default(); tx_advice_inputs.add_foreign_accounts([&foreign_account_inputs]); + // Extract and store slot names for this foreign account and store. + foreign_account_inputs.storage().header().slots().for_each(|slot| { + self.foreign_account_slot_names.insert(slot.id(), slot.name().clone()); + }); + self.base_host.load_foreign_account_code(foreign_account_inputs.code()); // Add the foreign account's code to the list of accessed code. @@ -409,6 +430,7 @@ where Vec, BTreeMap>, TransactionProgress, + BTreeMap, ) { let (account_delta, input_notes, output_notes) = self.base_host.into_parts(); @@ -419,6 +441,7 @@ where self.accessed_foreign_account_code, self.generated_signatures, self.tx_progress, + self.foreign_account_slot_names, ) } } diff --git a/crates/miden-tx/src/executor/mod.rs b/crates/miden-tx/src/executor/mod.rs index 105dbe7ff2..af7b87678d 100644 --- a/crates/miden-tx/src/executor/mod.rs +++ b/crates/miden-tx/src/executor/mod.rs @@ -367,6 +367,7 @@ fn build_executed_transaction Result { // Note that the account delta does not contain the removed transaction fee, so it is the // "pre-fee" delta of the transaction. + let ( pre_fee_account_delta, _input_notes, @@ -374,6 +375,7 @@ fn build_executed_transaction( } let active_account_id = process.get_active_account_id()?; - let hashed_map_key = StorageMap::hash_key(map_key); - let leaf_index = StorageMap::hashed_map_key_to_leaf_index(hashed_map_key); + let leaf_index: Felt = StorageMap::map_key_to_leaf_index(map_key) + .value() + .try_into() + .expect("expected key index to be a felt"); // For the native account we need to explicitly request the initial map root, // while for foreign accounts the current map root is always the initial one.