Skip to main content

Nethereum.Merkle.Patricia

NuGet: Nethereum.Merkle.Patricia | Source: src/Nethereum.Merkle.Patricia/ The Modified Merkle Patricia Trie — the data structure behind Ethereum account state, contract storage, and transaction and receipt roots. Build one, commit it, prove a key against its root, or verify someone else's proof without holding the state.

What you can do with it​

  • Compute a state, storage or transaction root from a set of key/value pairs.
  • Prove a key — generate the root-first, EIP-1186 node list a light client or contract needs.
  • Verify an account, a storage slot or a transaction against a trusted root, without the rest of the state.
  • Serve and verify snap/1 range proofs — hand out a slice of the trie with edge proofs, and check one you were given.
  • Store trie nodes by path instead of by hash, so an updated node overwrites its predecessor and the database does not grow with history.
  • Keep many contract storage tries in one store, separated by owner, without slot-key collisions.
  • Build a trie larger than memory, persisting and collapsing as you go.

Quick start​

Compute a root, prove a key against it, and verify the proof.

From ProofVerificationFrontDoorTests.QuickStart_ComputeARootProveAKeyAndVerifyIt (use case quick-start) — a tagged, passing test.

var hashProvider = Sha3KeccackHashProvider.Instance;
var trie = new PatriciaTrie(new InMemoryContentNodeStore());

for (var i = 0; i < 128; i++)
trie.Put(hashProvider.ComputeHash(new[] { (byte)i }), new byte[] { (byte)i, 0xAB });
trie.SaveNodesToStorage();

var root = trie.Root.GetHash();
var key = hashProvider.ComputeHash(new byte[] { 7 });
var proof = ProofGenerator.GenerateProof(trie, key);

Assert.Equal(root, hashProvider.ComputeHash(proof[0]));
Assert.True(ProofVerification.Current.Range.VerifyEntry(root, key, new byte[] { 7, 0xAB }, proof));

Entry points​

Start with PatriciaTrie. It holds its own node store, so Get, Put and Delete take no storage argument.

I want to…Reach for
Build a trie and get its rootnew PatriciaTrie(store) → Put(key, value) → SaveNodesToStorage() → Root.GetHash()
Reload a trie from a rootnew PatriciaTrie(rootHash, store)
Prove a keyProofGenerator.GenerateProof(trie, key)
Verify an account or storage proofProofVerification.Current.Account.Verify(…) / .Storage.Verify(…)
Serve a snap/1 rangePatriciaRangeIterator.EnumerateRange(…) + PatriciaRangeProofGenerator.GenerateProof(…)
Verify a snap/1 rangeProofVerification.Current.Range.Verify(…)
Keep a contract's storage trie apartnew PatriciaTrie(store, owner: keccak(address))
Build something bigger than memorySaveDirtyNodesToStorageAndCollapse() every N writes

Two node stores ship in the box: InMemoryContentNodeStore (keyed by hash — what proof verification uses) and InMemoryPathNodeStore (keyed by owner and path — what a node uses). Persistent RocksDB implementations live in Nethereum.CoreChain.RocksDB.

Installation​

dotnet add package Nethereum.Merkle.Patricia

Architecture: the trie holds one node store​

The trie holds a single node store (ITrieNodeStore) rather than threading a storage object through every Get/Put/Delete. Each node carries its own identity — its hash (GetHash()) and its location (Owner, Path) — and the store alone decides which of the two it keys on.

That one decision is the whole difference between the two storage models:

Content-addressed storePath-keyed (location-addressed) store
Keynode.GetHash()Owner ‖ Path
Nodes storedevery node, including embedded <32-byte onesonly nodes ≥32 bytes; shorter children are inlined in the parent and re-decoded on read
Effect of an updatea new key appears; the old node staysthe same key is overwritten — the trie does not grow with history
Deletionsnothing to doneed tombstones, emitted from an ITrieTracer
Integrity on readimplicit (the key is the hash)explicit — the store verifies the blob against the reference hash it was asked for
In-box implementationInMemoryContentNodeStoreInMemoryPathNodeStore

Owner is empty for the account trie and keccak(address) for a contract's storage trie, so many storage tries coexist in one path store without colliding on slot keys. This single-store model is what lets the same trie code run over an in-memory content store (proofs) or a persistent path-keyed database (a full node's state) with no API change.

Persistent RocksDB implementations of ITrieNodeStore — path-keyed and content-addressed, with node history — live in Nethereum.CoreChain.RocksDB (src/Nethereum.CoreChain.RocksDB/Stores/).

Key concepts​

  • Nodes — LeafNode (path + value), ExtendedNode (shared-prefix path + one child), BranchNode (16 children + optional value), HashNode (a 32-byte reference that lazily decodes its inner node through the held store), EmptyNode.
  • Nibbles — keys are traversed a nibble (4 bits) at a time; leaves and extensions carry hex-prefix-encoded nibble paths.
  • Node identity — every Node has GetHash() (keccak of its RLP), plus Owner and Path. The store keys on one or the other.
  • TrieNodeSet — the unit of a write: a batch of nodes to persist, plus TrieNodeDelete tombstones for removals. The trie collects dirty nodes into a set and calls store.Commit(set).
  • Hash provider — every constructor has an overload taking an IHashProvider; the default is Sha3KeccackHashProvider.Instance (Nethereum.Util.HashProviders).

The node store​

TypeRole
ITrieNodeStoreThe unified store the trie holds: Commit(TrieNodeSet nodes), Get(Node reference) (returns raw RLP; the store never decodes), Contains(Node reference), ContainsKey(byte[] stateRoot), Flush(), Clear().
InMemoryContentNodeStoreContent-addressed in-memory store (keyed by keccak). Also implements INodeBlobStore. Stores all nodes; self-verifying. The accumulator every proof verifier uses.
InMemoryPathNodeStoreLocation-addressed in-memory store (keyed by owner ‖ path). Skips <32-byte nodes, verifies on read, applies tombstones.
ContentAddressedNodeStoreAdapter presenting a raw INodeBlobStore as a content-addressed ITrieNodeStore (ContentAddressedNodeStore.Wrap(INodeBlobStore storage)).
INodeBlobStoreThe raw hash→blob byte store: Put(byte[] key, byte[] value), Get(byte[] key), Delete(byte[] key).
IRawNodeReaderTryGetRawNode(byte[] owner, byte[] path) — reattach a trie to a root read straight out of a path store (PatriciaTrie.ReattachFromRawRoot).
ITrieTracer / TrieTracerObserves node removals (OnRemove(byte[] owner, byte[] path, byte[] prevBlob), Removals, Reset()) so a path-store commit can emit tombstones. Zero cost when absent (content mode).
IContractStorageWipeableDeleteRange(byte[] owner) — wipes a whole contract's storage subtree (SELFDESTRUCT) on a path store.
TransientFlushUnavailableExceptionRaised by a store that cannot honour a Flush() at this moment.

Storage declarations​

These declarations are copied from the source files under src/Nethereum.Merkle.Patricia/Storage/.

ReadmeTraceabilityTests (tests/Nethereum.Merkle.Patricia.Tests/ReadmeTraceabilityTests.cs) checks that each tagged type is named in a README code block, and that every public member it declares — properties, fields and methods — is named somewhere in this file. Interfaces are covered by that: rename or remove TryGetRawNode, Put, DeleteRange or any member below and the test goes red naming the member.

Argument order is checked too, but only where this README writes the member with parentheses, and the check is a subsequence scan over the whole block rather than a signature parse. So a swapped pair is caught when the block names those parameters once, and can be masked when a neighbouring signature in the same block supplies the same names in the old order. Treat the suite as a reliable guard against renames and removals, and read the source file when the exact parameter order matters.

