mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-19 06:19:27 +00:00
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.
This commit is contained in:
parent
2a795c14f4
commit
7f78fa6912
9 changed files with 221 additions and 168 deletions
|
|
@ -119,13 +119,6 @@ func WriteStateID(db ethdb.KeyValueWriter, root common.Hash, id uint64) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteStateID deletes the specified state lookup from the database.
|
|
||||||
func DeleteStateID(db ethdb.KeyValueWriter, root common.Hash) {
|
|
||||||
if err := db.Delete(stateIDKey(root)); err != nil {
|
|
||||||
log.Crit("Failed to delete state ID", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadPersistentStateID retrieves the id of the persistent state from the database.
|
// ReadPersistentStateID retrieves the id of the persistent state from the database.
|
||||||
func ReadPersistentStateID(db ethdb.KeyValueReader) uint64 {
|
func ReadPersistentStateID(db ethdb.KeyValueReader) uint64 {
|
||||||
data, _ := db.Get(persistentStateIDKey)
|
data, _ := db.Get(persistentStateIDKey)
|
||||||
|
|
|
||||||
|
|
@ -334,7 +334,7 @@ func (db *Database) repairHistory() error {
|
||||||
}
|
}
|
||||||
// Truncate the extra state histories above in freezer in case it's not
|
// Truncate the extra state histories above in freezer in case it's not
|
||||||
// aligned with the disk layer. It might happen after a unclean shutdown.
|
// aligned with the disk layer. It might happen after a unclean shutdown.
|
||||||
pruned, err := truncateFromHead(db.diskdb, db.stateFreezer, id)
|
pruned, err := truncateFromHead(db.stateFreezer, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Crit("Failed to truncate extra state histories", "err", err)
|
log.Crit("Failed to truncate extra state histories", "err", err)
|
||||||
}
|
}
|
||||||
|
|
@ -590,7 +590,7 @@ func (db *Database) Recover(root common.Hash) error {
|
||||||
if err := db.diskdb.SyncKeyValue(); err != nil {
|
if err := db.diskdb.SyncKeyValue(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := truncateFromHead(db.diskdb, db.stateFreezer, dl.stateID())
|
_, err := truncateFromHead(db.stateFreezer, dl.stateID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -615,14 +615,14 @@ func (db *Database) Recoverable(root common.Hash) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// This is a temporary workaround for the unavailability of the freezer in
|
// This is a temporary workaround for the unavailability of the freezer in
|
||||||
// dev mode. As a consequence, the Pathdb loses the ability for deep reorg
|
// dev mode. As a consequence, the database loses the ability for deep reorg
|
||||||
// in certain cases.
|
// in certain cases.
|
||||||
// TODO(rjl493456442): Implement the in-memory ancient store.
|
// TODO(rjl493456442): Implement the in-memory ancient store.
|
||||||
if db.stateFreezer == nil {
|
if db.stateFreezer == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Ensure the requested state is a canonical state and all state
|
// Ensure the requested state is a canonical state and all state
|
||||||
// histories in range [id+1, disklayer.ID] are present and complete.
|
// histories in range [id+1, dl.ID] are present and complete.
|
||||||
return checkStateHistories(db.stateFreezer, *id+1, dl.stateID()-*id, func(m *meta) error {
|
return checkStateHistories(db.stateFreezer, *id+1, dl.stateID()-*id, func(m *meta) error {
|
||||||
if m.parent != root {
|
if m.parent != root {
|
||||||
return errors.New("unexpected state history")
|
return errors.New("unexpected state history")
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,7 @@ func (dl *diskLayer) writeStateHistory(diff *diffLayer) (bool, error) {
|
||||||
log.Debug("Skip tail truncation", "persistentID", persistentID, "tailID", tail+1, "headID", diff.stateID(), "limit", limit)
|
log.Debug("Skip tail truncation", "persistentID", persistentID, "tailID", tail+1, "headID", diff.stateID(), "limit", limit)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
pruned, err := truncateFromTail(dl.db.diskdb, dl.db.stateFreezer, newFirst-1)
|
pruned, err := truncateFromTail(dl.db.stateFreezer, newFirst-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
triedb/pathdb/history.go
Normal file
87
triedb/pathdb/history.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// 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 pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errHeadTruncationOutOfRange = errors.New("history head truncation out of range")
|
||||||
|
errTailTruncationOutOfRange = errors.New("history tail truncation out of range")
|
||||||
|
)
|
||||||
|
|
||||||
|
// truncateFromHead removes excess elements from the head of the freezer based
|
||||||
|
// on the given parameters. It returns the number of items that were removed.
|
||||||
|
func truncateFromHead(store ethdb.AncientStore, nhead uint64) (int, error) {
|
||||||
|
ohead, err := store.Ancients()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
otail, err := store.Tail()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
log.Info("Truncating from head", "ohead", ohead, "tail", otail, "nhead", nhead)
|
||||||
|
|
||||||
|
// Ensure that the truncation target falls within the valid range.
|
||||||
|
if ohead < nhead || nhead < otail {
|
||||||
|
return 0, fmt.Errorf("%w, tail: %d, head: %d, target: %d", errHeadTruncationOutOfRange, otail, ohead, nhead)
|
||||||
|
}
|
||||||
|
// Short circuit if nothing to truncate.
|
||||||
|
if ohead == nhead {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
ohead, err = store.TruncateHead(nhead)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// Associated root->id mappings are left in the database and wait
|
||||||
|
// for overwriting.
|
||||||
|
return int(ohead - nhead), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncateFromTail removes excess elements from the end of the freezer based
|
||||||
|
// on the given parameters. It returns the number of items that were removed.
|
||||||
|
func truncateFromTail(store ethdb.AncientStore, ntail uint64) (int, error) {
|
||||||
|
ohead, err := store.Ancients()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
otail, err := store.Tail()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// Ensure that the truncation target falls within the valid range.
|
||||||
|
if otail > ntail || ntail > ohead {
|
||||||
|
return 0, fmt.Errorf("%w, tail: %d, head: %d, target: %d", errTailTruncationOutOfRange, otail, ohead, ntail)
|
||||||
|
}
|
||||||
|
// Short circuit if nothing to truncate.
|
||||||
|
if otail == ntail {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
otail, err = store.TruncateTail(ntail)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// Associated root->id mappings are left in the database.
|
||||||
|
return int(ntail - otail), nil
|
||||||
|
}
|
||||||
|
|
@ -320,11 +320,12 @@ func (r *historyReader) read(state stateIdentQuery, stateID uint64, lastID uint6
|
||||||
tail, err := r.freezer.Tail()
|
tail, err := r.freezer.Tail()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
} // firstID = tail+1
|
||||||
// stateID == tail is allowed, as the first history object preserved
|
|
||||||
// is tail+1
|
// stateID+1 == firstID is allowed, as all the subsequent state histories
|
||||||
|
// are present with no gap inside.
|
||||||
if stateID < tail {
|
if stateID < tail {
|
||||||
return nil, errors.New("historical state has been pruned")
|
return nil, fmt.Errorf("historical state has been pruned, first: %d, state: %d", tail+1, stateID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// To serve the request, all state histories from stateID+1 to lastID
|
// To serve the request, all state histories from stateID+1 to lastID
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/testrand"
|
||||||
)
|
)
|
||||||
|
|
||||||
func waitIndexing(db *Database) {
|
func waitIndexing(db *Database) {
|
||||||
|
|
@ -36,11 +37,29 @@ func waitIndexing(db *Database) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkHistoricState(env *tester, root common.Hash, hr *historyReader) error {
|
func stateAvail(id uint64, env *tester) bool {
|
||||||
// Short circuit if the historical state is no longer available
|
if env.db.config.StateHistory == 0 {
|
||||||
if rawdb.ReadStateID(env.db.diskdb, root) == nil {
|
return true
|
||||||
|
}
|
||||||
|
dl := env.db.tree.bottom()
|
||||||
|
if dl.stateID() <= env.db.config.StateHistory {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
firstID := dl.stateID() - env.db.config.StateHistory + 1
|
||||||
|
|
||||||
|
return id+1 >= firstID
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHistoricalState(env *tester, root common.Hash, id uint64, hr *historyReader) error {
|
||||||
|
if !stateAvail(id, env) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Short circuit if the historical state is no longer available
|
||||||
|
if rawdb.ReadStateID(env.db.diskdb, root) == nil {
|
||||||
|
return fmt.Errorf("state not found %d %x", id, root)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dl = env.db.tree.bottom()
|
dl = env.db.tree.bottom()
|
||||||
stateID = rawdb.ReadStateID(env.db.diskdb, root)
|
stateID = rawdb.ReadStateID(env.db.diskdb, root)
|
||||||
|
|
@ -124,22 +143,22 @@ func testHistoryReader(t *testing.T, historyLimit uint64) {
|
||||||
defer func() {
|
defer func() {
|
||||||
maxDiffLayers = 128
|
maxDiffLayers = 128
|
||||||
}()
|
}()
|
||||||
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
|
|
||||||
|
|
||||||
|
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
|
||||||
env := newTester(t, historyLimit, false, 64, true, "")
|
env := newTester(t, historyLimit, false, 64, true, "")
|
||||||
defer env.release()
|
defer env.release()
|
||||||
waitIndexing(env.db)
|
waitIndexing(env.db)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
roots = env.roots
|
roots = env.roots
|
||||||
dRoot = env.db.tree.bottom().rootHash()
|
dl = env.db.tree.bottom()
|
||||||
hr = newHistoryReader(env.db.diskdb, env.db.stateFreezer)
|
hr = newHistoryReader(env.db.diskdb, env.db.stateFreezer)
|
||||||
)
|
)
|
||||||
for _, root := range roots {
|
for i, root := range roots {
|
||||||
if root == dRoot {
|
if root == dl.rootHash() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err := checkHistoricState(env, root, hr); err != nil {
|
if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -148,12 +167,41 @@ func testHistoryReader(t *testing.T, historyLimit uint64) {
|
||||||
env.extend(4)
|
env.extend(4)
|
||||||
waitIndexing(env.db)
|
waitIndexing(env.db)
|
||||||
|
|
||||||
for _, root := range roots {
|
for i, root := range roots {
|
||||||
if root == dRoot {
|
if root == dl.rootHash() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err := checkHistoricState(env, root, hr); err != nil {
|
if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHistoricalStateReader(t *testing.T) {
|
||||||
|
maxDiffLayers = 4
|
||||||
|
defer func() {
|
||||||
|
maxDiffLayers = 128
|
||||||
|
}()
|
||||||
|
|
||||||
|
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
|
||||||
|
env := newTester(t, 0, false, 64, true, "")
|
||||||
|
defer env.release()
|
||||||
|
waitIndexing(env.db)
|
||||||
|
|
||||||
|
// non-canonical state
|
||||||
|
fakeRoot := testrand.Hash()
|
||||||
|
rawdb.WriteStateID(env.db.diskdb, fakeRoot, 10)
|
||||||
|
|
||||||
|
_, err := env.db.HistoricReader(fakeRoot)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
t.Log(err)
|
||||||
|
|
||||||
|
// canonical state
|
||||||
|
realRoot := env.roots[9]
|
||||||
|
_, err = env.db.HistoricReader(realRoot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -504,6 +504,20 @@ func (h *stateHistory) decode(accountData, storageData, accountIndexes, storageI
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readStateHistoryMeta reads the metadata of state history with the specified id.
|
||||||
|
func readStateHistoryMeta(reader ethdb.AncientReader, id uint64) (*meta, error) {
|
||||||
|
data := rawdb.ReadStateHistoryMeta(reader, id)
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, fmt.Errorf("metadata is not found, %d", id)
|
||||||
|
}
|
||||||
|
var m meta
|
||||||
|
err := m.decode(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
|
||||||
// readStateHistory reads a single state history records with the specified id.
|
// readStateHistory reads a single state history records with the specified id.
|
||||||
func readStateHistory(reader ethdb.AncientReader, id uint64) (*stateHistory, error) {
|
func readStateHistory(reader ethdb.AncientReader, id uint64) (*stateHistory, error) {
|
||||||
mData, accountIndexes, storageIndexes, accountData, storageData, err := rawdb.ReadStateHistory(reader, id)
|
mData, accountIndexes, storageIndexes, accountData, storageData, err := rawdb.ReadStateHistory(reader, id)
|
||||||
|
|
@ -568,8 +582,8 @@ func writeStateHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkStateHistories retrieves a batch of meta objects with the specified range
|
// checkStateHistories retrieves a batch of metadata objects with the specified
|
||||||
// and performs the callback on each item.
|
// range and performs the callback on each item.
|
||||||
func checkStateHistories(reader ethdb.AncientReader, start, count uint64, check func(*meta) error) error {
|
func checkStateHistories(reader ethdb.AncientReader, start, count uint64, check func(*meta) error) error {
|
||||||
for count > 0 {
|
for count > 0 {
|
||||||
number := count
|
number := count
|
||||||
|
|
@ -594,87 +608,3 @@ func checkStateHistories(reader ethdb.AncientReader, start, count uint64, check
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// truncateFromHead removes the extra state histories from the head with the given
|
|
||||||
// parameters. It returns the number of items removed from the head.
|
|
||||||
func truncateFromHead(db ethdb.Batcher, store ethdb.AncientStore, nhead uint64) (int, error) {
|
|
||||||
ohead, err := store.Ancients()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
otail, err := store.Tail()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
// Ensure that the truncation target falls within the specified range.
|
|
||||||
if ohead < nhead || nhead < otail {
|
|
||||||
return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, nhead)
|
|
||||||
}
|
|
||||||
// Short circuit if nothing to truncate.
|
|
||||||
if ohead == nhead {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
// Load the meta objects in range [nhead+1, ohead]
|
|
||||||
blobs, err := rawdb.ReadStateHistoryMetaList(store, nhead+1, ohead-nhead)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
batch := db.NewBatch()
|
|
||||||
for _, blob := range blobs {
|
|
||||||
var m meta
|
|
||||||
if err := m.decode(blob); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
rawdb.DeleteStateID(batch, m.root)
|
|
||||||
}
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
ohead, err = store.TruncateHead(nhead)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return int(ohead - nhead), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// truncateFromTail removes the extra state histories from the tail with the given
|
|
||||||
// parameters. It returns the number of items removed from the tail.
|
|
||||||
func truncateFromTail(db ethdb.Batcher, store ethdb.AncientStore, ntail uint64) (int, error) {
|
|
||||||
ohead, err := store.Ancients()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
otail, err := store.Tail()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
// Ensure that the truncation target falls within the specified range.
|
|
||||||
if otail > ntail || ntail > ohead {
|
|
||||||
return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, ntail)
|
|
||||||
}
|
|
||||||
// Short circuit if nothing to truncate.
|
|
||||||
if otail == ntail {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
// Load the meta objects in range [otail+1, ntail]
|
|
||||||
blobs, err := rawdb.ReadStateHistoryMetaList(store, otail+1, ntail-otail)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
batch := db.NewBatch()
|
|
||||||
for _, blob := range blobs {
|
|
||||||
var m meta
|
|
||||||
if err := m.decode(blob); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
rawdb.DeleteStateID(batch, m.root)
|
|
||||||
}
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
otail, err = store.TruncateTail(ntail)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return int(ntail - otail), nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package pathdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -108,7 +109,7 @@ func testEncodeDecodeStateHistory(t *testing.T, rawStorageKey bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkStateHistory(t *testing.T, db ethdb.KeyValueReader, freezer ethdb.AncientReader, id uint64, root common.Hash, exist bool) {
|
func checkStateHistory(t *testing.T, freezer ethdb.AncientReader, id uint64, exist bool) {
|
||||||
blob := rawdb.ReadStateHistoryMeta(freezer, id)
|
blob := rawdb.ReadStateHistoryMeta(freezer, id)
|
||||||
if exist && len(blob) == 0 {
|
if exist && len(blob) == 0 {
|
||||||
t.Fatalf("Failed to load trie history, %d", id)
|
t.Fatalf("Failed to load trie history, %d", id)
|
||||||
|
|
@ -116,25 +117,17 @@ func checkStateHistory(t *testing.T, db ethdb.KeyValueReader, freezer ethdb.Anci
|
||||||
if !exist && len(blob) != 0 {
|
if !exist && len(blob) != 0 {
|
||||||
t.Fatalf("Unexpected trie history, %d", id)
|
t.Fatalf("Unexpected trie history, %d", id)
|
||||||
}
|
}
|
||||||
if exist && rawdb.ReadStateID(db, root) == nil {
|
|
||||||
t.Fatalf("Root->ID mapping is not found, %d", id)
|
|
||||||
}
|
|
||||||
if !exist && rawdb.ReadStateID(db, root) != nil {
|
|
||||||
t.Fatalf("Unexpected root->ID mapping, %d", id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkHistoriesInRange(t *testing.T, db ethdb.KeyValueReader, freezer ethdb.AncientReader, from, to uint64, roots []common.Hash, exist bool) {
|
func checkHistoriesInRange(t *testing.T, freezer ethdb.AncientReader, from, to uint64, exist bool) {
|
||||||
for i, j := from, 0; i <= to; i, j = i+1, j+1 {
|
for i := from; i <= to; i = i + 1 {
|
||||||
checkStateHistory(t, db, freezer, i, roots[j], exist)
|
checkStateHistory(t, freezer, i, exist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTruncateHeadStateHistory(t *testing.T) {
|
func TestTruncateHeadStateHistory(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
roots []common.Hash
|
|
||||||
hs = makeStateHistories(10)
|
hs = makeStateHistories(10)
|
||||||
db = rawdb.NewMemoryDatabase()
|
|
||||||
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
|
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
|
||||||
)
|
)
|
||||||
defer freezer.Close()
|
defer freezer.Close()
|
||||||
|
|
@ -142,27 +135,23 @@ func TestTruncateHeadStateHistory(t *testing.T) {
|
||||||
for i := 0; i < len(hs); i++ {
|
for i := 0; i < len(hs); i++ {
|
||||||
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
|
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
|
||||||
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
|
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
|
||||||
rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
|
|
||||||
roots = append(roots, hs[i].meta.root)
|
|
||||||
}
|
}
|
||||||
for size := len(hs); size > 0; size-- {
|
for size := len(hs); size > 0; size-- {
|
||||||
pruned, err := truncateFromHead(db, freezer, uint64(size-1))
|
pruned, err := truncateFromHead(freezer, uint64(size-1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to truncate from head %v", err)
|
t.Fatalf("Failed to truncate from head %v", err)
|
||||||
}
|
}
|
||||||
if pruned != 1 {
|
if pruned != 1 {
|
||||||
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
|
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
|
||||||
}
|
}
|
||||||
checkHistoriesInRange(t, db, freezer, uint64(size), uint64(10), roots[size-1:], false)
|
checkHistoriesInRange(t, freezer, uint64(size), uint64(10), false)
|
||||||
checkHistoriesInRange(t, db, freezer, uint64(1), uint64(size-1), roots[:size-1], true)
|
checkHistoriesInRange(t, freezer, uint64(1), uint64(size-1), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTruncateTailStateHistory(t *testing.T) {
|
func TestTruncateTailStateHistory(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
roots []common.Hash
|
|
||||||
hs = makeStateHistories(10)
|
hs = makeStateHistories(10)
|
||||||
db = rawdb.NewMemoryDatabase()
|
|
||||||
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
|
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
|
||||||
)
|
)
|
||||||
defer freezer.Close()
|
defer freezer.Close()
|
||||||
|
|
@ -170,16 +159,14 @@ func TestTruncateTailStateHistory(t *testing.T) {
|
||||||
for i := 0; i < len(hs); i++ {
|
for i := 0; i < len(hs); i++ {
|
||||||
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
|
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
|
||||||
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
|
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
|
||||||
rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
|
|
||||||
roots = append(roots, hs[i].meta.root)
|
|
||||||
}
|
}
|
||||||
for newTail := 1; newTail < len(hs); newTail++ {
|
for newTail := 1; newTail < len(hs); newTail++ {
|
||||||
pruned, _ := truncateFromTail(db, freezer, uint64(newTail))
|
pruned, _ := truncateFromTail(freezer, uint64(newTail))
|
||||||
if pruned != 1 {
|
if pruned != 1 {
|
||||||
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
|
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
|
||||||
}
|
}
|
||||||
checkHistoriesInRange(t, db, freezer, uint64(1), uint64(newTail), roots[:newTail], false)
|
checkHistoriesInRange(t, freezer, uint64(1), uint64(newTail), false)
|
||||||
checkHistoriesInRange(t, db, freezer, uint64(newTail+1), uint64(10), roots[newTail:], true)
|
checkHistoriesInRange(t, freezer, uint64(newTail+1), uint64(10), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,21 +178,29 @@ func TestTruncateTailStateHistories(t *testing.T) {
|
||||||
minUnpruned uint64
|
minUnpruned uint64
|
||||||
empty bool
|
empty bool
|
||||||
}{
|
}{
|
||||||
|
// history: id [10]
|
||||||
{
|
{
|
||||||
1, 9, 9, 10, false,
|
limit: 1,
|
||||||
|
expPruned: 9,
|
||||||
|
maxPruned: 9, minUnpruned: 10, empty: false,
|
||||||
},
|
},
|
||||||
|
// history: none
|
||||||
{
|
{
|
||||||
0, 10, 10, 0 /* no meaning */, true,
|
limit: 0,
|
||||||
|
expPruned: 10,
|
||||||
|
empty: true,
|
||||||
},
|
},
|
||||||
|
// history: id [1:10]
|
||||||
{
|
{
|
||||||
10, 0, 0, 1, false,
|
limit: 10,
|
||||||
|
expPruned: 0,
|
||||||
|
maxPruned: 0,
|
||||||
|
minUnpruned: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, c := range cases {
|
for i, c := range cases {
|
||||||
var (
|
var (
|
||||||
roots []common.Hash
|
|
||||||
hs = makeStateHistories(10)
|
hs = makeStateHistories(10)
|
||||||
db = rawdb.NewMemoryDatabase()
|
|
||||||
freezer, _ = rawdb.NewStateFreezer(t.TempDir()+fmt.Sprintf("%d", i), false, false)
|
freezer, _ = rawdb.NewStateFreezer(t.TempDir()+fmt.Sprintf("%d", i), false, false)
|
||||||
)
|
)
|
||||||
defer freezer.Close()
|
defer freezer.Close()
|
||||||
|
|
@ -213,19 +208,16 @@ func TestTruncateTailStateHistories(t *testing.T) {
|
||||||
for i := 0; i < len(hs); i++ {
|
for i := 0; i < len(hs); i++ {
|
||||||
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
|
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
|
||||||
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
|
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
|
||||||
rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
|
|
||||||
roots = append(roots, hs[i].meta.root)
|
|
||||||
}
|
}
|
||||||
pruned, _ := truncateFromTail(db, freezer, uint64(10)-c.limit)
|
pruned, _ := truncateFromTail(freezer, uint64(10)-c.limit)
|
||||||
if pruned != c.expPruned {
|
if pruned != c.expPruned {
|
||||||
t.Error("Unexpected pruned items", "want", c.expPruned, "got", pruned)
|
t.Error("Unexpected pruned items", "want", c.expPruned, "got", pruned)
|
||||||
}
|
}
|
||||||
if c.empty {
|
if c.empty {
|
||||||
checkHistoriesInRange(t, db, freezer, uint64(1), uint64(10), roots, false)
|
checkHistoriesInRange(t, freezer, uint64(1), uint64(10), false)
|
||||||
} else {
|
} else {
|
||||||
tail := 10 - int(c.limit)
|
checkHistoriesInRange(t, freezer, uint64(1), c.maxPruned, false)
|
||||||
checkHistoriesInRange(t, db, freezer, uint64(1), c.maxPruned, roots[:tail], false)
|
checkHistoriesInRange(t, freezer, c.minUnpruned, uint64(10), true)
|
||||||
checkHistoriesInRange(t, db, freezer, c.minUnpruned, uint64(10), roots[tail:], true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +225,6 @@ func TestTruncateTailStateHistories(t *testing.T) {
|
||||||
func TestTruncateOutOfRange(t *testing.T) {
|
func TestTruncateOutOfRange(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
hs = makeStateHistories(10)
|
hs = makeStateHistories(10)
|
||||||
db = rawdb.NewMemoryDatabase()
|
|
||||||
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
|
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
|
||||||
)
|
)
|
||||||
defer freezer.Close()
|
defer freezer.Close()
|
||||||
|
|
@ -241,9 +232,8 @@ func TestTruncateOutOfRange(t *testing.T) {
|
||||||
for i := 0; i < len(hs); i++ {
|
for i := 0; i < len(hs); i++ {
|
||||||
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
|
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
|
||||||
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
|
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
|
||||||
rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
|
|
||||||
}
|
}
|
||||||
truncateFromTail(db, freezer, uint64(len(hs)/2))
|
truncateFromTail(freezer, uint64(len(hs)/2))
|
||||||
|
|
||||||
// Ensure of-out-range truncations are rejected correctly.
|
// Ensure of-out-range truncations are rejected correctly.
|
||||||
head, _ := freezer.Ancients()
|
head, _ := freezer.Ancients()
|
||||||
|
|
@ -255,20 +245,20 @@ func TestTruncateOutOfRange(t *testing.T) {
|
||||||
expErr error
|
expErr error
|
||||||
}{
|
}{
|
||||||
{0, head, nil}, // nothing to delete
|
{0, head, nil}, // nothing to delete
|
||||||
{0, head + 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, head+1)},
|
{0, head + 1, errHeadTruncationOutOfRange},
|
||||||
{0, tail - 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, tail-1)},
|
{0, tail - 1, errHeadTruncationOutOfRange},
|
||||||
{1, tail, nil}, // nothing to delete
|
{1, tail, nil}, // nothing to delete
|
||||||
{1, head + 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, head+1)},
|
{1, head + 1, errTailTruncationOutOfRange},
|
||||||
{1, tail - 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, tail-1)},
|
{1, tail - 1, errTailTruncationOutOfRange},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
var gotErr error
|
var gotErr error
|
||||||
if c.mode == 0 {
|
if c.mode == 0 {
|
||||||
_, gotErr = truncateFromHead(db, freezer, c.target)
|
_, gotErr = truncateFromHead(freezer, c.target)
|
||||||
} else {
|
} else {
|
||||||
_, gotErr = truncateFromTail(db, freezer, c.target)
|
_, gotErr = truncateFromTail(freezer, c.target)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(gotErr, c.expErr) {
|
if !errors.Is(gotErr, c.expErr) {
|
||||||
t.Errorf("Unexpected error, want: %v, got: %v", c.expErr, gotErr)
|
t.Errorf("Unexpected error, want: %v, got: %v", c.expErr, gotErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,20 +213,24 @@ func (db *Database) HistoricReader(root common.Hash) (*HistoricalStateReader, er
|
||||||
if !db.stateIndexer.inited() {
|
if !db.stateIndexer.inited() {
|
||||||
return nil, errors.New("state histories haven't been fully indexed yet")
|
return nil, errors.New("state histories haven't been fully indexed yet")
|
||||||
}
|
}
|
||||||
// States at the current disk layer or above are directly accessible via
|
// - States at the current disk layer or above are directly accessible
|
||||||
// db.StateReader.
|
// via `db.StateReader`.
|
||||||
//
|
//
|
||||||
// States older than the current disk layer (including the disk layer
|
// - States older than the current disk layer (including the disk layer
|
||||||
// itself) are available through historic state access.
|
// itself) are available via `db.HistoricReader`.
|
||||||
//
|
|
||||||
// Note: the requested state may refer to a stale historic state that has
|
|
||||||
// already been pruned. This function does not validate availability, as
|
|
||||||
// underlying states may be pruned dynamically. Validity is checked during
|
|
||||||
// each actual state retrieval.
|
|
||||||
id := rawdb.ReadStateID(db.diskdb, root)
|
id := rawdb.ReadStateID(db.diskdb, root)
|
||||||
if id == nil {
|
if id == nil {
|
||||||
return nil, fmt.Errorf("state %#x is not available", root)
|
return nil, fmt.Errorf("state %#x is not available", root)
|
||||||
}
|
}
|
||||||
|
// Ensure the requested state is canonical, historical states on side chain
|
||||||
|
// are not accessible.
|
||||||
|
meta, err := readStateHistoryMeta(db.stateFreezer, *id+1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err // e.g., the referred state history has been pruned
|
||||||
|
}
|
||||||
|
if meta.parent != root {
|
||||||
|
return nil, fmt.Errorf("state %#x is not canonincal", root)
|
||||||
|
}
|
||||||
return &HistoricalStateReader{
|
return &HistoricalStateReader{
|
||||||
id: *id,
|
id: *id,
|
||||||
db: db,
|
db: db,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue