go-ethereum/triedb/pathdb/history_state_test.go
rjl493456442 7f78fa6912
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.
2025-08-29 15:43:58 +08:00

325 lines
8.9 KiB
Go

// Copyright 2023 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 (
"bytes"
"errors"
"fmt"
"reflect"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/internal/testrand"
"github.com/ethereum/go-ethereum/rlp"
)
// randomStateSet generates a random state change set.
func randomStateSet(n int) (map[common.Address][]byte, map[common.Address]map[common.Hash][]byte) {
var (
accounts = make(map[common.Address][]byte)
storages = make(map[common.Address]map[common.Hash][]byte)
)
for i := 0; i < n; i++ {
addr := testrand.Address()
storages[addr] = make(map[common.Hash][]byte)
for j := 0; j < 3; j++ {
v, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(testrand.Bytes(32)))
storages[addr][testrand.Hash()] = v
}
account := generateAccount(types.EmptyRootHash)
accounts[addr] = types.SlimAccountRLP(account)
}
return accounts, storages
}
func makeStateHistory(rawStorageKey bool) *stateHistory {
accounts, storages := randomStateSet(3)
return newStateHistory(testrand.Hash(), types.EmptyRootHash, 0, accounts, storages, rawStorageKey)
}
func makeStateHistories(n int) []*stateHistory {
var (
parent = types.EmptyRootHash
result []*stateHistory
)
for i := 0; i < n; i++ {
root := testrand.Hash()
accounts, storages := randomStateSet(3)
h := newStateHistory(root, parent, uint64(i), accounts, storages, false)
parent = root
result = append(result, h)
}
return result
}
func TestEncodeDecodeStateHistory(t *testing.T) {
testEncodeDecodeStateHistory(t, false)
testEncodeDecodeStateHistory(t, true)
}
func testEncodeDecodeStateHistory(t *testing.T, rawStorageKey bool) {
var (
m meta
dec stateHistory
obj = makeStateHistory(rawStorageKey)
)
// check if meta data can be correctly encode/decode
blob := obj.meta.encode()
if err := m.decode(blob); err != nil {
t.Fatalf("Failed to decode %v", err)
}
if !reflect.DeepEqual(&m, obj.meta) {
t.Fatal("meta is mismatched")
}
// check if account/storage data can be correctly encode/decode
accountData, storageData, accountIndexes, storageIndexes := obj.encode()
if err := dec.decode(accountData, storageData, accountIndexes, storageIndexes); err != nil {
t.Fatalf("Failed to decode, err: %v", err)
}
if !compareSet(dec.accounts, obj.accounts) {
t.Fatal("account data is mismatched")
}
if !compareStorages(dec.storages, obj.storages) {
t.Fatal("storage data is mismatched")
}
if !compareList(dec.accountList, obj.accountList) {
t.Fatal("account list is mismatched")
}
if !compareStorageList(dec.storageList, obj.storageList) {
t.Fatal("storage list is mismatched")
}
}
func checkStateHistory(t *testing.T, freezer ethdb.AncientReader, id uint64, exist bool) {
blob := rawdb.ReadStateHistoryMeta(freezer, id)
if exist && len(blob) == 0 {
t.Fatalf("Failed to load trie history, %d", id)
}
if !exist && len(blob) != 0 {
t.Fatalf("Unexpected trie history, %d", id)
}
}
func checkHistoriesInRange(t *testing.T, freezer ethdb.AncientReader, from, to uint64, exist bool) {
for i := from; i <= to; i = i + 1 {
checkStateHistory(t, freezer, i, exist)
}
}
func TestTruncateHeadStateHistory(t *testing.T) {
var (
hs = makeStateHistories(10)
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
}
for size := len(hs); size > 0; size-- {
pruned, err := truncateFromHead(freezer, uint64(size-1))
if err != nil {
t.Fatalf("Failed to truncate from head %v", err)
}
if pruned != 1 {
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
}
checkHistoriesInRange(t, freezer, uint64(size), uint64(10), false)
checkHistoriesInRange(t, freezer, uint64(1), uint64(size-1), true)
}
}
func TestTruncateTailStateHistory(t *testing.T) {
var (
hs = makeStateHistories(10)
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
}
for newTail := 1; newTail < len(hs); newTail++ {
pruned, _ := truncateFromTail(freezer, uint64(newTail))
if pruned != 1 {
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
}
checkHistoriesInRange(t, freezer, uint64(1), uint64(newTail), false)
checkHistoriesInRange(t, freezer, uint64(newTail+1), uint64(10), true)
}
}
func TestTruncateTailStateHistories(t *testing.T) {
var cases = []struct {
limit uint64
expPruned int
maxPruned uint64
minUnpruned uint64
empty bool
}{
// history: id [10]
{
limit: 1,
expPruned: 9,
maxPruned: 9, minUnpruned: 10, empty: false,
},
// history: none
{
limit: 0,
expPruned: 10,
empty: true,
},
// history: id [1:10]
{
limit: 10,
expPruned: 0,
maxPruned: 0,
minUnpruned: 1,
},
}
for i, c := range cases {
var (
hs = makeStateHistories(10)
freezer, _ = rawdb.NewStateFreezer(t.TempDir()+fmt.Sprintf("%d", i), false, false)
)
defer freezer.Close()
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
}
pruned, _ := truncateFromTail(freezer, uint64(10)-c.limit)
if pruned != c.expPruned {
t.Error("Unexpected pruned items", "want", c.expPruned, "got", pruned)
}
if c.empty {
checkHistoriesInRange(t, freezer, uint64(1), uint64(10), false)
} else {
checkHistoriesInRange(t, freezer, uint64(1), c.maxPruned, false)
checkHistoriesInRange(t, freezer, c.minUnpruned, uint64(10), true)
}
}
}
func TestTruncateOutOfRange(t *testing.T) {
var (
hs = makeStateHistories(10)
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
}
truncateFromTail(freezer, uint64(len(hs)/2))
// Ensure of-out-range truncations are rejected correctly.
head, _ := freezer.Ancients()
tail, _ := freezer.Tail()
cases := []struct {
mode int
target uint64
expErr error
}{
{0, head, nil}, // nothing to delete
{0, head + 1, errHeadTruncationOutOfRange},
{0, tail - 1, errHeadTruncationOutOfRange},
{1, tail, nil}, // nothing to delete
{1, head + 1, errTailTruncationOutOfRange},
{1, tail - 1, errTailTruncationOutOfRange},
}
for _, c := range cases {
var gotErr error
if c.mode == 0 {
_, gotErr = truncateFromHead(freezer, c.target)
} else {
_, gotErr = truncateFromTail(freezer, c.target)
}
if !errors.Is(gotErr, c.expErr) {
t.Errorf("Unexpected error, want: %v, got: %v", c.expErr, gotErr)
}
}
}
func compareSet[k comparable](a, b map[k][]byte) bool {
if len(a) != len(b) {
return false
}
for key, valA := range a {
valB, ok := b[key]
if !ok {
return false
}
if !bytes.Equal(valA, valB) {
return false
}
}
return true
}
func compareList[k comparable](a, b []k) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
func compareStorages(a, b map[common.Address]map[common.Hash][]byte) bool {
if len(a) != len(b) {
return false
}
for h, subA := range a {
subB, ok := b[h]
if !ok {
return false
}
if !compareSet(subA, subB) {
return false
}
}
return true
}
func compareStorageList(a, b map[common.Address][]common.Hash) bool {
if len(a) != len(b) {
return false
}
for h, la := range a {
lb, ok := b[h]
if !ok {
return false
}
if !compareList(la, lb) {
return false
}
}
return true
}