public interface ITrieNodeStore
{
void Commit(TrieNodeSet nodes);
byte[] Get(Node reference);
bool Contains(Node reference);
void Flush();
void Clear();

bool ContainsKey(byte[] stateRoot);
}

public interface INodeBlobStore
{
void Put(byte[] key, byte[] value);
byte[] Get(byte[] key);
void Delete(byte[] key);
}

public interface IRawNodeReader
{
byte[] TryGetRawNode(byte[] owner, byte[] path);
}

public interface IContractStorageWipeable
{
void DeleteRange(byte[] owner);
}

A commit carries removals as well as writes. TrieNodeSet.AddDelete(byte[] owner, byte[] path, byte[] prevBlob) records one:

public readonly struct TrieNodeDelete
{
public byte[] Owner { get; }
public byte[] Path { get; }
public byte[] PrevBlob { get; }

public TrieNodeDelete(byte[] owner, byte[] path, byte[] prevBlob);
}

A content store ignores those — its keys are hashes, so a superseded node is simply never referenced again. A path store must apply them, because the key would otherwise still resolve.

Usage​

Every snippet below is extracted verbatim from a [NethereumDocExample(DocSection.ChainInfrastructure, …)]-tagged passing test in tests/Nethereum.Merkle.Patricia.Tests.

They are fragments, not programs. Extraction keeps the body of the test method, so a snippet may use variables built in the test's arrange step or private fixture helpers (KeyHash, Value, RefOf, keys, values, rootHash, …) that are not shown here, and it may end in Assert. Open the named test file for the surrounding setup before copying — only the Quick Start above is self-contained.

Build, commit, reload — the store is held, not threaded​

From ProofVerificationFrontDoorTests.BuildCommitAndReloadThroughOneHeldStore (use case patricia-trie).

var keccak = new Sha3Keccack();
var store = new InMemoryContentNodeStore();
var trie = new PatriciaTrie(store);

var keys = new List<byte[]>();
var values = new List<byte[]>();
for (var i = 0; i < 200; i++)
{
var key = keccak.CalculateHash(new byte[] { (byte)i, (byte)(i >> 8) });
var value = Nethereum.RLP.RLP.EncodeElement(new byte[] { (byte)i });
keys.Add(key);
values.Add(value);
trie.Put(key, value);
}
trie.SaveNodesToStorage();

var rootHash = trie.Root.GetHash();
var reloaded = new PatriciaTrie(rootHash, store);

for (var i = 0; i < keys.Count; i++)
Assert.Equal(values[i], reloaded.Get(keys[i]));

HeldStoreTrieTests.Reads_Through_Held_Store_Without_Threading runs the same reload over both store kinds from one [Theory], which is the point: the trie code does not know which it is holding.

Get/Put/Delete take no storage parameter. PatriciaTrie.LoadFromStorage(rootHash, store) is the (rootHash, store) constructor plus an empty-root branch: a null or empty rootHash returns new PatriciaTrie(store, …) — an empty trie — where the constructor would build a HashNode over those bytes (PatriciaTrie.cs:120-125). Use LoadFromStorage when the root may not exist yet.

SaveNodesToStorage() commits the whole trie; SaveDirtyNodesToStorage() commits only what changed.

Account and contract-storage tries in one path store​

From PathStoreAppliesTombstonesTests.Storage_Trie_Owner_Keyed_Deletes_Do_Not_Touch_Account_Trie (use case key-path-storage).

var store = new InMemoryPathNodeStore();
var keccak = new Sha3Keccack();
var owner = keccak.CalculateHash(new byte[] { 0xC0, 0xDE });

var account = new PatriciaTrie(store) { Tracer = new TrieTracer() };
var storage = new PatriciaTrie(store, owner) { Tracer = new TrieTracer() };
foreach (var k in keys)
{
account.Put(k, Nethereum.RLP.RLP.EncodeElement(new byte[] { 0xAC }));
storage.Put(k, Nethereum.RLP.RLP.EncodeElement(new byte[] { 0x57 }));
}
account.SaveDirtyNodesToStorage();
storage.SaveDirtyNodesToStorage();

