diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d4a3b95..5a1f1db02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Added pagination to `GetNetworkAccountIds` endpoint ([#1452](https://github.com/0xMiden/miden-node/pull/1452)). - Improved tracing in `miden-network-monitor` binary ([#1366](https://github.com/0xMiden/miden-node/pull/1366)). - Integrated RPC stack with Validator component for transaction validation ([#1457](https://github.com/0xMiden/miden-node/pull/1457)). +- Add partial storage map queries to RPC ([#1428](https://github.com/0xMiden/miden-node/pull/1428)). - Added validated transactions check to block validation logc in Validator ([#1460](https://github.com/0xMiden/miden-node/pull/1460)). - Added explorer status to the `miden-network-monitor` binary ([#1450](https://github.com/0xMiden/miden-node/pull/1450)). - Added `GetLimits` endpoint to the RPC server ([#1410](https://github.com/0xMiden/miden-node/pull/1410)). diff --git a/Cargo.lock b/Cargo.lock index 7676ec423..1ad02438b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2749,6 +2749,7 @@ name = "miden-node-proto" version = "0.13.0" dependencies = [ "anyhow", + "assert_matches", "fs-err", "hex", "http", diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index 738eade6b..255b27c9d 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -28,7 +28,8 @@ tonic-prost = { workspace = true } url = { workspace = true } [dev-dependencies] -proptest = { version = "1.7" } +assert_matches = { workspace = true } +proptest = { version = "1.7" } [build-dependencies] fs-err = { workspace = true } diff --git a/crates/proto/src/domain/account.rs b/crates/proto/src/domain/account.rs index 5bc6b4ecc..6d736b243 100644 --- a/crates/proto/src/domain/account.rs +++ b/crates/proto/src/domain/account.rs @@ -16,6 +16,7 @@ use miden_protocol::asset::{Asset, AssetVault}; use miden_protocol::block::BlockNumber; use miden_protocol::block::account_tree::AccountWitness; use miden_protocol::crypto::merkle::SparseMerklePath; +use miden_protocol::crypto::merkle::smt::SmtProof; use miden_protocol::note::{NoteExecutionMode, NoteTag}; use miden_protocol::utils::{Deserializable, DeserializationError, Serializable}; use thiserror::Error; @@ -24,6 +25,9 @@ use super::try_convert; use crate::errors::{ConversionError, MissingFieldHelper}; use crate::generated::{self as proto}; +#[cfg(test)] +mod tests; + // ACCOUNT ID // ================================================================================================ @@ -187,52 +191,6 @@ impl TryFrom for Accoun } } -impl TryFrom - for AccountStorageMapDetails -{ - type Error = ConversionError; - - fn try_from( - value: proto::rpc::account_storage_details::AccountStorageMapDetails, - ) -> Result { - use proto::rpc::account_storage_details::account_storage_map_details::map_entries::StorageMapEntry; - let proto::rpc::account_storage_details::AccountStorageMapDetails { - slot_name, - too_many_entries, - entries, - } = value; - - let slot_name = StorageSlotName::new(slot_name)?; - - let entries = if too_many_entries { - StorageMapEntries::LimitExceeded - } else { - let map_entries = if let Some(entries) = entries { - entries - .entries - .into_iter() - .map(|entry| { - let key = entry - .key - .ok_or(StorageMapEntry::missing_field(stringify!(key)))? - .try_into()?; - let value = entry - .value - .ok_or(StorageMapEntry::missing_field(stringify!(value)))? - .try_into()?; - Ok((key, value)) - }) - .collect::, ConversionError>>()? - } else { - Vec::new() - }; - StorageMapEntries::Entries(map_entries) - }; - - Ok(Self { slot_name, entries }) - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct StorageMapRequest { pub slot_name: StorageSlotName, @@ -259,6 +217,7 @@ impl TryFrom), + /// All storage map entries (key-value pairs) without proofs. + /// Used when all entries are requested for small maps. + AllEntries(Vec<(Word, Word)>), + + /// Specific entries with their SMT proofs for client-side verification. + /// Used when specific keys are requested from the storage map. + EntriesWithProofs(Vec), } impl AccountStorageMapDetails { /// Maximum number of storage map entries that can be returned in a single response. pub const MAX_RETURN_ENTRIES: usize = 1000; - pub fn new(slot_name: StorageSlotName, slot_data: SlotData, storage_map: &StorageMap) -> Self { - match slot_data { - SlotData::All => Self::from_all_entries(slot_name, storage_map), - SlotData::MapKeys(keys) => Self::from_specific_keys(slot_name, &keys[..], storage_map), + /// Creates storage map details with all entries from the storage map. + /// + /// If the storage map has too many entries (> `MAX_RETURN_ENTRIES`), + /// returns `LimitExceeded` variant. + pub fn from_all_entries(slot_name: StorageSlotName, storage_map: &StorageMap) -> Self { + if storage_map.num_entries() > Self::MAX_RETURN_ENTRIES { + Self { + slot_name, + entries: StorageMapEntries::LimitExceeded, + } + } else { + let entries = Vec::from_iter(storage_map.entries().map(|(k, v)| (*k, *v))); + Self { + slot_name, + entries: StorageMapEntries::AllEntries(entries), + } } } - fn from_all_entries(slot_name: StorageSlotName, storage_map: &StorageMap) -> Self { - if storage_map.num_entries() > Self::MAX_RETURN_ENTRIES { + /// Creates storage map details from forest-queried entries. + /// + /// Returns `LimitExceeded` if too many entries. + pub fn from_forest_entries(slot_name: StorageSlotName, entries: Vec<(Word, Word)>) -> Self { + if entries.len() > Self::MAX_RETURN_ENTRIES { Self { slot_name, entries: StorageMapEntries::LimitExceeded, } } else { - let map_entries = Vec::from_iter(storage_map.entries().map(|(k, v)| (*k, *v))); Self { slot_name, - entries: StorageMapEntries::Entries(map_entries), + entries: StorageMapEntries::AllEntries(entries), } } } - fn from_specific_keys( - slot_name: StorageSlotName, - keys: &[Word], - storage_map: &StorageMap, - ) -> Self { - if keys.len() > Self::MAX_RETURN_ENTRIES { + /// Creates storage map details from pre-computed SMT proofs. + /// + /// Use this when the caller has already obtained the proofs from an `SmtForest`. + /// Returns `LimitExceeded` if too many proofs are provided. + pub fn from_proofs(slot_name: StorageSlotName, proofs: Vec) -> Self { + if proofs.len() > Self::MAX_RETURN_ENTRIES { Self { slot_name, entries: StorageMapEntries::LimitExceeded, } } else { - // TODO For now, we return all entries instead of specific keys with proofs - Self::from_all_entries(slot_name, storage_map) + Self { + slot_name, + entries: StorageMapEntries::EntriesWithProofs(proofs), + } } } } +impl TryFrom + for AccountStorageMapDetails +{ + type Error = ConversionError; + + fn try_from( + value: proto::rpc::account_storage_details::AccountStorageMapDetails, + ) -> Result { + use proto::rpc::account_storage_details::account_storage_map_details::{ + all_map_entries::StorageMapEntry, + map_entries_with_proofs::StorageMapEntryWithProof, + AllMapEntries, + Entries as ProtoEntries, + MapEntriesWithProofs, + }; + + let proto::rpc::account_storage_details::AccountStorageMapDetails { + slot_name, + too_many_entries, + entries, + } = value; + + let slot_name = StorageSlotName::new(slot_name)?; + + let entries = if too_many_entries { + StorageMapEntries::LimitExceeded + } else { + match entries { + None => { + return Err( + proto::rpc::account_storage_details::AccountStorageMapDetails::missing_field( + stringify!(entries), + ), + ); + }, + Some(ProtoEntries::AllEntries(AllMapEntries { entries })) => { + let entries = entries + .into_iter() + .map(|entry| { + let key = entry + .key + .ok_or(StorageMapEntry::missing_field(stringify!(key)))? + .try_into()?; + let value = entry + .value + .ok_or(StorageMapEntry::missing_field(stringify!(value)))? + .try_into()?; + Ok((key, value)) + }) + .collect::, ConversionError>>()?; + StorageMapEntries::AllEntries(entries) + }, + Some(ProtoEntries::EntriesWithProofs(MapEntriesWithProofs { entries })) => { + let proofs = entries + .into_iter() + .map(|entry| { + let smt_opening = entry.proof.ok_or( + StorageMapEntryWithProof::missing_field(stringify!(proof)), + )?; + SmtProof::try_from(smt_opening) + }) + .collect::, ConversionError>>()?; + StorageMapEntries::EntriesWithProofs(proofs) + }, + } + }; + + Ok(Self { slot_name, entries }) + } +} + impl From for proto::rpc::account_storage_details::AccountStorageMapDetails { fn from(value: AccountStorageMapDetails) -> Self { - use proto::rpc::account_storage_details::account_storage_map_details; + use proto::rpc::account_storage_details::account_storage_map_details::{ + AllMapEntries, + Entries as ProtoEntries, + MapEntriesWithProofs, + }; let AccountStorageMapDetails { slot_name, entries } = value; - match entries { - StorageMapEntries::LimitExceeded => Self { - slot_name: slot_name.to_string(), - too_many_entries: true, - entries: Some(account_storage_map_details::MapEntries { entries: Vec::new() }), + let (too_many_entries, proto_entries) = match entries { + StorageMapEntries::LimitExceeded => (true, None), + StorageMapEntries::AllEntries(entries) => { + let all = AllMapEntries { + entries: Vec::from_iter(entries.into_iter().map(|(key, value)| { + proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry { + key: Some(key.into()), + value: Some(value.into()), + } + })), + }; + (false, Some(ProtoEntries::AllEntries(all))) }, - StorageMapEntries::Entries(map_entries) => { - let entries = Some(account_storage_map_details::MapEntries { - entries: Vec::from_iter(map_entries.into_iter().map(|(key, value)| { - account_storage_map_details::map_entries::StorageMapEntry { + StorageMapEntries::EntriesWithProofs(proofs) => { + use miden_protocol::crypto::merkle::smt::SmtLeaf; + + let with_proofs = MapEntriesWithProofs { + entries: Vec::from_iter(proofs.into_iter().map(|proof| { + // Get key/value from the leaf before consuming the proof + let (key, value) = match proof.leaf() { + SmtLeaf::Empty(_) => { + (miden_protocol::EMPTY_WORD, miden_protocol::EMPTY_WORD) + }, + SmtLeaf::Single((k, v)) => (*k, *v), + SmtLeaf::Multiple(entries) => entries.iter().next().map_or( + (miden_protocol::EMPTY_WORD, miden_protocol::EMPTY_WORD), + |(k, v)| (*k, *v), + ), + }; + let smt_opening = proto::primitives::SmtOpening::from(proof); + proto::rpc::account_storage_details::account_storage_map_details::map_entries_with_proofs::StorageMapEntryWithProof { key: Some(key.into()), value: Some(value.into()), + proof: Some(smt_opening), } })), - }); - - Self { - slot_name: slot_name.to_string(), - too_many_entries: false, - entries, - } + }; + (false, Some(ProtoEntries::EntriesWithProofs(with_proofs))) }, + }; + + Self { + slot_name: slot_name.to_string(), + too_many_entries, + entries: proto_entries, } } } -// ACCOUNT STORAGE DETAILS DETAILS -//================================================================================================ - -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct AccountStorageDetails { pub header: AccountStorageHeader, pub map_details: Vec, diff --git a/crates/proto/src/domain/account/tests.rs b/crates/proto/src/domain/account/tests.rs new file mode 100644 index 000000000..695813d99 --- /dev/null +++ b/crates/proto/src/domain/account/tests.rs @@ -0,0 +1,41 @@ +use super::*; + +fn word_from_u32(arr: [u32; 4]) -> Word { + Word::from(arr) +} + +fn test_slot_name() -> StorageSlotName { + StorageSlotName::new("miden::test::storage::slot").unwrap() +} + +#[test] +fn account_storage_map_details_from_forest_entries() { + let slot_name = test_slot_name(); + let entries = vec![ + (word_from_u32([1, 2, 3, 4]), word_from_u32([5, 6, 7, 8])), + (word_from_u32([9, 10, 11, 12]), word_from_u32([13, 14, 15, 16])), + ]; + + let details = AccountStorageMapDetails::from_forest_entries(slot_name.clone(), entries.clone()); + + assert_eq!(details.slot_name, slot_name); + assert_eq!(details.entries, StorageMapEntries::AllEntries(entries)); +} + +#[test] +fn account_storage_map_details_from_forest_entries_limit_exceeded() { + let slot_name = test_slot_name(); + // Create more entries than MAX_RETURN_ENTRIES + let entries: Vec<_> = (0..=AccountStorageMapDetails::MAX_RETURN_ENTRIES) + .map(|i| { + let key = word_from_u32([i as u32, 0, 0, 0]); + let value = word_from_u32([0, 0, 0, i as u32]); + (key, value) + }) + .collect(); + + let details = AccountStorageMapDetails::from_forest_entries(slot_name.clone(), entries); + + assert_eq!(details.slot_name, slot_name); + assert_eq!(details.entries, StorageMapEntries::LimitExceeded); +} diff --git a/crates/proto/src/generated/rpc.rs b/crates/proto/src/generated/rpc.rs index f9a59e39c..755009e2c 100644 --- a/crates/proto/src/generated/rpc.rs +++ b/crates/proto/src/generated/rpc.rs @@ -233,25 +233,53 @@ pub mod account_storage_details { /// Storage slot name. #[prost(string, tag = "1")] pub slot_name: ::prost::alloc::string::String, - /// A flag that is set to `true` if the number of to-be-returned entries in the - /// storage map would exceed a threshold. This indicates to the user that `SyncStorageMaps` - /// endpoint should be used to get all storage map data. + /// True when the number of entries exceeds the response limit. + /// When set, clients should use the `SyncStorageMaps` endpoint. #[prost(bool, tag = "2")] pub too_many_entries: bool, - /// By default we provide all storage entries. - #[prost(message, optional, tag = "3")] - pub entries: ::core::option::Option, + /// The map entries (with or without proofs). Empty when too_many_entries is true. + #[prost(oneof = "account_storage_map_details::Entries", tags = "3, 4")] + pub entries: ::core::option::Option, } /// Nested message and enum types in `AccountStorageMapDetails`. pub mod account_storage_map_details { - /// Wrapper for repeated storage map entries + /// Wrapper for repeated storage map entries including their proofs. + /// Used when specific keys are requested to enable client-side verification. #[derive(Clone, PartialEq, ::prost::Message)] - pub struct MapEntries { + pub struct MapEntriesWithProofs { #[prost(message, repeated, tag = "1")] - pub entries: ::prost::alloc::vec::Vec, + pub entries: ::prost::alloc::vec::Vec< + map_entries_with_proofs::StorageMapEntryWithProof, + >, } - /// Nested message and enum types in `MapEntries`. - pub mod map_entries { + /// Nested message and enum types in `MapEntriesWithProofs`. + pub mod map_entries_with_proofs { + /// Definition of individual storage entries including a proof. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct StorageMapEntryWithProof { + #[prost(message, optional, tag = "1")] + pub key: ::core::option::Option< + super::super::super::super::primitives::Digest, + >, + #[prost(message, optional, tag = "2")] + pub value: ::core::option::Option< + super::super::super::super::primitives::Digest, + >, + #[prost(message, optional, tag = "3")] + pub proof: ::core::option::Option< + super::super::super::super::primitives::SmtOpening, + >, + } + } + /// Wrapper for repeated storage map entries (without proofs). + /// Used when all entries are requested for small maps. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct AllMapEntries { + #[prost(message, repeated, tag = "1")] + pub entries: ::prost::alloc::vec::Vec, + } + /// Nested message and enum types in `AllMapEntries`. + pub mod all_map_entries { /// Definition of individual storage entries. #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct StorageMapEntry { @@ -265,6 +293,16 @@ pub mod account_storage_details { >, } } + /// The map entries (with or without proofs). Empty when too_many_entries is true. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Entries { + /// All storage entries without proofs (for small maps or full requests). + #[prost(message, tag = "3")] + AllEntries(AllMapEntries), + /// Specific entries with their SMT proofs (for partial requests). + #[prost(message, tag = "4")] + EntriesWithProofs(MapEntriesWithProofs), + } } } /// List of nullifiers to return proofs for. diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 3d54a501f..a2dacd235 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -7,7 +7,7 @@ use diesel::{Connection, RunQueryDsl, SqliteConnection}; use miden_node_proto::domain::account::{AccountInfo, AccountSummary, NetworkAccountPrefix}; use miden_node_proto::generated as proto; use miden_protocol::Word; -use miden_protocol::account::{AccountHeader, AccountId, AccountStorage}; +use miden_protocol::account::{AccountHeader, AccountId, AccountStorageHeader}; use miden_protocol::asset::{Asset, AssetVaultKey}; use miden_protocol::block::{BlockHeader, BlockNoteIndex, BlockNumber, ProvenBlock}; use miden_protocol::crypto::merkle::SparseMerklePath; @@ -453,23 +453,6 @@ impl Db { .await } - /// Reconstructs account storage at a specific block from the database - /// - /// This method queries the decomposed storage tables and reconstructs the full - /// `AccountStorage` with SMT backing for Map slots. - // TODO split querying the header from the content - #[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)] - pub async fn select_account_storage_at_block( - &self, - account_id: AccountId, - block_num: BlockNumber, - ) -> Result { - self.transact("Get account storage at block", move |conn| { - queries::select_account_storage_at_block(conn, account_id, block_num) - }) - .await - } - /// Queries vault assets at a specific block #[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)] pub async fn select_account_vault_at_block( @@ -496,17 +479,17 @@ impl Db { .await } - /// Queries the account header for a specific account at a specific block number. + /// Queries the account header and storage header for a specific account at a block. /// + /// Returns both in a single query to avoid querying the database twice. /// Returns `None` if the account doesn't exist at that block. - pub async fn select_account_header_at_block( + pub async fn select_account_header_with_storage_header_at_block( &self, account_id: AccountId, block_num: BlockNumber, - ) -> Result> { - self.transact("Get account header at block", move |conn| { - queries::select_account_header_at_block(conn, account_id, block_num) - .map(|opt| opt.map(|(header, _storage_header)| header)) + ) -> Result> { + self.transact("Get account header with storage header at block", move |conn| { + queries::select_account_header_with_storage_header_at_block(conn, account_id, block_num) }) .await } diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 3c615c51b..f517360cd 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -50,8 +50,7 @@ use crate::errors::DatabaseError; mod at_block; pub(crate) use at_block::{ - select_account_header_at_block, - select_account_storage_at_block, + select_account_header_with_storage_header_at_block, select_account_vault_at_block, }; diff --git a/crates/store/src/db/models/queries/accounts/at_block.rs b/crates/store/src/db/models/queries/accounts/at_block.rs index 307edd0b7..41ec035f3 100644 --- a/crates/store/src/db/models/queries/accounts/at_block.rs +++ b/crates/store/src/db/models/queries/accounts/at_block.rs @@ -1,18 +1,7 @@ -use std::collections::BTreeMap; - use diesel::prelude::{Queryable, QueryableByName}; use diesel::query_dsl::methods::SelectDsl; use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SqliteConnection}; -use miden_protocol::account::{ - AccountHeader, - AccountId, - AccountStorage, - AccountStorageHeader, - StorageMap, - StorageSlot, - StorageSlotName, - StorageSlotType, -}; +use miden_protocol::account::{AccountHeader, AccountId, AccountStorageHeader}; use miden_protocol::asset::Asset; use miden_protocol::block::BlockNumber; use miden_protocol::utils::{Deserializable, Serializable}; @@ -48,10 +37,10 @@ struct AccountHeaderDataRaw { /// /// # Returns /// -/// * `Ok(Some(AccountHeader))` - The account header if found +/// * `Ok(Some((AccountHeader, AccountStorageHeader)))` - The headers if found /// * `Ok(None)` - If account doesn't exist at that block /// * `Err(DatabaseError)` - If there's a database error -pub(crate) fn select_account_header_at_block( +pub(crate) fn select_account_header_with_storage_header_at_block( conn: &mut SqliteConnection, account_id: AccountId, block_num: BlockNumber, @@ -87,15 +76,13 @@ pub(crate) fn select_account_header_at_block( return Ok(None); }; - let (storage_commitment, storage_header) = match storage_header_blob { - Some(blob) => { - let header = AccountStorageHeader::read_from_bytes(&blob)?; - let commitment = header.to_commitment(); - (commitment, header) - }, - None => (Word::default(), AccountStorageHeader::new(Vec::new())?), + let storage_header = match &storage_header_blob { + Some(blob) => AccountStorageHeader::read_from_bytes(blob)?, + None => AccountStorageHeader::new(Vec::new())?, }; + let storage_commitment = storage_header.to_commitment(); + let code_commitment = code_commitment_bytes .map(|bytes| Word::read_from_bytes(&bytes)) .transpose()? @@ -108,10 +95,10 @@ pub(crate) fn select_account_header_at_block( .transpose()? .unwrap_or(Word::default()); - Ok(Some(( - AccountHeader::new(account_id, nonce, vault_root, storage_commitment, code_commitment), - storage_header, - ))) + let account_header = + AccountHeader::new(account_id, nonce, vault_root, storage_commitment, code_commitment); + + Ok(Some((account_header, storage_header))) } // ACCOUNT VAULT @@ -175,90 +162,3 @@ struct AssetRow { #[diesel(sql_type = diesel::sql_types::Nullable)] asset: Option>, } - -// ACCOUNT STORAGE -// ================================================================================================ - -/// Returns account storage at a given block by reading from `accounts.storage_header` -/// (which contains the `AccountStorageHeader`) and reconstructing full storage from -/// map values in `account_storage_map_values` table. -pub(crate) fn select_account_storage_at_block( - conn: &mut SqliteConnection, - account_id: AccountId, - block_num: BlockNumber, -) -> Result { - use schema::account_storage_map_values as t; - - let account_id_bytes = account_id.to_bytes(); - let block_num_sql = block_num.to_raw_sql(); - - // Query storage header blob for this account at or before this block - let storage_blob: Option> = - SelectDsl::select(schema::accounts::table, schema::accounts::storage_header) - .filter(schema::accounts::account_id.eq(&account_id_bytes)) - .filter(schema::accounts::block_num.le(block_num_sql)) - .order(schema::accounts::block_num.desc()) - .limit(1) - .first(conn) - .optional()? - .flatten(); - - let Some(blob) = storage_blob else { - // No storage means empty storage - return Ok(AccountStorage::new(Vec::new())?); - }; - - // Deserialize the AccountStorageHeader from the blob - let header = AccountStorageHeader::read_from_bytes(&blob)?; - - // Query all map values for this account up to and including this block. - // Order by (slot_name, key) ascending, then block_num descending so the first entry - // for each (slot_name, key) pair is the latest one. - let map_values: Vec<(String, Vec, Vec)> = - SelectDsl::select(t::table, (t::slot_name, t::key, t::value)) - .filter(t::account_id.eq(&account_id_bytes)) - .filter(t::block_num.le(block_num_sql)) - .order((t::slot_name.asc(), t::key.asc(), t::block_num.desc())) - .load(conn)?; - - // For each (slot_name, key) pair, keep only the latest entry (first one due to ordering) - let mut latest_map_entries: BTreeMap<(StorageSlotName, Word), Word> = BTreeMap::new(); - - for (slot_name_str, key_bytes, value_bytes) in map_values { - let slot_name: StorageSlotName = slot_name_str.parse().map_err(|_| { - DatabaseError::DataCorrupted(format!("Invalid slot name: {slot_name_str}")) - })?; - let key = Word::read_from_bytes(&key_bytes)?; - let value = Word::read_from_bytes(&value_bytes)?; - - // Only insert if we haven't seen this (slot_name, key) yet - // (since results are ordered by block_num desc, first one is latest) - latest_map_entries.entry((slot_name, key)).or_insert(value); - } - - // Group entries by slot name - let mut map_entries_by_slot: BTreeMap> = BTreeMap::new(); - for ((slot_name, key), value) in latest_map_entries { - map_entries_by_slot.entry(slot_name).or_default().push((key, value)); - } - - // Reconstruct StorageSlots from header slots + map entries - let mut slots = Vec::new(); - for slot_header in header.slots() { - let slot = match slot_header.slot_type() { - StorageSlotType::Value => { - // For value slots, the header value IS the slot value - StorageSlot::with_value(slot_header.name().clone(), slot_header.value()) - }, - StorageSlotType::Map => { - // For map slots, reconstruct from map entries - let entries = map_entries_by_slot.remove(slot_header.name()).unwrap_or_default(); - let storage_map = StorageMap::with_entries(entries)?; - StorageSlot::with_map(slot_header.name().clone(), storage_map) - }, - }; - slots.push(slot); - } - - Ok(AccountStorage::new(slots)?) -} diff --git a/crates/store/src/db/models/queries/accounts/tests.rs b/crates/store/src/db/models/queries/accounts/tests.rs index a0a23f3b5..2df630987 100644 --- a/crates/store/src/db/models/queries/accounts/tests.rs +++ b/crates/store/src/db/models/queries/accounts/tests.rs @@ -1,7 +1,16 @@ //! Tests for the `accounts` module, specifically for account storage and historical queries. +use std::collections::BTreeMap; + use diesel::query_dsl::methods::SelectDsl; -use diesel::{Connection, OptionalExtension, QueryDsl, RunQueryDsl}; +use diesel::{ + BoolExpressionMethods, + Connection, + ExpressionMethods, + OptionalExtension, + QueryDsl, + RunQueryDsl, +}; use diesel_migrations::MigrationHarness; use miden_node_utils::fee::test_fee_params; use miden_protocol::account::auth::PublicKeyCommitment; @@ -13,20 +22,27 @@ use miden_protocol::account::{ AccountDelta, AccountId, AccountIdVersion, + AccountStorage, + AccountStorageHeader, AccountStorageMode, AccountType, + StorageMap, StorageSlot, StorageSlotName, + StorageSlotType, }; use miden_protocol::block::{BlockAccountUpdate, BlockHeader, BlockNumber}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; -use miden_protocol::utils::Serializable; +use miden_protocol::utils::{Deserializable, Serializable}; use miden_protocol::{EMPTY_WORD, Felt, Word}; use miden_standards::account::auth::AuthRpoFalcon512; use miden_standards::code_builder::CodeBuilder; use super::*; use crate::db::migrations::MIGRATIONS; +use crate::db::models::conv::SqlTypeConvert; +use crate::db::schema; +use crate::errors::DatabaseError; fn setup_test_db() -> SqliteConnection { let mut conn = @@ -37,6 +53,80 @@ fn setup_test_db() -> SqliteConnection { conn } +/// Test helper: reconstructs account storage at a given block from DB. +/// +/// Reads `accounts.storage_header` and `account_storage_map_values` to reconstruct +/// the full `AccountStorage` at the specified block. +fn reconstruct_account_storage_at_block( + conn: &mut SqliteConnection, + account_id: AccountId, + block_num: BlockNumber, +) -> Result { + use schema::account_storage_map_values as t; + + let account_id_bytes = account_id.to_bytes(); + let block_num_sql = block_num.to_raw_sql(); + + // Query storage header blob for this account at or before this block + let storage_blob: Option> = + SelectDsl::select(schema::accounts::table, schema::accounts::storage_header) + .filter(schema::accounts::account_id.eq(&account_id_bytes)) + .filter(schema::accounts::block_num.le(block_num_sql)) + .order(schema::accounts::block_num.desc()) + .limit(1) + .first(conn) + .optional()? + .flatten(); + + let Some(blob) = storage_blob else { + return Ok(AccountStorage::new(Vec::new())?); + }; + + let header = AccountStorageHeader::read_from_bytes(&blob)?; + + // Query all map values for this account up to and including this block. + let map_values: Vec<(i64, String, Vec, Vec)> = + SelectDsl::select(t::table, (t::block_num, t::slot_name, t::key, t::value)) + .filter(t::account_id.eq(&account_id_bytes).and(t::block_num.le(block_num_sql))) + .order((t::slot_name.asc(), t::key.asc(), t::block_num.desc())) + .load(conn)?; + + // For each (slot_name, key) pair, keep only the latest entry + let mut latest_map_entries: BTreeMap<(StorageSlotName, Word), Word> = BTreeMap::new(); + for (_, slot_name_str, key_bytes, value_bytes) in map_values { + let slot_name: StorageSlotName = slot_name_str.parse().map_err(|_| { + DatabaseError::DataCorrupted(format!("Invalid slot name: {slot_name_str}")) + })?; + let key = Word::read_from_bytes(&key_bytes)?; + let value = Word::read_from_bytes(&value_bytes)?; + latest_map_entries.entry((slot_name, key)).or_insert(value); + } + + // Group entries by slot name + let mut map_entries_by_slot: BTreeMap> = BTreeMap::new(); + for ((slot_name, key), value) in latest_map_entries { + map_entries_by_slot.entry(slot_name).or_default().push((key, value)); + } + + // Reconstruct StorageSlots from header slots + map entries + let mut slots = Vec::new(); + for slot_header in header.slots() { + let slot = match slot_header.slot_type() { + StorageSlotType::Value => { + StorageSlot::with_value(slot_header.name().clone(), slot_header.value()) + }, + StorageSlotType::Map => { + let entries = map_entries_by_slot.remove(slot_header.name()).unwrap_or_default(); + let storage_map = StorageMap::with_entries(entries)?; + StorageSlot::with_map(slot_header.name().clone(), storage_map) + }, + }; + slots.push(slot); + } + + Ok(AccountStorage::new(slots)?) +} + fn create_test_account_with_storage() -> (Account, AccountId) { // Create a simple public account with one value storage slot let account_id = AccountId::dummy( @@ -112,8 +202,9 @@ fn test_select_account_header_at_block_returns_none_for_nonexistent() { ); // Query for a non-existent account - let result = select_account_header_at_block(&mut conn, account_id, block_num) - .expect("Query should succeed"); + let result = + select_account_header_with_storage_header_at_block(&mut conn, account_id, block_num) + .expect("Query should succeed"); assert!(result.is_none(), "Should return None for non-existent account"); } @@ -139,7 +230,7 @@ fn test_select_account_header_at_block_returns_correct_header() { // Query the account header let (header, _storage_header) = - select_account_header_at_block(&mut conn, account_id, block_num) + select_account_header_with_storage_header_at_block(&mut conn, account_id, block_num) .expect("Query should succeed") .expect("Header should exist"); @@ -175,16 +266,18 @@ fn test_select_account_header_at_block_historical_query() { upsert_accounts(&mut conn, &[account_update_1], block_num_1).expect("First upsert failed"); // Query at block 1 - should return the account - let (header_1, _) = select_account_header_at_block(&mut conn, account_id, block_num_1) - .expect("Query should succeed") - .expect("Header should exist at block 1"); + let (header_1, _) = + select_account_header_with_storage_header_at_block(&mut conn, account_id, block_num_1) + .expect("Query should succeed") + .expect("Header should exist at block 1"); assert_eq!(header_1.nonce(), nonce_1, "Nonce at block 1 should match"); // Query at block 2 - should return the same account (most recent before block 2) - let (header_2, _) = select_account_header_at_block(&mut conn, account_id, block_num_2) - .expect("Query should succeed") - .expect("Header should exist at block 2"); + let (header_2, _) = + select_account_header_with_storage_header_at_block(&mut conn, account_id, block_num_2) + .expect("Query should succeed") + .expect("Header should exist at block 2"); assert_eq!(header_2.nonce(), nonce_1, "Nonce at block 2 should match block 1"); } @@ -221,38 +314,6 @@ fn test_select_account_vault_at_block_empty() { // ACCOUNT STORAGE AT BLOCK TESTS // ================================================================================================ -#[test] -fn test_select_account_storage_at_block_returns_storage() { - let mut conn = setup_test_db(); - let (account, _) = create_test_account_with_storage(); - let account_id = account.id(); - - let block_num = BlockNumber::from_epoch(0); - insert_block_header(&mut conn, block_num); - - let original_storage_commitment = account.storage().to_commitment(); - - // Insert the account - let delta = AccountDelta::try_from(account.clone()).unwrap(); - let account_update = BlockAccountUpdate::new( - account_id, - account.commitment(), - AccountUpdateDetails::Delta(delta), - ); - - upsert_accounts(&mut conn, &[account_update], block_num).expect("upsert_accounts failed"); - - // Query storage - let storage = select_account_storage_at_block(&mut conn, account_id, block_num) - .expect("Query should succeed"); - - assert_eq!( - storage.to_commitment(), - original_storage_commitment, - "Storage commitment should match" - ); -} - #[test] fn test_upsert_accounts_inserts_storage_header() { let mut conn = setup_test_db(); @@ -397,8 +458,9 @@ fn test_upsert_accounts_updates_is_latest_flag() { ); // Verify historical query returns first update - let storage_at_block_1 = select_account_storage_at_block(&mut conn, account_id, block_num_1) - .expect("Failed to query storage at block 1"); + let storage_at_block_1 = + reconstruct_account_storage_at_block(&mut conn, account_id, block_num_1) + .expect("Failed to query storage at block 1"); assert_eq!( storage_at_block_1.to_commitment(), diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index 7e0c326a2..7ebed5a74 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -134,6 +134,12 @@ pub enum DatabaseError { SqlValueConversion(#[from] DatabaseTypeConversionError), #[error("Not implemented: {0}")] NotImplemented(String), + #[error("storage root not found for account {account_id}, slot {slot_name}, block {block_num}")] + StorageRootNotFound { + account_id: AccountId, + slot_name: String, + block_num: BlockNumber, + }, } impl DatabaseError { diff --git a/crates/store/src/inner_forest/mod.rs b/crates/store/src/inner_forest/mod.rs index d368896f2..7a43e40f9 100644 --- a/crates/store/src/inner_forest/mod.rs +++ b/crates/store/src/inner_forest/mod.rs @@ -1,11 +1,12 @@ use std::collections::BTreeMap; +use miden_node_proto::domain::account::{AccountStorageMapDetails, StorageMapEntries}; use miden_protocol::account::delta::{AccountDelta, AccountStorageDelta, AccountVaultDelta}; use miden_protocol::account::{AccountId, NonFungibleDeltaAction, StorageSlotName}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::block::BlockNumber; -use miden_protocol::crypto::merkle::EmptySubtreeRoots; use miden_protocol::crypto::merkle::smt::{SMT_DEPTH, SmtForest}; +use miden_protocol::crypto::merkle::{EmptySubtreeRoots, MerkleError}; use miden_protocol::{EMPTY_WORD, Word}; use thiserror::Error; @@ -42,6 +43,10 @@ pub(crate) struct InnerForest { /// Populated during block import for all storage map slots. storage_map_roots: BTreeMap<(AccountId, StorageSlotName, BlockNumber), Word>, + /// Maps (`account_id`, `slot_name`, `block_num`) to all key-value entries in that storage map. + /// Accumulated from deltas - each block's entries include all entries up to that point. + storage_entries: BTreeMap<(AccountId, StorageSlotName, BlockNumber), BTreeMap>, + /// Maps (`account_id`, `block_num`) to vault SMT root. /// Tracks asset vault versions across all blocks with structural sharing. vault_roots: BTreeMap<(AccountId, BlockNumber), Word>, @@ -52,6 +57,7 @@ impl InnerForest { Self { forest: SmtForest::new(), storage_map_roots: BTreeMap::new(), + storage_entries: BTreeMap::new(), vault_roots: BTreeMap::new(), } } @@ -81,9 +87,8 @@ impl InnerForest { return Self::empty_smt_root(); } self.vault_roots - .range((account_id, BlockNumber::GENESIS)..) - .take_while(|((id, _), _)| *id == account_id) - .last() + .range((account_id, BlockNumber::GENESIS)..=(account_id, BlockNumber::from(u32::MAX))) + .next_back() .map_or_else(Self::empty_smt_root, |(_, root)| *root) } @@ -110,27 +115,97 @@ impl InnerForest { } self.storage_map_roots - .range((account_id, slot_name.clone(), BlockNumber::GENESIS)..) - .take_while(|((id, name, _), _)| *id == account_id && name == slot_name) - .last() + .range( + (account_id, slot_name.clone(), BlockNumber::GENESIS) + ..=(account_id, slot_name.clone(), BlockNumber::from(u32::MAX)), + ) + .next_back() .map_or_else(Self::empty_smt_root, |(_, root)| *root) } /// Retrieves the vault SMT root for an account at or before the given block. + /// Retrieves the storage map SMT root for an account slot at or before the given block. /// - /// Finds the most recent vault root entry for the account, since vault state persists + /// Finds the most recent storage root entry for the slot, since storage state persists /// across blocks where no changes occur. - // - // TODO: a fallback to DB lookup is required once pruning lands. - // Currently returns empty root which would be incorrect - #[cfg(test)] - fn get_vault_root(&self, account_id: AccountId, block_num: BlockNumber) -> Word { - self.vault_roots - .range((account_id, BlockNumber::GENESIS)..=(account_id, block_num)) + pub(crate) fn get_storage_root( + &self, + account_id: AccountId, + slot_name: &StorageSlotName, + block_num: BlockNumber, + ) -> Word { + self.storage_map_roots + .range( + (account_id, slot_name.clone(), BlockNumber::GENESIS) + ..=(account_id, slot_name.clone(), block_num), + ) .next_back() .map_or_else(Self::empty_smt_root, |(_, root)| *root) } + /// Opens a storage map and returns storage map details with SMT proofs for the given keys. + /// + /// Returns `None` if no storage root is tracked for this account/slot/block combination. + /// Returns a `MerkleError` if the forest doesn't contain sufficient data for the proofs. + pub(crate) fn open_storage_map( + &self, + account_id: AccountId, + slot_name: StorageSlotName, + block_num: BlockNumber, + keys: &[Word], + ) -> Option> { + let root = self.get_storage_root(account_id, &slot_name, block_num); + + // Empty root means no storage map exists for this account/slot + if root == Self::empty_smt_root() { + return None; + } + + if keys.len() > AccountStorageMapDetails::MAX_RETURN_ENTRIES { + return Some(Ok(AccountStorageMapDetails { + slot_name, + entries: StorageMapEntries::LimitExceeded, + })); + } + + // Collect SMT proofs for each key + let proofs = Result::from_iter(keys.iter().map(|key| self.forest.open(root, *key))); + + Some(proofs.map(|proofs| AccountStorageMapDetails::from_proofs(slot_name, proofs))) + } + + /// Returns all key-value entries for a specific account storage slot at or before a block. + /// + /// Uses range query semantics: finds the most recent entries at or before `block_num`. + /// Returns `None` if no entries exist for this account/slot up to the given block. + /// Returns `LimitExceeded` if there are too many entries to return. + pub(crate) fn storage_map_entries( + &self, + account_id: AccountId, + slot_name: StorageSlotName, + block_num: BlockNumber, + ) -> Option { + // Find the most recent entries at or before block_num + let entries = self + .storage_entries + .range( + (account_id, slot_name.clone(), BlockNumber::GENESIS) + ..=(account_id, slot_name.clone(), block_num), + ) + .next_back() + .map(|(_, entries)| entries)?; + + if entries.len() > AccountStorageMapDetails::MAX_RETURN_ENTRIES { + return Some(AccountStorageMapDetails { + slot_name, + entries: StorageMapEntries::LimitExceeded, + }); + } + let entries = Vec::from_iter(entries.iter().map(|(k, v)| (*k, *v))); + + Some(AccountStorageMapDetails::from_forest_entries(slot_name, entries)) + } + // PUBLIC INTERFACE // -------------------------------------------------------------------------------------------- @@ -297,7 +372,7 @@ impl InnerForest { /// Updates the forest with storage map changes from a delta. /// /// Processes storage map slot deltas, building SMTs for each modified slot - /// and tracking the new roots. + /// and tracking the new roots and accumulated entries. /// /// # Arguments /// @@ -313,27 +388,53 @@ impl InnerForest { for (slot_name, map_delta) in storage_delta.maps() { let prev_root = self.get_latest_storage_map_root(account_id, slot_name, is_full_state); - let entries: Vec<_> = + let delta_entries: Vec<_> = map_delta.entries().iter().map(|(key, value)| ((*key).into(), *value)).collect(); - if entries.is_empty() { + if delta_entries.is_empty() { continue; } let updated_root = self .forest - .batch_insert(prev_root, entries.iter().copied()) + .batch_insert(prev_root, delta_entries.iter().copied()) .expect("forest insertion should succeed"); self.storage_map_roots .insert((account_id, slot_name.clone(), block_num), updated_root); + // Accumulate entries: start from previous block's entries or empty for full state + let mut accumulated_entries = if is_full_state { + BTreeMap::new() + } else { + self.storage_entries + .range( + (account_id, slot_name.clone(), BlockNumber::GENESIS) + ..(account_id, slot_name.clone(), block_num), + ) + .next_back() + .map(|(_, entries)| entries.clone()) + .unwrap_or_default() + }; + + // Apply delta entries (insert or remove if value is EMPTY_WORD) + for (key, value) in &delta_entries { + if *value == EMPTY_WORD { + accumulated_entries.remove(key); + } else { + accumulated_entries.insert(*key, *value); + } + } + + self.storage_entries + .insert((account_id, slot_name.clone(), block_num), accumulated_entries); + tracing::debug!( target: crate::COMPONENT, %account_id, %block_num, ?slot_name, - entries = entries.len(), + delta_entries = delta_entries.len(), "Updated storage map in forest" ); } diff --git a/crates/store/src/inner_forest/tests.rs b/crates/store/src/inner_forest/tests.rs index fb6ceb917..216ef4206 100644 --- a/crates/store/src/inner_forest/tests.rs +++ b/crates/store/src/inner_forest/tests.rs @@ -205,6 +205,15 @@ fn test_vault_state_persists_across_blocks_without_changes() { let account_id = dummy_account(); let faucet_id = dummy_faucet(); + // Helper to query vault root at or before a block (range query) + let get_vault_root = |forest: &InnerForest, account_id: AccountId, block_num: BlockNumber| { + forest + .vault_roots + .range((account_id, BlockNumber::GENESIS)..=(account_id, block_num)) + .next_back() + .map(|(_, root)| *root) + }; + // Block 1: Add 100 tokens let block_1 = BlockNumber::GENESIS.child(); let mut vault_delta_1 = AccountVaultDelta::default(); @@ -228,18 +237,18 @@ fn test_vault_state_persists_across_blocks_without_changes() { let root_after_block_6 = forest.vault_roots[&(account_id, block_6)]; assert_ne!(root_after_block_1, root_after_block_6); - // Verify get_vault_root finds the correct previous root for intermediate blocks + // Verify range query finds the correct previous root for intermediate blocks // Block 3 should return block 1's root (most recent before block 3) - let root_at_block_3 = forest.get_vault_root(account_id, BlockNumber::from(3)); - assert_eq!(root_at_block_3, root_after_block_1); + let root_at_block_3 = get_vault_root(&forest, account_id, BlockNumber::from(3)); + assert_eq!(root_at_block_3, Some(root_after_block_1)); // Block 5 should also return block 1's root - let root_at_block_5 = forest.get_vault_root(account_id, BlockNumber::from(5)); - assert_eq!(root_at_block_5, root_after_block_1); + let root_at_block_5 = get_vault_root(&forest, account_id, BlockNumber::from(5)); + assert_eq!(root_at_block_5, Some(root_after_block_1)); // Block 6 should return block 6's root - let root_at_block_6 = forest.get_vault_root(account_id, block_6); - assert_eq!(root_at_block_6, root_after_block_6); + let root_at_block_6 = get_vault_root(&forest, account_id, block_6); + assert_eq!(root_at_block_6, Some(root_after_block_6)); } #[test] diff --git a/crates/store/src/state.rs b/crates/store/src/state.rs index 43e653dcf..e191051d9 100644 --- a/crates/store/src/state.rs +++ b/crates/store/src/state.rs @@ -18,13 +18,15 @@ use miden_node_proto::domain::account::{ AccountStorageMapDetails, AccountVaultDetails, NetworkAccountPrefix, + SlotData, StorageMapRequest, }; use miden_node_proto::domain::batch::BatchInputs; use miden_node_utils::ErrorReport; use miden_node_utils::formatting::format_array; +use miden_protocol::Word; +use miden_protocol::account::AccountId; use miden_protocol::account::delta::AccountUpdateDetails; -use miden_protocol::account::{AccountId, StorageSlotContent}; use miden_protocol::block::account_tree::{AccountTree, AccountWitness, account_id_to_smt_key}; use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness}; use miden_protocol::block::{BlockHeader, BlockInputs, BlockNumber, Blockchain, ProvenBlock}; @@ -39,7 +41,6 @@ use miden_protocol::crypto::merkle::smt::{ use miden_protocol::note::{NoteDetails, NoteId, NoteScript, Nullifier}; use miden_protocol::transaction::{OutputNote, PartialBlockchain}; use miden_protocol::utils::Serializable; -use miden_protocol::{AccountError, Word}; use tokio::sync::{Mutex, RwLock, oneshot}; use tracing::{info, info_span, instrument}; @@ -1008,6 +1009,10 @@ impl State { /// /// This method queries the database to fetch the account state and processes the detail /// request to return only the requested information. + /// + /// For specific key queries (`SlotData::MapKeys`), the forest is used to provide SMT proofs. + /// Returns an error if the forest doesn't have data for the requested slot. + /// All-entries queries (`SlotData::All`) use the forest to return all entries. async fn fetch_public_account_details( &self, account_id: AccountId, @@ -1027,11 +1032,12 @@ impl State { // Validate block exists in the blockchain before querying the database self.validate_block_exists(block_num).await?; - let account_header = - self.db - .select_account_header_at_block(account_id, block_num) - .await? - .ok_or(DatabaseError::AccountAtBlockHeightNotFoundInDb(account_id, block_num))?; + // Query account header and storage header together in a single DB call + let (account_header, storage_header) = self + .db + .select_account_header_with_storage_header_at_block(account_id, block_num) + .await? + .ok_or(DatabaseError::AccountAtBlockHeightNotFoundInDb(account_id, block_num))?; let account_code = match code_commitment { Some(commitment) if commitment == account_header.code_commitment() => None, @@ -1055,25 +1061,31 @@ impl State { None => AccountVaultDetails::empty(), }; - // TODO: don't load the entire storage at once, load what is required - let storage = self.db.select_account_storage_at_block(account_id, block_num).await?; - let storage_header = storage.to_header(); let mut storage_map_details = Vec::::with_capacity(storage_requests.len()); - for StorageMapRequest { slot_name, slot_data } in storage_requests { - let Some(slot) = storage.slots().iter().find(|s| s.name() == &slot_name) else { - continue; - }; + // Use forest for storage map queries + let forest_guard = self.forest.read().await; - let storage_map = match slot.content() { - StorageSlotContent::Map(map) => map, - StorageSlotContent::Value(_) => { - return Err(AccountError::StorageSlotNotMap(slot_name).into()); - }, + for StorageMapRequest { slot_name, slot_data } in storage_requests { + let details = match &slot_data { + SlotData::MapKeys(keys) => forest_guard + .open_storage_map(account_id, slot_name.clone(), block_num, keys) + .ok_or_else(|| DatabaseError::StorageRootNotFound { + account_id, + slot_name: slot_name.to_string(), + block_num, + })? + .map_err(DatabaseError::MerkleError)?, + SlotData::All => forest_guard + .storage_map_entries(account_id, slot_name.clone(), block_num) + .ok_or_else(|| DatabaseError::StorageRootNotFound { + account_id, + slot_name: slot_name.to_string(), + block_num, + })?, }; - let details = AccountStorageMapDetails::new(slot_name, slot_data, storage_map); storage_map_details.push(details); } diff --git a/proto/proto/rpc.proto b/proto/proto/rpc.proto index dccf44020..2918af848 100644 --- a/proto/proto/rpc.proto +++ b/proto/proto/rpc.proto @@ -318,8 +318,22 @@ message AccountVaultDetails { // Account storage details for AccountProofResponse message AccountStorageDetails { message AccountStorageMapDetails { - // Wrapper for repeated storage map entries - message MapEntries { + // Wrapper for repeated storage map entries including their proofs. + // Used when specific keys are requested to enable client-side verification. + message MapEntriesWithProofs { + // Definition of individual storage entries including a proof. + message StorageMapEntryWithProof { + primitives.Digest key = 1; + primitives.Digest value = 2; + primitives.SmtOpening proof = 3; + } + + repeated StorageMapEntryWithProof entries = 1; + } + + // Wrapper for repeated storage map entries (without proofs). + // Used when all entries are requested for small maps. + message AllMapEntries { // Definition of individual storage entries. message StorageMapEntry { primitives.Digest key = 1; @@ -332,13 +346,18 @@ message AccountStorageDetails { // Storage slot name. string slot_name = 1; - // A flag that is set to `true` if the number of to-be-returned entries in the - // storage map would exceed a threshold. This indicates to the user that `SyncStorageMaps` - // endpoint should be used to get all storage map data. + // True when the number of entries exceeds the response limit. + // When set, clients should use the `SyncStorageMaps` endpoint. bool too_many_entries = 2; - // By default we provide all storage entries. - MapEntries entries = 3; + // The map entries (with or without proofs). Empty when too_many_entries is true. + oneof entries { + // All storage entries without proofs (for small maps or full requests). + AllMapEntries all_entries = 3; + + // Specific entries with their SMT proofs (for partial requests). + MapEntriesWithProofs entries_with_proofs = 4; + } } // Account storage header (storage slot info for up to 256 slots)