forked from cerc-io/plugeth
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:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user