Help users build sparse Merkle trees with Poseidon or SHA-256 hashing for ZK circuits, privacy pools, and state commitments using Nethereum.Merkle (.NET). Use this skill whenever the user mentions sparse Merkle trees, SMT, Poseidon hashing, Celestia SMT, ZK-compatible state trees, nullifier sets, membership proofs, or PoseidonSmtHasher in a C#/.NET context.
Use this skill when a user wants to:
dotnet add package Nethereum.Merkle
dotnet add package Nethereum.Util
SparseMerkleBinaryTree<T> is a binary sparse Merkle tree where:
The ISmtHasher interface controls hashing. Three built-in strategies:
| Hasher | Hash Function | Use Case |
|---|---|---|
PoseidonSmtHasher | Poseidon (CircomT3 leaf, CircomT2 node) | ZK circuits |
CelestiaSmtHasher | SHA-256 with domain prefixes | Celestia compatibility |
DefaultSmtHasher | Any IHashProvider | Generic use |
The most common use case — a Poseidon-based tree whose root can be used directly as a public input in Circom proofs:
using Nethereum.Merkle.Sparse;
using Nethereum.Util.ByteArrayConvertors;
var smt = new SparseMerkleBinaryTree<byte[]>(
new PoseidonSmtHasher(),
new ByteArrayToByteArrayConvertor(),
new IdentitySmtKeyHasher(256));
smt.Put(key1, value1);
smt.Put(key2, value2);
var root = smt.ComputeRoot(); // Circom-compatible root hash
var value = smt.Get(key1);
smt.Delete(key1);
Poseidon hash details:
Poseidon(key, value, 1) using CircomT3 (3 inputs)Poseidon(left, right) using CircomT2 (2 inputs)var smt = new SparseMerkleBinaryTree<byte[]>(
new CelestiaSmtHasher(),
new ByteArrayToByteArrayConvertor());
smt.Put(key, value);
var root = smt.ComputeRoot();
Hash formulas:
SHA256(0x00 || path || SHA256(value))SHA256(0x01 || leftHash || rightHash)For trees that survive process restarts:
var storage = new InMemorySmtNodeStorage();
var smt = new SparseMerkleBinaryTree<byte[]>(
new PoseidonSmtHasher(),
new ByteArrayToByteArrayConvertor(),
new IdentitySmtKeyHasher(256),
storage: storage);
await smt.PutAsync(key1, value1);
await smt.PutAsync(key2, value2);
var root = await smt.ComputeRootAsync();
await smt.FlushAsync(); // Persist all nodes
// Later — reload from storage
var smt2 = new SparseMerkleBinaryTree<byte[]>(
new PoseidonSmtHasher(),
new ByteArrayToByteArrayConvertor(),
new IdentitySmtKeyHasher(256),
storage: storage);
await smt2.LoadRootAsync(root); // Lazy-loads nodes on demand
var entries = new Dictionary<byte[], byte[]>
{
{ key1, value1 },
{ key2, value2 },
{ key3, value3 }
};
smt.PutBatch(entries); // Sync
await smt.PutBatchAsync(entries); // Async
Console.WriteLine($"Leaves: {smt.LeafCount}");
| Implementation | Description |
|---|---|
IdentitySmtKeyHasher(n) | Key bits used directly as path, n-bit depth |
Sha256SmtKeyHasher | SHA256(key) → 256-bit path |
For custom storage backends:
byte[] encoded = SmtNodeCodec.EncodeLeaf(path, valueBytes);
SmtNodeCodec.DecodeLeaf(encoded, out var path, out var value);
byte[] branch = SmtNodeCodec.EncodeBranch(leftHash, rightHash);
SmtNodeCodec.DecodeBranch(branch, 32, out var left, out var right);
bool isLeaf = SmtNodeCodec.IsLeaf(data);
bool isBranch = SmtNodeCodec.IsBranch(data);
PoseidonSmtHasher uses LSB-first bit ordering, CelestiaSmtHasher uses MSB-firstInMemorySmtNodeStorage is thread-safe (ConcurrentDictionary) but for production use, implement ISmtNodeStorage with a database backendIdentitySmtKeyHasher requires keys to be the exact bit length specified — use Sha256SmtKeyHasher for variable-length keysFor full documentation, see: https://docs.nethereum.com/docs/consensus-and-cryptography/guide-sparse-merkle-zk