Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- [BREAKING] Remove public store API `GetAccountStateDelta` ([#1162](https://github.com/0xMiden/miden-node/pull/1162)).
- Removed `faucet` binary ([#1172](https://github.com/0xMiden/miden-node/pull/1172)).
- Add `genesis_commitment` in `Status` response ([#1181](https://github.com/0xMiden/miden-node/pull/1181)).
- [BREAKING] `GetAccountProofs` endpoint uses a `PartialSmt` for proofs. ([#1158](https://github.com/0xMiden/miden-node/pull/1158)).

### Fixes

Expand Down
20 changes: 16 additions & 4 deletions crates/proto/src/generated/rpc_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,26 @@ pub mod account_proofs {
/// the current one.
#[prost(bytes = "vec", optional, tag = "3")]
pub account_code: ::core::option::Option<::prost::alloc::vec::Vec<u8>>,
/// Storage slots information for this account
#[prost(message, repeated, tag = "4")]
pub storage_maps: ::prost::alloc::vec::Vec<
account_state_header::StorageSlotMapProof,
/// A sparse merkle tree per storage slot, including all relevant merkle proofs for storage entries.
#[prost(message, repeated, tag = "5")]
pub partial_storage_smts: ::prost::alloc::vec::Vec<
account_state_header::StorageSlotMap,
>,
}
/// Nested message and enum types in `AccountStateHeader`.
pub mod account_state_header {
/// Represents a single storage slot with the requested keys and their respective values.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct StorageSlotMap {
/// The storage slot index (\[0..255\]).
#[prost(uint32, tag = "1")]
pub storage_slot: u32,
/// Merkle proofs of the map value as partial sparse merkle tree for compression.
/// The respective rust types is `SparseMerkleTree` and the transformation to and from
/// bytes is done via the traits `Serializable::to_bytes` and `Deserializable::from_bytes`.
#[prost(bytes = "vec", tag = "2")]
pub partial_smt: ::prost::alloc::vec::Vec<u8>,
}
/// Represents a single storage slot with the requested keys and their respective values.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct StorageSlotMapProof {
Expand Down
24 changes: 15 additions & 9 deletions crates/store/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use miden_objects::crypto::merkle::{
MmrPeaks,
MmrProof,
PartialMmr,
PartialSmt,
SmtProof,
};
use miden_objects::note::{NoteDetails, NoteId, Nullifier};
Expand Down Expand Up @@ -919,22 +920,20 @@ impl State {
.expect("retrieved accounts were validated against request");

if let Some(details) = &account_info.details {
let mut storage_slot_map_keys = Vec::new();

let mut partials = BTreeMap::<u8, PartialSmt>::default();
for StorageMapKeysProof { storage_index, storage_keys } in
&request.storage_requests
{
if let Some(StorageSlot::Map(storage_map)) =
details.storage().slots().get(*storage_index as usize)
{
for map_key in storage_keys {
// only add the required storage keys to the partial representation
let proof = storage_map.open(map_key);

let slot_map_key = proto::rpc_store::account_proofs::account_proof::account_state_header::StorageSlotMapProof {
storage_slot: u32::from(*storage_index),
smt_proof: proof.to_bytes(),
};
storage_slot_map_keys.push(slot_map_key);
partials
.entry(*storage_index)
.or_insert_with(PartialSmt::new)
.add_proof(proof)?;
}
} else {
return Err(AccountError::StorageSlotNotMap(*storage_index).into());
Expand All @@ -947,12 +946,19 @@ impl State {
.not()
.then(|| details.code().to_bytes());

let partial_storage_smts = Vec::from_iter(
partials.into_iter()
.map(|(slot, partial_smt)| proto::rpc_store::account_proofs::account_proof::account_state_header::StorageSlotMap {
storage_slot: u32::from(slot),
partial_smt: partial_smt.to_bytes(),
})
);
let state_header =
proto::rpc_store::account_proofs::account_proof::AccountStateHeader {
header: Some(AccountHeader::from(details).into()),
storage_header: details.storage().to_header().to_bytes(),
account_code,
storage_maps: storage_slot_map_keys,
partial_storage_smts,
};

headers_map.insert(account_info.summary.account_id, state_header);
Expand Down
15 changes: 13 additions & 2 deletions proto/proto/store/rpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,17 @@ message AccountProofs {
message AccountProof {
// State header for public accounts.
message AccountStateHeader {
// Represents a single storage slot with the requested keys and their respective values.
message StorageSlotMap {
// The storage slot index ([0..255]).
uint32 storage_slot = 1;

// Merkle proofs of the map value as partial sparse merkle tree for compression.
// The respective rust types is `SparseMerkleTree` and the transformation to and from
// bytes is done via the traits `Serializable::to_bytes` and `Deserializable::from_bytes`.
bytes partial_smt = 2;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit out-of-the-loop here; how complex a data structure is this? Would it be easy enough to represent this within protobuf schema, similar to types::primitives::SparseMerklePath?

If kept as raw bytes I'd extend the comment to explain what actual rust type this is encoded by and how e.g. using winterfel's serde.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectively that'd re-implement the Serializable for PartialSmt with the generated types. I am not convinced that adds much value for introspectability of messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a comment with the relevant info

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this case, we can probably describe most of the structure using protobuf messages. One of the reasons is that we already have SmtLeaf message implemented (in the primitives.proto). So, we'll just need InnerNode implemented and then PartialSmt could look like so:

message PartialSmt {
    Digest root = 1;
    repeated SmtLeaf leaves = 2;
    repeated InnerNode nodes = 3;
}

And I would put these messages into primitives.proto.


Separately, I've just realized that the way we serialize PartialSmt right now is not entirely right. It is technically correct, but serializing all inner nodes is not necessary, and moreover, we don't check on deserialization if the data is actually correct (so, we assume that serialization was done by a trusted party). We have the same issue with many other structs/messages - so, it is not like PartialSmt is unique here - but it is something that we should start thinking about fixing.

So, if there is a simple way to make the protobuf-based serialization of PartialSmt work correct - that would be awesome. But also, if this requires too much work - we can come back to it later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PartialSmt is indeed serialized by adding all inner nodes, but by definition we only serialize those present. To my understanding by constructing the PartialSmt from MerklePaths, this is guaranteed to only contain relevant inner nodes.

We do check validity on the deserialization side after obtaining the PartialSmt by doing a ::open call which would fail at get_path if any element in the path would be missing.

Copy link
Contributor Author

@drahnr drahnr Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say that this is a partial SMT where we can open nodes 7 and 13. In the current implementation, all nodes with solid borders will be included because they are all needed to authenticate the two openings. But, we actually don't need nodes 6, 4, 14, 12, and 8 because we can compute them from the remaining nodes.

I'll reduce the included inner-nodes to child-less (in the sense of there is no such node present in the partial smt) nodes only.

The encoding of the minimal set of nodes is done, the reconstruction is WIP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to increased effort updating base to [email protected], the optimization is excluded from this PR / carved out to #1178

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not break the API twice - so, probably makes sense to go directly to #1178.

Copy link
Contributor Author

@drahnr drahnr Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1178 requires miden-base to bump to dependency miden-crypto, which is a bit more work, since it's still at 0.15.6 in next, and has a few more breaking changes 0xMiden/miden-vm#2113 and 0xMiden/miden-base#1831.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes! I'm hoping that we'll update miden-base to the next version of the VM some time next week.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are intending to deserialize this as PartialStorageMap (cc @igamigo or @SantiagoPittella to confirm). If so, I would name this message PartialStorageMap as well.

Also, we've recently modified the definition of PartialStorageMap to also include original keys (see 0xMiden/miden-base#1878) - so, serializing PartialSmt may no-longer be sufficient. So, maybe, what we should do here is serialize PartialStorageMap.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be using the serialization of PartialSmt inside the impl of PartialStorageMap iiuc.

}

// Represents a single storage slot with the requested keys and their respective values.
message StorageSlotMapProof {
// The storage slot index ([0..255]).
Expand All @@ -157,8 +168,8 @@ message AccountProofs {
// the current one.
optional bytes account_code = 3;

// Storage slots information for this account
repeated StorageSlotMapProof storage_maps = 4;
// A sparse merkle tree per storage slot, including all relevant merkle proofs for storage entries.
repeated StorageSlotMap partial_storage_smts = 5;
Comment on lines +171 to +172
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would name this field just storage_maps.

}

// The account witness for the current state commitment of one account ID.
Expand Down