Both tries use the same keys and the same store. Deleting every storage slot afterwards leaves the account trie intact, because the path store keys each node on owner ‖ path and owner differs. Attaching a TrieTracer is what lets the commit emit tombstones for the removed nodes.

A path store verifies what it hands back​

From UnifiedITrieNodeStoreTests.Path_Store_Commit_Resolves_Referenceable_Nodes_And_Rejects_Tamper (use case key-path-storage).

var pathStore = new InMemoryPathNodeStore();
((ITrieNodeStore)pathStore).Commit(set);

foreach (var node in referenceable)
Assert.Equal(node.GetEncodedData(), ((ITrieNodeStore)pathStore).Get(RefOf(node)));

var victim = referenceable[0];
var wrongRef = new HashNode { Hash = new byte[32], Owner = victim.Owner, Path = victim.Path };
Assert.Throws<InvalidOperationException>(() => ((ITrieNodeStore)pathStore).Get(wrongRef));

A content store cannot be lied to — the key is the hash. A path store can, so it re-hashes the blob on every read and throws when the blob does not match the hash the caller asked for. The same check is exposed directly as ProofVerification.Current.TrieNode.Verify(expectedHash, blob, hashProvider), and a store implementation reuses it through two static helpers on TrieNodeVerification:

From TrieOperationsDocExampleTests.TrieNodeVerification_RequireAuthenticAndRootBlobHashesTo (use case key-path-storage).

TrieNodeVerification.RequireAuthentic(reference, blob);
bool isRoot = TrieNodeVerification.RootBlobHashesTo(blob, stateRoot);

RequireAuthentic(Node reference, byte[] blob) throws InvalidOperationException when the blob does not hash to reference.GetHash() (a null or empty blob passes). RootBlobHashesTo(byte[] blob, byte[] stateRoot) is false unless stateRoot is 32 bytes, the blob is non-empty and its Keccak-256 equals stateRoot.

A missing node is an exception, not an empty subtree​

From TrieOperationsDocExampleTests.MissingTrieNode_IsThrownForAHashReferenceTheStoreCannotResolve (use case missing-trie-node).

var trie = PatriciaTrie.LoadFromStorage(root, rootOnlyStore);
try { trie.Get(key); }
catch (MissingTrieNodeException missing)
{
byte[] hash = missing.Hash;
byte[] owner = missing.Owner;
byte[] path = missing.Path;
}

NodeDecoder.Decode throws MissingTrieNodeException(byte[] owner, byte[] path, byte[] hash) when the store has no blob for a HashNode, unless that hash is the empty-trie root (then it is an EmptyNode). Owner is empty rather than null for the account trie. Earlier builds read a missing node as an empty subtree. The exception propagates from every public method that resolves a HashNode through the store: NodeRlpDecoder.Decode(HashNode, ITrieNodeStore, bool), HashNode.DecodeInnerNode, PatriciaTrie.Get, GetFromHashNode, Put and Delete, PatriciaPathWalker.WalkPath, ProofGenerator.GenerateProof and GeneratePathProof, PatriciaRangeProofGenerator.GenerateProof and PatriciaRangeIterator.EnumerateRange, and NodeDecoder.DecodeFromRlpData / NodeRlpDecoder.DecodeFromRlpData when decodeHashNodes is true.

Hash a sorted key stream without building the trie​

From TrieOperationsDocExampleTests.StackTrie_HashesASortedStreamToThePatriciaRoot (use case stack-trie).

var stack = new StackTrie(prefixNibbles: null, onNode: (path, blob) => store.Add(path, blob));
foreach (var (key, value) in sortedRows)
stack.Update(key, value);

byte[] root = stack.Hash();
byte[] reference = stack.Reference();

