triedb/pathdb: fix state revert on v2 history (#31060)

State history v2 has been shipped and will take effect after the Cancun fork.
However, the state revert function does not differentiate between v1 and v2,
instead blindly using the storage map key for state reversion. 

This mismatch between the keys of the live state set and the state history
can trigger a panic: `non-existent storage slot for reverting`.

This flaw has been fixed in this PR.
pull/31063/head
rjl493456442 1 week ago committed by GitHub
parent d10c61ca44
commit a840e9b59f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 43
      triedb/pathdb/database_test.go
  2. 22
      triedb/pathdb/disklayer.go
  3. 41
      triedb/pathdb/history.go

@ -73,10 +73,10 @@ const (
type genctx struct { type genctx struct {
stateRoot common.Hash stateRoot common.Hash
accounts map[common.Hash][]byte accounts map[common.Hash][]byte // Keyed by the hash of account address
storages map[common.Hash]map[common.Hash][]byte storages map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key
accountOrigin map[common.Address][]byte accountOrigin map[common.Address][]byte // Keyed by the account address
storageOrigin map[common.Address]map[common.Hash][]byte storageOrigin map[common.Address]map[common.Hash][]byte // Keyed by the account address and the hash of storage key
nodes *trienode.MergedNodeSet nodes *trienode.MergedNodeSet
} }
@ -113,22 +113,23 @@ type tester struct {
preimages map[common.Hash][]byte preimages map[common.Hash][]byte
// current state set // current state set
accounts map[common.Hash][]byte accounts map[common.Hash][]byte // Keyed by the hash of account address
storages map[common.Hash]map[common.Hash][]byte storages map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key
// state snapshots // state snapshots
snapAccounts map[common.Hash]map[common.Hash][]byte snapAccounts map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address
snapStorages map[common.Hash]map[common.Hash]map[common.Hash][]byte snapStorages map[common.Hash]map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key
} }
func newTester(t *testing.T, historyLimit uint64, isVerkle bool) *tester { func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int) *tester {
var ( var (
disk, _ = rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), t.TempDir(), "", false) disk, _ = rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), t.TempDir(), "", false)
db = New(disk, &Config{ db = New(disk, &Config{
StateHistory: historyLimit, StateHistory: historyLimit,
CleanCacheSize: 16 * 1024, CleanCacheSize: 256 * 1024,
WriteBufferSize: 16 * 1024, WriteBufferSize: 256 * 1024,
}, isVerkle) }, isVerkle)
obj = &tester{ obj = &tester{
db: db, db: db,
preimages: make(map[common.Hash][]byte), preimages: make(map[common.Hash][]byte),
@ -138,7 +139,7 @@ func newTester(t *testing.T, historyLimit uint64, isVerkle bool) *tester {
snapStorages: make(map[common.Hash]map[common.Hash]map[common.Hash][]byte), snapStorages: make(map[common.Hash]map[common.Hash]map[common.Hash][]byte),
} }
) )
for i := 0; i < 12; i++ { for i := 0; i < layers; i++ {
var parent = types.EmptyRootHash var parent = types.EmptyRootHash
if len(obj.roots) != 0 { if len(obj.roots) != 0 {
parent = obj.roots[len(obj.roots)-1] parent = obj.roots[len(obj.roots)-1]
@ -264,11 +265,11 @@ func (t *tester) generate(parent common.Hash, rawStorageKey bool) (common.Hash,
addr := testrand.Address() addr := testrand.Address()
addrHash := crypto.Keccak256Hash(addr.Bytes()) addrHash := crypto.Keccak256Hash(addr.Bytes())
// short circuit if the account was already existent // Short circuit if the account was already existent
if _, ok := t.accounts[addrHash]; ok { if _, ok := t.accounts[addrHash]; ok {
continue continue
} }
// short circuit if the account has been modified within the same transition // Short circuit if the account has been modified within the same transition
if _, ok := dirties[addrHash]; ok { if _, ok := dirties[addrHash]; ok {
continue continue
} }
@ -448,7 +449,7 @@ func TestDatabaseRollback(t *testing.T) {
}() }()
// Verify state histories // Verify state histories
tester := newTester(t, 0, false) tester := newTester(t, 0, false, 32)
defer tester.release() defer tester.release()
if err := tester.verifyHistory(); err != nil { if err := tester.verifyHistory(); err != nil {
@ -482,7 +483,7 @@ func TestDatabaseRecoverable(t *testing.T) {
}() }()
var ( var (
tester = newTester(t, 0, false) tester = newTester(t, 0, false, 12)
index = tester.bottomIndex() index = tester.bottomIndex()
) )
defer tester.release() defer tester.release()
@ -526,7 +527,7 @@ func TestDisable(t *testing.T) {
maxDiffLayers = 128 maxDiffLayers = 128
}() }()
tester := newTester(t, 0, false) tester := newTester(t, 0, false, 32)
defer tester.release() defer tester.release()
stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil)) stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil))
@ -568,7 +569,7 @@ func TestCommit(t *testing.T) {
maxDiffLayers = 128 maxDiffLayers = 128
}() }()
tester := newTester(t, 0, false) tester := newTester(t, 0, false, 12)
defer tester.release() defer tester.release()
if err := tester.db.Commit(tester.lastHash(), false); err != nil { if err := tester.db.Commit(tester.lastHash(), false); err != nil {
@ -598,7 +599,7 @@ func TestJournal(t *testing.T) {
maxDiffLayers = 128 maxDiffLayers = 128
}() }()
tester := newTester(t, 0, false) tester := newTester(t, 0, false, 12)
defer tester.release() defer tester.release()
if err := tester.db.Journal(tester.lastHash()); err != nil { if err := tester.db.Journal(tester.lastHash()); err != nil {
@ -628,7 +629,7 @@ func TestCorruptedJournal(t *testing.T) {
maxDiffLayers = 128 maxDiffLayers = 128
}() }()
tester := newTester(t, 0, false) tester := newTester(t, 0, false, 12)
defer tester.release() defer tester.release()
if err := tester.db.Journal(tester.lastHash()); err != nil { if err := tester.db.Journal(tester.lastHash()); err != nil {
@ -676,7 +677,7 @@ func TestTailTruncateHistory(t *testing.T) {
maxDiffLayers = 128 maxDiffLayers = 128
}() }()
tester := newTester(t, 10, false) tester := newTester(t, 10, false, 12)
defer tester.release() defer tester.release()
tester.db.Close() tester.db.Close()

@ -295,24 +295,6 @@ func (dl *diskLayer) revert(h *history) (*diskLayer, error) {
if dl.id == 0 { if dl.id == 0 {
return nil, fmt.Errorf("%w: zero state id", errStateUnrecoverable) return nil, fmt.Errorf("%w: zero state id", errStateUnrecoverable)
} }
var (
buff = crypto.NewKeccakState()
hashes = make(map[common.Address]common.Hash)
accounts = make(map[common.Hash][]byte)
storages = make(map[common.Hash]map[common.Hash][]byte)
)
for addr, blob := range h.accounts {
hash := crypto.HashData(buff, addr.Bytes())
hashes[addr] = hash
accounts[hash] = blob
}
for addr, storage := range h.storages {
hash, ok := hashes[addr]
if !ok {
panic(fmt.Errorf("storage history with no account %x", addr))
}
storages[hash] = storage
}
// Apply the reverse state changes upon the current state. This must // Apply the reverse state changes upon the current state. This must
// be done before holding the lock in order to access state in "this" // be done before holding the lock in order to access state in "this"
// layer. // layer.
@ -320,6 +302,10 @@ func (dl *diskLayer) revert(h *history) (*diskLayer, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Derive the state modification set from the history, keyed by the hash
// of the account address and the storage key.
accounts, storages := h.stateSet()
// Mark the diskLayer as stale before applying any mutations on top. // Mark the diskLayer as stale before applying any mutations on top.
dl.lock.Lock() dl.lock.Lock()
defer dl.lock.Unlock() defer dl.lock.Unlock()

@ -26,6 +26,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/crypto"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
@ -68,8 +69,9 @@ const (
slotIndexSize = common.HashLength + 5 // The length of encoded slot index slotIndexSize = common.HashLength + 5 // The length of encoded slot index
historyMetaSize = 9 + 2*common.HashLength // The length of encoded history meta historyMetaSize = 9 + 2*common.HashLength // The length of encoded history meta
stateHistoryV0 = uint8(0) // initial version of state history structure stateHistoryV0 = uint8(0) // initial version of state history structure
stateHistoryV1 = uint8(1) // use the storage slot raw key as the identifier instead of the key hash stateHistoryV1 = uint8(1) // use the storage slot raw key as the identifier instead of the key hash
historyVersion = stateHistoryV1 // the default state history version
) )
// Each state history entry is consisted of five elements: // Each state history entry is consisted of five elements:
@ -258,9 +260,9 @@ func newHistory(root common.Hash, parent common.Hash, block uint64, accounts map
slices.SortFunc(slist, common.Hash.Cmp) slices.SortFunc(slist, common.Hash.Cmp)
storageList[addr] = slist storageList[addr] = slist
} }
version := stateHistoryV0 version := historyVersion
if rawStorageKey { if !rawStorageKey {
version = stateHistoryV1 version = stateHistoryV0
} }
return &history{ return &history{
meta: &meta{ meta: &meta{
@ -276,6 +278,35 @@ func newHistory(root common.Hash, parent common.Hash, block uint64, accounts map
} }
} }
// stateSet returns the state set, keyed by the hash of the account address
// and the hash of the storage slot key.
func (h *history) stateSet() (map[common.Hash][]byte, map[common.Hash]map[common.Hash][]byte) {
var (
buff = crypto.NewKeccakState()
accounts = make(map[common.Hash][]byte)
storages = make(map[common.Hash]map[common.Hash][]byte)
)
for addr, blob := range h.accounts {
addrHash := crypto.HashData(buff, addr.Bytes())
accounts[addrHash] = blob
storage, exist := h.storages[addr]
if !exist {
continue
}
if h.meta.version == stateHistoryV0 {
storages[addrHash] = storage
} else {
subset := make(map[common.Hash][]byte)
for key, slot := range storage {
subset[crypto.HashData(buff, key.Bytes())] = slot
}
storages[addrHash] = subset
}
}
return accounts, storages
}
// encode serializes the state history and returns four byte streams represent // encode serializes the state history and returns four byte streams represent
// concatenated account/storage data, account/storage indexes respectively. // concatenated account/storage data, account/storage indexes respectively.
func (h *history) encode() ([]byte, []byte, []byte, []byte) { func (h *history) encode() ([]byte, []byte, []byte, []byte) {

Loading…
Cancel
Save