Skip to content

Commit 7f78fa6

Browse files
authored
triedb/pathdb, core: keep root->id mappings after truncation (#32502)
This pull request preserves the root->ID mappings in the path database even after the associated state histories are truncated, regardless of whether the truncation occurs at the head or the tail. The motivation is to support an additional history type, trienode history. Since the root->ID mappings are shared between two history instances, they must not be removed by either one. As a consequence, the root->ID mappings remain in the database even after the corresponding histories are pruned. While these mappings may become dangling, it is safe and cheap to keep them. Additionally, this pull request enhances validation during historical reader construction, ensuring that only canonical historical state will be served.
1 parent 2a795c1 commit 7f78fa6

File tree

9 files changed

+220
-167
lines changed

9 files changed

+220
-167
lines changed

core/rawdb/accessors_state.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,6 @@ func WriteStateID(db ethdb.KeyValueWriter, root common.Hash, id uint64) {
119119
}
120120
}
121121

122-
// DeleteStateID deletes the specified state lookup from the database.
123-
func DeleteStateID(db ethdb.KeyValueWriter, root common.Hash) {
124-
if err := db.Delete(stateIDKey(root)); err != nil {
125-
log.Crit("Failed to delete state ID", "err", err)
126-
}
127-
}
128-
129122
// ReadPersistentStateID retrieves the id of the persistent state from the database.
130123
func ReadPersistentStateID(db ethdb.KeyValueReader) uint64 {
131124
data, _ := db.Get(persistentStateIDKey)

triedb/pathdb/database.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ func (db *Database) repairHistory() error {
334334
}
335335
// Truncate the extra state histories above in freezer in case it's not
336336
// aligned with the disk layer. It might happen after a unclean shutdown.
337-
pruned, err := truncateFromHead(db.diskdb, db.stateFreezer, id)
337+
pruned, err := truncateFromHead(db.stateFreezer, id)
338338
if err != nil {
339339
log.Crit("Failed to truncate extra state histories", "err", err)
340340
}
@@ -590,7 +590,7 @@ func (db *Database) Recover(root common.Hash) error {
590590
if err := db.diskdb.SyncKeyValue(); err != nil {
591591
return err
592592
}
593-
_, err := truncateFromHead(db.diskdb, db.stateFreezer, dl.stateID())
593+
_, err := truncateFromHead(db.stateFreezer, dl.stateID())
594594
if err != nil {
595595
return err
596596
}
@@ -615,14 +615,14 @@ func (db *Database) Recoverable(root common.Hash) bool {
615615
return false
616616
}
617617
// This is a temporary workaround for the unavailability of the freezer in
618-
// dev mode. As a consequence, the Pathdb loses the ability for deep reorg
618+
// dev mode. As a consequence, the database loses the ability for deep reorg
619619
// in certain cases.
620620
// TODO(rjl493456442): Implement the in-memory ancient store.
621621
if db.stateFreezer == nil {
622622
return false
623623
}
624624
// Ensure the requested state is a canonical state and all state
625-
// histories in range [id+1, disklayer.ID] are present and complete.
625+
// histories in range [id+1, dl.ID] are present and complete.
626626
return checkStateHistories(db.stateFreezer, *id+1, dl.stateID()-*id, func(m *meta) error {
627627
if m.parent != root {
628628
return errors.New("unexpected state history")

triedb/pathdb/disklayer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ func (dl *diskLayer) writeStateHistory(diff *diffLayer) (bool, error) {
378378
log.Debug("Skip tail truncation", "persistentID", persistentID, "tailID", tail+1, "headID", diff.stateID(), "limit", limit)
379379
return true, nil
380380
}
381-
pruned, err := truncateFromTail(dl.db.diskdb, dl.db.stateFreezer, newFirst-1)
381+
pruned, err := truncateFromTail(dl.db.stateFreezer, newFirst-1)
382382
if err != nil {
383383
return false, err
384384
}

triedb/pathdb/history.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2025 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 pathdb
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
23+
"github.com/ethereum/go-ethereum/ethdb"
24+
"github.com/ethereum/go-ethereum/log"
25+
)
26+
27+
var (
28+
errHeadTruncationOutOfRange = errors.New("history head truncation out of range")
29+
errTailTruncationOutOfRange = errors.New("history tail truncation out of range")
30+
)
31+
32+
// truncateFromHead removes excess elements from the head of the freezer based
33+
// on the given parameters. It returns the number of items that were removed.
34+
func truncateFromHead(store ethdb.AncientStore, nhead uint64) (int, error) {
35+
ohead, err := store.Ancients()
36+
if err != nil {
37+
return 0, err
38+
}
39+
otail, err := store.Tail()
40+
if err != nil {
41+
return 0, err
42+
}
43+
log.Info("Truncating from head", "ohead", ohead, "tail", otail, "nhead", nhead)
44+
45+
// Ensure that the truncation target falls within the valid range.
46+
if ohead < nhead || nhead < otail {
47+
return 0, fmt.Errorf("%w, tail: %d, head: %d, target: %d", errHeadTruncationOutOfRange, otail, ohead, nhead)
48+
}
49+
// Short circuit if nothing to truncate.
50+
if ohead == nhead {
51+
return 0, nil
52+
}
53+
ohead, err = store.TruncateHead(nhead)
54+
if err != nil {
55+
return 0, err
56+
}
57+
// Associated root->id mappings are left in the database and wait
58+
// for overwriting.
59+
return int(ohead - nhead), nil
60+
}
61+
62+
// truncateFromTail removes excess elements from the end of the freezer based
63+
// on the given parameters. It returns the number of items that were removed.
64+
func truncateFromTail(store ethdb.AncientStore, ntail uint64) (int, error) {
65+
ohead, err := store.Ancients()
66+
if err != nil {
67+
return 0, err
68+
}
69+
otail, err := store.Tail()
70+
if err != nil {
71+
return 0, err
72+
}
73+
// Ensure that the truncation target falls within the valid range.
74+
if otail > ntail || ntail > ohead {
75+
return 0, fmt.Errorf("%w, tail: %d, head: %d, target: %d", errTailTruncationOutOfRange, otail, ohead, ntail)
76+
}
77+
// Short circuit if nothing to truncate.
78+
if otail == ntail {
79+
return 0, nil
80+
}
81+
otail, err = store.TruncateTail(ntail)
82+
if err != nil {
83+
return 0, err
84+
}
85+
// Associated root->id mappings are left in the database.
86+
return int(ntail - otail), nil
87+
}

triedb/pathdb/history_reader.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,11 +320,12 @@ func (r *historyReader) read(state stateIdentQuery, stateID uint64, lastID uint6
320320
tail, err := r.freezer.Tail()
321321
if err != nil {
322322
return nil, err
323-
}
324-
// stateID == tail is allowed, as the first history object preserved
325-
// is tail+1
323+
} // firstID = tail+1
324+
325+
// stateID+1 == firstID is allowed, as all the subsequent state histories
326+
// are present with no gap inside.
326327
if stateID < tail {
327-
return nil, errors.New("historical state has been pruned")
328+
return nil, fmt.Errorf("historical state has been pruned, first: %d, state: %d", tail+1, stateID)
328329
}
329330

330331
// To serve the request, all state histories from stateID+1 to lastID

triedb/pathdb/history_reader_test.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/ethereum/go-ethereum/common"
2626
"github.com/ethereum/go-ethereum/core/rawdb"
27+
"github.com/ethereum/go-ethereum/internal/testrand"
2728
)
2829

2930
func waitIndexing(db *Database) {
@@ -36,11 +37,29 @@ func waitIndexing(db *Database) {
3637
}
3738
}
3839

39-
func checkHistoricState(env *tester, root common.Hash, hr *historyReader) error {
40+
func stateAvail(id uint64, env *tester) bool {
41+
if env.db.config.StateHistory == 0 {
42+
return true
43+
}
44+
dl := env.db.tree.bottom()
45+
if dl.stateID() <= env.db.config.StateHistory {
46+
return true
47+
}
48+
firstID := dl.stateID() - env.db.config.StateHistory + 1
49+
50+
return id+1 >= firstID
51+
}
52+
53+
func checkHistoricalState(env *tester, root common.Hash, id uint64, hr *historyReader) error {
54+
if !stateAvail(id, env) {
55+
return nil
56+
}
57+
4058
// Short circuit if the historical state is no longer available
4159
if rawdb.ReadStateID(env.db.diskdb, root) == nil {
42-
return nil
60+
return fmt.Errorf("state not found %d %x", id, root)
4361
}
62+
4463
var (
4564
dl = env.db.tree.bottom()
4665
stateID = rawdb.ReadStateID(env.db.diskdb, root)
@@ -124,22 +143,22 @@ func testHistoryReader(t *testing.T, historyLimit uint64) {
124143
defer func() {
125144
maxDiffLayers = 128
126145
}()
127-
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
128146

147+
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
129148
env := newTester(t, historyLimit, false, 64, true, "")
130149
defer env.release()
131150
waitIndexing(env.db)
132151

133152
var (
134153
roots = env.roots
135-
dRoot = env.db.tree.bottom().rootHash()
154+
dl = env.db.tree.bottom()
136155
hr = newHistoryReader(env.db.diskdb, env.db.stateFreezer)
137156
)
138-
for _, root := range roots {
139-
if root == dRoot {
157+
for i, root := range roots {
158+
if root == dl.rootHash() {
140159
break
141160
}
142-
if err := checkHistoricState(env, root, hr); err != nil {
161+
if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
143162
t.Fatal(err)
144163
}
145164
}
@@ -148,12 +167,41 @@ func testHistoryReader(t *testing.T, historyLimit uint64) {
148167
env.extend(4)
149168
waitIndexing(env.db)
150169

151-
for _, root := range roots {
152-
if root == dRoot {
170+
for i, root := range roots {
171+
if root == dl.rootHash() {
153172
break
154173
}
155-
if err := checkHistoricState(env, root, hr); err != nil {
174+
if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
156175
t.Fatal(err)
157176
}
158177
}
159178
}
179+
180+
func TestHistoricalStateReader(t *testing.T) {
181+
maxDiffLayers = 4
182+
defer func() {
183+
maxDiffLayers = 128
184+
}()
185+
186+
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
187+
env := newTester(t, 0, false, 64, true, "")
188+
defer env.release()
189+
waitIndexing(env.db)
190+
191+
// non-canonical state
192+
fakeRoot := testrand.Hash()
193+
rawdb.WriteStateID(env.db.diskdb, fakeRoot, 10)
194+
195+
_, err := env.db.HistoricReader(fakeRoot)
196+
if err == nil {
197+
t.Fatal("expected error")
198+
}
199+
t.Log(err)
200+
201+
// canonical state
202+
realRoot := env.roots[9]
203+
_, err = env.db.HistoricReader(realRoot)
204+
if err != nil {
205+
t.Fatalf("Unexpected error: %v", err)
206+
}
207+
}

triedb/pathdb/history_state.go

Lines changed: 16 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,20 @@ func (h *stateHistory) decode(accountData, storageData, accountIndexes, storageI
504504
return nil
505505
}
506506

507+
// readStateHistoryMeta reads the metadata of state history with the specified id.
508+
func readStateHistoryMeta(reader ethdb.AncientReader, id uint64) (*meta, error) {
509+
data := rawdb.ReadStateHistoryMeta(reader, id)
510+
if len(data) == 0 {
511+
return nil, fmt.Errorf("metadata is not found, %d", id)
512+
}
513+
var m meta
514+
err := m.decode(data)
515+
if err != nil {
516+
return nil, err
517+
}
518+
return &m, nil
519+
}
520+
507521
// readStateHistory reads a single state history records with the specified id.
508522
func readStateHistory(reader ethdb.AncientReader, id uint64) (*stateHistory, error) {
509523
mData, accountIndexes, storageIndexes, accountData, storageData, err := rawdb.ReadStateHistory(reader, id)
@@ -568,8 +582,8 @@ func writeStateHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
568582
return nil
569583
}
570584

571-
// checkStateHistories retrieves a batch of meta objects with the specified range
572-
// and performs the callback on each item.
585+
// checkStateHistories retrieves a batch of metadata objects with the specified
586+
// range and performs the callback on each item.
573587
func checkStateHistories(reader ethdb.AncientReader, start, count uint64, check func(*meta) error) error {
574588
for count > 0 {
575589
number := count
@@ -594,87 +608,3 @@ func checkStateHistories(reader ethdb.AncientReader, start, count uint64, check
594608
}
595609
return nil
596610
}
597-
598-
// truncateFromHead removes the extra state histories from the head with the given
599-
// parameters. It returns the number of items removed from the head.
600-
func truncateFromHead(db ethdb.Batcher, store ethdb.AncientStore, nhead uint64) (int, error) {
601-
ohead, err := store.Ancients()
602-
if err != nil {
603-
return 0, err
604-
}
605-
otail, err := store.Tail()
606-
if err != nil {
607-
return 0, err
608-
}
609-
// Ensure that the truncation target falls within the specified range.
610-
if ohead < nhead || nhead < otail {
611-
return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, nhead)
612-
}
613-
// Short circuit if nothing to truncate.
614-
if ohead == nhead {
615-
return 0, nil
616-
}
617-
// Load the meta objects in range [nhead+1, ohead]
618-
blobs, err := rawdb.ReadStateHistoryMetaList(store, nhead+1, ohead-nhead)
619-
if err != nil {
620-
return 0, err
621-
}
622-
batch := db.NewBatch()
623-
for _, blob := range blobs {
624-
var m meta
625-
if err := m.decode(blob); err != nil {
626-
return 0, err
627-
}
628-
rawdb.DeleteStateID(batch, m.root)
629-
}
630-
if err := batch.Write(); err != nil {
631-
return 0, err
632-
}
633-
ohead, err = store.TruncateHead(nhead)
634-
if err != nil {
635-
return 0, err
636-
}
637-
return int(ohead - nhead), nil
638-
}
639-
640-
// truncateFromTail removes the extra state histories from the tail with the given
641-
// parameters. It returns the number of items removed from the tail.
642-
func truncateFromTail(db ethdb.Batcher, store ethdb.AncientStore, ntail uint64) (int, error) {
643-
ohead, err := store.Ancients()
644-
if err != nil {
645-
return 0, err
646-
}
647-
otail, err := store.Tail()
648-
if err != nil {
649-
return 0, err
650-
}
651-
// Ensure that the truncation target falls within the specified range.
652-
if otail > ntail || ntail > ohead {
653-
return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, ntail)
654-
}
655-
// Short circuit if nothing to truncate.
656-
if otail == ntail {
657-
return 0, nil
658-
}
659-
// Load the meta objects in range [otail+1, ntail]
660-
blobs, err := rawdb.ReadStateHistoryMetaList(store, otail+1, ntail-otail)
661-
if err != nil {
662-
return 0, err
663-
}
664-
batch := db.NewBatch()
665-
for _, blob := range blobs {
666-
var m meta
667-
if err := m.decode(blob); err != nil {
668-
return 0, err
669-
}
670-
rawdb.DeleteStateID(batch, m.root)
671-
}
672-
if err := batch.Write(); err != nil {
673-
return 0, err
674-
}
675-
otail, err = store.TruncateTail(ntail)
676-
if err != nil {
677-
return 0, err
678-
}
679-
return int(ntail - otail), nil
680-
}

0 commit comments

Comments
 (0)