core/state: perform updates before deletions when mutating tries (#29201)
This addresses an edge-case (detailed in the code comment) where the computation of the intermediate trie root would force the unnecessary resolution of a hash node. The change makes it so that when we process changes from a block, we first process trie-updates and afterwards process trie-deletions.
This commit is contained in:
parent
1dd898c24e
commit
58a3e2f180
|
@ -298,6 +298,18 @@ func (s *stateObject) updateTrie() (Trie, error) {
|
||||||
}
|
}
|
||||||
// Insert all the pending storage updates into the trie
|
// Insert all the pending storage updates into the trie
|
||||||
usedStorage := make([][]byte, 0, len(s.pendingStorage))
|
usedStorage := make([][]byte, 0, len(s.pendingStorage))
|
||||||
|
|
||||||
|
// Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes
|
||||||
|
// in circumstances similar to the following:
|
||||||
|
//
|
||||||
|
// Consider nodes `A` and `B` who share the same full node parent `P` and have no other siblings.
|
||||||
|
// During the execution of a block:
|
||||||
|
// - `A` is deleted,
|
||||||
|
// - `C` is created, and also shares the parent `P`.
|
||||||
|
// If the deletion is handled first, then `P` would be left with only one child, thus collapsed
|
||||||
|
// into a shortnode. This requires `B` to be resolved from disk.
|
||||||
|
// Whereas if the created node is handled first, then the collapse is avoided, and `B` is not resolved.
|
||||||
|
var deletions []common.Hash
|
||||||
for key, value := range s.pendingStorage {
|
for key, value := range s.pendingStorage {
|
||||||
// Skip noop changes, persist actual changes
|
// Skip noop changes, persist actual changes
|
||||||
if value == s.originStorage[key] {
|
if value == s.originStorage[key] {
|
||||||
|
@ -307,13 +319,7 @@ func (s *stateObject) updateTrie() (Trie, error) {
|
||||||
s.originStorage[key] = value
|
s.originStorage[key] = value
|
||||||
|
|
||||||
var encoded []byte // rlp-encoded value to be used by the snapshot
|
var encoded []byte // rlp-encoded value to be used by the snapshot
|
||||||
if (value == common.Hash{}) {
|
if (value != common.Hash{}) {
|
||||||
if err := tr.DeleteStorage(s.address, key[:]); err != nil {
|
|
||||||
s.db.setError(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.db.StorageDeleted += 1
|
|
||||||
} else {
|
|
||||||
// Encoding []byte cannot fail, ok to ignore the error.
|
// Encoding []byte cannot fail, ok to ignore the error.
|
||||||
trimmed := common.TrimLeftZeroes(value[:])
|
trimmed := common.TrimLeftZeroes(value[:])
|
||||||
encoded, _ = rlp.EncodeToBytes(trimmed)
|
encoded, _ = rlp.EncodeToBytes(trimmed)
|
||||||
|
@ -322,6 +328,8 @@ func (s *stateObject) updateTrie() (Trie, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.db.StorageUpdated += 1
|
s.db.StorageUpdated += 1
|
||||||
|
} else {
|
||||||
|
deletions = append(deletions, key)
|
||||||
}
|
}
|
||||||
// Cache the mutated storage slots until commit
|
// Cache the mutated storage slots until commit
|
||||||
if storage == nil {
|
if storage == nil {
|
||||||
|
@ -353,6 +361,13 @@ func (s *stateObject) updateTrie() (Trie, error) {
|
||||||
// Cache the items for preloading
|
// Cache the items for preloading
|
||||||
usedStorage = append(usedStorage, common.CopyBytes(key[:])) // Copy needed for closure
|
usedStorage = append(usedStorage, common.CopyBytes(key[:])) // Copy needed for closure
|
||||||
}
|
}
|
||||||
|
for _, key := range deletions {
|
||||||
|
if err := tr.DeleteStorage(s.address, key[:]); err != nil {
|
||||||
|
s.db.setError(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.db.StorageDeleted += 1
|
||||||
|
}
|
||||||
if s.db.prefetcher != nil {
|
if s.db.prefetcher != nil {
|
||||||
s.db.prefetcher.used(s.addrHash, s.data.Root, usedStorage)
|
s.db.prefetcher.used(s.addrHash, s.data.Root, usedStorage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -541,12 +541,11 @@ func (s *StateDB) updateStateObject(obj *stateObject) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteStateObject removes the given object from the state trie.
|
// deleteStateObject removes the given object from the state trie.
|
||||||
func (s *StateDB) deleteStateObject(obj *stateObject) {
|
func (s *StateDB) deleteStateObject(addr common.Address) {
|
||||||
// Track the amount of time wasted on deleting the account from the trie
|
// Track the amount of time wasted on deleting the account from the trie
|
||||||
defer func(start time.Time) { s.AccountUpdates += time.Since(start) }(time.Now())
|
defer func(start time.Time) { s.AccountUpdates += time.Since(start) }(time.Now())
|
||||||
|
|
||||||
// Delete the account from the trie
|
// Delete the account from the trie
|
||||||
addr := obj.Address()
|
|
||||||
if err := s.trie.DeleteAccount(addr); err != nil {
|
if err := s.trie.DeleteAccount(addr); err != nil {
|
||||||
s.setError(fmt.Errorf("deleteStateObject (%x) error: %v", addr[:], err))
|
s.setError(fmt.Errorf("deleteStateObject (%x) error: %v", addr[:], err))
|
||||||
}
|
}
|
||||||
|
@ -917,16 +916,30 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
usedAddrs := make([][]byte, 0, len(s.stateObjectsPending))
|
usedAddrs := make([][]byte, 0, len(s.stateObjectsPending))
|
||||||
|
// Perform updates before deletions. This prevents resolution of unnecessary trie nodes
|
||||||
|
// in circumstances similar to the following:
|
||||||
|
//
|
||||||
|
// Consider nodes `A` and `B` who share the same full node parent `P` and have no other siblings.
|
||||||
|
// During the execution of a block:
|
||||||
|
// - `A` self-destructs,
|
||||||
|
// - `C` is created, and also shares the parent `P`.
|
||||||
|
// If the self-destruct is handled first, then `P` would be left with only one child, thus collapsed
|
||||||
|
// into a shortnode. This requires `B` to be resolved from disk.
|
||||||
|
// Whereas if the created node is handled first, then the collapse is avoided, and `B` is not resolved.
|
||||||
|
var deletedAddrs []common.Address
|
||||||
for addr := range s.stateObjectsPending {
|
for addr := range s.stateObjectsPending {
|
||||||
if obj := s.stateObjects[addr]; obj.deleted {
|
if obj := s.stateObjects[addr]; !obj.deleted {
|
||||||
s.deleteStateObject(obj)
|
|
||||||
s.AccountDeleted += 1
|
|
||||||
} else {
|
|
||||||
s.updateStateObject(obj)
|
s.updateStateObject(obj)
|
||||||
s.AccountUpdated += 1
|
s.AccountUpdated += 1
|
||||||
|
} else {
|
||||||
|
deletedAddrs = append(deletedAddrs, obj.address)
|
||||||
}
|
}
|
||||||
usedAddrs = append(usedAddrs, common.CopyBytes(addr[:])) // Copy needed for closure
|
usedAddrs = append(usedAddrs, common.CopyBytes(addr[:])) // Copy needed for closure
|
||||||
}
|
}
|
||||||
|
for _, deletedAddr := range deletedAddrs {
|
||||||
|
s.deleteStateObject(deletedAddr)
|
||||||
|
s.AccountDeleted += 1
|
||||||
|
}
|
||||||
if prefetcher != nil {
|
if prefetcher != nil {
|
||||||
prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs)
|
prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue