Skip to content

refactor(rpc/get_account): batch storage map proofs - #2439

Open
kkovaacs wants to merge 4 commits into
nextfrom
krisztian/get-account-batched-storage-proofs
Open

refactor(rpc/get_account): batch storage map proofs#2439
kkovaacs wants to merge 4 commits into
nextfrom
krisztian/get-account-batched-storage-proofs

Conversation

@kkovaacs

@kkovaacs kkovaacs commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

GetAccount now returns a PartialSmt with all requested key-value pairs.

  • A request for explicit map keys always returns one partial SMT covering those keys.
  • Partial SMTs are scoped to one storage map at one block. Proofs from different maps or roots are never merged.
  • The response carries the original, unhashed StorageMapKeys. The SMT contains hashed keys, so the raw keys cannot be recovered from the tree.
  • Values are not duplicated outside the SMT. A client obtains a value by hashing the raw key and calling PartialSmt::get_value().
  • too_many_entries, all_entries, and partial_map become mutually exclusive results.
  • The removed entries_with_proofs field number and name are reserved. Even though compatibility is intentionally broken, reusing the field could make an old client try to decode the new length-delimited message as the old one.
  • The compact protobuf representation mirrors miden_crypto::merkle::smt::UniqueNodes. Do not encode PartialSmt as an opaque byte string.

Closes #617

Changelog

[[entry]]
scope       = "rpc"
impact      = "breaking"
description = "Storage map proofs for multiple keys in `AccountResponse` are now represented as a batched `PartialSmt`."

GetAccount now returns a PartialSmt with all requested key-value pairs.

- A request for explicit map keys always returns one partial SMT
  covering those keys.
- Partial SMTs are scoped to one storage map at one block. Proofs from
  different maps or roots are never merged.
- The response carries the original, unhashed `StorageMapKey`s. The SMT
  contains hashed keys, so the raw keys cannot be recovered from the tree.
- Values are not duplicated outside the SMT. A client obtains a value by
  hashing the raw key and calling `PartialSmt::get_value()`.
- `too_many_entries`, `all_entries`, and `partial_map` become mutually
  exclusive results.
- The removed `entries_with_proofs` field number and name are reserved.
  Even though compatibility is intentionally broken, reusing the field
  could make an old client try to decode the new length-delimited message
  as the old one.
- The compact protobuf representation mirrors
  `miden_crypto::merkle::smt::UniqueNodes`. Do not encode `PartialSmt`
  as an opaque byte string.
@kkovaacs
kkovaacs force-pushed the krisztian/get-account-batched-storage-proofs branch from 5ae4953 to 1a99dc6 Compare August 5, 2026 14:01
@kkovaacs

kkovaacs commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Looks like there is a bug in PartialSmt::from_unique_nodes() that might prevent us from taking full advantage of this optimization: 0xMiden/miden-vm#3470

@kkovaacs
kkovaacs marked this pull request as ready for review August 5, 2026 15:34
@bobbinth

bobbinth commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Looks like there is a bug in PartialSmt::from_unique_nodes() that might prevent us from taking full advantage of this optimization: 0xMiden/miden-vm#3470

Once 0xMiden/miden-vm#3471 is merged, we should be able to do a patch release and it'll flow through to here immediately.

@kkovaacs

kkovaacs commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

@igamigo Do you have an estimate on how many storage keys a client usually requests from a storage map in a single GetAccount request?

As far as I understand there are two cases where the client is requesting specific keys from a storage map:

  • When preparing transactions involving public foreign accounts: the client fetches all keys explicitly referenced in AccountStorageRequirements when adding the foreign account(s) to the TransactionRequestBuilder.
  • Lazily during transaction execution: when a required storage-map witness is absent locally and from the in-memory cache.

As far as I understand in the lazy (execution) use case the client is always requesting a single key from a single storage slot. There is nothing to be gained in that case from the optimization this PR implements.

In the first case though: how much this optimization helps depends on the number of the storage map keys typically required. Do we have an estimate on how many keys we're expecting there?

CC @Mirko-von-Leipzig

@igamigo

igamigo commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@igamigo Do you have an estimate on how many storage keys a client usually requests from a storage map in a single GetAccount request?

Not really, and as we discussed the other day, this is very transaction/smart contract-specific. It's very hard to generalize so I would not let any specific usecase inform any limits here. For example, I think Zoro is the only user who has hit the 64 key limit before, and they worked around it by sharding data in different accounts AFAIK. They were going to try locally following the foreign account as an alternative, but surely they would benefit from a higher limit.

Asset callbacks will probably be the dominant FPI usecase but at first sight I don't think you would need to fetch a large amount of keys there on average (not sure though).


let proof = partial_smt.open(&map_key.hash().as_word()).map_err(|err| {
RpcError::InvalidResponse(format!(
"response did not track the requested storage map key: {err}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

{err} likely truncates the error

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Why? err here is a MerkleError (from miden-crypto) that has a thiserror-derived Display implementation that seems to contain reasonably useful information.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I just mean that it relies on the underlying error to have a report style display implemented.

@kkovaacs

Copy link
Copy Markdown
Collaborator Author

FYI: I've removed that workaround for PartialSmt reconstruction now that we're on miden-crypto == 0.29.1 that does have the bugfix.

@igamigo igamigo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM! Apologies for the delay in the review. In terms of whether we can merge this for 0.16, I don't see why not. This is mostly an optimization but it does change up things a bit so maybe the sooner the better. Not a strong opinion either way though.

Comment on lines +67 to +68
// Leaves for which only the hash is known.
repeated IndexedDigest value_only_leaves = 4;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit (feel free to disregard): Would hash_only_leaves be a better name? value_only intuitively makes me think that the original value is there.

Comment on lines +637 to +649
let map_root = match self.forest.root_info(tree) {
RootInfo::LatestVersion(root) | RootInfo::HistoricalVersion(root) => root,
RootInfo::Missing => return None,
};

let proofs = Result::from_iter(raw_keys.iter().map(|raw_key| {
let key_hashed = raw_key.hash().into();
self.forest.open(tree, key_hashed).map_err(Self::map_forest_error)
}));

Some(proofs.map(|proofs| AccountStorageMapDetails::from_proofs(slot_name, proofs)))
Some(proofs.and_then(|proofs| {
AccountStorageMapDetails::from_proofs(slot_name, map_root, raw_keys.to_vec(), proofs)
}))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I guess it would be nice to have an LargeSmtForest::open_many() or something similar here. This is probably allocating a lot more than it needs to, and probably re-hashing stuff that's been validated already.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In this same line, can we pass raw_keys as a Vec<> here to avoid the to_vec()? It sounds unlikely that it would be used after having built the response, so maybe there's no need to pass a slice. With 64 keys being the limit I doubt this is bad in any way so this is definitely mostly a nit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Return one batch proof per storage map on GetAccount

4 participants