Skip to content

Commit bd6530a

Browse files
authored
triedb, triedb/internal, triedb/pathdb: add GenerateTrie + extract shared pipeline into triedb/internal (#34654)
This PR adds `GenerateTrie(db, scheme, root)` to the `triedb` package, which rebuilds all tries from flat snapshot KV data. This is needed by snap/2 sync so it can rebuild the trie after downloading the flat state. The shared trie generation pipeline from `pathdb/verifier.go` was moved into `triedb/internal/conversion.go` so both `GenerateTrie` and `VerifyState` reuse the same code.
1 parent 4425795 commit bd6530a

File tree

5 files changed

+663
-322
lines changed

5 files changed

+663
-322
lines changed

triedb/generate.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2026 The 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 triedb
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/ethereum/go-ethereum/common"
23+
"github.com/ethereum/go-ethereum/core/rawdb"
24+
"github.com/ethereum/go-ethereum/ethdb"
25+
"github.com/ethereum/go-ethereum/triedb/internal"
26+
)
27+
28+
// kvAccountIterator wraps an ethdb.Iterator to iterate over account snapshot
29+
// entries in the database, implementing internal.AccountIterator.
30+
type kvAccountIterator struct {
31+
it ethdb.Iterator
32+
hash common.Hash
33+
}
34+
35+
func newKVAccountIterator(db ethdb.Iteratee) *kvAccountIterator {
36+
it := rawdb.NewKeyLengthIterator(
37+
db.NewIterator(rawdb.SnapshotAccountPrefix, nil),
38+
len(rawdb.SnapshotAccountPrefix)+common.HashLength,
39+
)
40+
return &kvAccountIterator{it: it}
41+
}
42+
43+
func (it *kvAccountIterator) Next() bool {
44+
if !it.it.Next() {
45+
return false
46+
}
47+
key := it.it.Key()
48+
copy(it.hash[:], key[len(rawdb.SnapshotAccountPrefix):])
49+
return true
50+
}
51+
52+
func (it *kvAccountIterator) Hash() common.Hash { return it.hash }
53+
func (it *kvAccountIterator) Account() []byte { return it.it.Value() }
54+
func (it *kvAccountIterator) Error() error { return it.it.Error() }
55+
func (it *kvAccountIterator) Release() { it.it.Release() }
56+
57+
// kvStorageIterator wraps an ethdb.Iterator to iterate over storage snapshot
58+
// entries for a specific account, implementing internal.StorageIterator.
59+
type kvStorageIterator struct {
60+
it ethdb.Iterator
61+
hash common.Hash
62+
}
63+
64+
func newKVStorageIterator(db ethdb.Iteratee, accountHash common.Hash) *kvStorageIterator {
65+
it := rawdb.IterateStorageSnapshots(db, accountHash)
66+
return &kvStorageIterator{it: it}
67+
}
68+
69+
func (it *kvStorageIterator) Next() bool {
70+
if !it.it.Next() {
71+
return false
72+
}
73+
key := it.it.Key()
74+
copy(it.hash[:], key[len(rawdb.SnapshotStoragePrefix)+common.HashLength:])
75+
return true
76+
}
77+
78+
func (it *kvStorageIterator) Hash() common.Hash { return it.hash }
79+
func (it *kvStorageIterator) Slot() []byte { return it.it.Value() }
80+
func (it *kvStorageIterator) Error() error { return it.it.Error() }
81+
func (it *kvStorageIterator) Release() { it.it.Release() }
82+
83+
// GenerateTrie rebuilds all tries (storage + account) from flat snapshot data
84+
// in the database. It reads account and storage snapshots from the KV store,
85+
// builds tries using StackTrie with streaming node writes, and verifies the
86+
// computed state root matches the expected root.
87+
func GenerateTrie(db ethdb.Database, scheme string, root common.Hash) error {
88+
acctIt := newKVAccountIterator(db)
89+
defer acctIt.Release()
90+
91+
got, err := internal.GenerateTrieRoot(db, scheme, acctIt, common.Hash{}, internal.StackTrieGenerate, func(dst ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *internal.GenerateStats) (common.Hash, error) {
92+
storageIt := newKVStorageIterator(db, accountHash)
93+
defer storageIt.Release()
94+
95+
hash, err := internal.GenerateTrieRoot(dst, scheme, storageIt, accountHash, internal.StackTrieGenerate, nil, stat, false)
96+
if err != nil {
97+
return common.Hash{}, err
98+
}
99+
return hash, nil
100+
}, internal.NewGenerateStats(), true)
101+
if err != nil {
102+
return err
103+
}
104+
if got != root {
105+
return fmt.Errorf("state root mismatch: got %x, want %x", got, root)
106+
}
107+
return nil
108+
}