StackTrie(byte[] prefixNibbles = null, NodeEmitted onNode = null) hashes 32-byte keys supplied in strictly ascending order and keeps only the open path in memory. Update(byte[] key, byte[] value) throws ArgumentException for a key that is not 32 bytes, not ascending or outside the prefix, and for an empty value, and InvalidOperationException after Hash() or Reference() has run. Hash() is the Keccak-256 root and equals the PatriciaTrie root over the same rows; Reference() is the parent's reference to the subtree, the 32-byte hash or, for a node whose encoding is shorter than 32 bytes, the encoding itself (the empty-trie root when no key was added). NodeEmitted is delegate void NodeEmitted(byte[] path, byte[] blob); it is called with the node's nibble path (the prefix nibbles included) and its RLP encoding for each node whose encoding is 32 bytes or longer. prefixNibbles (at most 64 nibbles, each 0 to 15) hashes the subtree under that prefix.

Bounded memory on large builds — save and collapse​

From PatriciaTrieSaveAndCollapseTests.RootHash_IsIdentical_WithPeriodicCollapse (use case patricia-trie).

var store = new InMemoryContentNodeStore();
var trie = new PatriciaTrie(store);
for (int i = 0; i < 2000; i++)
{
trie.Put(KeyHash(i), Value(i));
if (i % 250 == 249) trie.SaveDirtyNodesToStorageAndCollapse();
}
var actual = trie.Root.GetHash();

SaveDirtyNodesToStorageAndCollapse() persists the dirty nodes and then drops the materialised subtrees back to HashNodes, so a multi-million-key build does not have to fit in memory. The root is byte-for-byte the same as building without collapsing.

Proofs​

Generating an account/storage proof (root-first, EIP-1186 order)​

From ProofGeneratorRootFirstOrderingTests.FirstProofNode_HashesTo_StateRoot (use case state-proofs).

var proof = ProofGenerator.GenerateProof(trie, keys[0]);

Assert.NotNull(proof);
Assert.NotEmpty(proof);
Assert.Equal(rootHash, new Sha3Keccack().CalculateHash(proof[0]));
public static List<byte[]> GenerateProof(PatriciaTrie trie, byte[] key)

It returns a List<byte[]> of RLP node blobs, root first. It returns null when the key is not in the trie — there is no path to prove, so a caller must null-check rather than expect an empty list.

Verifying against a trusted root​

All verifiers hang off the ProofVerification.Current front door — Account, Storage, Range, Transaction, TrieNode:

VerifierSignature
IAccountProofVerifierbool Verify(byte[] stateRoot, IEnumerable<byte[]> proof, string accountAddress, Account account)
IStorageProofVerifierbool Verify(byte[] stateRoot, IList<byte[]> proof, byte[] key, byte[] value)
IRangeProofVerifierRangeProofResult Verify(byte[] rootHash, byte[] firstKey, IList<byte[]> keys, IList<byte[]> values, IList<byte[]> proofNodes) and bool VerifyEntry(byte[] root, byte[] keyHash, byte[] expectedValue, IList<byte[]> proof)
ITransactionProofVerifierbool Verify(string transactionsRoot, List<IndexedSignedTransaction> transactions)
ITrieNodeVerifierbool Verify(byte[] expectedHash, byte[] blob, IHashProvider hashProvider)

A malformed proof is a false result, never an exception: the account, storage, range and transaction verifiers and ProofVerification.Current.Range.VerifyEntry catch decoding failures (an empty compact key, an undecodable node) and return false (RangeProofResult.Invalid for Verify).

From TrieOperationsDocExampleTests.Verifiers_ReturnFalseForMalformedProofs (use case proof-verification).

var accepted = ProofVerification.Current.Range.VerifyEntry(root, keyHash, expectedValue, new List<byte[]> { new byte[] { 0xC0 } });

Each concrete verifier also exposes its own Current singleton, and ProofVerification has a constructor taking all five so you can substitute one. The concrete class names do not follow one pattern — the range verifier is PatriciaRangeProofVerifier, not a RangeProofVerification:

