cmd, core/state, eth, tests, trie: improve state reader (#27428)

The state availability is checked during the creation of a state reader.

-    In hash-based database, if the specified root node does not exist on disk disk, then
    the state reader won't be created and an error will be returned.

-    In path-based database, if the specified state layer is not available, then the
    state reader won't be created and an error will be returned.

This change also contains a stricter semantics regarding the `Commit` operation: once it has been performed, the trie is no longer usable, and certain operations will return an error.
This commit is contained in:
rjl493456442
2023-06-20 15:31:45 -04:00
committed by GitHub
parent 8c288b528d
commit 6d2aeb43d5
32 changed files with 384 additions and 151 deletions
+3 -2
View File
@@ -113,8 +113,9 @@ type Trie interface {
Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet)
// NodeIterator returns an iterator that returns nodes of the trie. Iteration
// starts at the key after the given start key.
NodeIterator(startKey []byte) trie.NodeIterator
// starts at the key after the given start key. And error will be returned
// if fails to create node iterator.
NodeIterator(startKey []byte) (trie.NodeIterator, error)
// Prove constructs a Merkle proof for key. The result contains all encoded nodes
// on the path to the value at key. The value itself is also included in the last
+11 -2
View File
@@ -140,7 +140,11 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
log.Info("Trie dumping started", "root", s.trie.Hash())
c.OnRoot(s.trie.Hash())
it := trie.NewIterator(s.trie.NodeIterator(conf.Start))
trieIt, err := s.trie.NodeIterator(conf.Start)
if err != nil {
return nil
}
it := trie.NewIterator(trieIt)
for it.Next() {
var data types.StateAccount
if err := rlp.DecodeBytes(it.Value, &data); err != nil {
@@ -178,7 +182,12 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
log.Error("Failed to load storage trie", "err", err)
continue
}
storageIt := trie.NewIterator(tr.NodeIterator(nil))
trieIt, err := tr.NodeIterator(nil)
if err != nil {
log.Error("Failed to create trie iterator", "err", err)
continue
}
storageIt := trie.NewIterator(trieIt)
for storageIt.Next() {
_, content, _, err := rlp.Split(storageIt.Value)
if err != nil {
+9 -2
View File
@@ -74,8 +74,12 @@ func (it *nodeIterator) step() error {
return nil
}
// Initialize the iterator if we've just started
var err error
if it.stateIt == nil {
it.stateIt = it.state.trie.NodeIterator(nil)
it.stateIt, err = it.state.trie.NodeIterator(nil)
if err != nil {
return err
}
}
// If we had data nodes previously, we surely have at least state nodes
if it.dataIt != nil {
@@ -113,7 +117,10 @@ func (it *nodeIterator) step() error {
if err != nil {
return err
}
it.dataIt = dataTrie.NodeIterator(nil)
it.dataIt, err = dataTrie.NodeIterator(nil)
if err != nil {
return err
}
if !it.dataIt.Next(true) {
it.dataIt = nil
}
+8 -2
View File
@@ -420,7 +420,10 @@ func extractGenesis(db ethdb.Database, stateBloom *stateBloom) error {
if err != nil {
return err
}
accIter := t.NodeIterator(nil)
accIter, err := t.NodeIterator(nil)
if err != nil {
return err
}
for accIter.Next(true) {
hash := accIter.Hash()
@@ -441,7 +444,10 @@ func extractGenesis(db ethdb.Database, stateBloom *stateBloom) error {
if err != nil {
return err
}
storageIter := storageTrie.NodeIterator(nil)
storageIter, err := storageTrie.NodeIterator(nil)
if err != nil {
return err
}
for storageIter.Next(true) {
hash := storageIter.Hash()
if hash != (common.Hash{}) {
+5 -2
View File
@@ -382,8 +382,6 @@ func (dl *diskLayer) generateRange(ctx *generatorContext, trieId *trie.ID, prefi
}
var (
trieMore bool
nodeIt = tr.NodeIterator(origin)
iter = trie.NewIterator(nodeIt)
kvkeys, kvvals = result.keys, result.vals
// counters
@@ -397,7 +395,12 @@ func (dl *diskLayer) generateRange(ctx *generatorContext, trieId *trie.ID, prefi
start = time.Now()
internal time.Duration
)
nodeIt, err := tr.NodeIterator(origin)
if err != nil {
return false, nil, err
}
nodeIt.AddResolver(resolver)
iter := trie.NewIterator(nodeIt)
for iter.Next() {
if last != nil && bytes.Compare(iter.Key, last) > 0 {
+8 -4
View File
@@ -43,7 +43,8 @@ func newStateTest() *stateTest {
func TestDump(t *testing.T) {
db := rawdb.NewMemoryDatabase()
sdb, _ := New(types.EmptyRootHash, NewDatabaseWithConfig(db, &trie.Config{Preimages: true}), nil)
tdb := NewDatabaseWithConfig(db, &trie.Config{Preimages: true})
sdb, _ := New(types.EmptyRootHash, tdb, nil)
s := &stateTest{db: db, state: sdb}
// generate a few entries
@@ -57,9 +58,10 @@ func TestDump(t *testing.T) {
// write some of them to the trie
s.state.updateStateObject(obj1)
s.state.updateStateObject(obj2)
s.state.Commit(false)
root, _ := s.state.Commit(false)
// check that DumpToCollector contains the state objects that are in trie
s.state, _ = New(root, tdb, nil)
got := string(s.state.Dump(nil))
want := `{
"root": "71edff0130dd2385947095001c73d9e28d862fc286fca2b922ca6f6f3cddfdd2",
@@ -95,7 +97,8 @@ func TestDump(t *testing.T) {
func TestIterativeDump(t *testing.T) {
db := rawdb.NewMemoryDatabase()
sdb, _ := New(types.EmptyRootHash, NewDatabaseWithConfig(db, &trie.Config{Preimages: true}), nil)
tdb := NewDatabaseWithConfig(db, &trie.Config{Preimages: true})
sdb, _ := New(types.EmptyRootHash, tdb, nil)
s := &stateTest{db: db, state: sdb}
// generate a few entries
@@ -111,7 +114,8 @@ func TestIterativeDump(t *testing.T) {
// write some of them to the trie
s.state.updateStateObject(obj1)
s.state.updateStateObject(obj2)
s.state.Commit(false)
root, _ := s.state.Commit(false)
s.state, _ = New(root, tdb, nil)
b := &bytes.Buffer{}
s.state.IterativeDump(nil, json.NewEncoder(b))
+19 -5
View File
@@ -56,8 +56,14 @@ func (n *proofList) Delete(key []byte) error {
// StateDB structs within the ethereum protocol are used to store anything
// within the merkle trie. StateDBs take care of caching and storing
// nested states. It's the general query interface to retrieve:
//
// * Contracts
// * Accounts
//
// Once the state is committed, tries cached in stateDB (including account
// trie, storage tries) will no longer be functional. A new state instance
// must be created with new root and updated database for accessing post-
// commit states.
type StateDB struct {
db Database
prefetcher *triePrefetcher
@@ -680,19 +686,23 @@ func (s *StateDB) CreateAccount(addr common.Address) {
}
}
func (db *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error {
so := db.getStateObject(addr)
func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error {
so := s.getStateObject(addr)
if so == nil {
return nil
}
tr, err := so.getTrie(db.db)
tr, err := so.getTrie(s.db)
if err != nil {
return err
}
it := trie.NewIterator(tr.NodeIterator(nil))
trieIt, err := tr.NodeIterator(nil)
if err != nil {
return err
}
it := trie.NewIterator(trieIt)
for it.Next() {
key := common.BytesToHash(db.trie.GetKey(it.Key))
key := common.BytesToHash(s.trie.GetKey(it.Key))
if value, dirty := so.dirtyStorage[key]; dirty {
if !cb(key, value) {
return nil
@@ -977,6 +987,10 @@ func (s *StateDB) clearJournalAndRefund() {
}
// Commit writes the state to the underlying in-memory trie database.
// Once the state is committed, tries cached in stateDB (including account
// trie, storage tries) will no longer be functional. A new state instance
// must be created with new root and updated database for accessing post-
// commit states.
func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
// Short circuit in case any database failure occurred earlier.
if s.dbErr != nil {
+66 -31
View File
@@ -19,6 +19,7 @@ package state
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math"
"math/big"
@@ -521,7 +522,8 @@ func TestCopyOfCopy(t *testing.T) {
//
// See https://github.com/ethereum/go-ethereum/issues/20106.
func TestCopyCommitCopy(t *testing.T) {
state, _ := New(types.EmptyRootHash, NewDatabase(rawdb.NewMemoryDatabase()), nil)
tdb := NewDatabase(rawdb.NewMemoryDatabase())
state, _ := New(types.EmptyRootHash, tdb, nil)
// Create an account and check if the retrieved balance is correct
addr := common.HexToAddress("0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe")
@@ -558,20 +560,6 @@ func TestCopyCommitCopy(t *testing.T) {
if val := copyOne.GetCommittedState(addr, skey); val != (common.Hash{}) {
t.Fatalf("first copy pre-commit committed storage slot mismatch: have %x, want %x", val, common.Hash{})
}
copyOne.Commit(false)
if balance := copyOne.GetBalance(addr); balance.Cmp(big.NewInt(42)) != 0 {
t.Fatalf("first copy post-commit balance mismatch: have %v, want %v", balance, 42)
}
if code := copyOne.GetCode(addr); !bytes.Equal(code, []byte("hello")) {
t.Fatalf("first copy post-commit code mismatch: have %x, want %x", code, []byte("hello"))
}
if val := copyOne.GetState(addr, skey); val != sval {
t.Fatalf("first copy post-commit non-committed storage slot mismatch: have %x, want %x", val, sval)
}
if val := copyOne.GetCommittedState(addr, skey); val != sval {
t.Fatalf("first copy post-commit committed storage slot mismatch: have %x, want %x", val, sval)
}
// Copy the copy and check the balance once more
copyTwo := copyOne.Copy()
if balance := copyTwo.GetBalance(addr); balance.Cmp(big.NewInt(42)) != 0 {
@@ -583,8 +571,23 @@ func TestCopyCommitCopy(t *testing.T) {
if val := copyTwo.GetState(addr, skey); val != sval {
t.Fatalf("second copy non-committed storage slot mismatch: have %x, want %x", val, sval)
}
if val := copyTwo.GetCommittedState(addr, skey); val != sval {
t.Fatalf("second copy post-commit committed storage slot mismatch: have %x, want %x", val, sval)
if val := copyTwo.GetCommittedState(addr, skey); val != (common.Hash{}) {
t.Fatalf("second copy committed storage slot mismatch: have %x, want %x", val, sval)
}
// Commit state, ensure states can be loaded from disk
root, _ := state.Commit(false)
state, _ = New(root, tdb, nil)
if balance := state.GetBalance(addr); balance.Cmp(big.NewInt(42)) != 0 {
t.Fatalf("state post-commit balance mismatch: have %v, want %v", balance, 42)
}
if code := state.GetCode(addr); !bytes.Equal(code, []byte("hello")) {
t.Fatalf("state post-commit code mismatch: have %x, want %x", code, []byte("hello"))
}
if val := state.GetState(addr, skey); val != sval {
t.Fatalf("state post-commit non-committed storage slot mismatch: have %x, want %x", val, sval)
}
if val := state.GetCommittedState(addr, skey); val != sval {
t.Fatalf("state post-commit committed storage slot mismatch: have %x, want %x", val, sval)
}
}
@@ -644,19 +647,6 @@ func TestCopyCopyCommitCopy(t *testing.T) {
if val := copyTwo.GetCommittedState(addr, skey); val != (common.Hash{}) {
t.Fatalf("second copy pre-commit committed storage slot mismatch: have %x, want %x", val, common.Hash{})
}
copyTwo.Commit(false)
if balance := copyTwo.GetBalance(addr); balance.Cmp(big.NewInt(42)) != 0 {
t.Fatalf("second copy post-commit balance mismatch: have %v, want %v", balance, 42)
}
if code := copyTwo.GetCode(addr); !bytes.Equal(code, []byte("hello")) {
t.Fatalf("second copy post-commit code mismatch: have %x, want %x", code, []byte("hello"))
}
if val := copyTwo.GetState(addr, skey); val != sval {
t.Fatalf("second copy post-commit non-committed storage slot mismatch: have %x, want %x", val, sval)
}
if val := copyTwo.GetCommittedState(addr, skey); val != sval {
t.Fatalf("second copy post-commit committed storage slot mismatch: have %x, want %x", val, sval)
}
// Copy the copy-copy and check the balance once more
copyThree := copyTwo.Copy()
if balance := copyThree.GetBalance(addr); balance.Cmp(big.NewInt(42)) != 0 {
@@ -668,11 +658,56 @@ func TestCopyCopyCommitCopy(t *testing.T) {
if val := copyThree.GetState(addr, skey); val != sval {
t.Fatalf("third copy non-committed storage slot mismatch: have %x, want %x", val, sval)
}
if val := copyThree.GetCommittedState(addr, skey); val != sval {
if val := copyThree.GetCommittedState(addr, skey); val != (common.Hash{}) {
t.Fatalf("third copy committed storage slot mismatch: have %x, want %x", val, sval)
}
}
// TestCommitCopy tests the copy from a committed state is not functional.
func TestCommitCopy(t *testing.T) {
state, _ := New(types.EmptyRootHash, NewDatabase(rawdb.NewMemoryDatabase()), nil)
// Create an account and check if the retrieved balance is correct
addr := common.HexToAddress("0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe")
skey := common.HexToHash("aaa")
sval := common.HexToHash("bbb")
state.SetBalance(addr, big.NewInt(42)) // Change the account trie
state.SetCode(addr, []byte("hello")) // Change an external metadata
state.SetState(addr, skey, sval) // Change the storage trie
if balance := state.GetBalance(addr); balance.Cmp(big.NewInt(42)) != 0 {
t.Fatalf("initial balance mismatch: have %v, want %v", balance, 42)
}
if code := state.GetCode(addr); !bytes.Equal(code, []byte("hello")) {
t.Fatalf("initial code mismatch: have %x, want %x", code, []byte("hello"))
}
if val := state.GetState(addr, skey); val != sval {
t.Fatalf("initial non-committed storage slot mismatch: have %x, want %x", val, sval)
}
if val := state.GetCommittedState(addr, skey); val != (common.Hash{}) {
t.Fatalf("initial committed storage slot mismatch: have %x, want %x", val, common.Hash{})
}
// Copy the committed state database, the copied one is not functional.
state.Commit(true)
copied := state.Copy()
if balance := copied.GetBalance(addr); balance.Cmp(big.NewInt(0)) != 0 {
t.Fatalf("unexpected balance: have %v", balance)
}
if code := copied.GetCode(addr); code != nil {
t.Fatalf("unexpected code: have %x", code)
}
if val := copied.GetState(addr, skey); val != (common.Hash{}) {
t.Fatalf("unexpected storage slot: have %x", val)
}
if val := copied.GetCommittedState(addr, skey); val != (common.Hash{}) {
t.Fatalf("unexpected storage slot: have %x", val)
}
if !errors.Is(copied.Error(), trie.ErrCommitted) {
t.Fatalf("unexpected state error, %v", copied.Error())
}
}
// TestDeleteCreateRevert tests a weird state transition corner case that we hit
// while changing the internals of StateDB. The workflow is that a contract is
// self-destructed, then in a follow-up transaction (but same block) it's created
+6 -2
View File
@@ -109,7 +109,7 @@ func checkTrieConsistency(db ethdb.Database, root common.Hash) error {
if err != nil {
return err
}
it := trie.NodeIterator(nil)
it := trie.MustNodeIterator(nil)
for it.Next(true) {
}
return it.Error()
@@ -566,6 +566,10 @@ func TestIncompleteStateSync(t *testing.T) {
addedPaths []string
addedHashes []common.Hash
)
reader, err := srcDb.TrieDB().Reader(srcRoot)
if err != nil {
t.Fatalf("state is not available %x", srcRoot)
}
nodeQueue := make(map[string]stateElement)
codeQueue := make(map[common.Hash]struct{})
paths, nodes, codes := sched.Missing(1)
@@ -603,7 +607,7 @@ func TestIncompleteStateSync(t *testing.T) {
results := make([]trie.NodeSyncResult, 0, len(nodeQueue))
for path, element := range nodeQueue {
owner, inner := trie.ResolvePath([]byte(element.path))
data, err := srcDb.TrieDB().Reader(srcRoot).Node(owner, inner, element.hash)
data, err := reader.Node(owner, inner, element.hash)
if err != nil {
t.Fatalf("failed to retrieve node data for %x", element.hash)
}