From 0acc0a1f863d2aa885fe9b9e55a722448e73ee79 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Fri, 1 Sep 2023 02:33:18 +0800 Subject: [PATCH] core/state: simplify storage trie update and commit (#28030) This change improves function description and simplifies logic in statedb update and commit operations. --- core/state/state_object.go | 66 +++++++++++++++++++------------------ core/state/statedb.go | 67 +++++++------------------------------- eth/api_debug.go | 24 +++++++------- eth/api_debug_test.go | 17 +++++----- internal/ethapi/api.go | 43 ++++++++++++------------ 5 files changed, 88 insertions(+), 129 deletions(-) diff --git a/core/state/state_object.go b/core/state/state_object.go index cd72f3fb9..d42d2c34d 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -264,12 +264,17 @@ func (s *stateObject) finalise(prefetch bool) { } } -// updateTrie writes cached storage modifications into the object's storage trie. -// It will return nil if the trie has not been loaded and no changes have been -// made. An error will be returned if the trie can't be loaded/updated correctly. +// updateTrie is responsible for persisting cached storage changes into the +// object's storage trie. In case the storage trie is not yet loaded, this +// function will load the trie automatically. If any issues arise during the +// loading or updating of the trie, an error will be returned. Furthermore, +// this function will return the mutated storage trie, or nil if there is no +// storage change at all. func (s *stateObject) updateTrie() (Trie, error) { // Make sure all dirty slots are finalized into the pending storage area - s.finalise(false) // Don't prefetch anymore, pull directly if need be + s.finalise(false) + + // Short circuit if nothing changed, don't bother with hashing anything if len(s.pendingStorage) == 0 { return s.trie, nil } @@ -281,14 +286,13 @@ func (s *stateObject) updateTrie() (Trie, error) { var ( storage map[common.Hash][]byte origin map[common.Hash][]byte - hasher = s.db.hasher ) tr, err := s.getTrie() if err != nil { s.db.setError(err) return nil, err } - // Insert all the pending updates into the trie + // Insert all the pending storage updates into the trie usedStorage := make([][]byte, 0, len(s.pendingStorage)) for key, value := range s.pendingStorage { // Skip noop changes, persist actual changes @@ -298,8 +302,7 @@ func (s *stateObject) updateTrie() (Trie, error) { prev := s.originStorage[key] s.originStorage[key] = value - // rlp-encoded value to be used by the snapshot - var snapshotVal []byte + var encoded []byte // rlp-encoded value to be used by the snapshot if (value == common.Hash{}) { if err := tr.DeleteStorage(s.address, key[:]); err != nil { s.db.setError(err) @@ -307,10 +310,10 @@ func (s *stateObject) updateTrie() (Trie, error) { } s.db.StorageDeleted += 1 } else { - trimmedVal := common.TrimLeftZeroes(value[:]) // Encoding []byte cannot fail, ok to ignore the error. - snapshotVal, _ = rlp.EncodeToBytes(trimmedVal) - if err := tr.UpdateStorage(s.address, key[:], trimmedVal); err != nil { + trimmed := common.TrimLeftZeroes(value[:]) + encoded, _ = rlp.EncodeToBytes(trimmed) + if err := tr.UpdateStorage(s.address, key[:], trimmed); err != nil { s.db.setError(err) return nil, err } @@ -323,8 +326,8 @@ func (s *stateObject) updateTrie() (Trie, error) { s.db.storages[s.addrHash] = storage } } - khash := crypto.HashData(hasher, key[:]) - storage[khash] = snapshotVal // snapshotVal will be nil if it's deleted + khash := crypto.HashData(s.db.hasher, key[:]) + storage[khash] = encoded // encoded will be nil if it's deleted // Cache the original value of mutated storage slots if origin == nil { @@ -349,21 +352,17 @@ func (s *stateObject) updateTrie() (Trie, error) { if s.db.prefetcher != nil { s.db.prefetcher.used(s.addrHash, s.data.Root, usedStorage) } - if len(s.pendingStorage) > 0 { - s.pendingStorage = make(Storage) - } + s.pendingStorage = make(Storage) // reset pending map return tr, nil } -// UpdateRoot sets the trie root to the current root hash of. An error -// will be returned if trie root hash is not computed correctly. +// updateRoot flushes all cached storage mutations to trie, recalculating the +// new storage trie root. func (s *stateObject) updateRoot() { + // Flush cached storage mutations into trie, short circuit if any error + // is occurred or there is not change in the trie. tr, err := s.updateTrie() - if err != nil { - return - } - // If nothing changed, don't bother with hashing anything - if tr == nil { + if err != nil || tr == nil { return } // Track the amount of time wasted on hashing the storage trie @@ -373,14 +372,12 @@ func (s *stateObject) updateRoot() { s.data.Root = tr.Hash() } -// commit returns the changes made in storage trie and updates the account data. +// commit obtains a set of dirty storage trie nodes and updates the account data. +// The returned set can be nil if nothing to commit. This function assumes all +// storage mutations have already been flushed into trie by updateRoot. func (s *stateObject) commit() (*trienode.NodeSet, error) { - tr, err := s.updateTrie() - if err != nil { - return nil, err - } - // If nothing changed, don't bother with committing anything - if tr == nil { + // Short circuit if trie is not even loaded, don't bother with committing anything + if s.trie == nil { s.origin = s.data.Copy() return nil, nil } @@ -388,7 +385,10 @@ func (s *stateObject) commit() (*trienode.NodeSet, error) { if metrics.EnabledExpensive { defer func(start time.Time) { s.db.StorageCommits += time.Since(start) }(time.Now()) } - root, nodes, err := tr.Commit(false) + // The trie is currently in an open state and could potentially contain + // cached mutations. Call commit to acquire a set of nodes that have been + // modified, the set can be nil if nothing to commit. + root, nodes, err := s.trie.Commit(false) if err != nil { return nil, err } @@ -536,3 +536,7 @@ func (s *stateObject) Balance() *big.Int { func (s *stateObject) Nonce() uint64 { return s.data.Nonce } + +func (s *stateObject) Root() common.Hash { + return s.data.Root +} diff --git a/core/state/statedb.go b/core/state/statedb.go index 17bf99180..48680a562 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -18,7 +18,6 @@ package state import ( - "errors" "fmt" "math/big" "sort" @@ -48,17 +47,6 @@ type revision struct { journalIndex int } -type proofList [][]byte - -func (n *proofList) Put(key []byte, value []byte) error { - *n = append(*n, value) - return nil -} - -func (n *proofList) Delete(key []byte) error { - panic("not supported") -} - // 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: @@ -297,6 +285,7 @@ func (s *StateDB) GetBalance(addr common.Address) *big.Int { return common.Big0 } +// GetNonce retrieves the nonce from the given address or 0 if object not found func (s *StateDB) GetNonce(addr common.Address) uint64 { stateObject := s.getStateObject(addr) if stateObject != nil { @@ -306,6 +295,16 @@ func (s *StateDB) GetNonce(addr common.Address) uint64 { return 0 } +// GetStorageRoot retrieves the storage root from the given address or empty +// if object not found. +func (s *StateDB) GetStorageRoot(addr common.Address) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Root() + } + return common.Hash{} +} + // TxIndex returns the current transaction index set by Prepare. func (s *StateDB) TxIndex() int { return s.txIndex @@ -344,35 +343,6 @@ func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { return common.Hash{} } -// GetProof returns the Merkle proof for a given account. -func (s *StateDB) GetProof(addr common.Address) ([][]byte, error) { - return s.GetProofByHash(crypto.Keccak256Hash(addr.Bytes())) -} - -// GetProofByHash returns the Merkle proof for a given account. -func (s *StateDB) GetProofByHash(addrHash common.Hash) ([][]byte, error) { - var proof proofList - err := s.trie.Prove(addrHash[:], &proof) - return proof, err -} - -// GetStorageProof returns the Merkle proof for given storage slot. -func (s *StateDB) GetStorageProof(a common.Address, key common.Hash) ([][]byte, error) { - trie, err := s.StorageTrie(a) - if err != nil { - return nil, err - } - if trie == nil { - return nil, errors.New("storage trie for requested address does not exist") - } - var proof proofList - err = trie.Prove(crypto.Keccak256(key.Bytes()), &proof) - if err != nil { - return nil, err - } - return proof, nil -} - // GetCommittedState retrieves a value from the given account's committed storage trie. func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { stateObject := s.getStateObject(addr) @@ -387,21 +357,6 @@ func (s *StateDB) Database() Database { return s.db } -// StorageTrie returns the storage trie of an account. The return value is a copy -// and is nil for non-existent accounts. An error will be returned if storage trie -// is existent but can't be loaded correctly. -func (s *StateDB) StorageTrie(addr common.Address) (Trie, error) { - stateObject := s.getStateObject(addr) - if stateObject == nil { - return nil, nil - } - cpy := stateObject.deepCopy(s) - if _, err := cpy.updateTrie(); err != nil { - return nil, err - } - return cpy.getTrie() -} - func (s *StateDB) HasSelfDestructed(addr common.Address) bool { stateObject := s.getStateObject(addr) if stateObject != nil { diff --git a/eth/api_debug.go b/eth/api_debug.go index 6afa04678..22dd6b0cf 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" @@ -216,7 +217,6 @@ func (api *DebugAPI) StorageRangeAt(ctx context.Context, blockNrOrHash rpc.Block if err != nil { return StorageRangeResult{}, err } - if block == nil { return StorageRangeResult{}, fmt.Errorf("block %v not found", blockNrOrHash) } @@ -226,18 +226,20 @@ func (api *DebugAPI) StorageRangeAt(ctx context.Context, blockNrOrHash rpc.Block } defer release() - st, err := statedb.StorageTrie(contractAddress) + return storageRangeAt(statedb, block.Root(), contractAddress, keyStart, maxResult) +} + +func storageRangeAt(statedb *state.StateDB, root common.Hash, address common.Address, start []byte, maxResult int) (StorageRangeResult, error) { + storageRoot := statedb.GetStorageRoot(address) + if storageRoot == types.EmptyRootHash || storageRoot == (common.Hash{}) { + return StorageRangeResult{}, nil // empty storage + } + id := trie.StorageTrieID(root, crypto.Keccak256Hash(address.Bytes()), storageRoot) + tr, err := trie.NewStateTrie(id, statedb.Database().TrieDB()) if err != nil { return StorageRangeResult{}, err } - if st == nil { - return StorageRangeResult{}, fmt.Errorf("account %x doesn't exist", contractAddress) - } - return storageRangeAt(st, keyStart, maxResult) -} - -func storageRangeAt(st state.Trie, start []byte, maxResult int) (StorageRangeResult, error) { - trieIt, err := st.NodeIterator(start) + trieIt, err := tr.NodeIterator(start) if err != nil { return StorageRangeResult{}, err } @@ -249,7 +251,7 @@ func storageRangeAt(st state.Trie, start []byte, maxResult int) (StorageRangeRes return StorageRangeResult{}, err } e := storageEntry{Value: common.BytesToHash(content)} - if preimage := st.GetKey(it.Key); preimage != nil { + if preimage := tr.GetKey(it.Key); preimage != nil { preimage := common.BytesToHash(preimage) e.Key = &preimage } diff --git a/eth/api_debug_test.go b/eth/api_debug_test.go index 6a1b537c2..3d3444a87 100644 --- a/eth/api_debug_test.go +++ b/eth/api_debug_test.go @@ -159,9 +159,10 @@ func TestStorageRangeAt(t *testing.T) { // Create a state where account 0x010000... has a few storage entries. var ( - state, _ = state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) - addr = common.Address{0x01} - keys = []common.Hash{ // hashes of Keys of storage + db = state.NewDatabaseWithConfig(rawdb.NewMemoryDatabase(), &trie.Config{Preimages: true}) + sdb, _ = state.New(types.EmptyRootHash, db, nil) + addr = common.Address{0x01} + keys = []common.Hash{ // hashes of Keys of storage common.HexToHash("340dd630ad21bf010b4e676dbfa9ba9a02175262d1fa356232cfde6cb5b47ef2"), common.HexToHash("426fcb404ab2d5d8e61a3d918108006bbb0a9be65e92235bb10eefbdb6dcd053"), common.HexToHash("48078cfed56339ea54962e72c37c7f588fc4f8e5bc173827ba75cb10a63a96a5"), @@ -175,8 +176,10 @@ func TestStorageRangeAt(t *testing.T) { } ) for _, entry := range storage { - state.SetState(addr, *entry.Key, entry.Value) + sdb.SetState(addr, *entry.Key, entry.Value) } + root, _ := sdb.Commit(0, false) + sdb, _ = state.New(root, db, nil) // Check a few combinations of limit and start/end. tests := []struct { @@ -206,11 +209,7 @@ func TestStorageRangeAt(t *testing.T) { }, } for _, test := range tests { - tr, err := state.StorageTrie(addr) - if err != nil { - t.Error(err) - } - result, err := storageRangeAt(tr, test.start, test.limit) + result, err := storageRangeAt(sdb, root, addr, test.start, test.limit) if err != nil { t.Error(err) } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index ceef482e7..c37e716de 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -46,6 +46,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/trie" "github.com/tyler-smith/go-bip39" ) @@ -674,9 +675,10 @@ func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, st keys = make([]common.Hash, len(storageKeys)) keyLengths = make([]int, len(storageKeys)) storageProof = make([]StorageResult, len(storageKeys)) - storageTrie state.Trie - storageHash = types.EmptyRootHash - codeHash = types.EmptyCodeHash + + storageTrie state.Trie + storageHash = types.EmptyRootHash + codeHash = types.EmptyCodeHash ) // Deserialize all keys. This prevents state access on invalid input. for i, hexKey := range storageKeys { @@ -686,15 +688,18 @@ func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, st return nil, err } } - - state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + state, header, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if state == nil || err != nil { return nil, err } - if storageTrie, err = state.StorageTrie(address); err != nil { - return nil, err + if storageRoot := state.GetStorageRoot(address); storageRoot != types.EmptyRootHash && storageRoot != (common.Hash{}) { + id := trie.StorageTrieID(header.Root, crypto.Keccak256Hash(address.Bytes()), storageRoot) + tr, err := trie.NewStateTrie(id, state.Database().TrieDB()) + if err != nil { + return nil, err + } + storageTrie = tr } - // If we have a storageTrie, the account exists and we must update // the storage root hash and the code hash. if storageTrie != nil { @@ -727,14 +732,17 @@ func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, st } // Create the accountProof. - accountProof, proofErr := state.GetProof(address) - if proofErr != nil { - return nil, proofErr + tr, err := trie.NewStateTrie(trie.StateTrieID(header.Root), state.Database().TrieDB()) + if err != nil { + return nil, err + } + var accountProof proofList + if err := tr.Prove(crypto.Keccak256(address.Bytes()), &accountProof); err != nil { + return nil, err } - return &AccountResult{ Address: address, - AccountProof: toHexSlice(accountProof), + AccountProof: accountProof, Balance: (*hexutil.Big)(state.GetBalance(address)), CodeHash: codeHash, Nonce: hexutil.Uint64(state.GetNonce(address)), @@ -2245,12 +2253,3 @@ func checkTxFee(gasPrice *big.Int, gas uint64, cap float64) error { } return nil } - -// toHexSlice creates a slice of hex-strings based on []byte. -func toHexSlice(b [][]byte) []string { - r := make([]string, len(b)) - for i := range b { - r[i] = hexutil.Encode(b[i]) - } - return r -}