Nethereum.Merkle
7.1.0
Prefix Reserved
dotnet add package Nethereum.Merkle --version 7.1.0
NuGet\Install-Package Nethereum.Merkle -Version 7.1.0
<PackageReference Include="Nethereum.Merkle" Version="7.1.0" />
<PackageVersion Include="Nethereum.Merkle" Version="7.1.0" />
<PackageReference Include="Nethereum.Merkle" />
paket add Nethereum.Merkle --version 7.1.0
#r "nuget: Nethereum.Merkle, 7.1.0"
#:package Nethereum.Merkle@7.1.0
#addin nuget:?package=Nethereum.Merkle&version=7.1.0
#tool nuget:?package=Nethereum.Merkle&version=7.1.0
Nethereum.Merkle
Merkle trees for smart-contract verification: prove that an address is on an airdrop list, a whitelist or a snapshot, without putting the list on-chain.
What you can do with it
- Build an airdrop or whitelist tree from a list of recipients and get the root your contract stores.
- Produce a proof a claimant passes to
MerkleProof.verifyon-chain. - Interoperate with OpenZeppelin — the same root and the same proofs as their JavaScript
StandardMerkleTreeandMerkleProof.sol. - Verify a proof off-chain, with or without rebuilding the tree.
- Update a tree without rebuilding it — insert, update or batch-update leaves incrementally.
- Append cheaply up to a fixed capacity — a frontier tree keeps O(depth) memory however many leaves have gone in, but holds at most
1 << depthof them and throws once full. - Commit a huge key space — a sparse tree materialises only the occupied leaves, over in-memory or database storage.
- Target a ZK circuit — Poseidon or Celestia-compatible hashing, with the bit-order and empty-hash conventions those circuits expect.
This is not the Ethereum state trie. For the Modified Merkle Patricia Trie use
Nethereum.Merkle.Patricia; for the EIP-7864 binary state tree useNethereum.Merkle.Binary.
Quick start
Build a tree from a list, take the root, and produce a proof.
From SimpleMerkleTest in tests/Nethereum.Contracts.IntegrationTests/Trie/MerkleDrop/MerkleUnitTests.cs:37 (use case merkle-tree) — a tagged, passing test. Two classes named MerkleUnitTests exist in that project; the tagged one is the Trie.MerkleDrop namespace, under the Trie/ folder.
var elements = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".ToCharArray().ToList();
var merkleTree = new MerkleTree<char>(new Sha3KeccackHashProvider(), new ChartByteArrayConvertor());
merkleTree.BuildTree(elements);
var hexRoot = merkleTree.Root.Hash.ToHex(true);
Assert.Equal("0xec0dffcb601ee38fa372bbf1d89ed16761db0a0b215480032b783f8c33230783", hexRoot);
var proofs = merkleTree.GetProof('A');
For an on-chain airdrop or whitelist you almost always want the OpenZeppelin-compatible tree instead, over an ABI struct that mirrors your Solidity one. SingleParam below stands for that struct — in the tests it is a one-field type declared alongside them (tests/Nethereum.Contracts.IntegrationTests/Trie/MerkleDrop/OpenZeppelinMerkleUnitTests.cs:82-87), and items is a List<SingleParam>:
[Struct("SingleParam")]
public class SingleParam
{
[Parameter("address", 1)]
public string User { get; set; }
}
var merkleTree = new OpenZeppelinStandardMerkleTree<SingleParam>();
merkleTree.BuildTree(items);
var hexRoot = merkleTree.Root.Hash.ToHex(true);
Entry points
Start with OpenZeppelinStandardMerkleTree<T> if the proof will be checked by a contract — it fixes the hashing and pairing conventions OpenZeppelin's MerkleProof.sol expects, and takes no constructor arguments. Use MerkleTree<T> when you control both ends.
| I want to… | Reach for |
|---|---|
| Airdrop or whitelist, verified on-chain | new OpenZeppelinStandardMerkleTree<T>() → BuildTree(items) → Root.Hash / GetProof(item) |
| A tree over my own data type | new MerkleTree<T>(hashProvider, byteArrayConvertor) |
A ready-made (address, amount) airdrop leaf |
new MerkleDropMerkleTree() with MerkleDropItem |
| Verify a proof without the tree | MerkleTree<T>.VerifyProof(proof, rootHash, itemHash, hashProvider, pairingType) |
| Add leaves without rebuilding | new LeanIncrementalMerkleTree<T>(…) → InsertLeaf / Update |
| Append-only, fixed depth, O(depth) memory | new FrontierMerkleTree(depth, hashProvider) → Append(leafHash) |
| Commit millions of keys | new SparseMerkleTree<T>(depth, hashProvider, convertor, storage) |
| Feed a ZK circuit | new SparseMerkleBinaryTree<T>(new PoseidonSmtHasher(), …) |
On MerkleTree<T> and its subclasses Root is a MerkleTreeNode, so the value your contract stores is tree.Root.Hash. The other three trees expose the root differently: LeanIncrementalMerkleTree<T>.Root and FrontierMerkleTree.Root are raw byte[], SparseMerkleTree<T> has GetRootHashAsync(), and SparseMerkleBinaryTree<T> has ComputeRoot().
Installation
dotnet add package Nethereum.Merkle
Key concepts
Hashing and leaf serialisation are injected
MerkleTree<T> never assumes how your leaf becomes bytes or how bytes become a hash:
MerkleTree(
IHashProvider hashProvider,
IByteArrayConvertor<T> byteArrayConvertor,
PairingConcatType pairingConcatType = PairingConcatType.Sorted)
IHashProvider (Nethereum.Util.HashProviders) is a single method, byte[] ComputeHash(byte[] data); Sha3KeccackHashProvider is the Keccak-256 one. IByteArrayConvertor<T> (Nethereum.Util.ByteArrayConvertors) is a two-way contract — implement both halves:
byte[] ConvertToByteArray(T data);
T ConvertFromByteArray(byte[] data);
The convertors that ship in Nethereum.Util.ByteArrayConvertors are StringByteArrayConvertor (UTF-8), HexToByteArrayConvertor (hex string ⇄ bytes), ChartByteArrayConvertor (char), and ByteArrayToByteArrayConvertor (identity).
Pairing strategies
When combining a pair of hashes, the order matters and must match the verifier on the other side:
PairingConcatType |
Strategy | Meaning |
|---|---|---|
Sorted |
SortedPairConcatStrategy |
sort the two hashes lexicographically before concatenating — the OpenZeppelin convention |
Normal |
PairConcatStrategy |
concatenate left‖right as given |
public enum PairingConcatType
{
Normal,
Sorted
}
public interface IPairConcatStrategy
{
byte[] Concat(byte[] left, byte[] right);
}
PairingConcatFactory.GetPairConcatStrategy(PairingConcatType type) returns the strategy for a given type.
Sorted pairing is why MerkleTree<T>.GetProof returns only sibling hashes and no direction bits — the verifier can re-sort. LeanIncrementalMerkleTree<T> defaults to Normal and therefore returns a MerkleProof carrying explicit PathIndices.
Use cases
Token airdrops, whitelists / allowlists, state commitments, fraud proofs, and off-chain metadata attestation — anywhere a contract must check membership of a large set without storing it.
Usage
Snippets marked From … are extracted verbatim from [NethereumDocExample(DocSection.SmartContracts, …)]-tagged passing tests under tests/Nethereum.Contracts.IntegrationTests/Trie/.
They are fragments, not programs. Extraction keeps the body of the test method, so a snippet may use the fixture's fields and constants (_hashProvider, _convertor, Leaves, TreeSize, ExpectedRootAfterInsert) or a test-only [Struct] type such as SingleParam, and it may end in Assert. Open the named test file for the setup before copying.
Build a tree, prove a leaf, verify the proof
From Trie.MerkleDrop.MerkleUnitTests.SimpleMerkleTest (use case merkle-tree) — tests/Nethereum.Contracts.IntegrationTests/Trie/MerkleDrop/MerkleUnitTests.cs:37.
var elements = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".ToCharArray().ToList();
var merkleTree = new MerkleTree<char>(new Sha3KeccackHashProvider(), new ChartByteArrayConvertor());
merkleTree.BuildTree(elements);
var hexRoot = merkleTree.Root.Hash.ToHex(true);
Assert.Equal("0xec0dffcb601ee38fa372bbf1d89ed16761db0a0b215480032b783f8c33230783", hexRoot);
var proofs = merkleTree.GetProof('A');
Root is a MerkleTreeNode, so the on-chain root is merkleTree.Root.Hash. GetProof has three overloads — by item (T), by leaf hash (byte[]), and by leaf index (int).
OpenZeppelin-compatible whitelist over an ABI struct
OpenZeppelinStandardMerkleTree<T> is AbiStructSha3KeccackMerkleTree<T> under a name that says what it is compatible with — it adds nothing and takes no constructor arguments. Its leaf is keccak(keccak(abi.encode(params))): the convertor hashes the ABI encoding, and MerkleTree<T> hashes the leaf again — matching OpenZeppelin's StandardMerkleTree and MerkleProof.sol.
AbiStructSha3KeccackMerkleTree<T> also sorts the leaves by hash before building (AbiStructSha3KeccackMerkleTree.cs:15-19 overrides InitialiseLeavesAndLayersAndBuildTree to leaves.Sort(new MerkleTreeNodeComparer())), which is the other half of OpenZeppelin compatibility. Two consequences: the tree's leaf order is not the order you passed items in, so Leaves[i] and GetProof(int index) index the sorted order; and the root is independent of input order. AbiStructMerkleTree<T> does not sort — it is the same ABI leaf without that override (AbiStructMerkleTree.cs:7-12). T must carry [Struct] and its properties [Parameter], exactly as the Solidity struct declares them.
From Trie.MerkleDrop.OpenZeppelinMerkleUnitTests.SingleParamMultipleItems (use case merkle-tree) — tests/Nethereum.Contracts.IntegrationTests/Trie/MerkleDrop/OpenZeppelinMerkleUnitTests.cs:32. This class is duplicated in the same project too; the tagged copy is the one under Trie/.
var item1 = new SingleParam { User = "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5" };
var item2 = new SingleParam { User = "0xA61b1fB89Dd42fcDDD2D3fA19c2B715c426692c7" };
var item3 = new SingleParam { User = "0xfa6179E49EE57a06391F218965b35B632F930472" };
var item4 = new SingleParam { User = "0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326" };
var items = new List<SingleParam> { item1, item2, item3, item4 };
var merkleTree = new OpenZeppelinStandardMerkleTree<SingleParam>();
merkleTree.BuildTree(items);
var hexRoot = merkleTree.Root.Hash.ToHex(true);
The root here is 0xa1b819a3413fb3120a911642ee7fbdb17572e844090a95a0848d07aa230eb702 — pinned by the test, and produced by OpenZeppelin's JS StandardMerkleTree for the same input. MultiParam_MultipleItems in the same file pins a two-field struct.
Send merkleTree.Root.Hash to your contract's constructor, and give each claimant merkleTree.GetProof(theirItem) to pass to MerkleProof.verify.
Airdrops: MerkleDropMerkleTree
MerkleDropMerkleTree : AbiStructMerkleTree<MerkleDropItem> is the pre-wired airdrop tree. MerkleDropItem is a [Struct("MerkleDropItem")] with exactly two parameters:
[Struct("MerkleDropItem")]
public class MerkleDropItem
{
[Parameter("address", "address")]
public string Address { get; set; }
[Parameter("uint256", "amount", 2)]
public BigInteger Amount { get; set; }
}
From Trie.MerkleDrop.MerkleUnitTests.MerkleDropTree_ProvesARecipientAgainstItsRoot (use case merkle-tree) — tests/Nethereum.Contracts.IntegrationTests/Trie/MerkleDrop/MerkleUnitTests.cs:15.
var tree = new MerkleDropMerkleTree();
tree.BuildTree(items);
var root = tree.Root.Hash.ToHex(true);
var proof = tree.GetProof(items[0]);
Assert.StartsWith("0x", root);
Assert.NotEmpty(proof);
Assert.True(tree.VerifyProof(proof, items[0]));
There is no index field — the leaf is (address, amount). If your contract's leaf includes an index or any other field, define your own [Struct] type and use AbiStructMerkleTree<T> (packed ABI encoding) or AbiStructSha3KeccackMerkleTree<T> (Keccak of the ABI encoding) instead.
Incremental updates: LeanIncrementalMerkleTree<T>
From LeanIncrementalMerkleTreeBigIntTests.InsertLeaves_ShouldProduceExpectedRootAndDepth (use case incremental-merkle-tree). _hashProvider in that fixture is a toy SumHashProvider that adds two bytes and returns one (tests/Nethereum.Contracts.IntegrationTests/Trie/LeanIMT/LeanIncrementalMerkleTreeBigIntTests.cs:17,36-47), which is the only reason the last two assertions hold: the root is one byte long, and ExpectedRootAfterInsert is the arithmetic sum ((0+1)+(2+3))+4 = 10 (:27). Under a real Sha3KeccackHashProvider the root is 32 bytes and Assert.Single would fail — read those two lines as fixture arithmetic, not as tree behaviour.
var tree = new LeanIncrementalMerkleTree<byte[]>(_hashProvider, _convertor);
foreach (var leaf in Leaves)
tree.InsertLeaf(leaf);
Assert.Equal(TreeSize, tree.Size);
Assert.Equal((int)Math.Ceiling(Math.Log2(TreeSize)), tree.Depth);
Assert.Single(tree.Root);
Assert.Equal(ExpectedRootAfterInsert, tree.Root[0]);
The two assertions that do generalise are the first two: Size counts the leaves inserted, and Depth is ceil(log2(Size)). Root here is a raw byte[] — its length is whatever the hash provider returns — not a MerkleTreeNode; the lean tree is a different type, not a MerkleTree<T> subclass. Inserting does not rebuild the whole tree.
Proofs carry direction, because the lean tree pairs in Normal order:
From LeanIncrementalMerkleTreeBigIntTests.GenerateAndVerifyProof_ForEachLeaf_ShouldBeValid (use case incremental-merkle-tree).
var tree = new LeanIncrementalMerkleTree<byte[]>(_hashProvider, _convertor);
tree.InsertMany(Leaves);
for (int i = 0; i < TreeSize; i++)
{
var proof = tree.GenerateProof(i);
Assert.True(tree.VerifyProof(proof, Leaves[i], tree.Root));
}
A tree can be serialised and rebuilt — Export() produces JSON, Import(...) takes it back with a mapper from the exported leaf string to T:
From LeanIncrementalMerkleTreeBigIntTests.ExportImport_RoundTripPreservesTree (use case incremental-merkle-tree).
var tree = new LeanIncrementalMerkleTree<byte[]>(_hashProvider, _convertor);
tree.InsertMany(Leaves);
var json = tree.Export();
var imported = LeanIncrementalMerkleTree<byte[]>.Import(
_hashProvider,
_convertor,
json,
str => Convert.FromBase64String(str)
);
Assert.Equal(tree.Size, imported.Size);
Assert.Equal(tree.Depth, imported.Depth);
Assert.Equal(tree.Root[0], imported.Root[0]);
Import verifies the imported node set against the leaves by default (verifyIntegrity: true); Import_WithMismatchedTreeData_ShouldThrow is the test that pins that.
Append-only: FrontierMerkleTree
FrontierMerkleTree is a fixed-depth append-only tree that keeps only the right-hand frontier — O(depth) memory regardless of how many leaves have been appended. It is the shape a Solidity deposit contract uses.
public class FrontierMerkleTree
{
public FrontierMerkleTree(
int depth,
IHashProvider hashProvider,
PairingConcatType pairingConcatType = PairingConcatType.Sorted);
public byte[] Root { get; }
public int NextIndex { get; }
public int Capacity { get; }
public void Append(byte[] leafHash);
public static bool VerifyProof(
MerkleProof proof,
byte[] root,
byte[] leafHash,
IHashProvider hashProvider,
PairingConcatType pairingConcatType = PairingConcatType.Sorted);
}
depth must be 1–30, Capacity is 1 << depth, and Append takes a pre-hashed leaf.
Sparse trees over a large key space: SparseMerkleTree<T>
From SparseMerkleTreeNewTests.NewImplementation_SimpleTest_ShouldWork (use case sparse-merkle-tree).
var storage = new InMemorySparseMerkleTreeStorage<string>();
var convertor = new StringByteArrayConvertor();
var tree = new SparseMerkleTree<string>(8, _hashProvider, convertor, storage);
var emptyRoot = await tree.GetRootHashAsync();
await tree.SetLeafAsync("10", "test_value");
var newRoot = await tree.GetRootHashAsync();
Assert.NotEqual(emptyRoot, newRoot);
var leafCount = await tree.GetLeafCountAsync();
Assert.Equal(1, leafCount);
The API is async only — there are no synchronous SetLeaf / GetLeaf / GetRootHash methods, because the storage behind it may be a database. SetLeavesAsync(Dictionary<string, T> keyValuePairs) batches a block's worth of updates into one pass instead of recomputing the root per key.
Storage is ISparseMerkleTreeStorage<T>, with two implementations: InMemorySparseMerkleTreeStorage<T> for tests and small sets, and DatabaseSparseMerkleTreeStorage<T> for production, whose single constructor parameter is an ISparseMerkleRepository<T> you implement against your database. (Both the repository and the storage are generic in the leaf type T.)
ZK-oriented sparse tree: SparseMerkleBinaryTree<T>
SparseMerkleBinaryTree<T> is the binary sparse tree used for zero-knowledge circuits. It takes the hash strategy rather than a plain hash provider, because a ZK-friendly SMT differs in bit order, empty-hash handling and single-leaf collapsing:
SparseMerkleBinaryTree(
ISmtHasher hasher,
IByteArrayConvertor<T> valueConvertor,
ISmtKeyHasher keyHasher = null, // defaults to IdentitySmtKeyHasher(256)
ISmtNodeStorage storage = null)
| Sync | Async |
|---|---|
Put(byte[] key, T value) |
PutAsync |
Get(byte[] key) |
GetAsync |
Delete(byte[] key) |
DeleteAsync |
ComputeRoot() |
ComputeRootAsync |
PutBatch(IEnumerable<KeyValuePair<byte[], T>> entries) |
PutBatchAsync |
Three more members have no counterpart in the other column and are not async pairs of anything: void Clear() (SparseMerkleBinaryTree.cs:75) drops the in-memory tree and has no ClearAsync; Task FlushAsync() (:126) writes dirty nodes back to ISmtNodeStorage; Task LoadRootAsync(byte[] rootHash) (:132) attaches to a stored root and lazy-loads nodes as the walk needs them.
Plus Depth, EmptyLeafHash and LeafCount.
ISmtHasher — hashing strategies
public interface ISmtHasher
{
bool MsbFirst { get; }
bool UseFixedEmptyHash { get; }
bool CollapseSingleLeaf { get; }
byte[] EmptyLeaf { get; }
byte[] HashLeaf(byte[] path, byte[] valueBytes);
byte[] HashNode(byte[] leftHash, byte[] rightHash);
}
| Hasher | MsbFirst |
UseFixedEmptyHash |
CollapseSingleLeaf |
Use case |
|---|---|---|---|---|
PoseidonSmtHasher |
false |
true |
true |
ZK circuits (Circom, Privacy Pools). PoseidonSmtHasher(PoseidonHasher leafHasher, PoseidonHasher nodeHasher) swaps the permutations. |
CelestiaSmtHasher |
true |
true |
true |
Celestia sparse-Merkle-tree compatibility |
DefaultSmtHasher(IHashProvider hashProvider) |
false |
false |
false |
generic — Keccak, SHA-256, anything |
ISmtKeyHasher — key → path
public interface ISmtKeyHasher
{
byte[] ComputePath(byte[] key);
int PathBitLength { get; }
}
IdentitySmtKeyHasher(int bitLength) uses the key itself as the path; Sha256SmtKeyHasher hashes it to a 256-bit path. The tree's depth is PathBitLength, and must be 1–256.
ISmtNodeStorage and SmtNodeCodec
public interface ISmtNodeStorage
{
Task<byte[]> GetAsync(byte[] hash);
Task PutAsync(byte[] hash, byte[] data);
Task DeleteAsync(byte[] hash);
}
InMemorySmtNodeStorage is the in-box implementation, and SmtNodeCodec is the wire format those nodes are stored in:
public static class SmtNodeCodec
{
public static byte[] EncodeLeaf(byte[] path, byte[] valueBytes);
public static void DecodeLeaf(byte[] data, out byte[] path, out byte[] valueBytes);
public static byte[] EncodeBranch(byte[] leftHash, byte[] rightHash);
public static void DecodeBranch(byte[] data, int hashSize, out byte[] leftHash, out byte[] rightHash);
public static bool IsLeaf(byte[] data);
public static bool IsBranch(byte[] data);
}
API reference
MerkleTree<T>
| Member | |
|---|---|
MerkleTree(IHashProvider hashProvider, IByteArrayConvertor<T> byteArrayConvertor, PairingConcatType pairingConcatType = PairingConcatType.Sorted) |
constructor |
MerkleTreeNode Root |
root node |
List<MerkleTreeNode> Leaves |
leaf nodes |
List<List<MerkleTreeNode>> Layers |
every layer, bottom-up |
void BuildTree(List<T> items) |
build from items |
MerkleTreeNode BuildTree(List<MerkleTreeNode> nodes) |
build from pre-hashed nodes; returns the root |
void InsertLeaf(T item) / void InsertLeaves(IEnumerable<T> items) |
add and rebuild (both virtual) |
List<byte[]> GetProof(T item) / GetProof(byte[] hashLeaf) / GetProof(int index) |
sibling hashes, leaf-to-root |
bool VerifyProof(IEnumerable<byte[]> proof, T item) / VerifyProof(IEnumerable<byte[]> proof, byte[] itemHash) |
verify against this tree's root |
static bool VerifyProof(IEnumerable<byte[]> proof, byte[] rootHash, byte[] itemHash, IHashProvider hashProvider, PairingConcatType pairingConcatType = PairingConcatType.Sorted) |
verify without a tree |
static byte[] ConcatAndHashPair(byte[] left, byte[] right, IHashProvider hashProvider, PairingConcatType pairingConcatType = PairingConcatType.Sorted) |
one pairing step |
Subclasses: AbiStructMerkleTree<T> (packed ABI encoding, Keccak, sorted), AbiStructSha3KeccackMerkleTree<T> (Keccak-of-ABI leaf hashing, Keccak, sorted), OpenZeppelinStandardMerkleTree<T> : AbiStructSha3KeccackMerkleTree<T>, MerkleDropMerkleTree : AbiStructMerkleTree<MerkleDropItem>. All four are parameterless.
LeanIncrementalMerkleTree<T>
| Member | |
|---|---|
LeanIncrementalMerkleTree(IHashProvider hashProvider, IByteArrayConvertor<T> byteArrayConvertor, PairingConcatType pairingConcatType = PairingConcatType.Normal, bool hashLeafOnInsert = true, ILeanIMTNodeStorage storage = null) |
constructor |
byte[] Root |
current root (raw bytes) |
IReadOnlyList<T> Leaves, int Size, int Depth |
state |
ILeanIMTNodeStorage Storage |
the node store in use |
void InsertLeaf(T leaf) / void InsertMany(IEnumerable<T> leaves) |
append |
void Update(int index, T newLeaf) / void UpdateMany(int[] indices, T[] newLeaves) |
in-place update |
bool Has(T leaf) / int IndexOf(T leaf) |
lookup |
MerkleProof GenerateProof(int leafIndex) / bool VerifyProof(MerkleProof proof, T leaf, byte[] root) |
proofs |
string Export(Func<byte[], string> nodeFormatter = null) |
JSON |
static LeanIncrementalMerkleTree<T> Import(IHashProvider hashProvider, IByteArrayConvertor<T> byteArrayConvertor, string json, Func<string, T> leafMapper, Func<string, byte[]> nodeParser = null, PairingConcatType pairingConcatType = PairingConcatType.Normal, bool hashLeafOnInsert = true, ILeanIMTNodeStorage storage = null, bool verifyIntegrity = true) |
rebuild from JSON |
void VerifyStorageIntegrity() |
re-derive every node and throw on mismatch |
Node storage is pluggable, and defaults to InMemoryLeanIMTNodeStorage:
public interface ILeanIMTNodeStorage
{
byte[] GetNode(int level, int index);
void SetNode(int level, int index, byte[] value);
void SetNodesBatch(IEnumerable<LeanIMTNodeEntry> nodes);
int GetNodeCount(int level);
void EnsureLevel(int level);
int GetLevelCount();
void Clear();
}
public class LeanIMTNodeEntry
{
public int Level { get; set; }
public int Index { get; set; }
public byte[] Value { get; set; }
public LeanIMTNodeEntry(int level, int index, byte[] value);
}
SparseMerkleTree<T>
| Member | |
|---|---|
SparseMerkleTree(int depth, IHashProvider hashProvider, IByteArrayConvertor<T> byteArrayConvertor, ISparseMerkleTreeStorage<T> storage) and the overload adding ISmtHasher hasher |
constructors; depth 1–256 |
int Depth, string EmptyLeafHash, IHashProvider HashProvider |
state |
Task SetLeafAsync(string key, T value) / Task<T> GetLeafAsync(string key) |
leaf access |
Task SetLeavesAsync(Dictionary<string, T> keyValuePairs) |
batch update |
Task<string> GetRootHashAsync() |
cached root |
Task<long> GetLeafCountAsync() / Task ClearAsync() |
maintenance |
Supporting types
public class MerkleProof
{
public List<byte[]> ProofNodes { get; set; }
public List<int> PathIndices { get; set; }
}
public class MerkleTreeNode
{
public byte[] Hash { get; set; }
public MerkleTreeNode(byte[] hash);
public int Compare(MerkleTreeNode other);
public int Compare(byte[] hashOther);
public bool Matches(byte[] hashOther);
public bool Matches(MerkleTreeNode other);
public MerkleTreeNode Clone();
}
MerkleTreeNodeComparer is the IComparer<MerkleTreeNode> over those hash bytes, reachable as MerkleTreeNodeComparer.Current.
MerkleProof is default-constructible (both lists start empty), but MerkleTreeNode and LeanIMTNodeEntry are not — each declares only the constructor shown, so new MerkleTreeNode { Hash = h } does not compile; write new MerkleTreeNode(h) and new LeanIMTNodeEntry(level, index, value).
Notes and gotchas
MerkleProofis not whatMerkleTree<T>returns.MerkleTree<T>.GetProofreturns a bareList<byte[]>(sorted pairing needs no direction);LeanIncrementalMerkleTree<T>.GenerateProofandFrontierMerkleTree.VerifyProofuse theMerkleProoftype with itsPathIndices.- Pairing must match the verifier. Sorted pairing is OpenZeppelin's convention; a contract written against a normal-order tree will reject sorted proofs and vice versa.
- A
[Struct]type must mirror the Solidity struct: same parameter types, same order, correct[Parameter]order indices. A mismatch produces a different leaf hash and a silently unverifiable proof. FrontierMerkleTree.Appendtakes a hash, not an item — hash the leaf yourself before appending.IByteArrayConvertor<T>has two methods. A convertor that only implementsConvertToByteArraywill not compile.MerkleTree<T>andAbiStructMerkleTree<T>use a single hash for leaves and internal nodes. A leaf whose encoding is exactly 64 bytes (two 32-byte words, such as an ABI struct of twobytes32fields) can therefore be confused with an internal node: the preimage of an internal node verifies as a leaf against the sibling-side proof. The hashing is kept so that roots match the on-chain OpenZeppelinMerkleProofverifiers deployed against them. For new designs useOpenZeppelinStandardMerkleTree<T>, which double-hashes leaves and rejects this forgery.- Duplicate leaves are permitted, but
GetProof(T item)andLeanIncrementalMerkleTree<T>.IndexOfboth resolve to the first match. GetProofthrows when the leaf is absent —MerkleTree<T>.GetProof(byte[] hashLeaf)raisesException("Leaf not found")rather than returning an empty proof.- The static
VerifyProofreturnsfalsefor a null proof, root, item hash or proof element instead of throwing (MerkleTreeSoundnessTests.Given_a_proof_with_a_null_node_when_verified_then_it_returns_false_without_throwing). - The
byte[] itemHashoverload of the instanceVerifyProofalways verifies withSortedpairing, whatever the tree was constructed with — it does not forward the tree'sPairingConcatType. On aNormal-order tree, use theT itemoverload or the static one and pass the pairing explicitly (MerkleTree.cs:119). AbiStructSha3KeccackHashByteArrayConvertor<T>.ConvertFromByteArraythrowsNotSupportedException— hashing is one-way, so an OpenZeppelin tree can prove a leaf but cannot recover it.FrontierMerkleTree.AppendthrowsInvalidOperationExceptiononceNextIndexreachesCapacity— the tree is fixed-depth and does not grow.
Related packages
Nethereum.Merkle.Patricia— the Ethereum state trie: Modified Merkle Patricia Trie, path-keyed node storage, EIP-1186 and snap/1 range proofs.Nethereum.Merkle.Binary— EIP-7864 binary Merkle trie for stateless execution.Nethereum.ABI— the[Struct]/[Parameter]attributes and the packed encoders the ABI-struct trees use.Nethereum.Util—IHashProvider,IByteArrayConvertor<T>,Sha3Keccack,PoseidonHasher.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net451 is compatible. net452 was computed. net46 was computed. net461 is compatible. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.5.1
- Nethereum.ABI (>= 7.1.0)
- Nethereum.Util (>= 7.1.0)
- Newtonsoft.Json (>= 13.0.4 && < 14.0.0)
-
.NETFramework 4.6.1
- Nethereum.ABI (>= 7.1.0)
- Nethereum.Util (>= 7.1.0)
- Newtonsoft.Json (>= 13.0.4 && < 14.0.0)
-
.NETStandard 2.0
- Nethereum.ABI (>= 7.1.0)
- Nethereum.Util (>= 7.1.0)
- NETStandard.Library (>= 2.0.3)
- Newtonsoft.Json (>= 13.0.4 && < 14.0.0)
-
net10.0
- Nethereum.ABI (>= 7.1.0)
- Nethereum.Util (>= 7.1.0)
- Newtonsoft.Json (>= 13.0.4 && < 14.0.0)
-
net6.0
- Nethereum.ABI (>= 7.1.0)
- Nethereum.Util (>= 7.1.0)
- Newtonsoft.Json (>= 13.0.4 && < 14.0.0)
-
net8.0
- Nethereum.ABI (>= 7.1.0)
- Nethereum.Util (>= 7.1.0)
- Newtonsoft.Json (>= 13.0.4 && < 14.0.0)
-
net9.0
- Nethereum.ABI (>= 7.1.0)
- Nethereum.Util (>= 7.1.0)
- Newtonsoft.Json (>= 13.0.4 && < 14.0.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Nethereum.Merkle:
| Package | Downloads |
|---|---|
|
Nethereum.PrivacyPools
Nethereum PrivacyPools - 0xbow Privacy Pools protocol SDK with Poseidon commitments, LeanIMT, and Groth16 proof verification |
|
|
Nethereum.AppChain.Anchoring
Nethereum AppChain Anchoring - L1/L2 state root anchoring service for committing AppChain state to Ethereum mainnet |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 7.1.0 | 67 | 11/10/2026 |
| 7.0.0 | 190 | 10/2/2026 |
| 6.1.0 | 1,078 | 3/25/2026 |
| 6.0.4 | 211 | 3/18/2026 |
| 6.0.3 | 165 | 3/18/2026 |
| 6.0.1 | 179 | 3/17/2026 |
| 6.0.0 | 160 | 3/16/2026 |
| 5.8.0 | 479 | 1/6/2026 |
| 5.0.0 | 727 | 5/28/2025 |
| 4.29.0 | 3,643 | 2/10/2025 |
| 4.28.0 | 1,297 | 1/7/2025 |
| 4.27.1 | 325 | 12/24/2024 |
| 4.27.0 | 267 | 12/24/2024 |
| 4.26.0 | 955 | 10/1/2024 |
| 4.25.0 | 557 | 9/19/2024 |
| 4.21.4 | 376 | 8/9/2024 |
| 4.21.3 | 989 | 7/22/2024 |
| 4.21.2 | 461 | 6/26/2024 |
| 4.21.1 | 285 | 6/26/2024 |
| 4.21.0 | 282 | 6/18/2024 |