trie{,/utils}: add tests for eip7864 binary trees

Signed-off-by: Guillaume Ballet
<3272758+gballet@users.noreply.github.com>
This commit is contained in:
Guillaume Ballet 2025-08-07 16:35:58 +02:00
parent 9b2e8e7ce3
commit 2656abc317
12 changed files with 1568 additions and 3 deletions

3
go.mod
View file

@ -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

10
go.sum
View file

@ -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=

319
trie/binary.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

236
trie/binary_iterator.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

198
trie/binary_test.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

121
trie/bintrie/binary_node.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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("", "")
}

71
trie/bintrie/empty.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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")
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

216
trie/bintrie/stem_node.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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[:]
}

68
trie/utils/binary.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}