Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Add serialization of sealing and unsealing keys in IES ([#637](https://github.com/0xMiden/crypto/pull/637)).
- Fixed undefined `BaseElement` in rescue arch optimizations ([#644](https://github.com/0xMiden/crypto/pull/644)).
- Added `MerkleStore::has_path()` ([#649](https://github.com/0xMiden/crypto/pull/649)).
- Added `SmtForest::insert_path()` to support partial SMTs in `SmtForest` ([#650](https://github.com/0xMiden/crypto/pull/650)).

## 0.18.2 (2025-11-08)

Expand Down
20 changes: 20 additions & 0 deletions miden-crypto/src/merkle/smt/forest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ impl SmtForest {
// STATE MUTATORS
// --------------------------------------------------------------------------------------------

/// Inserts all nodes present in the provided [`SmtProof`] into the forest and returns
/// the root computed from the proof.
///
/// If the computed root already exists, returns without modifying the forest.
pub fn insert_path(&mut self, proof: SmtProof) -> Word {
let root = proof.compute_root();
let path_nodes: Vec<_> = proof.authenticated_nodes().collect();
let (_path, leaf) = proof.into_parts();

if !self.roots.insert(root) {
return root;
}

let leaf_hash = leaf.hash();
self.leaves.insert(leaf_hash, leaf);
self.store.insert_nodes_from_path(root, path_nodes);

root
}

/// Inserts the specified key-value pair into an SMT with the specified root. This will also
/// add a new root to the forest. Returns the new root.
///
Expand Down
58 changes: 36 additions & 22 deletions miden-crypto/src/merkle/smt/forest/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use alloc::vec::Vec;
use crate::{
Map, Word,
hash::rpo::Rpo256,
merkle::{EmptySubtreeRoots, MerkleError, MerklePath, MerkleProof, NodeIndex, SMT_DEPTH},
merkle::{
EmptySubtreeRoots, InnerNodeInfo, MerkleError, MerklePath, MerkleProof, NodeIndex,
SMT_DEPTH,
},
};

// SMT FOREST STORE
Expand Down Expand Up @@ -242,31 +245,26 @@ impl SmtStore {
.ok_or(MerkleError::NodeIndexNotFoundInStore(root, NodeIndex::root()))?;

// The update was computed successfully, update ref counts and insert into the store
fn dfs(
node: Word,
store: &mut Map<Word, ForestInnerNode>,
new_nodes: &mut Map<Word, ForestInnerNode>,
) {
if node == Word::empty() {
return;
}
if let Some(node) = store.get_mut(&node) {
// This node already exists in the store, increase its reference count.
// Stops the dfs descent here to leave children ref counts unchanged.
node.rc += 1;
} else if let Some(mut smt_node) = new_nodes.remove(&node) {
// This is a non-leaf node, insert it into the store and process its children.
smt_node.rc = 1;
store.insert(node, smt_node);
dfs(smt_node.left, store, new_nodes);
dfs(smt_node.right, store, new_nodes);
}
}
dfs(new_root, &mut self.nodes, &mut new_nodes);
self.insert_node_recursive(new_root, &mut new_nodes);

Ok(new_root)
}

/// Inserts the nodes described by `path_nodes` into the store, updating reference counts and
/// ensuring that shared subtrees remain tracked only once.
pub(super) fn insert_nodes_from_path(
&mut self,
root: Word,
path_nodes: impl IntoIterator<Item = InnerNodeInfo>,
) {
let mut new_nodes: Map<Word, ForestInnerNode> = Map::new();
for InnerNodeInfo { value, left, right } in path_nodes {
new_nodes.insert(value, ForestInnerNode { left, right, rc: 0 });
}

self.insert_node_recursive(root, &mut new_nodes);
}

/// Decreases the reference count of the specified node and releases memory if the count
/// reached zero.
///
Expand Down Expand Up @@ -303,6 +301,22 @@ impl SmtStore {
}
removed_leaves
}

/// Inserts nodes from `new_nodes` into the store, increasing reference counts for existing
/// nodes and recursively adding unseen nodes together with their subtrees.
fn insert_node_recursive(&mut self, node: Word, new_nodes: &mut Map<Word, ForestInnerNode>) {
if node == Word::empty() {
return;
}
if let Some(existing) = self.nodes.get_mut(&node) {
existing.rc += 1;
} else if let Some(mut smt_node) = new_nodes.remove(&node) {
smt_node.rc = 1;
self.nodes.insert(node, smt_node);
self.insert_node_recursive(smt_node.left, new_nodes);
self.insert_node_recursive(smt_node.right, new_nodes);
}
}
}

// HELPER FUNCTIONS
Expand Down
27 changes: 26 additions & 1 deletion miden-crypto/src/merkle/smt/forest/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use itertools::Itertools;
use super::{EmptySubtreeRoots, MerkleError, SmtForest, Word};
use crate::{
Felt, ONE, WORD_SIZE, ZERO,
merkle::{int_to_node, smt::SMT_DEPTH},
merkle::{
int_to_node,
smt::{SMT_DEPTH, Smt},
},
};

// TESTS
Expand Down Expand Up @@ -89,6 +92,28 @@ fn test_insert_multiple_values() -> Result<(), MerkleError> {
Ok(())
}

#[test]
fn test_insert_path() -> Result<(), MerkleError> {
let mut smt = Smt::new();
let key = Word::new([ZERO, ZERO, ZERO, ONE]);
let value = Word::new([ONE; WORD_SIZE]);
smt.insert(key, value)?;
let proof = smt.open(&key);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we make the smt a contain a few entries (in different leaves)? IIUC, right now, smt contains a single entry, and thus, the corresponding proof fully describes it. Would be good to test the situation where smt is a superset of the data in the forest (i.e., forest does not have all leaves from the smt).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I’ve updated the test to use an SMT with multiple entries and verified that the forest correctly handles the partial view (returning UntrackedKey or NodeIndexNotFoundInStore for missing data).

One question: currently insert_proof returns early if the root already exists (as per its docstring), which means we can’t incrementally merge multiple proofs for the same root to build up a larger partial view.
I’ve started looking into this, but it seems that supporting merging correctly would require more complex node reference counting.

Another potential solution would be to introduce batch_insert_proofs, but then we’d need to have all proofs we want to insert up front.

Copy link
Contributor

Choose a reason for hiding this comment

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

One question: currently insert_proof returns early if the root already exists (as per its docstring), which means we can’t incrementally merge multiple proofs for the same root to build up a larger partial view.
I’ve started looking into this, but it seems that supporting merging correctly would require more complex node reference counting.

Ah - interesting! Yeah, I didn't think of this before and it does seems like if we are supporting partial views, we should be able to support them more comprehensively (i.e., not just single-leaf partial views).

Maybe in light of the approach we landed on in #670 it would make sense to put this work on hold because making this work in the overly-centric design should be a bit easier.

This also does mean that we should design the storage of the persistent SMT forest in such a way that we support both full and partial SMTs - though, the support for partial SMTs could come a bit later.

cc @iamrecursion


let mut forest = SmtForest::new();
let root = forest.insert_path(proof);
assert_eq!(root, smt.root());

let stored_proof = forest.open(root, key)?;
assert!(stored_proof.verify_membership(&key, &value, &root));

forest.pop_smts(vec![root]);
assert!(forest.roots.is_empty());
assert!(forest.leaves.is_empty());

Ok(())
}

#[test]
fn test_batch_insert() -> Result<(), MerkleError> {
let forest = SmtForest::new();
Expand Down
Loading