triedb/generate_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright 2026 The 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 triedb
18+
19+
import (
20+
"bytes"
21+
"sort"
22+
"testing"
23+
24+
"github.com/ethereum/go-ethereum/common"
25+
"github.com/ethereum/go-ethereum/core/rawdb"
26+
"github.com/ethereum/go-ethereum/core/types"
27+
"github.com/ethereum/go-ethereum/rlp"
28+
"github.com/ethereum/go-ethereum/trie"
29+
"github.com/holiman/uint256"
30+
)
31+
32+
// testAccount is a helper for building test state with deterministic ordering.
33+
type testAccount struct {
34+
hash common.Hash
35+
account types.StateAccount
36+
storage []testSlot // must be sorted by hash
37+
}
38+
39+
type testSlot struct {
40+
hash common.Hash
41+
value []byte
42+
}
43+
44+
// buildExpectedRoot computes the state root from sorted test accounts using
45+
// StackTrie (which requires sorted key insertion).
46+
func buildExpectedRoot(t *testing.T, accounts []testAccount) common.Hash {
47+
t.Helper()
48+
// Sort accounts by hash
49+
sort.Slice(accounts, func(i, j int) bool {
50+
return bytes.Compare(accounts[i].hash[:], accounts[j].hash[:]) < 0
51+
})
52+
acctTrie := trie.NewStackTrie(nil)
53+
for i := range accounts {
54+
data, err := rlp.EncodeToBytes(&accounts[i].account)
55+
if err != nil {
56+
t.Fatal(err)
57+
}
58+
acctTrie.Update(accounts[i].hash[:], data)
59+
}
60+
return acctTrie.Hash()
61+
}
62+
63+
// computeStorageRoot computes the storage trie root from sorted slots.
64+
func computeStorageRoot(slots []testSlot) common.Hash {
65+
sort.Slice(slots, func(i, j int) bool {
66+
return bytes.Compare(slots[i].hash[:], slots[j].hash[:]) < 0
67+
})
68+
st := trie.NewStackTrie(nil)
69+
for _, s := range slots {
70+
st.Update(s.hash[:], s.value)
71+
}
72+
return st.Hash()
73+
}
74+
75+
func TestGenerateTrieEmpty(t *testing.T) {
76+
db := rawdb.NewMemoryDatabase()
77+
if err := GenerateTrie(db, rawdb.HashScheme, types.EmptyRootHash); err != nil {
78+
t.Fatalf("GenerateTrie on empty state failed: %v", err)
79+
}
80+
}
81+
82+
func TestGenerateTrieAccountsOnly(t *testing.T) {
83+
db := rawdb.NewMemoryDatabase()
84+
85+
accounts := []testAccount{
86+
{
87+
hash: common.HexToHash("0x01"),
88+
account: types.StateAccount{
89+
Nonce: 1,
90+
Balance: uint256.NewInt(100),
91+
Root: types.EmptyRootHash,
92+
CodeHash: types.EmptyCodeHash.Bytes(),
93+
},
94+
},
95+
{
96+
hash: common.HexToHash("0x02"),
97+
account: types.StateAccount{
98+
Nonce: 2,
99+
Balance: uint256.NewInt(200),
100+
Root: types.EmptyRootHash,
101+
CodeHash: types.EmptyCodeHash.Bytes(),
102+
},
103+
},
104+
}
105+
for _, a := range accounts {
106+
rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account))
107+
}
108+
root := buildExpectedRoot(t, accounts)
109+
110+
if err := GenerateTrie(db, rawdb.HashScheme, root); err != nil {
111+
t.Fatalf("GenerateTrie failed: %v", err)
112+
}
113+
}
114+
115+
func TestGenerateTrieWithStorage(t *testing.T) {
116+
db := rawdb.NewMemoryDatabase()
117+
118+
slots := []testSlot{
119+
{hash: common.HexToHash("0xaa"), value: []byte{0x01, 0x02, 0x03}},
120+
{hash: common.HexToHash("0xbb"), value: []byte{0x04, 0x05, 0x06}},
121+
}
122+
storageRoot := computeStorageRoot(slots)
123+
124+
accounts := []testAccount{
125+
{
126+
hash: common.HexToHash("0x01"),
127+
account: types.StateAccount{
128+
Nonce: 1,
129+
Balance: uint256.NewInt(100),
130+
Root: storageRoot,
131+
CodeHash: types.EmptyCodeHash.Bytes(),
132+
},
133+
storage: slots,
134+
},
135+
{
136+
hash: common.HexToHash("0x02"),
137+
account: types.StateAccount{
138+
Nonce: 0,
139+
Balance: uint256.NewInt(50),
140+
Root: types.EmptyRootHash,
141+
CodeHash: types.EmptyCodeHash.Bytes(),
142+
},
143+
},
144+
}
145+
// Write account snapshots
146+
for _, a := range accounts {
147+
rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account))
148+
}
149+
// Write storage snapshots
150+
for _, a := range accounts {
151+
for _, s := range a.storage {
152+
rawdb.WriteStorageSnapshot(db, a.hash, s.hash, s.value)
153+
}
154+
}
155+
root := buildExpectedRoot(t, accounts)
156+
157+
if err := GenerateTrie(db, rawdb.HashScheme, root); err != nil {
158+
t.Fatalf("GenerateTrie failed: %v", err)
159+
}
160+
}
161+
162+
func TestGenerateTrieRootMismatch(t *testing.T) {
163+
db := rawdb.NewMemoryDatabase()
164+
165+
acct := types.StateAccount{
166+
Nonce: 1,
167+
Balance: uint256.NewInt(100),
168+
Root: types.EmptyRootHash,
169+
CodeHash: types.EmptyCodeHash.Bytes(),
170+
}
171+
rawdb.WriteAccountSnapshot(db, common.HexToHash("0x01"), types.SlimAccountRLP(acct))
172+
173+
wrongRoot := common.HexToHash("0xdeadbeef")
174+
err := GenerateTrie(db, rawdb.HashScheme, wrongRoot)
175+
if err == nil {
176+
t.Fatal("expected error for root mismatch, got nil")
177+
}
178+
}

0 commit comments

Comments
 (0)