Skip to content

Commit 2656abc

Browse files
committed
trie{,/utils}: add tests for eip7864 binary trees
Signed-off-by: Guillaume Ballet <[email protected]>
1 parent 9b2e8e7 commit 2656abc

File tree

12 files changed

+1568
-3
lines changed

12 files changed

+1568
-3
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ require (
6262
github.com/supranational/blst v0.3.14
6363
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
6464
github.com/urfave/cli/v2 v2.27.5
65+
github.com/zeebo/blake3 v0.2.4
6566
go.uber.org/automaxprocs v1.5.2
6667
go.uber.org/goleak v1.3.0
6768
golang.org/x/crypto v0.36.0
@@ -115,7 +116,7 @@ require (
115116
github.com/jmespath/go-jmespath v0.4.0 // indirect
116117
github.com/kilic/bls12-381 v0.1.0 // indirect
117118
github.com/klauspost/compress v1.16.0 // indirect
118-
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
119+
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
119120
github.com/kr/pretty v0.3.1 // indirect
120121
github.com/kr/text v0.2.0 // indirect
121122
github.com/mattn/go-runewidth v0.0.13 // indirect

go.sum

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
220220
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
221221
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
222222
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
223-
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
224-
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
223+
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
224+
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
225225
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
226226
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
227227
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
365365
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
366366
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
367367
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
368+
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
369+
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
370+
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
371+
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
372+
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
373+
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
368374
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
369375
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
370376
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=

trie/binary.go

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
// Copyright 2025 go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package trie
18+
19+
import (
20+
"bytes"
21+
"encoding/binary"
22+
"fmt"
23+
24+
"github.com/ethereum/go-ethereum/common"
25+
"github.com/ethereum/go-ethereum/core/types"
26+
"github.com/ethereum/go-ethereum/ethdb"
27+
"github.com/ethereum/go-ethereum/trie/bintrie"
28+
"github.com/ethereum/go-ethereum/trie/trienode"
29+
"github.com/ethereum/go-ethereum/trie/utils"
30+
"github.com/ethereum/go-ethereum/triedb/database"
31+
"github.com/holiman/uint256"
32+
)
33+
34+
// zero is the zero value for a 32-byte array.
35+
var zero [32]byte
36+
37+
// NewBinaryNode creates a new empty binary trie
38+
func NewBinaryNode() bintrie.BinaryNode {
39+
return bintrie.Empty{}
40+
}
41+
42+
// BinaryTrie is a wrapper around VerkleNode that implements the trie.Trie
43+
// interface so that Verkle trees can be reused verbatim.
44+
type BinaryTrie struct {
45+
root bintrie.BinaryNode
46+
reader *trieReader
47+
}
48+
49+
// ToDot converts the binary trie to a DOT language representation. Useful for debugging.
50+
func (trie *BinaryTrie) ToDot() string {
51+
trie.root.Hash()
52+
return bintrie.ToDot(trie.root)
53+
}
54+
55+
// NewBinaryTrie creates a new binary trie.
56+
func NewBinaryTrie(root common.Hash, db database.NodeDatabase) (*BinaryTrie, error) {
57+
reader, err := newTrieReader(root, common.Hash{}, db)
58+
if err != nil {
59+
return nil, err
60+
}
61+
// Parse the root verkle node if it's not empty.
62+
node := NewBinaryNode()
63+
if root != types.EmptyVerkleHash && root != types.EmptyRootHash {
64+
blob, err := reader.node(nil, common.Hash{})
65+
if err != nil {
66+
return nil, err
67+
}
68+
node, err = bintrie.DeserializeNode(blob, 0)
69+
if err != nil {
70+
return nil, err
71+
}
72+
}
73+
return &BinaryTrie{
74+
root: node,
75+
reader: reader,
76+
}, nil
77+
}
78+
79+
// FlatdbNodeResolver is a node resolver that reads nodes from the flatdb.
80+
func (trie *BinaryTrie) FlatdbNodeResolver(path []byte, hash common.Hash) ([]byte, error) {
81+
// empty nodes will be serialized as common.Hash{}, so capture
82+
// this special use case.
83+
if hash == (common.Hash{}) {
84+
return nil, nil // empty node
85+
}
86+
return trie.reader.node(path, hash)
87+
}
88+
89+
// GetKey returns the sha3 preimage of a hashed key that was previously used
90+
// to store a value.
91+
func (trie *BinaryTrie) GetKey(key []byte) []byte {
92+
return key
93+
}
94+
95+
// Get returns the value for key stored in the trie. The value bytes must
96+
// not be modified by the caller. If a node was not found in the database, a
97+
// trie.MissingNodeError is returned.
98+
func (trie *BinaryTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
99+
return trie.root.Get(utils.GetBinaryTreeKey(addr, key), trie.FlatdbNodeResolver)
100+
}
101+
102+
// GetWithHashedKey returns the value, assuming that the key has already
103+
// been hashed.
104+
func (trie *BinaryTrie) GetWithHashedKey(key []byte) ([]byte, error) {
105+
return trie.root.Get(key, trie.FlatdbNodeResolver)
106+
}
107+
108+
// GetAccount returns the account information for the given address.
109+
func (trie *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error) {
110+
acc := &types.StateAccount{}
111+
versionkey := utils.GetBinaryTreeKey(addr, zero[:])
112+
var (
113+
values [][]byte
114+
err error
115+
)
116+
switch r := trie.root.(type) {
117+
case *bintrie.InternalNode:
118+
values, err = r.GetValuesAtStem(versionkey[:31], trie.FlatdbNodeResolver)
119+
case *bintrie.StemNode:
120+
values = r.Values
121+
case bintrie.Empty:
122+
return nil, nil
123+
default:
124+
// This will cover HashedNode but that should be fine since the
125+
// root node should always be resolved.
126+
return nil, errInvalidRootType
127+
}
128+
if err != nil {
129+
return nil, fmt.Errorf("GetAccount (%x) error: %v", addr, err)
130+
}
131+
132+
// The following code is required for the MPT->VKT conversion.
133+
// An account can be partially migrated, where storage slots were moved to the VKT
134+
// but not yet the account. This means some account information as (header) storage slots
135+
// are in the VKT but basic account information must be read in the base tree (MPT).
136+
// TODO: we can simplify this logic depending if the conversion is in progress or finished.
137+
emptyAccount := true
138+
139+
for i := 0; values != nil && i <= utils.CodeHashLeafKey && emptyAccount; i++ {
140+
emptyAccount = emptyAccount && values[i] == nil
141+
}
142+
if emptyAccount {
143+
return nil, nil
144+
}
145+
146+
// if the account has been deleted, then values[10] will be 0 and not nil. If it has
147+
// been recreated after that, then its code keccak will NOT be 0. So return `nil` if
148+
// the nonce, and values[10], and code keccak is 0.
149+
if bytes.Equal(values[utils.BasicDataLeafKey], zero[:]) && len(values) > 10 && len(values[10]) > 0 && bytes.Equal(values[utils.CodeHashLeafKey], zero[:]) {
150+
return nil, nil
151+
}
152+
153+
acc.Nonce = binary.BigEndian.Uint64(values[utils.BasicDataLeafKey][utils.BasicDataNonceOffset:])
154+
var balance [16]byte
155+
copy(balance[:], values[utils.BasicDataLeafKey][utils.BasicDataBalanceOffset:])
156+
acc.Balance = new(uint256.Int).SetBytes(balance[:])
157+
acc.CodeHash = values[utils.CodeHashLeafKey]
158+
159+
return acc, nil
160+
}
161+
162+
// UpdateAccount updates the account information for the given address.
163+
func (trie *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {
164+
var (
165+
err error
166+
basicData [32]byte
167+
values = make([][]byte, bintrie.NodeWidth)
168+
stem = utils.GetBinaryTreeKey(addr, zero[:])
169+
)
170+
171+
binary.BigEndian.PutUint32(basicData[utils.BasicDataCodeSizeOffset-1:], uint32(codeLen))
172+
binary.BigEndian.PutUint64(basicData[utils.BasicDataNonceOffset:], acc.Nonce)
173+
// Because the balance is a max of 16 bytes, truncate
174+
// the extra values. This happens in devmode, where
175+
// 0xff**32 is allocated to the developer account.
176+
balanceBytes := acc.Balance.Bytes()
177+
// TODO: reduce the size of the allocation in devmode, then panic instead
178+
// of truncating.
179+
if len(balanceBytes) > 16 {
180+
balanceBytes = balanceBytes[16:]
181+
}
182+
copy(basicData[32-len(balanceBytes):], balanceBytes[:])
183+
values[utils.BasicDataLeafKey] = basicData[:]
184+
values[utils.CodeHashLeafKey] = acc.CodeHash[:]
185+
186+
trie.root, err = trie.root.InsertValuesAtStem(stem, values, trie.FlatdbNodeResolver, 0)
187+
return err
188+
}
189+
190+
// UpdateStem updates the values for the given stem key.
191+
func (trie *BinaryTrie) UpdateStem(key []byte, values [][]byte) error {
192+
var err error
193+
trie.root, err = trie.root.InsertValuesAtStem(key, values, trie.FlatdbNodeResolver, 0)
194+
return err
195+
}
196+
197+
// Update associates key with value in the trie. If value has length zero, any
198+
// existing value is deleted from the trie. The value bytes must not be modified
199+
// by the caller while they are stored in the trie. If a node was not found in the
200+
// database, a trie.MissingNodeError is returned.
201+
func (trie *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) error {
202+
k := utils.GetBinaryTreeKeyStorageSlot(address, key)
203+
var v [32]byte
204+
if len(value) >= 32 {
205+
copy(v[:], value[:32])
206+
} else {
207+
copy(v[32-len(value):], value[:])
208+
}
209+
root, err := trie.root.Insert(k, v[:], trie.FlatdbNodeResolver)
210+
if err != nil {
211+
return fmt.Errorf("UpdateStorage (%x) error: %v", address, err)
212+
}
213+
trie.root = root
214+
return nil
215+
}
216+
217+
// DeleteAccount is a no-op as it is disabled in stateless.
218+
func (trie *BinaryTrie) DeleteAccount(addr common.Address) error {
219+
return nil
220+
}
221+
222+
// Delete removes any existing value for key from the trie. If a node was not
223+
// found in the database, a trie.MissingNodeError is returned.
224+
func (trie *BinaryTrie) DeleteStorage(addr common.Address, key []byte) error {
225+
k := utils.GetBinaryTreeKey(addr, key)
226+
var zero [32]byte
227+
root, err := trie.root.Insert(k, zero[:], trie.FlatdbNodeResolver)
228+
if err != nil {
229+
return fmt.Errorf("DeleteStorage (%x) error: %v", addr, err)
230+
}
231+
trie.root = root
232+
return nil
233+
}
234+
235+
// Hash returns the root hash of the trie. It does not write to the database and
236+
// can be used even if the trie doesn't have one.
237+
func (trie *BinaryTrie) Hash() common.Hash {
238+
return trie.root.Hash()
239+
}
240+
241+
// Commit writes all nodes to the trie's memory database, tracking the internal
242+
// and external (for account tries) references.
243+
func (trie *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet, error) {
244+
root := trie.root.(*bintrie.InternalNode)
245+
nodeset := trienode.NewNodeSet(common.Hash{})
246+
247+
err := root.CollectNodes(nil, func(path []byte, node bintrie.BinaryNode) {
248+
serialized := bintrie.SerializeNode(node)
249+
nodeset.AddNode(path, trienode.New(common.Hash{}, serialized))
250+
})
251+
if err != nil {
252+
panic(fmt.Errorf("CollectNodes failed: %v", err))
253+
}
254+
255+
// Serialize root commitment form
256+
return trie.Hash(), nodeset, nil
257+
}
258+
259+
// NodeIterator returns an iterator that returns nodes of the trie. Iteration
260+
// starts at the key after the given start key.
261+
func (trie *BinaryTrie) NodeIterator(startKey []byte) (NodeIterator, error) {
262+
return newBinaryNodeIterator(trie, nil)
263+
}
264+
265+
// Prove constructs a Merkle proof for key. The result contains all encoded nodes
266+
// on the path to the value at key. The value itself is also included in the last
267+
// node and can be retrieved by verifying the proof.
268+
//
269+
// If the trie does not contain a value for key, the returned proof contains all
270+
// nodes of the longest existing prefix of the key (at least the root), ending
271+
// with the node that proves the absence of the key.
272+
func (trie *BinaryTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
273+
panic("not implemented")
274+
}
275+
276+
// Copy creates a deep copy of the trie.
277+
func (trie *BinaryTrie) Copy() *BinaryTrie {
278+
return &BinaryTrie{
279+
root: trie.root.Copy(),
280+
reader: trie.reader,
281+
}
282+
}
283+
284+
// IsVerkle returns true if the trie is a Verkle tree.
285+
func (trie *BinaryTrie) IsVerkle() bool {
286+
// TODO @gballet This is technically NOT a verkle tree, but it has the same
287+
// behavior and basic structure, so for all intents and purposes, it can be
288+
// treated as such. Rename this when verkle gets removed.
289+
return true
290+
}
291+
292+
// Note: the basic data leaf needs to have been previously created for this to work
293+
func (trie *BinaryTrie) UpdateContractCode(addr common.Address, codeHash common.Hash, code []byte) error {
294+
var (
295+
chunks = ChunkifyCode(code)
296+
values [][]byte
297+
key []byte
298+
err error
299+
)
300+
for i, chunknr := 0, uint64(0); i < len(chunks); i, chunknr = i+32, chunknr+1 {
301+
groupOffset := (chunknr + 128) % 256
302+
if groupOffset == 0 /* start of new group */ || chunknr == 0 /* first chunk in header group */ {
303+
values = make([][]byte, bintrie.NodeWidth)
304+
var offset [32]byte
305+
binary.LittleEndian.PutUint64(offset[24:], chunknr+128)
306+
key = utils.GetBinaryTreeKey(addr, offset[:])
307+
}
308+
values[groupOffset] = chunks[i : i+32]
309+
310+
if groupOffset == 255 || len(chunks)-i <= 32 {
311+
err = trie.UpdateStem(key[:31], values)
312+
313+
if err != nil {
314+
return fmt.Errorf("UpdateContractCode (addr=%x) error: %w", addr[:], err)
315+
}
316+
}
317+
}
318+
return nil
319+
}

0 commit comments

Comments
 (0)