Front-door propertyConcrete class with Current
AccountAccountProofVerification
StorageStorageProofVerification
RangePatriciaRangeProofVerifier
TransactionTransactionProofVerification
TrieNodeTrieNodeVerification
public interface IProofVerification
{
IAccountProofVerifier Account { get; }
IStorageProofVerifier Storage { get; }
IRangeProofVerifier Range { get; }
ITransactionProofVerifier Transaction { get; }
ITrieNodeVerifier TrieNode { get; }
}

From ProofVerificationFrontDoorTests.AccountProof_VerifiesAgainstTheStateRoot (use case state-proofs).

var stateRoot = accountTrie.Root.GetHash();
var proof = ProofGenerator.GenerateProof(accountTrie, accountKey);

Assert.True(ProofVerification.Current.Account.Verify(stateRoot, proof, TargetAddress, account));

Swapping the balance for a value the state root does not commit to makes the same call return false (AccountProof_WithATamperedBalance_IsRejected).

Storage slots go through AccountStorage.EncodeKeyForStorage / EncodeValueForStorage (from Nethereum.Model) — the verifier applies that encoding itself, so you pass the raw slot and value:

From ProofVerificationFrontDoorTests.StorageProof_VerifiesASetSlot_AndAnAbsentKeyYieldsNoProof (use case state-proofs).

var storageRoot = storageTrie.Root.GetHash();
var inclusion = ProofGenerator.GenerateProof(
storageTrie, AccountStorage.EncodeKeyForStorage(slot, hashProvider));

Assert.True(ProofVerification.Current.Storage.Verify(storageRoot, inclusion, slot, slotValue));

Under the hood a verifier loads the proof nodes into an InMemoryContentNodeStore and walks the key's path from the root through them — a proof that reconstructs the authentic root is unforgeable.

snap/1 range proofs​

For snap sync, a range of the trie is served with edge proofs and verified without the full trie:

  • PatriciaRangeProofGenerator.GenerateProof(Node root, ITrieNodeStore store, byte[] startKey) and the four-argument overload (…, byte[] startKey, byte[] lastReturnedKey) — boundary proof for a served range.
  • PatriciaRangeIterator.EnumerateRange(Node root, ITrieNodeStore store, byte[] startKey, int maxCount = int.MaxValue, long maxResponseBytes = long.MaxValue) — lexicographic enumeration yielding PatriciaRangeIterator.RangeEntry { KeyBytes, Value } — a class nested inside PatriciaRangeIterator, not a top-level type — resolving HashNodes lazily through the store. startKey must be 32 bytes.
  • ProofVerification.Current.Range.Verify(…) → RangeProofResult { Valid, HasMore } — the full snap/1 range-proof algorithm (proof-to-path, unset-internal, has-right-element).
public static IEnumerable<RangeEntry> EnumerateRange(
Node root,
ITrieNodeStore store,
byte[] startKey,
int maxCount = int.MaxValue,
long maxResponseBytes = long.MaxValue)

public class RangeEntry
{
public byte[] KeyBytes { get; set; }
public byte[] Value { get; set; }
}

public readonly struct RangeProofResult
{
public bool Valid { get; }
public bool HasMore { get; }

public RangeProofResult(bool valid, bool hasMore);

public static readonly RangeProofResult Invalid;
}

From PatriciaRangeProofVerifierTests.Verify_BoundedRange_RoundTrip_Succeeds_WithHasMore (use case snap-range-proofs).

var proof = PatriciaRangeProofGenerator.GenerateProof(trie.Root, storage, startKey, lastKey);

var r = ProofVerification.Current.Range.Verify(rootHash, startKey, keys, values, proof);
Assert.True(r.Valid);
Assert.True(r.HasMore);

HasMore tells the syncing peer whether another chunk follows. Tampering with a value, dropping a key, or presenting keys out of order all make Valid false — the companion tests in the same file assert each of those.

Node types (reference)​

