Sparse Merkle tree
SparseMerkleTree<H> models a fixed-depth 256-bit key space: 2²⁵⁶ leaf slots, almost all
empty. It stores only non-empty nodes, precomputes the empty-subtree hash at every level, and
compresses single-leaf subtrees into shortcut nodes (Buterin, 2018) and small subtrees into batch
entries (Ouvrard, 2018/2019).
Use it for account state, key-value commitments, allowlists, and rollup state, especially when you must prove that a key is absent.
Keys and values
- A key is
[u8; 32]. If your natural key is longer or variable-length (an address, a user ID), hash it to 32 bytes first. - A value is any non-empty byte slice. The tree stores
H::hash(value).
use merkle_core::prelude::*;
use merkle_variants::SparseMerkleTree;
use merkleforge_hash::Sha256;
/// Derive a 32-byte tree key from an arbitrary identifier.
fn key_for(id: &str) -> [u8; 32] {
Sha256::hash(id.as_bytes())
}
fn main() -> Result<(), MerkleError> {
let mut state = SparseMerkleTree::<Sha256>::new();
assert_eq!(state.root(), None); // root() is None while empty
assert_eq!(state.depth(), 256);
state.insert(key_for("alice"), b"balance:100")?;
state.insert(key_for("bob"), b"balance:250")?;
// Inserting an existing key overwrites its value.
state.insert(key_for("alice"), b"balance:90")?;
assert_eq!(state.leaf_count(), 2);
// get() returns the stored leaf *hash*, not the value.
assert_eq!(state.get(key_for("alice")), Some(&Sha256::hash(b"balance:90")));
assert_eq!(state.get(key_for("carol")), None);
state.remove(key_for("bob"))?;
assert_eq!(state.leaf_count(), 1);
Ok(())
}root() returns None for an empty tree. If you need the canonical root of an empty tree (for
example as a genesis state), use empty_root().
Membership proofs
use merkle_variants::SparseMerkleTree;
use merkleforge_hash::Sha256;
fn main() -> Result<(), merkle_core::error::MerkleError> {
let mut state = SparseMerkleTree::<Sha256>::new();
let alice = [7_u8; 32];
let bob = [9_u8; 32];
state.insert(alice, b"balance:100")?;
state.insert(bob, b"balance:250")?;
let proof = state.generate_membership_proof(alice)?;
let root = state.root().expect("state is not empty");
assert!(proof.is_membership());
assert_eq!(proof.siblings.len(), 256);
// Checks the key, the value, and the path.
assert!(SparseMerkleTree::<Sha256>::verify_membership(
root,
alice,
b"balance:100",
&proof,
));
// The same proof does not verify for another key or value.
assert!(!SparseMerkleTree::<Sha256>::verify_membership(root, bob, b"balance:100", &proof));
assert!(!SparseMerkleTree::<Sha256>::verify_membership(root, alice, b"balance:999", &proof));
Ok(())
}A SparseMerkleProof<D> has three public fields: key, siblings (256 entries ordered leaf to
root, with None for empty subtrees), and leaf (the stored leaf hash, or None).
Non-membership proofs
The same function proves absence. For a missing key, generate_membership_proof returns a proof
with leaf: None, and the verifier rebuilds the root with H::empty() in the leaf slot.
use merkle_variants::SparseMerkleTree;
use merkleforge_hash::Sha256;
fn main() -> Result<(), merkle_core::error::MerkleError> {
let mut state = SparseMerkleTree::<Sha256>::new();
state.insert([1_u8; 32], b"registered")?;
let missing = [2_u8; 32];
let proof = state.generate_membership_proof(missing)?;
let root = state.root().expect("non-empty");
assert!(proof.is_non_membership());
assert!(SparseMerkleTree::<Sha256>::verify_non_membership(root, missing, &proof));
// A non-membership proof can't be replayed for a key that *is* present.
let present = state.generate_membership_proof([1_u8; 32])?;
assert!(!SparseMerkleTree::<Sha256>::verify_non_membership(root, [1_u8; 32], &present));
Ok(())
}Batch operations
batch_insert and batch_remove apply every change first, then recompute each affected node
once (one-phase batch update, Ma et al.). For many updates this is much faster than a loop of
single inserts.
use merkle_variants::SparseMerkleTree;
use merkleforge_hash::Sha256;
fn main() -> Result<(), merkle_core::error::MerkleError> {
let mut batched = SparseMerkleTree::<Sha256>::new();
let mut sequential = SparseMerkleTree::<Sha256>::new();
let updates: Vec<([u8; 32], Vec<u8>)> = (0_u8..100)
.map(|i| ([i; 32], format!("account:{i}").into_bytes()))
.collect();
batched.batch_insert(&updates)?;
for (key, value) in &updates {
sequential.insert(*key, value)?;
}
assert_eq!(batched.root(), sequential.root());
assert_eq!(batched.leaf_count(), 100);
// Remove several keys in one pass. Every key must exist.
batched.batch_remove(&[[0_u8; 32], [1_u8; 32], [2_u8; 32]])?;
assert_eq!(batched.leaf_count(), 97);
Ok(())
}Rules:
- If a key appears more than once in
batch_insert, the last value wins. batch_insertfails withEmptyLeafDataif any value is empty, before changing the tree.batch_removefails withUnsupportedOperationif any key is missing, before changing the tree.- Single
removeof a missing key also returnsUnsupportedOperation.
In the published benchmark run (2026-08-17), a 1,000-key batch insert took 20.33 ms versus 8.69 s for 1,000 sequential inserts. That run predates the v0.5.0 fix that makes single inserts recompute only the changed path, so today's sequential numbers are far lower; batching still saves work on shared ancestors. See Benchmarks.
Empty hashes and storage introspection
use merkle_core::prelude::*;
use merkle_variants::SparseMerkleTree;
use merkleforge_hash::Sha256;
fn main() -> Result<(), MerkleError> {
let mut tree = SparseMerkleTree::<Sha256>::new();
// Level 0 is the empty leaf; level 256 is the empty-tree root.
assert_eq!(tree.empty_hash_at(0), &Sha256::empty());
assert_eq!(tree.empty_root(), tree.empty_hash_at(256));
assert_eq!(tree.empty_hashes().len(), 257);
tree.insert([0xAB; 32], b"value")?;
println!("materialized nodes: {}", tree.stored_node_count());
println!("nodes written by last mutation: {}", tree.last_node_access_count());
println!("estimate without batching: {}", tree.unbatched_node_count_estimate());
Ok(())
}The merkle_variants::sparse module also exports the constants SPARSE_TREE_DEPTH (256),
EMPTY_HASH_LEVELS (257), and BATCH_DEPTH (4), and the helper bit_at(key, depth) that
returns the path bit for a key at a depth (bit 0 is the high bit of key[0]).
Through the generic trait
SparseMerkleTree implements MerkleTree<H> by mapping leaf index i to a key whose last 8 bytes
are i (big-endian). MerkleTree::generate_proof returns an ordinary MerkleProof with 256 path
nodes. Prefer the key-addressed API above for real state.