Choosing a tree
MerkleForge ships three tree variants. They share the MerkleTree<H> trait but are built for
different shapes of data.
BinaryMerkleTree<H> | SparseMerkleTree<H> | MerklePatriciaTrie<H> | |
|---|---|---|---|
| Data shape | Ordered list | Map from 32-byte keys to values | Map from byte keys to values |
| Addressed by | LeafIndex | [u8; 32] key | &[u8] key |
| Proofs | Inclusion | Membership and non-membership | Membership and non-membership (RLP witness) |
| Proof size | ⌈log₂ n⌉ siblings | 256 siblings, empty ones compressed to None | One RLP node per trie level on the path |
| Best hash | Sha256 or Blake3 | Sha256 or Blake3 | Keccak256 for Ethereum roots |
| Typical use | Transaction batches, logs, file chunks, SPV | Account state, rollups, allowlists | Ethereum state, receipts, storage proofs |
Quick decision guide
- You have a list and prove items by position: use
BinaryMerkleTree. It has the smallest proofs and the fastest construction. - You have a key-value map and need to prove a key is absent: use
SparseMerkleTree. Keys are 32 bytes, so hash longer keys first. - You need roots or witnesses that Ethereum tooling understands: use
MerklePatriciaTrie<Keccak256>.
Writing code that works with any variant
The shared trait lets you write generic code. Each variant also has a native API (keys for the sparse tree and trie); the trait maps leaf indices onto it.
use merkle_core::prelude::*;
use merkle_variants::{BinaryMerkleTree, MerklePatriciaTrie, SparseMerkleTree};
use merkleforge_hash::Sha256;
/// Insert rows through the shared trait and return a metadata snapshot.
fn commit<H, T>(tree: &mut T, rows: &[&[u8]]) -> Result<TreeMetadata, MerkleError>
where
H: HashFunction,
T: MerkleTree<H>,
{
for row in rows {
tree.insert(row)?;
}
Ok(tree.metadata())
}
fn main() -> Result<(), MerkleError> {
let rows: &[&[u8]] = &[b"alice:100", b"bob:250", b"carol:75"];
let binary = commit(&mut BinaryMerkleTree::<Sha256>::new(), rows)?;
let sparse = commit(&mut SparseMerkleTree::<Sha256>::new(), rows)?;
let trie = commit(&mut MerklePatriciaTrie::<Sha256>::new(), rows)?;
for meta in [binary, sparse, trie] {
println!("{:<18} leaves={} height={}", meta.variant, meta.leaf_count, meta.height);
}
Ok(())
}How the trait maps onto each variant
- Binary:
insertappends and returns the nextLeafIndex.removereplaces the leaf withH::empty(); later indices are unchanged. - Sparse: leaf index
iis stored at a key whose last 8 bytes areiin big-endian order.insertreturns the first unoccupied index at or above the leaf count, so it never overwrites a leaf after a removal.generate_proofreturns aMerkleProofwith 256 path nodes. - Patricia: leaf index
iis stored under the keyb"merkleforge:mpt:trait-leaf:" ‖ i(8 bytes, big-endian).generate_proofthrough the trait returnsMerkleError::UnsupportedOperation, because Patricia proofs are key-addressed. UseMerklePatriciaTrie::generate_proof(key)instead.