From 2656abc317d78a7c8f2f902cd93b002a0826d154 Mon Sep 17 00:00:00 2001
From: Guillaume Ballet <3272758+gballet@users.noreply.github.com>
Date: Thu, 7 Aug 2025 16:35:58 +0200
Subject: [PATCH] trie{,/utils}: add tests for eip7864 binary trees
Signed-off-by: Guillaume Ballet
<3272758+gballet@users.noreply.github.com>
---
go.mod | 3 +-
go.sum | 10 +-
trie/binary.go | 319 ++++++++++++++++++++++++++++++++++
trie/binary_iterator.go | 236 +++++++++++++++++++++++++
trie/binary_iterator_test.go | 69 ++++++++
trie/binary_test.go | 198 +++++++++++++++++++++
trie/bintrie/binary_node.go | 121 +++++++++++++
trie/bintrie/empty.go | 71 ++++++++
trie/bintrie/hashed_node.go | 66 +++++++
trie/bintrie/internal_node.go | 194 +++++++++++++++++++++
trie/bintrie/stem_node.go | 216 +++++++++++++++++++++++
trie/utils/binary.go | 68 ++++++++
12 files changed, 1568 insertions(+), 3 deletions(-)
create mode 100644 trie/binary.go
create mode 100644 trie/binary_iterator.go
create mode 100644 trie/binary_iterator_test.go
create mode 100644 trie/binary_test.go
create mode 100644 trie/bintrie/binary_node.go
create mode 100644 trie/bintrie/empty.go
create mode 100644 trie/bintrie/hashed_node.go
create mode 100644 trie/bintrie/internal_node.go
create mode 100644 trie/bintrie/stem_node.go
create mode 100644 trie/utils/binary.go
diff --git a/go.mod b/go.mod
index 363d7d3dfb..ffde28f005 100644
--- a/go.mod
+++ b/go.mod
@@ -62,6 +62,7 @@ require (
github.com/supranational/blst v0.3.14
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
github.com/urfave/cli/v2 v2.27.5
+ github.com/zeebo/blake3 v0.2.4
go.uber.org/automaxprocs v1.5.2
go.uber.org/goleak v1.3.0
golang.org/x/crypto v0.36.0
@@ -115,7 +116,7 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kilic/bls12-381 v0.1.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
- github.com/klauspost/cpuid/v2 v2.0.9 // indirect
+ github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
diff --git a/go.sum b/go.sum
index 099d432ba4..4a3aac1c12 100644
--- a/go.sum
+++ b/go.sum
@@ -220,8 +220,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
-github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@@ -365,6 +365,12 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBi
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
+github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
+github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
+github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
+github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
diff --git a/trie/binary.go b/trie/binary.go
new file mode 100644
index 0000000000..2cad0999c7
--- /dev/null
+++ b/trie/binary.go
@@ -0,0 +1,319 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package trie
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/trie/bintrie"
+ "github.com/ethereum/go-ethereum/trie/trienode"
+ "github.com/ethereum/go-ethereum/trie/utils"
+ "github.com/ethereum/go-ethereum/triedb/database"
+ "github.com/holiman/uint256"
+)
+
+// zero is the zero value for a 32-byte array.
+var zero [32]byte
+
+// NewBinaryNode creates a new empty binary trie
+func NewBinaryNode() bintrie.BinaryNode {
+ return bintrie.Empty{}
+}
+
+// BinaryTrie is a wrapper around VerkleNode that implements the trie.Trie
+// interface so that Verkle trees can be reused verbatim.
+type BinaryTrie struct {
+ root bintrie.BinaryNode
+ reader *trieReader
+}
+
+// ToDot converts the binary trie to a DOT language representation. Useful for debugging.
+func (trie *BinaryTrie) ToDot() string {
+ trie.root.Hash()
+ return bintrie.ToDot(trie.root)
+}
+
+// NewBinaryTrie creates a new binary trie.
+func NewBinaryTrie(root common.Hash, db database.NodeDatabase) (*BinaryTrie, error) {
+ reader, err := newTrieReader(root, common.Hash{}, db)
+ if err != nil {
+ return nil, err
+ }
+ // Parse the root verkle node if it's not empty.
+ node := NewBinaryNode()
+ if root != types.EmptyVerkleHash && root != types.EmptyRootHash {
+ blob, err := reader.node(nil, common.Hash{})
+ if err != nil {
+ return nil, err
+ }
+ node, err = bintrie.DeserializeNode(blob, 0)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return &BinaryTrie{
+ root: node,
+ reader: reader,
+ }, nil
+}
+
+// FlatdbNodeResolver is a node resolver that reads nodes from the flatdb.
+func (trie *BinaryTrie) FlatdbNodeResolver(path []byte, hash common.Hash) ([]byte, error) {
+ // empty nodes will be serialized as common.Hash{}, so capture
+ // this special use case.
+ if hash == (common.Hash{}) {
+ return nil, nil // empty node
+ }
+ return trie.reader.node(path, hash)
+}
+
+// GetKey returns the sha3 preimage of a hashed key that was previously used
+// to store a value.
+func (trie *BinaryTrie) GetKey(key []byte) []byte {
+ return key
+}
+
+// Get returns the value for key stored in the trie. The value bytes must
+// not be modified by the caller. If a node was not found in the database, a
+// trie.MissingNodeError is returned.
+func (trie *BinaryTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
+ return trie.root.Get(utils.GetBinaryTreeKey(addr, key), trie.FlatdbNodeResolver)
+}
+
+// GetWithHashedKey returns the value, assuming that the key has already
+// been hashed.
+func (trie *BinaryTrie) GetWithHashedKey(key []byte) ([]byte, error) {
+ return trie.root.Get(key, trie.FlatdbNodeResolver)
+}
+
+// GetAccount returns the account information for the given address.
+func (trie *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error) {
+ acc := &types.StateAccount{}
+ versionkey := utils.GetBinaryTreeKey(addr, zero[:])
+ var (
+ values [][]byte
+ err error
+ )
+ switch r := trie.root.(type) {
+ case *bintrie.InternalNode:
+ values, err = r.GetValuesAtStem(versionkey[:31], trie.FlatdbNodeResolver)
+ case *bintrie.StemNode:
+ values = r.Values
+ case bintrie.Empty:
+ return nil, nil
+ default:
+ // This will cover HashedNode but that should be fine since the
+ // root node should always be resolved.
+ return nil, errInvalidRootType
+ }
+ if err != nil {
+ return nil, fmt.Errorf("GetAccount (%x) error: %v", addr, err)
+ }
+
+ // The following code is required for the MPT->VKT conversion.
+ // An account can be partially migrated, where storage slots were moved to the VKT
+ // but not yet the account. This means some account information as (header) storage slots
+ // are in the VKT but basic account information must be read in the base tree (MPT).
+ // TODO: we can simplify this logic depending if the conversion is in progress or finished.
+ emptyAccount := true
+
+ for i := 0; values != nil && i <= utils.CodeHashLeafKey && emptyAccount; i++ {
+ emptyAccount = emptyAccount && values[i] == nil
+ }
+ if emptyAccount {
+ return nil, nil
+ }
+
+ // if the account has been deleted, then values[10] will be 0 and not nil. If it has
+ // been recreated after that, then its code keccak will NOT be 0. So return `nil` if
+ // the nonce, and values[10], and code keccak is 0.
+ if bytes.Equal(values[utils.BasicDataLeafKey], zero[:]) && len(values) > 10 && len(values[10]) > 0 && bytes.Equal(values[utils.CodeHashLeafKey], zero[:]) {
+ return nil, nil
+ }
+
+ acc.Nonce = binary.BigEndian.Uint64(values[utils.BasicDataLeafKey][utils.BasicDataNonceOffset:])
+ var balance [16]byte
+ copy(balance[:], values[utils.BasicDataLeafKey][utils.BasicDataBalanceOffset:])
+ acc.Balance = new(uint256.Int).SetBytes(balance[:])
+ acc.CodeHash = values[utils.CodeHashLeafKey]
+
+ return acc, nil
+}
+
+// UpdateAccount updates the account information for the given address.
+func (trie *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {
+ var (
+ err error
+ basicData [32]byte
+ values = make([][]byte, bintrie.NodeWidth)
+ stem = utils.GetBinaryTreeKey(addr, zero[:])
+ )
+
+ binary.BigEndian.PutUint32(basicData[utils.BasicDataCodeSizeOffset-1:], uint32(codeLen))
+ binary.BigEndian.PutUint64(basicData[utils.BasicDataNonceOffset:], acc.Nonce)
+ // Because the balance is a max of 16 bytes, truncate
+ // the extra values. This happens in devmode, where
+ // 0xff**32 is allocated to the developer account.
+ balanceBytes := acc.Balance.Bytes()
+ // TODO: reduce the size of the allocation in devmode, then panic instead
+ // of truncating.
+ if len(balanceBytes) > 16 {
+ balanceBytes = balanceBytes[16:]
+ }
+ copy(basicData[32-len(balanceBytes):], balanceBytes[:])
+ values[utils.BasicDataLeafKey] = basicData[:]
+ values[utils.CodeHashLeafKey] = acc.CodeHash[:]
+
+ trie.root, err = trie.root.InsertValuesAtStem(stem, values, trie.FlatdbNodeResolver, 0)
+ return err
+}
+
+// UpdateStem updates the values for the given stem key.
+func (trie *BinaryTrie) UpdateStem(key []byte, values [][]byte) error {
+ var err error
+ trie.root, err = trie.root.InsertValuesAtStem(key, values, trie.FlatdbNodeResolver, 0)
+ return err
+}
+
+// Update associates key with value in the trie. If value has length zero, any
+// existing value is deleted from the trie. The value bytes must not be modified
+// by the caller while they are stored in the trie. If a node was not found in the
+// database, a trie.MissingNodeError is returned.
+func (trie *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) error {
+ k := utils.GetBinaryTreeKeyStorageSlot(address, key)
+ var v [32]byte
+ if len(value) >= 32 {
+ copy(v[:], value[:32])
+ } else {
+ copy(v[32-len(value):], value[:])
+ }
+ root, err := trie.root.Insert(k, v[:], trie.FlatdbNodeResolver)
+ if err != nil {
+ return fmt.Errorf("UpdateStorage (%x) error: %v", address, err)
+ }
+ trie.root = root
+ return nil
+}
+
+// DeleteAccount is a no-op as it is disabled in stateless.
+func (trie *BinaryTrie) DeleteAccount(addr common.Address) error {
+ return nil
+}
+
+// Delete removes any existing value for key from the trie. If a node was not
+// found in the database, a trie.MissingNodeError is returned.
+func (trie *BinaryTrie) DeleteStorage(addr common.Address, key []byte) error {
+ k := utils.GetBinaryTreeKey(addr, key)
+ var zero [32]byte
+ root, err := trie.root.Insert(k, zero[:], trie.FlatdbNodeResolver)
+ if err != nil {
+ return fmt.Errorf("DeleteStorage (%x) error: %v", addr, err)
+ }
+ trie.root = root
+ return nil
+}
+
+// Hash returns the root hash of the trie. It does not write to the database and
+// can be used even if the trie doesn't have one.
+func (trie *BinaryTrie) Hash() common.Hash {
+ return trie.root.Hash()
+}
+
+// Commit writes all nodes to the trie's memory database, tracking the internal
+// and external (for account tries) references.
+func (trie *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet, error) {
+ root := trie.root.(*bintrie.InternalNode)
+ nodeset := trienode.NewNodeSet(common.Hash{})
+
+ err := root.CollectNodes(nil, func(path []byte, node bintrie.BinaryNode) {
+ serialized := bintrie.SerializeNode(node)
+ nodeset.AddNode(path, trienode.New(common.Hash{}, serialized))
+ })
+ if err != nil {
+ panic(fmt.Errorf("CollectNodes failed: %v", err))
+ }
+
+ // Serialize root commitment form
+ return trie.Hash(), nodeset, nil
+}
+
+// NodeIterator returns an iterator that returns nodes of the trie. Iteration
+// starts at the key after the given start key.
+func (trie *BinaryTrie) NodeIterator(startKey []byte) (NodeIterator, error) {
+ return newBinaryNodeIterator(trie, nil)
+}
+
+// Prove constructs a Merkle proof for key. The result contains all encoded nodes
+// on the path to the value at key. The value itself is also included in the last
+// node and can be retrieved by verifying the proof.
+//
+// If the trie does not contain a value for key, the returned proof contains all
+// nodes of the longest existing prefix of the key (at least the root), ending
+// with the node that proves the absence of the key.
+func (trie *BinaryTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
+ panic("not implemented")
+}
+
+// Copy creates a deep copy of the trie.
+func (trie *BinaryTrie) Copy() *BinaryTrie {
+ return &BinaryTrie{
+ root: trie.root.Copy(),
+ reader: trie.reader,
+ }
+}
+
+// IsVerkle returns true if the trie is a Verkle tree.
+func (trie *BinaryTrie) IsVerkle() bool {
+ // TODO @gballet This is technically NOT a verkle tree, but it has the same
+ // behavior and basic structure, so for all intents and purposes, it can be
+ // treated as such. Rename this when verkle gets removed.
+ return true
+}
+
+// Note: the basic data leaf needs to have been previously created for this to work
+func (trie *BinaryTrie) UpdateContractCode(addr common.Address, codeHash common.Hash, code []byte) error {
+ var (
+ chunks = ChunkifyCode(code)
+ values [][]byte
+ key []byte
+ err error
+ )
+ for i, chunknr := 0, uint64(0); i < len(chunks); i, chunknr = i+32, chunknr+1 {
+ groupOffset := (chunknr + 128) % 256
+ if groupOffset == 0 /* start of new group */ || chunknr == 0 /* first chunk in header group */ {
+ values = make([][]byte, bintrie.NodeWidth)
+ var offset [32]byte
+ binary.LittleEndian.PutUint64(offset[24:], chunknr+128)
+ key = utils.GetBinaryTreeKey(addr, offset[:])
+ }
+ values[groupOffset] = chunks[i : i+32]
+
+ if groupOffset == 255 || len(chunks)-i <= 32 {
+ err = trie.UpdateStem(key[:31], values)
+
+ if err != nil {
+ return fmt.Errorf("UpdateContractCode (addr=%x) error: %w", addr[:], err)
+ }
+ }
+ }
+ return nil
+}
diff --git a/trie/binary_iterator.go b/trie/binary_iterator.go
new file mode 100644
index 0000000000..e4799d207a
--- /dev/null
+++ b/trie/binary_iterator.go
@@ -0,0 +1,236 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package trie
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/trie/bintrie"
+)
+
+type binaryNodeIteratorState struct {
+ Node bintrie.BinaryNode
+ Index int
+}
+
+type binaryNodeIterator struct {
+ trie *BinaryTrie
+ current bintrie.BinaryNode
+ lastErr error
+
+ stack []binaryNodeIteratorState
+}
+
+func newBinaryNodeIterator(trie *BinaryTrie, _ []byte) (NodeIterator, error) {
+ if trie.Hash() == zero {
+ return new(nodeIterator), nil
+ }
+ it := &binaryNodeIterator{trie: trie, current: trie.root}
+ // it.err = it.seek(start)
+ return it, nil
+}
+
+// Next moves the iterator to the next node. If the parameter is false, any child
+// nodes will be skipped.
+func (it *binaryNodeIterator) Next(descend bool) bool {
+ if it.lastErr == errIteratorEnd {
+ it.lastErr = errIteratorEnd
+ return false
+ }
+
+ if len(it.stack) == 0 {
+ it.stack = append(it.stack, binaryNodeIteratorState{Node: it.trie.root})
+ it.current = it.trie.root
+
+ return true
+ }
+
+ switch node := it.current.(type) {
+ case *bintrie.InternalNode:
+ // index: 0 = nothing visited, 1=left visited, 2=right visited
+ context := &it.stack[len(it.stack)-1]
+
+ // recurse into both children
+ if context.Index == 0 {
+ if _, isempty := node.Left.(bintrie.Empty); node.Left != nil && !isempty {
+ it.stack = append(it.stack, binaryNodeIteratorState{Node: node.Left})
+ it.current = node.Left
+ return it.Next(descend)
+ }
+
+ context.Index++
+ }
+
+ if context.Index == 1 {
+ if _, isempty := node.Right.(bintrie.Empty); node.Right != nil && !isempty {
+ it.stack = append(it.stack, binaryNodeIteratorState{Node: node.Right})
+ it.current = node.Right
+ return it.Next(descend)
+ }
+
+ context.Index++
+ }
+
+ // Reached the end of this node, go back to the parent, if
+ // this isn't root.
+ if len(it.stack) == 1 {
+ it.lastErr = errIteratorEnd
+ return false
+ }
+ it.stack = it.stack[:len(it.stack)-1]
+ it.current = it.stack[len(it.stack)-1].Node
+ it.stack[len(it.stack)-1].Index++
+ return it.Next(descend)
+ case *bintrie.StemNode:
+ // Look for the next non-empty value
+ for i := it.stack[len(it.stack)-1].Index; i < 256; i++ {
+ if node.Values[i] != nil {
+ it.stack[len(it.stack)-1].Index = i + 1
+ return true
+ }
+ }
+
+ // go back to parent to get the next leaf
+ it.stack = it.stack[:len(it.stack)-1]
+ it.current = it.stack[len(it.stack)-1].Node
+ it.stack[len(it.stack)-1].Index++
+ return it.Next(descend)
+ case bintrie.HashedNode:
+ // resolve the node
+ data, err := it.trie.FlatdbNodeResolver(it.Path(), common.Hash(node))
+ if err != nil {
+ panic(err)
+ }
+ it.current, err = bintrie.DeserializeNode(data, len(it.stack)-1)
+ if err != nil {
+ panic(err)
+ }
+
+ // update the stack and parent with the resolved node
+ it.stack[len(it.stack)-1].Node = it.current
+ parent := &it.stack[len(it.stack)-2]
+ if parent.Index == 0 {
+ parent.Node.(*bintrie.InternalNode).Left = it.current
+ } else {
+ parent.Node.(*bintrie.InternalNode).Right = it.current
+ }
+ return it.Next(descend)
+ case bintrie.Empty:
+ // do nothing
+ return false
+ default:
+ panic("invalid node type")
+ }
+}
+
+// Error returns the error status of the iterator.
+func (it *binaryNodeIterator) Error() error {
+ if it.lastErr == errIteratorEnd {
+ return nil
+ }
+ return it.lastErr
+}
+
+// Hash returns the hash of the current node.
+func (it *binaryNodeIterator) Hash() common.Hash {
+ return it.current.Hash()
+}
+
+// Parent returns the hash of the parent of the current node. The hash may be the one
+// grandparent if the immediate parent is an internal node with no hash.
+func (it *binaryNodeIterator) Parent() common.Hash {
+ return it.stack[len(it.stack)-1].Node.Hash()
+}
+
+// Path returns the hex-encoded path to the current node.
+// Callers must not retain references to the return value after calling Next.
+// For leaf nodes, the last element of the path is the 'terminator symbol' 0x10.
+func (it *binaryNodeIterator) Path() []byte {
+ if it.Leaf() {
+ return it.LeafKey()
+ }
+ var path []byte
+ for i, state := range it.stack {
+ // skip the last byte
+ if i >= len(it.stack)-1 {
+ break
+ }
+ path = append(path, byte(state.Index))
+ }
+ return path
+}
+
+func (it *binaryNodeIterator) NodeBlob() []byte {
+ panic("not completely implemented")
+}
+
+// Leaf returns true iff the current node is a leaf node.
+func (it *binaryNodeIterator) Leaf() bool {
+ _, ok := it.current.(*bintrie.StemNode)
+ return ok
+}
+
+// LeafKey returns the key of the leaf. The method panics if the iterator is not
+// positioned at a leaf. Callers must not retain references to the value after
+// calling Next.
+func (it *binaryNodeIterator) LeafKey() []byte {
+ leaf, ok := it.current.(*bintrie.StemNode)
+ if !ok {
+ panic("Leaf() called on an binary node iterator not at a leaf location")
+ }
+
+ return leaf.Key(it.stack[len(it.stack)-1].Index - 1)
+}
+
+// LeafBlob returns the content of the leaf. The method panics if the iterator
+// is not positioned at a leaf. Callers must not retain references to the value
+// after calling Next.
+func (it *binaryNodeIterator) LeafBlob() []byte {
+ leaf, ok := it.current.(*bintrie.StemNode)
+ if !ok {
+ panic("LeafBlob() called on an binary node iterator not at a leaf location")
+ }
+
+ return leaf.Values[it.stack[len(it.stack)-1].Index-1]
+}
+
+// LeafProof returns the Merkle proof of the leaf. The method panics if the
+// iterator is not positioned at a leaf. Callers must not retain references
+// to the value after calling Next.
+func (it *binaryNodeIterator) LeafProof() [][]byte {
+ _, ok := it.current.(*bintrie.StemNode)
+ if !ok {
+ panic("LeafProof() called on an binary node iterator not at a leaf location")
+ }
+
+ // return it.trie.Prove(leaf.Key())
+ panic("not completely implemented")
+}
+
+// AddResolver sets an intermediate database to use for looking up trie nodes
+// before reaching into the real persistent layer.
+//
+// This is not required for normal operation, rather is an optimization for
+// cases where trie nodes can be recovered from some external mechanism without
+// reading from disk. In those cases, this resolver allows short circuiting
+// accesses and returning them from memory.
+//
+// Before adding a similar mechanism to any other place in Geth, consider
+// making trie.Database an interface and wrapping at that level. It's a huge
+// refactor, but it could be worth it if another occurrence arises.
+func (it *binaryNodeIterator) AddResolver(NodeResolver) {
+ // Not implemented, but should not panic
+}
diff --git a/trie/binary_iterator_test.go b/trie/binary_iterator_test.go
new file mode 100644
index 0000000000..01a4373cd2
--- /dev/null
+++ b/trie/binary_iterator_test.go
@@ -0,0 +1,69 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package trie
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/holiman/uint256"
+)
+
+func TestBinaryIterator(t *testing.T) {
+ trie, err := NewBinaryTrie(types.EmptyVerkleHash, newTestDatabase(rawdb.NewMemoryDatabase(), rawdb.PathScheme))
+ if err != nil {
+ t.Fatal(err)
+ }
+ account0 := &types.StateAccount{
+ Nonce: 1,
+ Balance: uint256.NewInt(2),
+ Root: types.EmptyRootHash,
+ CodeHash: nil,
+ }
+ // NOTE: the code size isn't written to the trie via TryUpdateAccount
+ // so it will be missing from the test nodes.
+ trie.UpdateAccount(common.Address{}, account0, 0)
+ account1 := &types.StateAccount{
+ Nonce: 1337,
+ Balance: uint256.NewInt(2000),
+ Root: types.EmptyRootHash,
+ CodeHash: nil,
+ }
+ // This address is meant to hash to a value that has the same first byte as 0xbf
+ var clash = common.HexToAddress("69fd8034cdb20934dedffa7dccb4fb3b8062a8be")
+ trie.UpdateAccount(clash, account1, 0)
+
+ // Manually go over every node to check that we get all
+ // the correct nodes.
+ it, err := trie.NodeIterator(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var leafcount int
+ for it.Next(true) {
+ t.Logf("Node: %x", it.Path())
+ if it.Leaf() {
+ leafcount++
+ t.Logf("\tLeaf: %x", it.LeafKey())
+ }
+ }
+ if leafcount != 2 {
+ t.Fatalf("invalid leaf count: %d != 6", leafcount)
+ }
+}
diff --git a/trie/binary_test.go b/trie/binary_test.go
new file mode 100644
index 0000000000..082ca9b303
--- /dev/null
+++ b/trie/binary_test.go
@@ -0,0 +1,198 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package trie
+
+import (
+ "bytes"
+ "encoding/binary"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/trie/bintrie"
+)
+
+var (
+ zeroKey = [32]byte{}
+ oneKey = common.HexToHash("0101010101010101010101010101010101010101010101010101010101010101")
+ twoKey = common.HexToHash("0202020202020202020202020202020202020202020202020202020202020202")
+ threeKey = common.HexToHash("0303030303030303030303030303030303030303030303030303030303030303")
+ fourKey = common.HexToHash("0404040404040404040404040404040404040404040404040404040404040404")
+ ffKey = common.HexToHash("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
+)
+
+func TestSingleEntry(t *testing.T) {
+ tree := NewBinaryNode()
+ tree, err := tree.Insert(zeroKey[:], oneKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 1 {
+ t.Fatal("invalid depth")
+ }
+ expected := common.HexToHash("694545468677064fd833cddc8455762fe6b21c6cabe2fc172529e0f573181cd5")
+ got := tree.Hash()
+ if got != expected {
+ t.Fatalf("invalid tree root, got %x, want %x", got, expected)
+ }
+}
+
+func TestTwoEntriesDiffFirstBit(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ tree, err = tree.Insert(zeroKey[:], oneKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(), twoKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 2 {
+ t.Fatal("invalid height")
+ }
+ if tree.Hash() != common.HexToHash("85fc622076752a6fcda2c886c18058d639066a83473d9684704b5a29455ed2ed") {
+ t.Fatal("invalid tree root")
+ }
+}
+
+func TestOneStemColocatedValues(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000009").Bytes(), threeKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("00000000000000000000000000000000000000000000000000000000000000FF").Bytes(), fourKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 1 {
+ t.Fatal("invalid height")
+ }
+}
+
+func TestTwoStemColocatedValues(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ // stem: 0...0
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // stem: 10...0
+ tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 2 {
+ t.Fatal("invalid height")
+ }
+}
+
+func TestTwoKeysMatchFirst42Bits(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ // key1 and key 2 have the same prefix of 42 bits (b0*42+b1+b1) and differ after.
+ key1 := common.HexToHash("0000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0").Bytes()
+ key2 := common.HexToHash("0000000000E00000000000000000000000000000000000000000000000000000").Bytes()
+ tree, err = tree.Insert(key1, oneKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(key2, twoKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 1+42+1 {
+ t.Fatal("invalid height")
+ }
+}
+func TestInsertDuplicateKey(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ tree, err = tree.Insert(oneKey[:], oneKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(oneKey[:], twoKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 1 {
+ t.Fatal("invalid height")
+ }
+ // Verify that the value is updated
+ if !bytes.Equal(tree.(*bintrie.StemNode).Values[1], twoKey[:]) {
+ t.Fatal("invalid height")
+ }
+}
+func TestLargeNumberOfEntries(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ for i := range 256 {
+ var key [32]byte
+ key[0] = byte(i)
+ tree, err = tree.Insert(key[:], ffKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ height := tree.GetHeight()
+ if height != 1+8 {
+ t.Fatalf("invalid height, wanted %d, got %d", 1+8, height)
+ }
+}
+
+func TestMerkleizeMultipleEntries(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ keys := [][]byte{
+ zeroKey[:],
+ common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(),
+ common.HexToHash("0100000000000000000000000000000000000000000000000000000000000000").Bytes(),
+ common.HexToHash("8100000000000000000000000000000000000000000000000000000000000000").Bytes(),
+ }
+ for i, key := range keys {
+ var v [32]byte
+ binary.LittleEndian.PutUint64(v[:8], uint64(i))
+ tree, err = tree.Insert(key, v[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ got := tree.Hash()
+ expected := common.HexToHash("8c74de28e6bb6b2296cae37cff16266e2dbf533bc204fa4cb0c237761ae8a2c8")
+ if got != expected {
+ t.Fatalf("invalid root, expected=%x, got = %x", expected, got)
+ }
+}
diff --git a/trie/bintrie/binary_node.go b/trie/bintrie/binary_node.go
new file mode 100644
index 0000000000..b97adeb21e
--- /dev/null
+++ b/trie/bintrie/binary_node.go
@@ -0,0 +1,121 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "errors"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+type (
+ NodeFlushFn func([]byte, BinaryNode)
+ NodeResolverFn func([]byte, common.Hash) ([]byte, error)
+)
+
+// zero is the zero value for a 32-byte array.
+var zero [32]byte
+
+const (
+ NodeWidth = 256 // Number of child per leaf node
+ StemSize = 31 // Number of bytes to travel before reaching a group of leaves
+)
+
+// BinaryNode is an interface for a binary trie node.
+type BinaryNode interface {
+ Get([]byte, NodeResolverFn) ([]byte, error)
+ Insert([]byte, []byte, NodeResolverFn) (BinaryNode, error)
+ // Commit() common.Hash
+ Copy() BinaryNode
+ Hash() common.Hash
+ GetValuesAtStem([]byte, NodeResolverFn) ([][]byte, error)
+ InsertValuesAtStem([]byte, [][]byte, NodeResolverFn, int) (BinaryNode, error)
+ CollectNodes([]byte, NodeFlushFn) error
+
+ toDot(parent, path string) string
+ GetHeight() int
+}
+
+// SerializeNode serializes a binary trie node into a byte slice.
+func SerializeNode(node BinaryNode) []byte {
+ switch n := (node).(type) {
+ case *InternalNode:
+ var serialized [65]byte
+ serialized[0] = 1
+ copy(serialized[1:33], n.Left.Hash().Bytes())
+ copy(serialized[33:65], n.Right.Hash().Bytes())
+ return serialized[:]
+ case *StemNode:
+ var serialized [32 + 256*32]byte
+ serialized[0] = 2
+ copy(serialized[1:32], node.(*StemNode).Stem)
+ bitmap := serialized[32:64]
+ offset := 64
+ for i, v := range node.(*StemNode).Values {
+ if v != nil {
+ bitmap[i/8] |= 1 << (7 - (i % 8))
+ copy(serialized[offset:offset+32], v)
+ offset += 32
+ }
+ }
+ return serialized[:]
+ default:
+ panic("invalid node type")
+ }
+}
+
+// DeserializeNode deserializes a binary trie node from a byte slice.
+func DeserializeNode(serialized []byte, depth int) (BinaryNode, error) {
+ if len(serialized) == 0 {
+ return Empty{}, nil
+ }
+
+ switch serialized[0] {
+ case 1:
+ if len(serialized) != 65 {
+ return nil, errors.New("invalid serialized node length")
+ }
+ return &InternalNode{
+ depth: depth,
+ Left: HashedNode(common.BytesToHash(serialized[1:33])),
+ Right: HashedNode(common.BytesToHash(serialized[33:65])),
+ }, nil
+ case 2:
+ var values [256][]byte
+ bitmap := serialized[32:64]
+ offset := 64
+
+ for i := range 256 {
+ if bitmap[i/8]>>(7-(i%8))&1 == 1 {
+ values[i] = serialized[offset : offset+32]
+ offset += 32
+ }
+ }
+ return &StemNode{
+ Stem: serialized[1:32],
+ Values: values[:],
+ depth: depth,
+ }, nil
+ default:
+ return nil, errors.New("invalid node type")
+ }
+}
+
+// ToDot converts the binary trie to a DOT language representation. Useful for debugging.
+func ToDot(root BinaryNode) string {
+ return root.toDot("", "")
+}
diff --git a/trie/bintrie/empty.go b/trie/bintrie/empty.go
new file mode 100644
index 0000000000..1effd7f03a
--- /dev/null
+++ b/trie/bintrie/empty.go
@@ -0,0 +1,71 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "slices"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+type Empty struct{}
+
+func (e Empty) Get(_ []byte, _ NodeResolverFn) ([]byte, error) {
+ return nil, nil
+}
+
+func (e Empty) Insert(key []byte, value []byte, _ NodeResolverFn) (BinaryNode, error) {
+ var values [256][]byte
+ values[key[31]] = value
+ return &StemNode{
+ Stem: slices.Clone(key[:31]),
+ Values: values[:],
+ }, nil
+}
+
+func (e Empty) Copy() BinaryNode {
+ return Empty{}
+}
+
+func (e Empty) Hash() common.Hash {
+ return common.Hash{}
+}
+
+func (e Empty) GetValuesAtStem(_ []byte, _ NodeResolverFn) ([][]byte, error) {
+ var values [256][]byte
+ return values[:], nil
+}
+
+func (e Empty) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
+ return &StemNode{
+ Stem: slices.Clone(key[:31]),
+ Values: values,
+ depth: depth,
+ }, nil
+}
+
+func (e Empty) CollectNodes(_ []byte, _ NodeFlushFn) error {
+ return nil
+}
+
+func (e Empty) toDot(parent string, path string) string {
+ return ""
+}
+
+func (e Empty) GetHeight() int {
+ return 0
+}
diff --git a/trie/bintrie/hashed_node.go b/trie/bintrie/hashed_node.go
new file mode 100644
index 0000000000..e5ee79a460
--- /dev/null
+++ b/trie/bintrie/hashed_node.go
@@ -0,0 +1,66 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+type HashedNode common.Hash
+
+func (h HashedNode) Get(_ []byte, _ NodeResolverFn) ([]byte, error) {
+ panic("not implemented") // TODO: Implement
+}
+
+func (h HashedNode) Insert(key []byte, value []byte, resolver NodeResolverFn) (BinaryNode, error) {
+ return nil, errors.New("insert not implemented for hashed node")
+}
+
+func (h HashedNode) Copy() BinaryNode {
+ nh := common.Hash(h)
+ return HashedNode(nh)
+}
+
+func (h HashedNode) Hash() common.Hash {
+ return common.Hash(h)
+}
+
+func (h HashedNode) GetValuesAtStem(_ []byte, _ NodeResolverFn) ([][]byte, error) {
+ return nil, errors.New("attempted to get values from an unresolved node")
+}
+
+func (h HashedNode) InsertValuesAtStem(key []byte, values [][]byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
+ return nil, errors.New("insertValuesAtStem not implemented for hashed node")
+}
+
+func (h HashedNode) toDot(parent string, path string) string {
+ me := fmt.Sprintf("hash%s", path)
+ ret := fmt.Sprintf("%s [label=\"%x\"]\n", me, h)
+ ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
+ return ret
+}
+
+func (h HashedNode) CollectNodes([]byte, NodeFlushFn) error {
+ panic("not implemented") // TODO: Implement
+}
+
+func (h HashedNode) GetHeight() int {
+ panic("tried to get the height of a hashed node, this is a bug")
+}
diff --git a/trie/bintrie/internal_node.go b/trie/bintrie/internal_node.go
new file mode 100644
index 0000000000..6287606c50
--- /dev/null
+++ b/trie/bintrie/internal_node.go
@@ -0,0 +1,194 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/zeebo/blake3"
+)
+
+func keyToPath(depth int, key []byte) ([]byte, error) {
+ path := make([]byte, 0, depth+1)
+
+ if depth > 31*8 {
+ return nil, errors.New("node too deep")
+ }
+
+ for i := range depth + 1 {
+ bit := key[i/8] >> (7 - (i % 8)) & 1
+ path = append(path, bit)
+ }
+
+ return path, nil
+}
+
+// InternalNode is a binary trie internal node.
+type InternalNode struct {
+ Left, Right BinaryNode
+ depth int
+}
+
+// GetValuesAtStem retrieves the group of values located at the given stem key.
+func (bt *InternalNode) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([][]byte, error) {
+ if bt.depth > 31*8 {
+ return nil, errors.New("node too deep")
+ }
+
+ bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
+ var child *BinaryNode
+ if bit == 0 {
+ child = &bt.Left
+ } else {
+ child = &bt.Right
+ }
+
+ if hn, ok := (*child).(HashedNode); ok {
+ path, err := keyToPath(bt.depth, stem)
+ if err != nil {
+ return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
+ }
+ data, err := resolver(path, common.Hash(hn))
+ if err != nil {
+ return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
+ }
+ node, err := DeserializeNode(data, bt.depth+1)
+ if err != nil {
+ return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err)
+ }
+ *child = node
+ }
+ return (*child).GetValuesAtStem(stem, resolver)
+}
+
+// Get retrieves the value for the given key.
+func (bt *InternalNode) Get(key []byte, resolver NodeResolverFn) ([]byte, error) {
+ values, err := bt.GetValuesAtStem(key[:31], resolver)
+ if err != nil {
+ return nil, fmt.Errorf("Get error: %w", err)
+ }
+ return values[key[31]], nil
+}
+
+// Insert inserts a new key-value pair into the trie.
+func (bt *InternalNode) Insert(key []byte, value []byte, resolver NodeResolverFn) (BinaryNode, error) {
+ var values [256][]byte
+ values[key[31]] = value
+ return bt.InsertValuesAtStem(key[:31], values[:], resolver, 0)
+}
+
+// Copy creates a deep copy of the node.
+func (bt *InternalNode) Copy() BinaryNode {
+ return &InternalNode{
+ Left: bt.Left.Copy(),
+ Right: bt.Right.Copy(),
+ depth: bt.depth,
+ }
+}
+
+// Hash returns the hash of the node.
+func (bt *InternalNode) Hash() common.Hash {
+ h := blake3.New()
+ if bt.Left != nil {
+ h.Write(bt.Left.Hash().Bytes())
+ } else {
+ h.Write(zero[:])
+ }
+ if bt.Right != nil {
+ h.Write(bt.Right.Hash().Bytes())
+ } else {
+ h.Write(zero[:])
+ }
+ return common.BytesToHash(h.Sum(nil))
+}
+
+// InsertValuesAtStem inserts a full value group at the given stem in the internal node.
+// Already-existing values will be overwritten.
+func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
+ bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
+ var (
+ child *BinaryNode
+ err error
+ )
+ if bit == 0 {
+ child = &bt.Left
+ } else {
+ child = &bt.Right
+ }
+
+ *child, err = (*child).InsertValuesAtStem(stem, values, resolver, depth+1)
+ return bt, err
+}
+
+// CollectNodes collects all child nodes at a given path, and flushes it
+// into the provided node collector.
+func (bt *InternalNode) CollectNodes(path []byte, flushfn NodeFlushFn) error {
+ if bt.Left != nil {
+ var p [256]byte
+ copy(p[:], path)
+ childpath := p[:len(path)]
+ childpath = append(childpath, 0)
+ if err := bt.Left.CollectNodes(childpath, flushfn); err != nil {
+ return err
+ }
+ }
+ if bt.Right != nil {
+ var p [256]byte
+ copy(p[:], path)
+ childpath := p[:len(path)]
+ childpath = append(childpath, 1)
+ if err := bt.Right.CollectNodes(childpath, flushfn); err != nil {
+ return err
+ }
+ }
+ flushfn(path, bt)
+ return nil
+}
+
+// GetHeight returns the height of the node.
+func (bt *InternalNode) GetHeight() int {
+ var (
+ leftHeight int
+ rightHeight int
+ )
+ if bt.Left != nil {
+ leftHeight = bt.Left.GetHeight()
+ }
+ if bt.Right != nil {
+ rightHeight = bt.Right.GetHeight()
+ }
+ return 1 + max(leftHeight, rightHeight)
+}
+
+func (bt *InternalNode) toDot(parent, path string) string {
+ me := fmt.Sprintf("internal%s", path)
+ ret := fmt.Sprintf("%s [label=\"I: %x\"]\n", me, bt.Hash())
+ if len(parent) > 0 {
+ ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
+ }
+
+ if bt.Left != nil {
+ ret = fmt.Sprintf("%s%s", ret, bt.Left.toDot(me, fmt.Sprintf("%s%02x", path, 0)))
+ }
+ if bt.Right != nil {
+ ret = fmt.Sprintf("%s%s", ret, bt.Right.toDot(me, fmt.Sprintf("%s%02x", path, 1)))
+ }
+
+ return ret
+}
diff --git a/trie/bintrie/stem_node.go b/trie/bintrie/stem_node.go
new file mode 100644
index 0000000000..568d3ff4f2
--- /dev/null
+++ b/trie/bintrie/stem_node.go
@@ -0,0 +1,216 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "slices"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/zeebo/blake3"
+)
+
+// StemNode represents a group of `NodeWith` values sharing the same stem.
+type StemNode struct {
+ Stem []byte // Stem path to get to 256 values
+ Values [][]byte // All values, indexed by the last byte of the key.
+ depth int // Depth of the node
+}
+
+// Get retrieves the value for the given key.
+func (bt *StemNode) Get(key []byte, _ NodeResolverFn) ([]byte, error) {
+ panic("this should not be called directly")
+}
+
+// Insert inserts a new key-value pair into the node.
+func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn) (BinaryNode, error) {
+ if !bytes.Equal(bt.Stem, key[:31]) {
+ bitStem := bt.Stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
+
+ new := &InternalNode{depth: bt.depth}
+ bt.depth++
+ var child, other *BinaryNode
+ if bitStem == 0 {
+ new.Left = bt
+ child = &new.Left
+ other = &new.Right
+ } else {
+ new.Right = bt
+ child = &new.Right
+ other = &new.Left
+ }
+
+ bitKey := key[new.depth/8] >> (7 - (new.depth % 8)) & 1
+ if bitKey == bitStem {
+ var err error
+ *child, err = (*child).Insert(key, value, nil)
+ if err != nil {
+ return new, fmt.Errorf("insert error: %w", err)
+ }
+ *other = Empty{}
+ } else {
+ var values [256][]byte
+ values[key[31]] = value
+ *other = &StemNode{
+ Stem: slices.Clone(key[:31]),
+ Values: values[:],
+ depth: new.depth + 1,
+ }
+ }
+
+ return new, nil
+ }
+ if len(value) != 32 {
+ return bt, errors.New("invalid insertion: value length")
+ }
+
+ bt.Values[key[31]] = value
+ return bt, nil
+}
+
+// Copy creates a deep copy of the node.
+func (bt *StemNode) Copy() BinaryNode {
+ var values [256][]byte
+ for i, v := range bt.Values {
+ values[i] = slices.Clone(v)
+ }
+ return &StemNode{
+ Stem: slices.Clone(bt.Stem),
+ Values: values[:],
+ depth: bt.depth,
+ }
+}
+
+// GetHeight returns the height of the node.
+func (bt *StemNode) GetHeight() int {
+ return 1
+}
+
+// Hash returns the hash of the node.
+func (bt *StemNode) Hash() common.Hash {
+ var data [NodeWidth]common.Hash
+ for i, v := range bt.Values {
+ if v != nil {
+ h := blake3.Sum256(v)
+ data[i] = common.BytesToHash(h[:])
+ }
+ }
+
+ h := blake3.New()
+ for level := 1; level <= 8; level++ {
+ for i := range NodeWidth / (1 << level) {
+ h.Reset()
+
+ if data[i*2] == (common.Hash{}) && data[i*2+1] == (common.Hash{}) {
+ data[i] = common.Hash{}
+ continue
+ }
+
+ h.Write(data[i*2][:])
+ h.Write(data[i*2+1][:])
+ data[i] = common.Hash(h.Sum(nil))
+ }
+ }
+
+ h.Reset()
+ h.Write(bt.Stem)
+ h.Write([]byte{0})
+ h.Write(data[0][:])
+ return common.BytesToHash(h.Sum(nil))
+}
+
+// CollectNodes collects all child nodes at a given path, and flushes it
+// into the provided node collector.
+func (bt *StemNode) CollectNodes(path []byte, flush NodeFlushFn) error {
+ flush(path, bt)
+ return nil
+}
+
+// GetValuesAtStem retrieves the group of values located at the given stem key.
+func (bt *StemNode) GetValuesAtStem(_ []byte, _ NodeResolverFn) ([][]byte, error) {
+ return bt.Values[:], nil
+}
+
+// InsertValuesAtStem inserts a full value group at the given stem in the internal node.
+// Already-existing values will be overwritten.
+func (bt *StemNode) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
+ if !bytes.Equal(bt.Stem, key[:31]) {
+ bitStem := bt.Stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
+
+ new := &InternalNode{depth: bt.depth}
+ bt.depth++
+ var child, other *BinaryNode
+ if bitStem == 0 {
+ new.Left = bt
+ child = &new.Left
+ other = &new.Right
+ } else {
+ new.Right = bt
+ child = &new.Right
+ other = &new.Left
+ }
+
+ bitKey := key[new.depth/8] >> (7 - (new.depth % 8)) & 1
+ if bitKey == bitStem {
+ var err error
+ *child, err = (*child).InsertValuesAtStem(key, values, nil, depth+1)
+ if err != nil {
+ return new, fmt.Errorf("insert error: %w", err)
+ }
+ *other = Empty{}
+ } else {
+ *other = &StemNode{
+ Stem: slices.Clone(key[:31]),
+ Values: values,
+ depth: new.depth + 1,
+ }
+ }
+
+ return new, nil
+ }
+
+ // same stem, just merge the two value lists
+ for i, v := range values {
+ if v != nil {
+ bt.Values[i] = v
+ }
+ }
+ return bt, nil
+}
+
+func (bt *StemNode) toDot(parent, path string) string {
+ me := fmt.Sprintf("stem%s", path)
+ ret := fmt.Sprintf("%s [label=\"stem=%x c=%x\"]\n", me, bt.Stem, bt.Hash())
+ ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
+ for i, v := range bt.Values {
+ if v != nil {
+ ret = fmt.Sprintf("%s%s%x [label=\"%x\"]\n", ret, me, i, v)
+ ret = fmt.Sprintf("%s%s -> %s%x\n", ret, me, me, i)
+ }
+ }
+ return ret
+}
+
+// Key returns the full key for the given index.
+func (bt *StemNode) Key(i int) []byte {
+ var ret [32]byte
+ copy(ret[:], bt.Stem)
+ ret[StemSize] = byte(i)
+ return ret[:]
+}
diff --git a/trie/utils/binary.go b/trie/utils/binary.go
new file mode 100644
index 0000000000..d50b5d4167
--- /dev/null
+++ b/trie/utils/binary.go
@@ -0,0 +1,68 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package utils
+
+import (
+ "bytes"
+ "crypto/sha256"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/holiman/uint256"
+)
+
+var zeroHash = common.Hash{}
+
+func GetBinaryTreeKey(addr common.Address, key []byte) []byte {
+ hasher := sha256.New()
+ hasher.Write(zeroHash[:12])
+ hasher.Write(addr[:])
+ hasher.Write(key[:31])
+ k := hasher.Sum(nil)
+ k[31] = key[31]
+ return k
+}
+
+func GetBinaryTreeKeyCodeHash(addr common.Address) []byte {
+ var k [32]byte
+ k[31] = CodeHashLeafKey
+ return GetBinaryTreeKey(addr, k[:])
+}
+
+func GetBinaryTreeKeyStorageSlot(address common.Address, key []byte) []byte {
+ var k [32]byte
+
+ // Case when the key belongs to the account header
+ if bytes.Equal(key[:31], zeroHash[:31]) && key[31] < 64 {
+ k[31] = 64 + key[31]
+ return GetBinaryTreeKey(address, k[:])
+ }
+
+ // Set the main storage offset
+ // note that the first 64 bytes of the main offset storage
+ // are unreachable, which is consistent with the spec and
+ // what verkle does.
+ k[0] = 1 // 1 << 248
+ copy(k[1:], key[:31])
+ k[31] = key[31]
+
+ return GetBinaryTreeKey(address, k[:])
+}
+
+func GetBinaryTreeKeyCodeChunk(address common.Address, chunknr *uint256.Int) []byte {
+ chunkOffset := new(uint256.Int).Add(codeOffset, chunknr).Bytes()
+ return GetBinaryTreeKey(address, chunkOffset)
+}