NodeMembers
Node (abstract base)Owner, Path, GetHash(), GetEncodedData(), MarkDirty(), IsDirty, NeedsPersist, ClearNeedsPersist(), MarkPersisted()
LeafNodeNibbles, Value, GetPrefixedNibbles()
ExtendedNodeNibbles, InnerNode, CollapseInner(), GetPrefixedNibbles()
BranchNodeChildren (16), Value, SetChild(int nibble, Node node), RemoveChild(int nibble), CollapseChild(int nibble)
HashNodeHash, lazy InnerNode, DecodeInnerNode(ITrieNodeStore store, bool decodeInnerHashNodes), ReleaseInnerNode()
EmptyNodethe empty subtree; shared EmptyNode.Instance

Decoding is one unified path through NodeDecoder, which threads Owner/Path onto each child reference — no storage-type sniffing. Both members are instance methods, so you construct a NodeDecoder first (Nodes/Rlp/NodeDecoder.cs:8,13):

public class NodeDecoder
{
public Node Decode(HashNode reference, ITrieNodeStore store, bool decodeHashNodes);
public Node DecodeFromRlpData(byte[] currentData, byte[] owner, byte[] path, bool decodeHashNodes, ITrieNodeStore store);
}

A 32-byte child becomes a HashNode carrying {Hash, Owner, Path}; a shorter child is decoded inline.

PatriciaPathWalker exposes the nibble/compact conversions the trie uses — CompactToNibbles(byte[] compact), NibblesToCompact(byte[] nibbles), NibblesToCompact(byte[] nibbles, bool leaf), WalkPath(Node root, ITrieNodeStore store, byte[] pathNibbles).

From TrieOperationsDocExampleTests.NibblesToCompact_SetsTheLeafAndOddFlags (use case compact-encoding).

var nibbles = new byte[] { 1, 2, 3 };
byte[] extension = PatriciaPathWalker.NibblesToCompact(nibbles, leaf: false);
byte[] leaf = PatriciaPathWalker.NibblesToCompact(nibbles, leaf: true);

extension is 0x1123 and leaf is 0x3123: leaf sets the 0x20 flag and an odd length sets 0x10. NibblesToCompact(nibbles) is leaf: false, and a null or empty path gives 0x00 (0x20 for a leaf).

PatriciaTrie surface (reference)​

MemberNotes
Constructors() (empty trie, Keccak), (IHashProvider), (byte[] hashRoot), (Node root), (ITrieNodeStore store), (byte[] hashRoot, ITrieNodeStore store), (Node root, ITrieNodeStore store), (ITrieNodeStore store, byte[] owner), (byte[] hashRoot, ITrieNodeStore store, byte[] owner), (Node root, ITrieNodeStore store, byte[] owner) — each with an extra IHashProvider overload
LoadFromStorage(byte[] rootHash, ITrieNodeStore store[, byte[] owner][, IHashProvider])
ReattachFromRawRoot(IRawNodeReader rawReader, ITrieNodeStore store, byte[] owner[, IHashProvider])
PropertiesRoot, Store, HashProvider, Tracer, ClearExtensionSpansOnSave
ReadsGet(byte[] key)
WritesPut(byte[] key, byte[] value), Delete(byte[] key)
CommitsSaveNodesToStorage(), SaveDirtyNodesToStorage(), SaveDirtyNodesToStorageAndCollapse()
ClearExtensionSpansOnSavewhen true and the store is an IRawNodeReader, a commit also deletes the stored standalone nodes (32 bytes or longer) at the paths an extension node now spans
  • Nethereum.Merkle.Binary — the EIP-7864 binary trie, the proposed replacement for this structure.
  • Nethereum.CoreChain.RocksDB — persistent path-keyed and content-addressed ITrieNodeStore implementations with node history and reorg rewind.
  • Nethereum.Merkle — general-purpose Merkle trees (airdrops, whitelists, sparse trees), unrelated to the state trie.