diff --git a/core/state/database.go b/core/state/database.go index c603e3ad7a..e790bb3a63 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -17,8 +17,6 @@ package state import ( - "fmt" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/overlay" "github.com/ethereum/go-ethereum/core/rawdb" @@ -26,10 +24,8 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/bintrie" - "github.com/ethereum/go-ethereum/trie/transitiontrie" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/triedb" ) @@ -43,6 +39,9 @@ type Database interface { // through which the account iterator and storage iterator can be created. Iteratee(root common.Hash) (Iteratee, error) + // Hasher returns a state hasher associated with the specified state root. + Hasher(root common.Hash) (Hasher, error) + // OpenTrie opens the main account trie. OpenTrie(root common.Hash) (Trie, error) @@ -221,6 +220,10 @@ func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) { return newReader(db.codedb.Reader(), sr), nil } +func (db *CachingDB) Hasher(stateRoot common.Hash) (Hasher, error) { + return &noopHasher{}, nil +} + // ReadersWithCacheStats creates a pair of state readers that share the same // underlying state reader and internal state cache, while maintaining separate // statistics respectively. @@ -297,16 +300,16 @@ func (db *CachingDB) Commit(update *stateUpdate) error { } // If snapshotting is enabled, update the snapshot tree with this new version if db.snap != nil && db.snap.Snapshot(update.originRoot) != nil { - if err := db.snap.Update(update.root, update.originRoot, update.accounts, update.storages); err != nil { - log.Warn("Failed to update snapshot tree", "from", update.originRoot, "to", update.root, "err", err) - } - // Keep 128 diff layers in the memory, persistent layer is 129th. - // - head layer is paired with HEAD state - // - head-1 layer is paired with HEAD-1 state - // - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state - if err := db.snap.Cap(update.root, TriesInMemory); err != nil { - log.Warn("Failed to cap snapshot tree", "root", update.root, "layers", TriesInMemory, "err", err) - } + //if err := db.snap.Update(update.root, update.originRoot, update.accounts, update.storages); err != nil { + // log.Warn("Failed to update snapshot tree", "from", update.originRoot, "to", update.root, "err", err) + //} + //// Keep 128 diff layers in the memory, persistent layer is 129th. + //// - head layer is paired with HEAD state + //// - head-1 layer is paired with HEAD-1 state + //// - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state + //if err := db.snap.Cap(update.root, TriesInMemory); err != nil { + // log.Warn("Failed to cap snapshot tree", "root", update.root, "layers", TriesInMemory, "err", err) + //} } return db.triedb.Update(update.root, update.originRoot, update.blockNumber, update.nodes, update.stateSet()) } @@ -316,15 +319,3 @@ func (db *CachingDB) Commit(update *stateUpdate) error { func (db *CachingDB) Iteratee(root common.Hash) (Iteratee, error) { return newStateIteratee(!db.triedb.IsVerkle(), root, db.triedb, db.snap) } - -// mustCopyTrie returns a deep-copied trie. -func mustCopyTrie(t Trie) Trie { - switch t := t.(type) { - case *trie.StateTrie: - return t.Copy() - case *transitiontrie.TransitionTrie: - return t.Copy() - default: - panic(fmt.Errorf("unknown trie type %T", t)) - } -} diff --git a/core/state/database_hasher.go b/core/state/database_hasher.go index 340d3cd523..4046645284 100644 --- a/core/state/database_hasher.go +++ b/core/state/database_hasher.go @@ -25,10 +25,17 @@ import ( // AccountMutation describes a state transition for a single account. type AccountMutation struct { - Account *Account // Null for deletion + Account *Account // Null for deletion + DirtyCode bool // Flag whether the code is changed + Code []byte // Null for deletion +} - CodeDirty bool // Flag whether the code is changed - Code []byte // Null for deletion +// SecondaryHash encapsulates the secondary hash of storage tries. +// It is only relevant in the context of the Merkle Patricia Trie and +// includes both the post-transition root and the original root. +type SecondaryHash struct { + Hash common.Hash + Prev common.Hash } // Hasher defines the minimal interface for computing state root hashes. @@ -63,7 +70,7 @@ type Hasher interface { // Additionally, if the hasher uses a two-layer structure, the roots of the // secondary tries together with their original hashes will also be returned // for all mutated accounts, regardless of whether their storage was modified. - Commit() (common.Hash, *trienode.MergedNodeSet, map[common.Address]common.Hash, map[common.Address]common.Hash, error) + Commit() (common.Hash, *trienode.MergedNodeSet, map[common.Address]SecondaryHash, error) // Copy returns a deep-copied hasher instance. Copy() Hasher @@ -112,3 +119,30 @@ type Prover interface { // the nodes required to prove its absence. ProveStorage(addr common.Address, key common.Hash, proofDb ethdb.KeyValueWriter) error } + +type noopHasher struct{} + +func (n noopHasher) UpdateAccount(addresses []common.Address, accounts []AccountMutation) error { + //TODO implement me + panic("implement me") +} + +func (n noopHasher) UpdateStorage(address common.Address, keys []common.Hash, values []common.Hash) error { + //TODO implement me + panic("implement me") +} + +func (n noopHasher) Hash() common.Hash { + //TODO implement me + panic("implement me") +} + +func (n noopHasher) Commit() (common.Hash, *trienode.MergedNodeSet, map[common.Address]SecondaryHash, error) { + //TODO implement me + panic("implement me") +} + +func (n noopHasher) Copy() Hasher { + //TODO implement me + panic("implement me") +} diff --git a/core/state/database_history.go b/core/state/database_history.go index 0dbb8cc546..1b46388c2f 100644 --- a/core/state/database_history.go +++ b/core/state/database_history.go @@ -44,7 +44,7 @@ func newHistoricStateReader(r *pathdb.HistoricalStateReader) *historicStateReade } // Account implements StateReader, retrieving the account specified by the address. -func (r *historicStateReader) Account(addr common.Address) (*types.StateAccount, error) { +func (r *historicStateReader) Account(addr common.Address) (*Account, error) { r.lock.Lock() defer r.lock.Unlock() @@ -55,18 +55,14 @@ func (r *historicStateReader) Account(addr common.Address) (*types.StateAccount, if account == nil { return nil, nil } - acct := &types.StateAccount{ + acct := &Account{ Nonce: account.Nonce, Balance: account.Balance, CodeHash: account.CodeHash, - Root: common.BytesToHash(account.Root), } if len(acct.CodeHash) == 0 { acct.CodeHash = types.EmptyCodeHash.Bytes() } - if acct.Root == (common.Hash{}) { - acct.Root = types.EmptyRootHash - } return acct, nil } @@ -150,17 +146,25 @@ func newHistoricalTrieReader(root common.Hash, r *pathdb.HistoricalNodeReader) ( } // account is the inner version of Account and assumes the r.lock is already held. -func (r *historicalTrieReader) account(addr common.Address) (*types.StateAccount, error) { +func (r *historicalTrieReader) account(addr common.Address) (*Account, error) { account, err := r.tr.GetAccount(addr) if err != nil { return nil, err } if account == nil { r.subRoots[addr] = types.EmptyRootHash + return nil, nil } else { r.subRoots[addr] = account.Root + + // Account objects resolved from the trie always include + // the full code hash. + return &Account{ + Nonce: account.Nonce, + Balance: account.Balance, + CodeHash: account.CodeHash, + }, nil } - return account, nil } // Account implements StateReader, retrieving the account specified by the address. @@ -169,7 +173,7 @@ func (r *historicalTrieReader) account(addr common.Address) (*types.StateAccount // the requested account is not yet covered by the snapshot. // // The returned account might be nil if it's not existent. -func (r *historicalTrieReader) Account(addr common.Address) (*types.StateAccount, error) { +func (r *historicalTrieReader) Account(addr common.Address) (*Account, error) { r.lock.Lock() defer r.lock.Unlock() @@ -255,6 +259,10 @@ func (db *HistoricDB) Reader(stateRoot common.Hash) (Reader, error) { return newReader(db.codedb.Reader(), combined), nil } +func (db *HistoricDB) Hasher(stateRoot common.Hash) (Hasher, error) { + return &noopHasher{}, nil +} + // OpenTrie opens the main account trie. It's not supported by historic database. func (db *HistoricDB) OpenTrie(root common.Hash) (Trie, error) { nr, err := db.triedb.HistoricNodeReader(root) diff --git a/core/state/dump.go b/core/state/dump.go index 71138143d9..cd059cde49 100644 --- a/core/state/dump.go +++ b/core/state/dump.go @@ -168,7 +168,11 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] address = &addrBytes account.Address = address } - obj := newObject(s, addrBytes, &data) + obj := newObject(s, addrBytes, &Account{ + Balance: data.Balance, + Nonce: data.Nonce, + CodeHash: data.CodeHash, + }) if !conf.SkipCode { account.Code = obj.Code() } diff --git a/core/state/reader.go b/core/state/reader.go index 87eba796a9..9c144e71e2 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -71,6 +71,19 @@ func newEmptyAccount() *Account { } } +// copy returns a deep-copied account object. +func (acct *Account) copy() *Account { + var balance *uint256.Int + if acct.Balance != nil { + balance = new(uint256.Int).Set(acct.Balance) + } + return &Account{ + Nonce: acct.Nonce, + Balance: balance, + CodeHash: common.CopyBytes(acct.CodeHash), + } +} + // StateReader defines the interface for accessing accounts and storage slots // associated with a specific state. // @@ -81,7 +94,7 @@ type StateReader interface { // - Returns a nil account if it does not exist // - Returns an error only if an unexpected issue occurs // - The returned account is safe to modify after the call - Account(addr common.Address) (*types.StateAccount, error) + Account(addr common.Address) (*Account, error) // Storage retrieves the storage slot associated with a particular account // address and slot key. @@ -118,7 +131,7 @@ func newFlatReader(reader database.StateReader) *flatReader { // the requested account is not yet covered by the snapshot. // // The returned account might be nil if it's not existent. -func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) { +func (r *flatReader) Account(addr common.Address) (*Account, error) { account, err := r.reader.Account(crypto.Keccak256Hash(addr[:])) if err != nil { return nil, err @@ -126,18 +139,16 @@ func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) { if account == nil { return nil, nil } - acct := &types.StateAccount{ + acct := &Account{ Nonce: account.Nonce, Balance: account.Balance, CodeHash: account.CodeHash, - Root: common.BytesToHash(account.Root), } + // Account objects resolved from the flat state always omit the + // empty code hash. if len(acct.CodeHash) == 0 { acct.CodeHash = types.EmptyCodeHash.Bytes() } - if acct.Root == (common.Hash{}) { - acct.Root = types.EmptyRootHash - } return acct, nil } @@ -242,24 +253,32 @@ func newTrieReader(root common.Hash, db *triedb.Database) (*trieReader, error) { } // account is the inner version of Account and assumes the r.lock is already held. -func (r *trieReader) account(addr common.Address) (*types.StateAccount, error) { +func (r *trieReader) account(addr common.Address) (*Account, error) { account, err := r.mainTrie.GetAccount(addr) if err != nil { return nil, err } if account == nil { r.subRoots[addr] = types.EmptyRootHash + return nil, nil } else { r.subRoots[addr] = account.Root + + // Account objects resolved from the trie always include + // the full code hash. + return &Account{ + Nonce: account.Nonce, + Balance: account.Balance, + CodeHash: account.CodeHash, + }, nil } - return account, nil } // Account implements StateReader, retrieving the account specified by the address. // // An error will be returned if the trie state is corrupted. An nil account // will be returned if it's not existent in the trie. -func (r *trieReader) Account(addr common.Address) (*types.StateAccount, error) { +func (r *trieReader) Account(addr common.Address) (*Account, error) { r.lock.Lock() defer r.lock.Unlock() @@ -340,7 +359,7 @@ func newMultiStateReader(readers ...StateReader) (*multiStateReader, error) { // - Returns a nil account if it does not exist // - Returns an error only if an unexpected issue occurs // - The returned account is safe to modify after the call -func (r *multiStateReader) Account(addr common.Address) (*types.StateAccount, error) { +func (r *multiStateReader) Account(addr common.Address) (*Account, error) { var errs []error for _, reader := range r.readers { acct, err := reader.Account(addr) @@ -376,7 +395,7 @@ type stateReaderWithCache struct { StateReader // Previously resolved state entries. - accounts map[common.Address]*types.StateAccount + accounts map[common.Address]*Account accountLock sync.RWMutex // List of storage buckets, each of which is thread-safe. @@ -393,7 +412,7 @@ type stateReaderWithCache struct { func newStateReaderWithCache(sr StateReader) *stateReaderWithCache { r := &stateReaderWithCache{ StateReader: sr, - accounts: make(map[common.Address]*types.StateAccount), + accounts: make(map[common.Address]*Account), } for i := range r.storageBuckets { r.storageBuckets[i].storages = make(map[common.Address]map[common.Hash]common.Hash) @@ -406,7 +425,7 @@ func newStateReaderWithCache(sr StateReader) *stateReaderWithCache { // might be nil if it's not existent. // // An error will be returned if the state is corrupted in the underlying reader. -func (r *stateReaderWithCache) account(addr common.Address) (*types.StateAccount, bool, error) { +func (r *stateReaderWithCache) account(addr common.Address) (*Account, bool, error) { // Try to resolve the requested account in the local cache r.accountLock.RLock() acct, ok := r.accounts[addr] @@ -429,7 +448,7 @@ func (r *stateReaderWithCache) account(addr common.Address) (*types.StateAccount // The returned account might be nil if it's not existent. // // An error will be returned if the state is corrupted in the underlying reader. -func (r *stateReaderWithCache) Account(addr common.Address) (*types.StateAccount, error) { +func (r *stateReaderWithCache) Account(addr common.Address) (*Account, error) { account, _, err := r.account(addr) return account, err } @@ -502,7 +521,7 @@ func newStateReaderWithStats(sr *stateReaderWithCache) *stateReaderWithStats { // The returned account might be nil if it's not existent. // // An error will be returned if the state is corrupted in the underlying reader. -func (r *stateReaderWithStats) Account(addr common.Address) (*types.StateAccount, error) { +func (r *stateReaderWithStats) Account(addr common.Address) (*Account, error) { account, incache, err := r.stateReaderWithCache.account(addr) if err != nil { return nil, err diff --git a/core/state/state_object.go b/core/state/state_object.go index e6405b0f05..f93cd96848 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -27,11 +27,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum/go-ethereum/trie/bintrie" - "github.com/ethereum/go-ethereum/trie/transitiontrie" - "github.com/ethereum/go-ethereum/trie/trienode" "github.com/holiman/uint256" ) @@ -49,13 +44,12 @@ func (s Storage) Copy() Storage { // - Finally, call commit to return the changes of storage trie and update account data. type stateObject struct { db *StateDB - address common.Address // address of ethereum account - addressHash *common.Hash // hash of ethereum address of the account - origin *types.StateAccount // Account original data without any change applied, nil means it was not existent - data types.StateAccount // Account data with all mutations applied in the scope of block + address common.Address // address of ethereum account + addressHash *common.Hash // hash of ethereum address of the account + origin *Account // Account original data without any change applied, nil means it was not existent + data Account // Account data with all mutations applied in the scope of block // Write caches. - trie Trie // storage trie, which becomes non-nil on first access code []byte // contract bytecode, which gets set when code is loaded originStorage Storage // Storage entries that have been accessed within the current block @@ -94,10 +88,10 @@ func (s *stateObject) empty() bool { } // newObject creates a state object. -func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *stateObject { +func newObject(db *StateDB, address common.Address, acct *Account) *stateObject { origin := acct if acct == nil { - acct = types.NewEmptyStateAccount() + acct = newEmptyAccount() } return &stateObject{ db: db, @@ -133,15 +127,7 @@ func (s *stateObject) touch() { // If a new trie is opened, it will be cached within the state object to allow // subsequent reads to expand the same trie instead of reloading from disk. func (s *stateObject) getTrie() (Trie, error) { - if s.trie == nil { - // Assumes the primary account trie is already loaded - tr, err := s.db.db.OpenStorageTrie(s.db.originalRoot, s.address, s.data.Root, s.db.trie) - if err != nil { - return nil, err - } - s.trie = tr - } - return s.trie, nil + return nil, nil } // GetState retrieves a value associated with the given storage key. @@ -268,36 +254,16 @@ func (s *stateObject) finalise() { } // 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. -// -// It assumes all the dirty storage slots have been finalized before. -func (s *stateObject) updateTrie() (Trie, error) { +// state hasher. It assumes all the dirty storage slots have been finalized +// before. +func (s *stateObject) updateTrie() error { // Short circuit if nothing was accessed if len(s.uncommittedStorage) == 0 { - return s.trie, nil + return nil } - // Fetcher not running or empty trie, fallback to the database trie - tr, err := s.getTrie() - if err != nil { - s.db.setError(err) - return nil, err - } - // 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 + keys = make([]common.Hash, 0, len(s.uncommittedStorage)) + vals = make([]common.Hash, 0, len(s.uncommittedStorage)) ) for key, origin := range s.uncommittedStorage { // Skip noop changes, persist actual changes @@ -310,51 +276,16 @@ func (s *stateObject) updateTrie() (Trie, error) { log.Error("Storage slot is not found in pending area", "address", s.address, "slot", key) continue } - if (value != common.Hash{}) { - if err := tr.UpdateStorage(s.address, key[:], common.TrimLeftZeroes(value[:])); err != nil { - s.db.setError(err) - return nil, err - } - s.db.StorageUpdated.Add(1) - } else { - deletions = append(deletions, key) - } - } - for _, key := range deletions { - if err := tr.DeleteStorage(s.address, key[:]); err != nil { - s.db.setError(err) - return nil, err - } - s.db.StorageDeleted.Add(1) + keys = append(keys, key) + vals = append(vals, value) } s.uncommittedStorage = make(Storage) // empties the commit markers - return tr, nil -} - -// 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 no change in the trie. - tr, err := s.updateTrie() - if err != nil || tr == nil { - return - } - s.data.Root = tr.Hash() + return s.db.hasher.UpdateStorage(s.address, keys, vals) } // commitStorage overwrites the clean storage with the storage changes and // fulfills the storage diffs into the given accountUpdate struct. func (s *stateObject) commitStorage(op *accountUpdate) { - var ( - encode = func(val common.Hash) []byte { - if val == (common.Hash{}) { - return nil - } - blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(val[:])) - return blob - } - ) for key, val := range s.pendingStorage { // Skip the noop storage changes, it might be possible the value // of tracked slot is same in originStorage and pendingStorage @@ -365,17 +296,17 @@ func (s *stateObject) commitStorage(op *accountUpdate) { } hash := crypto.Keccak256Hash(key[:]) if op.storages == nil { - op.storages = make(map[common.Hash][]byte) + op.storages = make(map[common.Hash]common.Hash) } - op.storages[hash] = encode(val) + op.storages[hash] = val if op.storagesOriginByKey == nil { - op.storagesOriginByKey = make(map[common.Hash][]byte) + op.storagesOriginByKey = make(map[common.Hash]common.Hash) } if op.storagesOriginByHash == nil { - op.storagesOriginByHash = make(map[common.Hash][]byte) + op.storagesOriginByHash = make(map[common.Hash]common.Hash) } - origin := encode(s.originStorage[key]) + origin := s.originStorage[key] op.storagesOriginByKey[key] = origin op.storagesOriginByHash[hash] = origin @@ -390,14 +321,12 @@ func (s *stateObject) commitStorage(op *accountUpdate) { // // Note, commit may run concurrently across all the state objects. Do not assume // thread-safe access to the statedb. -func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) { +func (s *stateObject) commit() (*accountUpdate, error) { // commit the account metadata changes op := &accountUpdate{ address: s.address, - data: types.SlimAccountRLP(s.data), - } - if s.origin != nil { - op.origin = types.SlimAccountRLP(*s.origin) + data: &s.data, + origin: s.origin, } // commit the contract code if it's modified if s.dirtyCode { @@ -415,24 +344,8 @@ func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) { } // Commit storage changes and the associated storage trie s.commitStorage(op) - if len(op.storages) == 0 { - // nothing changed, don't bother to commit the trie - s.origin = s.data.Copy() - return op, nil, nil - } - // In Verkle/binary trie mode, all state objects share one unified trie. - // The main account trie commit in stateDB.commit() already calls - // CollectNodes on this trie, so calling Commit here again would - // redundantly traverse and serialize the entire tree per dirty account. - if s.db.GetTrie().IsVerkle() { - s.origin = s.data.Copy() - return op, nil, nil - } - // The storage trie root is omitted, as it has already been updated in the - // previous updateRoot step. - _, nodes := s.trie.Commit(false) - s.origin = s.data.Copy() - return op, nodes, nil + s.origin = s.data.copy() + return op, nil } // AddBalance adds amount to s's balance. @@ -478,21 +391,6 @@ func (s *stateObject) deepCopy(db *StateDB) *stateObject { selfDestructed: s.selfDestructed, newContract: s.newContract, } - - switch s.trie.(type) { - case *bintrie.BinaryTrie: - // UBT uses only one tree, and the copy has already been - // made in mustCopyTrie. - obj.trie = db.trie - case *transitiontrie.TransitionTrie: - // Same thing for the transition tree, since the MPT is - // read-only. - obj.trie = db.trie - case *trie.StateTrie: - obj.trie = mustCopyTrie(s.trie) - case nil: - // do nothing - } return obj } @@ -584,5 +482,5 @@ func (s *stateObject) Nonce() uint64 { } func (s *stateObject) Root() common.Hash { - return s.data.Root + return common.Hash{} } diff --git a/core/state/state_sizer.go b/core/state/state_sizer.go index 02b73e5575..16b8aec444 100644 --- a/core/state/state_sizer.go +++ b/core/state/state_sizer.go @@ -28,7 +28,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" @@ -41,6 +40,7 @@ const ( ) // Database key scheme for states. +// nolint:unused var ( accountKeySize = int64(len(rawdb.SnapshotAccountPrefix) + common.HashLength) storageKeySize = int64(len(rawdb.SnapshotStoragePrefix) + common.HashLength*2) @@ -126,133 +126,134 @@ func (s SizeStats) add(diff SizeStats) SizeStats { // calSizeStats measures the state size changes of the provided state update. func calSizeStats(update *stateUpdate) (SizeStats, error) { - stats := SizeStats{ - BlockNumber: update.blockNumber, - StateRoot: update.root, - } - - // Measure the account changes - for addr, oldValue := range update.accountsOrigin { - addrHash := crypto.Keccak256Hash(addr.Bytes()) - newValue, exists := update.accounts[addrHash] - if !exists { - return SizeStats{}, fmt.Errorf("account %x not found", addr) - } - oldLen, newLen := len(oldValue), len(newValue) - - switch { - case oldLen > 0 && newLen == 0: - // Account deletion - stats.Accounts -= 1 - stats.AccountBytes -= accountKeySize + int64(oldLen) - case oldLen == 0 && newLen > 0: - // Account creation - stats.Accounts += 1 - stats.AccountBytes += accountKeySize + int64(newLen) - default: - // Account update - stats.AccountBytes += int64(newLen - oldLen) - } - } - - // Measure storage changes - for addr, slots := range update.storagesOrigin { - addrHash := crypto.Keccak256Hash(addr.Bytes()) - subset, exists := update.storages[addrHash] - if !exists { - return SizeStats{}, fmt.Errorf("storage %x not found", addr) - } - for key, oldValue := range slots { - var ( - exists bool - newValue []byte - ) - if update.rawStorageKey { - newValue, exists = subset[crypto.Keccak256Hash(key.Bytes())] - } else { - newValue, exists = subset[key] - } - if !exists { - return SizeStats{}, fmt.Errorf("storage slot %x-%x not found", addr, key) - } - oldLen, newLen := len(oldValue), len(newValue) - - switch { - case oldLen > 0 && newLen == 0: - // Storage deletion - stats.Storages -= 1 - stats.StorageBytes -= storageKeySize + int64(oldLen) - case oldLen == 0 && newLen > 0: - // Storage creation - stats.Storages += 1 - stats.StorageBytes += storageKeySize + int64(newLen) - default: - // Storage update - stats.StorageBytes += int64(newLen - oldLen) - } - } - } - - // Measure trienode changes - for owner, subset := range update.nodes.Sets { - var ( - keyPrefix int64 - isAccount = owner == (common.Hash{}) - ) - if isAccount { - keyPrefix = accountTrienodePrefixSize - } else { - keyPrefix = storageTrienodePrefixSize - } - - // Iterate over Origins since every modified node has an origin entry - for path, oldNode := range subset.Origins { - newNode, exists := subset.Nodes[path] - if !exists { - return SizeStats{}, fmt.Errorf("node %x-%v not found", owner, path) - } - keySize := keyPrefix + int64(len(path)) - - switch { - case len(oldNode) > 0 && len(newNode.Blob) == 0: - // Node deletion - if isAccount { - stats.AccountTrienodes -= 1 - stats.AccountTrienodeBytes -= keySize + int64(len(oldNode)) - } else { - stats.StorageTrienodes -= 1 - stats.StorageTrienodeBytes -= keySize + int64(len(oldNode)) - } - case len(oldNode) == 0 && len(newNode.Blob) > 0: - // Node creation - if isAccount { - stats.AccountTrienodes += 1 - stats.AccountTrienodeBytes += keySize + int64(len(newNode.Blob)) - } else { - stats.StorageTrienodes += 1 - stats.StorageTrienodeBytes += keySize + int64(len(newNode.Blob)) - } - default: - // Node update - if isAccount { - stats.AccountTrienodeBytes += int64(len(newNode.Blob) - len(oldNode)) - } else { - stats.StorageTrienodeBytes += int64(len(newNode.Blob) - len(oldNode)) - } - } - } - } - - codeExists := make(map[common.Hash]struct{}) - for _, code := range update.codes { - if _, ok := codeExists[code.hash]; ok || code.duplicate { - continue - } - stats.ContractCodes += 1 - stats.ContractCodeBytes += codeKeySize + int64(len(code.blob)) - codeExists[code.hash] = struct{}{} - } - return stats, nil + return SizeStats{}, nil + //stats := SizeStats{ + // BlockNumber: update.blockNumber, + // StateRoot: update.root, + //} + // + //// Measure the account changes + //for addr, oldValue := range update.accountsOrigin { + // addrHash := crypto.Keccak256Hash(addr.Bytes()) + // newValue, exists := update.accounts[addrHash] + // if !exists { + // return SizeStats{}, fmt.Errorf("account %x not found", addr) + // } + // oldLen, newLen := len(oldValue), len(newValue) + // + // switch { + // case oldLen > 0 && newLen == 0: + // // Account deletion + // stats.Accounts -= 1 + // stats.AccountBytes -= accountKeySize + int64(oldLen) + // case oldLen == 0 && newLen > 0: + // // Account creation + // stats.Accounts += 1 + // stats.AccountBytes += accountKeySize + int64(newLen) + // default: + // // Account update + // stats.AccountBytes += int64(newLen - oldLen) + // } + //} + // + //// Measure storage changes + //for addr, slots := range update.storagesOrigin { + // addrHash := crypto.Keccak256Hash(addr.Bytes()) + // subset, exists := update.storages[addrHash] + // if !exists { + // return SizeStats{}, fmt.Errorf("storage %x not found", addr) + // } + // for key, oldValue := range slots { + // var ( + // exists bool + // newValue []byte + // ) + // if update.rawStorageKey { + // newValue, exists = subset[crypto.Keccak256Hash(key.Bytes())] + // } else { + // newValue, exists = subset[key] + // } + // if !exists { + // return SizeStats{}, fmt.Errorf("storage slot %x-%x not found", addr, key) + // } + // oldLen, newLen := len(oldValue), len(newValue) + // + // switch { + // case oldLen > 0 && newLen == 0: + // // Storage deletion + // stats.Storages -= 1 + // stats.StorageBytes -= storageKeySize + int64(oldLen) + // case oldLen == 0 && newLen > 0: + // // Storage creation + // stats.Storages += 1 + // stats.StorageBytes += storageKeySize + int64(newLen) + // default: + // // Storage update + // stats.StorageBytes += int64(newLen - oldLen) + // } + // } + //} + // + //// Measure trienode changes + //for owner, subset := range update.nodes.Sets { + // var ( + // keyPrefix int64 + // isAccount = owner == (common.Hash{}) + // ) + // if isAccount { + // keyPrefix = accountTrienodePrefixSize + // } else { + // keyPrefix = storageTrienodePrefixSize + // } + // + // // Iterate over Origins since every modified node has an origin entry + // for path, oldNode := range subset.Origins { + // newNode, exists := subset.Nodes[path] + // if !exists { + // return SizeStats{}, fmt.Errorf("node %x-%v not found", owner, path) + // } + // keySize := keyPrefix + int64(len(path)) + // + // switch { + // case len(oldNode) > 0 && len(newNode.Blob) == 0: + // // Node deletion + // if isAccount { + // stats.AccountTrienodes -= 1 + // stats.AccountTrienodeBytes -= keySize + int64(len(oldNode)) + // } else { + // stats.StorageTrienodes -= 1 + // stats.StorageTrienodeBytes -= keySize + int64(len(oldNode)) + // } + // case len(oldNode) == 0 && len(newNode.Blob) > 0: + // // Node creation + // if isAccount { + // stats.AccountTrienodes += 1 + // stats.AccountTrienodeBytes += keySize + int64(len(newNode.Blob)) + // } else { + // stats.StorageTrienodes += 1 + // stats.StorageTrienodeBytes += keySize + int64(len(newNode.Blob)) + // } + // default: + // // Node update + // if isAccount { + // stats.AccountTrienodeBytes += int64(len(newNode.Blob) - len(oldNode)) + // } else { + // stats.StorageTrienodeBytes += int64(len(newNode.Blob) - len(oldNode)) + // } + // } + // } + //} + // + //codeExists := make(map[common.Hash]struct{}) + //for _, code := range update.codes { + // if _, ok := codeExists[code.hash]; ok || code.duplicate { + // continue + // } + // stats.ContractCodes += 1 + // stats.ContractCodeBytes += codeKeySize + int64(len(code.blob)) + // codeExists[code.hash] = struct{}{} + //} + //return stats, nil } type stateSizeQuery struct { diff --git a/core/state/statedb.go b/core/state/statedb.go index d085bd31c6..3c1241428e 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -23,7 +23,6 @@ import ( "maps" "slices" "sort" - "sync" "sync/atomic" "time" @@ -33,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/holiman/uint256" @@ -76,7 +76,7 @@ func (m *mutation) isDelete() bool { type StateDB struct { db Database reader Reader - trie Trie // it's resolved on first access + hasher Hasher // originalRoot is the pre-state root, before any changes were made. // It will be updated when the Commit is called. @@ -172,10 +172,15 @@ func New(root common.Hash, db Database) (*StateDB, error) { // NewWithReader creates a new state for the specified state root. Unlike New, // this function accepts an additional Reader which is bound to the given root. func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, error) { + hasher, err := db.Hasher(root) + if err != nil { + return nil, err + } sdb := &StateDB{ db: db, originalRoot: root, reader: reader, + hasher: hasher, stateObjects: make(map[common.Address]*stateObject), stateObjectsDestruct: make(map[common.Address]*stateObject), mutations: make(map[common.Address]*mutation), @@ -522,24 +527,6 @@ func (s *StateDB) GetTransientState(addr common.Address, key common.Hash) common // Setting, updating & deleting state object methods. // -// updateStateObject writes the given object to the trie. -func (s *StateDB) updateStateObject(obj *stateObject) { - // Encode the account and update the account trie - if err := s.trie.UpdateAccount(obj.Address(), &obj.data, len(obj.code)); err != nil { - s.setError(fmt.Errorf("updateStateObject (%x) error: %v", obj.Address(), err)) - } - if obj.dirtyCode { - s.trie.UpdateContractCode(obj.Address(), common.BytesToHash(obj.CodeHash()), obj.code) - } -} - -// deleteStateObject removes the given object from the state trie. -func (s *StateDB) deleteStateObject(addr common.Address) { - if err := s.trie.DeleteAccount(addr); err != nil { - s.setError(fmt.Errorf("deleteStateObject (%x) error: %v", addr[:], err)) - } -} - // getStateObject retrieves a state object given by the address, returning nil if // the object is not found or was deleted in this execution context. func (s *StateDB) getStateObject(addr common.Address) *stateObject { @@ -631,6 +618,7 @@ func (s *StateDB) Copy() *StateDB { state := &StateDB{ db: s.db, reader: s.reader, + hasher: s.hasher.Copy(), originalRoot: s.originalRoot, stateObjects: make(map[common.Address]*stateObject, len(s.stateObjects)), stateObjectsDestruct: make(map[common.Address]*stateObject, len(s.stateObjectsDestruct)), @@ -653,9 +641,6 @@ func (s *StateDB) Copy() *StateDB { transientStorage: s.transientStorage.Copy(), journal: s.journal.copy(), } - if s.trie != nil { - state.trie = mustCopyTrie(s.trie) - } if s.accessEvents != nil { state.accessEvents = s.accessEvents.Copy() } @@ -776,20 +761,6 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // Finalise all the dirty storage states and write them into the tries s.Finalise(deleteEmptyObjects) - // Initialize the trie if it's not constructed yet. If the prefetch - // is enabled, the trie constructed below will be replaced by the - // prefetched one. - // - // This operation must be done before state object storage hashing, - // as it assumes the main trie is already loaded. - if s.trie == nil { - tr, err := s.db.OpenTrie(s.originalRoot) - if err != nil { - s.setError(err) - return common.Hash{} - } - s.trie = tr - } // Process all storage updates concurrently. The state object update root // method will internally call a blocking trie fetch from the prefetcher, // so there's no need to explicitly wait for the prefetchers to finish. @@ -797,62 +768,12 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { start = time.Now() workers errgroup.Group ) - if s.db.TrieDB().IsVerkle() { - // Bypass per-account updateTrie() for binary trie. In binary trie mode - // there is only one unified trie (OpenStorageTrie returns self), so the - // per-account trie setup in updateTrie() (getPrefetchedTrie, getTrie, - // prefetcher.used) is redundant overhead. Apply all storage updates - // directly in a single pass. - for addr, op := range s.mutations { - if op.applied || op.isDelete() { - continue - } - obj := s.stateObjects[addr] - if len(obj.uncommittedStorage) == 0 { - continue - } - for key, origin := range obj.uncommittedStorage { - value, exist := obj.pendingStorage[key] - if value == origin || !exist { - continue - } - if (value != common.Hash{}) { - if err := s.trie.UpdateStorage(addr, key[:], common.TrimLeftZeroes(value[:])); err != nil { - s.setError(err) - } - s.StorageUpdated.Add(1) - } else { - if err := s.trie.DeleteStorage(addr, key[:]); err != nil { - s.setError(err) - } - s.StorageDeleted.Add(1) - } - } - } - // Clear uncommittedStorage and assign trie on each touched object. - // obj.trie must be set because this path bypasses updateTrie(), which - // is where obj.trie normally gets lazily loaded via getTrie(). - for addr, op := range s.mutations { - if op.applied || op.isDelete() { - continue - } - obj := s.stateObjects[addr] - if len(obj.uncommittedStorage) > 0 { - obj.uncommittedStorage = make(Storage) - } - obj.trie = s.trie - } - } else { - for addr, op := range s.mutations { - if op.applied || op.isDelete() { - continue - } - obj := s.stateObjects[addr] // closure for the task runner below - workers.Go(func() error { - obj.updateRoot() - return nil - }) + for addr, op := range s.mutations { + if op.applied || op.isDelete() { + continue } + obj := s.stateObjects[addr] // closure for the task runner below + workers.Go(obj.updateTrie) } workers.Wait() s.StorageUpdates += time.Since(start) @@ -866,18 +787,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // here could result in losing uncommitted changes from storage. start = time.Now() - // 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 + addresses []common.Address + accounts []AccountMutation ) for addr, op := range s.mutations { if op.applied { @@ -885,11 +797,18 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { } op.applied = true + addresses = append(addresses, addr) if op.isDelete() { - deletedAddrs = append(deletedAddrs, addr) + accounts = append(accounts, AccountMutation{Account: nil}) } else { obj := s.stateObjects[addr] - s.updateStateObject(obj) + mut := AccountMutation{ + Account: &obj.data, + DirtyCode: obj.dirtyCode, + Code: obj.code, + } + accounts = append(accounts, mut) + s.AccountUpdated += 1 // Count code writes post-Finalise so reverted CREATEs are excluded. @@ -899,16 +818,15 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { } } } - for _, deletedAddr := range deletedAddrs { - s.deleteStateObject(deletedAddr) - s.AccountDeleted += 1 + if err := s.hasher.UpdateAccount(addresses, accounts); err != nil { + return common.Hash{} } s.AccountUpdates += time.Since(start) // Track the amount of time wasted on hashing the account trie defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now()) - return s.trie.Hash() + return s.hasher.Hash() } // SetTxContext sets the current transaction hash and index which are @@ -925,11 +843,11 @@ func (s *StateDB) clearJournalAndRefund() { } // deleteStorage is designed to delete the storage trie of a designated account. -func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) { +func (s *StateDB) deleteStorage(addrHash common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) { var ( - nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil) - storages = make(map[common.Hash][]byte) // the set for storage mutations (value is nil) - storageOrigins = make(map[common.Hash][]byte) // the set for tracking the original value of slot + nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil) + storages = make(map[common.Hash]common.Hash) // the set for storage mutations (value is nil) + storageOrigins = make(map[common.Hash]common.Hash) // the set for tracking the original value of slot ) iteratee, err := s.db.Iteratee(s.originalRoot) if err != nil { @@ -950,8 +868,15 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com return nil, nil, nil, err } key := it.Hash() - storages[key] = nil - storageOrigins[key] = slot + storages[key] = common.Hash{} + + _, content, _, err := rlp.Split(it.Slot()) + if err != nil { + return nil, nil, nil, err + } + var value common.Hash + value.SetBytes(content) + storageOrigins[key] = value if err := stack.Update(key.Bytes(), slot); err != nil { return nil, nil, nil, err @@ -960,9 +885,7 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com if err := it.Error(); err != nil { // error might occur during iteration return nil, nil, nil, err } - if stack.Hash() != root { - return nil, nil, nil, fmt.Errorf("snapshot is not matched, exp %x, got %x", root, stack.Hash()) - } + stack.Hash() // Commit the right boundary return storages, storageOrigins, nodes, nil } @@ -984,9 +907,9 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com // with their values be tracked as original value. // In case (d), **original** account along with its storages should be deleted, // with their values be tracked as original value. -func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*accountDelete, []*trienode.NodeSet, error) { +func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*accountDelete, *trienode.MergedNodeSet, error) { var ( - nodes []*trienode.NodeSet + nodes = trienode.NewMergedNodeSet() deletes = make(map[common.Hash]*accountDelete) ) for addr, prevObj := range s.stateObjectsDestruct { @@ -1004,19 +927,19 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco addrHash := crypto.Keccak256Hash(addr.Bytes()) op := &accountDelete{ address: addr, - origin: types.SlimAccountRLP(*prev), + origin: *prev, } deletes[addrHash] = op // Short circuit if the origin storage was empty. - if prev.Root == types.EmptyRootHash || s.db.TrieDB().IsVerkle() { + if s.db.TrieDB().IsVerkle() { continue } if noStorageWiping { return nil, nil, fmt.Errorf("unexpected storage wiping, %x", addr) } // Remove storage slots belonging to the account. - storages, storagesOrigin, set, err := s.deleteStorage(addrHash, prev.Root) + storages, storagesOrigin, set, err := s.deleteStorage(addrHash) if err != nil { return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err) } @@ -1024,16 +947,13 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco op.storagesOrigin = storagesOrigin // Aggregate the associated trie node changes. - nodes = append(nodes, set) + if err := nodes.Merge(set); err != nil { + return nil, nil, err + } } return deletes, nodes, nil } -// GetTrie returns the account trie. -func (s *StateDB) GetTrie() Trie { - return s.trie -} - // commit gathers the state mutations accumulated along with the associated // trie changes, resetting all internal flags with the new state as the base. func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNumber uint64) (*stateUpdate, error) { @@ -1055,82 +975,16 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum storageTrieNodesUpdated int storageTrieNodesDeleted int - lock sync.Mutex // protect two maps below - nodes = trienode.NewMergedNodeSet() // aggregated trie nodes updates = make(map[common.Hash]*accountUpdate, len(s.mutations)) // aggregated account updates - - // merge aggregates the dirty trie nodes into the global set. - // - // Given that some accounts may be destroyed and then recreated within - // the same block, it's possible that a node set with the same owner - // may already exist. In such cases, these two sets are combined, with - // the later one overwriting the previous one if any nodes are modified - // or deleted in both sets. - // - // merge run concurrently across all the state objects and account trie. - merge = func(set *trienode.NodeSet) error { - if set == nil { - return nil - } - lock.Lock() - defer lock.Unlock() - - updates, deletes := set.Size() - if set.Owner == (common.Hash{}) { - accountTrieNodesUpdated += updates - accountTrieNodesDeleted += deletes - } else { - storageTrieNodesUpdated += updates - storageTrieNodesDeleted += deletes - } - return nodes.Merge(set) - } ) // Given that some accounts could be destroyed and then recreated within // the same block, account deletions must be processed first. This ensures // that the storage trie nodes deleted during destruction and recreated // during subsequent resurrection can be combined correctly. - deletes, delNodes, err := s.handleDestruction(noStorageWiping) + deletes, nodes, err := s.handleDestruction(noStorageWiping) if err != nil { return nil, err } - for _, set := range delNodes { - if err := merge(set); err != nil { - return nil, err - } - } - // Handle all state updates afterwards, concurrently to one another to shave - // off some milliseconds from the commit operation. Also accumulate the code - // writes to run in parallel with the computations. - var ( - start = time.Now() - workers errgroup.Group - ) - // Schedule the account trie first since that will be the biggest, so give - // it the most time to crunch. - // - // TODO(karalabe): This account trie commit is *very* heavy. 5-6ms at chain - // heads, which seems excessive given that it doesn't do hashing, it just - // shuffles some data. For comparison, the *hashing* at chain head is 2-3ms. - // We need to investigate what's happening as it seems something's wonky. - // Obviously it's not an end of the world issue, just something the original - // code didn't anticipate for. - workers.Go(func() error { - // Write the account trie changes, measuring the amount of wasted time - _, set := s.trie.Commit(true) - if err := merge(set); err != nil { - return err - } - s.AccountCommits = time.Since(start) - return nil - }) - // Schedule each of the storage tries that need to be updated, so they can - // run concurrently to one another. - // - // TODO(karalabe): Experimentally, the account commit takes approximately the - // same time as all the storage commits combined, so we could maybe only have - // 2 threads in total. But that kind of depends on the account commit being - // more expensive than it should be, so let's fix that and revisit this todo. for addr, op := range s.mutations { if op.isDelete() { continue @@ -1140,25 +994,20 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum if obj == nil { return nil, errors.New("missing state object") } - // Run the storage updates concurrently to one another - workers.Go(func() error { - // Write any storage changes in the state object to its storage trie - update, set, err := obj.commit() - if err != nil { - return err - } - if err := merge(set); err != nil { - return err - } - lock.Lock() - updates[obj.addrHash()] = update - s.StorageCommits = time.Since(start) // overwrite with the longest storage commit runtime - lock.Unlock() - return nil - }) + update, err := obj.commit() + if err != nil { + return nil, err + } + updates[obj.addrHash()] = update } - // Wait for everything to finish and update the metrics - if err := workers.Wait(); err != nil { + // Handle all state updates afterwards, concurrently to one another to shave + // off some milliseconds from the commit operation. Also accumulate the code + // writes to run in parallel with the computations. + root, set, secondaryHashes, err := s.hasher.Commit() + if err != nil { + return nil, err + } + if err := nodes.MergeSet(set); err != nil { return nil, err } accountReadMeters.Mark(int64(s.AccountLoaded)) @@ -1185,7 +1034,7 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum origin := s.originalRoot s.originalRoot = root - return newStateUpdate(noStorageWiping, origin, root, blockNumber, deletes, updates, nodes), nil + return newStateUpdate(noStorageWiping, origin, root, blockNumber, deletes, updates, nodes, secondaryHashes), nil } // commitAndFlush is a wrapper of commit which also commits the state mutations @@ -1209,6 +1058,7 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag // The reader update must be performed as the final step, otherwise, // the new state would not be visible before db.commit. s.reader, _ = s.db.Reader(s.originalRoot) + s.hasher, _ = s.db.Hasher(s.originalRoot) return ret, err } diff --git a/core/state/statedb_fuzz_test.go b/core/state/statedb_fuzz_test.go index 3582185344..82866628f8 100644 --- a/core/state/statedb_fuzz_test.go +++ b/core/state/statedb_fuzz_test.go @@ -21,7 +21,6 @@ import ( "encoding/binary" "errors" "fmt" - "maps" "math" "math/rand" "reflect" @@ -183,10 +182,10 @@ func (test *stateTest) run() bool { storages []map[common.Hash]map[common.Hash][]byte storageOrigin []map[common.Address]map[common.Hash][]byte copyUpdate = func(update *stateUpdate) { - accounts = append(accounts, maps.Clone(update.accounts)) - accountOrigin = append(accountOrigin, maps.Clone(update.accountsOrigin)) - storages = append(storages, maps.Clone(update.storages)) - storageOrigin = append(storageOrigin, maps.Clone(update.storagesOrigin)) + //accounts = append(accounts, maps.Clone(update.accounts)) + //accountOrigin = append(accountOrigin, maps.Clone(update.accountsOrigin)) + //storages = append(storages, maps.Clone(update.storages)) + //storageOrigin = append(storageOrigin, maps.Clone(update.storagesOrigin)) } disk = rawdb.NewMemoryDatabase() tdb = triedb.NewDatabase(disk, &triedb.Config{PathDB: pathdb.Defaults}) diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 6936372c50..3ec715d237 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -32,13 +32,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb/hashdb" "github.com/ethereum/go-ethereum/triedb/pathdb" @@ -232,7 +229,7 @@ func TestCopyWithDirtyJournal(t *testing.T) { for i := byte(0); i < 255; i++ { obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i})) obj.AddBalance(uint256.NewInt(uint64(i))) - obj.data.Root = common.HexToHash("0xdeadbeef") + //obj.data.Root = common.HexToHash("0xdeadbeef") } root, _ := orig.Commit(0, true, false) orig, _ = New(root, db) @@ -275,7 +272,7 @@ func TestCopyObjectState(t *testing.T) { for i := byte(0); i < 5; i++ { obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i})) obj.AddBalance(uint256.NewInt(uint64(i))) - obj.data.Root = common.HexToHash("0xdeadbeef") + //obj.data.Root = common.HexToHash("0xdeadbeef") } orig.Finalise(true) cpy := orig.Copy() @@ -1269,60 +1266,6 @@ func TestStateDBTransientStorage(t *testing.T) { } } -func TestDeleteStorage(t *testing.T) { - var ( - disk = rawdb.NewMemoryDatabase() - tdb = triedb.NewDatabase(disk, nil) - snaps, _ = snapshot.New(snapshot.Config{CacheSize: 10}, disk, tdb, types.EmptyRootHash) - db = NewDatabase(tdb, nil).WithSnapshot(snaps) - state, _ = New(types.EmptyRootHash, db) - addr = common.HexToAddress("0x1") - ) - // Initialize account and populate storage - state.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified) - state.CreateAccount(addr) - for i := 0; i < 1000; i++ { - slot := common.Hash(uint256.NewInt(uint64(i)).Bytes32()) - value := common.Hash(uint256.NewInt(uint64(10 * i)).Bytes32()) - state.SetState(addr, slot, value) - } - root, _ := state.Commit(0, true, false) - // Init phase done, create two states, one with snap and one without - fastState, _ := New(root, NewDatabase(tdb, nil).WithSnapshot(snaps)) - slowState, _ := New(root, NewDatabase(tdb, nil)) - - obj := fastState.getOrNewStateObject(addr) - storageRoot := obj.data.Root - - _, _, fastNodes, err := fastState.deleteStorage(crypto.Keccak256Hash(addr[:]), storageRoot) - if err != nil { - t.Fatal(err) - } - - _, _, slowNodes, err := slowState.deleteStorage(crypto.Keccak256Hash(addr[:]), storageRoot) - if err != nil { - t.Fatal(err) - } - check := func(set *trienode.NodeSet) string { - var a []string - set.ForEachWithOrder(func(path string, n *trienode.Node) { - if n.Hash != (common.Hash{}) { - t.Fatal("delete should have empty hashes") - } - if len(n.Blob) != 0 { - t.Fatal("delete should have empty blobs") - } - a = append(a, fmt.Sprintf("%x", path)) - }) - return strings.Join(a, ",") - } - slowRes := check(slowNodes) - fastRes := check(fastNodes) - if slowRes != fastRes { - t.Fatalf("difference found:\nfast: %v\nslow: %v\n", fastRes, slowRes) - } -} - func TestStorageDirtiness(t *testing.T) { var ( disk = rawdb.NewMemoryDatabase() diff --git a/core/state/stateupdate.go b/core/state/stateupdate.go index 1c171cbd5e..6402733f4c 100644 --- a/core/state/stateupdate.go +++ b/core/state/stateupdate.go @@ -23,77 +23,71 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/triedb" ) -// contractCode represents contract bytecode along with its associated metadata. +// contractCode encapsulates contract bytecode and its associated metadata. type contractCode struct { hash common.Hash // hash is the cryptographic hash of the current contract code. - blob []byte // blob is the binary representation of the current contract code. - originHash common.Hash // originHash is the cryptographic hash of the code before mutation. + blob []byte // blob is the raw byte representation of the current contract code. + originHash common.Hash // originHash is the cryptographic hash of the code prior to mutation. // Derived fields, populated only when state tracking is enabled. duplicate bool // duplicate indicates whether the updated code already exists. - originBlob []byte // originBlob is the original binary representation of the contract code. + originBlob []byte // originBlob is the original byte representation of the contract code. } -// accountDelete represents an operation for deleting an Ethereum account. +// accountDelete represents a deletion operation for an Ethereum account. type accountDelete struct { - address common.Address // address is the unique account identifier - origin []byte // origin is the original value of account data in slim-RLP encoding. + address common.Address // address uniquely identifies the account. + origin Account // origin is the account state prior to deletion. - // storages stores mutated slots, the value should be nil. - storages map[common.Hash][]byte - - // storagesOrigin stores the original values of mutated slots in - // prefix-zero-trimmed RLP format. The map key refers to the **HASH** - // of the raw storage slot key. - storagesOrigin map[common.Hash][]byte + storages map[common.Hash]common.Hash // storages contains mutated storage slots. + storagesOrigin map[common.Hash]common.Hash // storagesOrigin holds original values of mutated slots; keys are hashes of raw storage slot keys. } -// accountUpdate represents an operation for updating an Ethereum account. +// accountUpdate represents an update operation for an Ethereum account. type accountUpdate struct { - address common.Address // address is the unique account identifier - data []byte // data is the slim-RLP encoded account data. - origin []byte // origin is the original value of account data in slim-RLP encoding. - code *contractCode // code represents mutated contract code; nil means it's not modified. - storages map[common.Hash][]byte // storages stores mutated slots in prefix-zero-trimmed RLP format. + address common.Address // address uniquely identifies the account. + data *Account // data is the updated account state; nil indicates deletion. + origin *Account // origin is the previous account state; nil indicates non-existence. + code *contractCode // code contains updated contract code; nil if unchanged. + storages map[common.Hash]common.Hash // storages contains updated storage slots. - // storagesOriginByKey and storagesOriginByHash both store the original values - // of mutated slots in prefix-zero-trimmed RLP format. The difference is that - // storagesOriginByKey uses the **raw** storage slot key as the map ID, while - // storagesOriginByHash uses the **hash** of the storage slot key instead. - storagesOriginByKey map[common.Hash][]byte - storagesOriginByHash map[common.Hash][]byte + // storagesOriginByKey and storagesOriginByHash both record original values + // of mutated storage slots: + // - storagesOriginByKey uses raw storage slot keys. + // - storagesOriginByHash uses hashed storage slot keys. + storagesOriginByKey map[common.Hash]common.Hash + storagesOriginByHash map[common.Hash]common.Hash } -// stateUpdate represents the difference between two states resulting from state -// execution. It contains information about mutated contract codes, accounts, -// and storage slots, along with their original values. +// stateUpdate captures the difference between two states resulting from +// execution. It records all mutated accounts, contract codes, and storage +// slots, along with their original values. type stateUpdate struct { - originRoot common.Hash // hash of the state before applying mutation - root common.Hash // hash of the state after applying mutation - blockNumber uint64 // Associated block number + originRoot common.Hash // originRoot is the state root before applying changes. + root common.Hash // root is the state root after applying changes. + blockNumber uint64 // blockNumber is the associated block height. - accounts map[common.Hash][]byte // accounts stores mutated accounts in 'slim RLP' encoding - accountsOrigin map[common.Address][]byte // accountsOrigin stores the original values of mutated accounts in 'slim RLP' encoding + accounts map[common.Hash]*Account // accounts contains mutated accounts, keyed by account hash. + accountsOrigin map[common.Address]*Account // accountsOrigin holds original values of mutated accounts, keyed by address. - // storages stores mutated slots in 'prefix-zero-trimmed' RLP format. - // The value is keyed by account hash and **storage slot key hash**. - storages map[common.Hash]map[common.Hash][]byte + // storages contains mutated storage slots, keyed by account hash and + // storage slot key hash. + storages map[common.Hash]map[common.Hash]common.Hash - // storagesOrigin stores the original values of mutated slots in - // 'prefix-zero-trimmed' RLP format. - // (a) the value is keyed by account hash and **storage slot key** if rawStorageKey is true; - // (b) the value is keyed by account hash and **storage slot key hash** if rawStorageKey is false; - storagesOrigin map[common.Address]map[common.Hash][]byte + // storagesOrigin holds original values of mutated storage slots. + // The key format depends on rawStorageKey: + // - if true: keyed by account address and raw storage slot key. + // - if false: keyed by account address and storage slot key hash. + storagesOrigin map[common.Address]map[common.Hash]common.Hash rawStorageKey bool - codes map[common.Address]*contractCode // codes contains the set of dirty codes - nodes *trienode.MergedNodeSet // Aggregated dirty nodes caused by state changes + codes map[common.Address]*contractCode // codes contains mutated contract codes, keyed by address. + nodes *trienode.MergedNodeSet // nodes aggregates all dirty trie nodes produced by the update. + secondaryHashes map[common.Address]SecondaryHash // hashes of secondary tries } // empty returns a flag indicating the state transition is empty or not. @@ -107,12 +101,12 @@ func (sc *stateUpdate) empty() bool { // // rawStorageKey is a flag indicating whether to use the raw storage slot key or // the hash of the slot key for constructing state update object. -func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash, blockNumber uint64, deletes map[common.Hash]*accountDelete, updates map[common.Hash]*accountUpdate, nodes *trienode.MergedNodeSet) *stateUpdate { +func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash, blockNumber uint64, deletes map[common.Hash]*accountDelete, updates map[common.Hash]*accountUpdate, nodes *trienode.MergedNodeSet, secondaryHashes map[common.Address]SecondaryHash) *stateUpdate { var ( - accounts = make(map[common.Hash][]byte) - accountsOrigin = make(map[common.Address][]byte) - storages = make(map[common.Hash]map[common.Hash][]byte) - storagesOrigin = make(map[common.Address]map[common.Hash][]byte) + accounts = make(map[common.Hash]*Account) + accountsOrigin = make(map[common.Address]*Account) + storages = make(map[common.Hash]map[common.Hash]common.Hash) + storagesOrigin = make(map[common.Address]map[common.Hash]common.Hash) codes = make(map[common.Address]*contractCode) ) // Since some accounts might be destroyed and recreated within the same @@ -120,7 +114,7 @@ func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash for addrHash, op := range deletes { addr := op.address accounts[addrHash] = nil - accountsOrigin[addr] = op.origin + accountsOrigin[addr] = &op.origin // If storage wiping exists, the hash of the storage slot key must be used if len(op.storages) > 0 { @@ -174,16 +168,17 @@ func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash } } return &stateUpdate{ - originRoot: originRoot, - root: root, - blockNumber: blockNumber, - accounts: accounts, - accountsOrigin: accountsOrigin, - storages: storages, - storagesOrigin: storagesOrigin, - rawStorageKey: rawStorageKey, - codes: codes, - nodes: nodes, + originRoot: originRoot, + root: root, + blockNumber: blockNumber, + accounts: accounts, + accountsOrigin: accountsOrigin, + storages: storages, + storagesOrigin: storagesOrigin, + rawStorageKey: rawStorageKey, + codes: codes, + nodes: nodes, + secondaryHashes: secondaryHashes, } } @@ -192,13 +187,14 @@ func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash // struct and formats it into the StateSet structure consumed by the triedb // package. func (sc *stateUpdate) stateSet() *triedb.StateSet { - return &triedb.StateSet{ - Accounts: sc.accounts, - AccountsOrigin: sc.accountsOrigin, - Storages: sc.storages, - StoragesOrigin: sc.storagesOrigin, - RawStorageKey: sc.rawStorageKey, - } + return nil + //return &triedb.StateSet{ + // Accounts: sc.accounts, + // AccountsOrigin: sc.accountsOrigin, + // Storages: sc.storages, + // StoragesOrigin: sc.storagesOrigin, + // RawStorageKey: sc.rawStorageKey, + //} } // deriveCodeFields derives the missing fields of contract code changes @@ -230,140 +226,141 @@ func (sc *stateUpdate) deriveCodeFields(reader ContractCodeReader) error { // ToTracingUpdate converts the internal stateUpdate to an exported tracing.StateUpdate. func (sc *stateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) { - update := &tracing.StateUpdate{ - OriginRoot: sc.originRoot, - Root: sc.root, - BlockNumber: sc.blockNumber, - AccountChanges: make(map[common.Address]*tracing.AccountChange, len(sc.accountsOrigin)), - StorageChanges: make(map[common.Address]map[common.Hash]*tracing.StorageChange), - CodeChanges: make(map[common.Address]*tracing.CodeChange, len(sc.codes)), - TrieChanges: make(map[common.Hash]map[string]*tracing.TrieNodeChange), - } - // Gather all account changes - for addr, oldData := range sc.accountsOrigin { - addrHash := crypto.Keccak256Hash(addr.Bytes()) - newData, exists := sc.accounts[addrHash] - if !exists { - return nil, fmt.Errorf("account %x not found", addr) - } - change := &tracing.AccountChange{} - - if len(oldData) > 0 { - acct, err := types.FullAccount(oldData) - if err != nil { - return nil, err - } - change.Prev = &types.StateAccount{ - Nonce: acct.Nonce, - Balance: acct.Balance, - Root: acct.Root, - CodeHash: acct.CodeHash, - } - } - if len(newData) > 0 { - acct, err := types.FullAccount(newData) - if err != nil { - return nil, err - } - change.New = &types.StateAccount{ - Nonce: acct.Nonce, - Balance: acct.Balance, - Root: acct.Root, - CodeHash: acct.CodeHash, - } - } - update.AccountChanges[addr] = change - } - - // Gather all storage slot changes - for addr, slots := range sc.storagesOrigin { - addrHash := crypto.Keccak256Hash(addr.Bytes()) - subset, exists := sc.storages[addrHash] - if !exists { - return nil, fmt.Errorf("storage %x not found", addr) - } - storageChanges := make(map[common.Hash]*tracing.StorageChange, len(slots)) - - for key, encPrev := range slots { - // Get new value - handle both raw and hashed key formats - var ( - exists bool - encNew []byte - decPrev []byte - decNew []byte - err error - ) - if sc.rawStorageKey { - encNew, exists = subset[crypto.Keccak256Hash(key.Bytes())] - } else { - encNew, exists = subset[key] - } - if !exists { - return nil, fmt.Errorf("storage slot %x-%x not found", addr, key) - } - - // Decode the prev and new values - if len(encPrev) > 0 { - _, decPrev, _, err = rlp.Split(encPrev) - if err != nil { - return nil, fmt.Errorf("failed to decode prevValue: %v", err) - } - } - if len(encNew) > 0 { - _, decNew, _, err = rlp.Split(encNew) - if err != nil { - return nil, fmt.Errorf("failed to decode newValue: %v", err) - } - } - storageChanges[key] = &tracing.StorageChange{ - Prev: common.BytesToHash(decPrev), - New: common.BytesToHash(decNew), - } - } - update.StorageChanges[addr] = storageChanges - } - - // Gather all contract code changes - for addr, code := range sc.codes { - change := &tracing.CodeChange{ - New: &tracing.ContractCode{ - Hash: code.hash, - Code: code.blob, - Exists: code.duplicate, - }, - } - if code.originHash != types.EmptyCodeHash { - change.Prev = &tracing.ContractCode{ - Hash: code.originHash, - Code: code.originBlob, - Exists: true, - } - } - update.CodeChanges[addr] = change - } - - // Gather all trie node changes - if sc.nodes != nil { - for owner, subset := range sc.nodes.Sets { - nodeChanges := make(map[string]*tracing.TrieNodeChange, len(subset.Origins)) - for path, oldNode := range subset.Origins { - newNode, exists := subset.Nodes[path] - if !exists { - return nil, fmt.Errorf("node %x-%v not found", owner, path) - } - nodeChanges[path] = &tracing.TrieNodeChange{ - Prev: &trienode.Node{ - Hash: crypto.Keccak256Hash(oldNode), - Blob: oldNode, - }, - New: &trienode.Node{ - Hash: newNode.Hash, - Blob: newNode.Blob, - }, - } - } - update.TrieChanges[owner] = nodeChanges - } - } - return update, nil + return nil, nil + //update := &tracing.StateUpdate{ + // OriginRoot: sc.originRoot, + // Root: sc.root, + // BlockNumber: sc.blockNumber, + // AccountChanges: make(map[common.Address]*tracing.AccountChange, len(sc.accountsOrigin)), + // StorageChanges: make(map[common.Address]map[common.Hash]*tracing.StorageChange), + // CodeChanges: make(map[common.Address]*tracing.CodeChange, len(sc.codes)), + // TrieChanges: make(map[common.Hash]map[string]*tracing.TrieNodeChange), + //} + //// Gather all account changes + //for addr, oldData := range sc.accountsOrigin { + // addrHash := crypto.Keccak256Hash(addr.Bytes()) + // newData, exists := sc.accounts[addrHash] + // if !exists { + // return nil, fmt.Errorf("account %x not found", addr) + // } + // change := &tracing.AccountChange{} + // + // if len(oldData) > 0 { + // acct, err := types.FullAccount(oldData) + // if err != nil { + // return nil, err + // } + // change.Prev = &types.StateAccount{ + // Nonce: acct.Nonce, + // Balance: acct.Balance, + // Root: acct.Root, + // CodeHash: acct.CodeHash, + // } + // } + // if len(newData) > 0 { + // acct, err := types.FullAccount(newData) + // if err != nil { + // return nil, err + // } + // change.New = &types.StateAccount{ + // Nonce: acct.Nonce, + // Balance: acct.Balance, + // Root: acct.Root, + // CodeHash: acct.CodeHash, + // } + // } + // update.AccountChanges[addr] = change + //} + // + //// Gather all storage slot changes + //for addr, slots := range sc.storagesOrigin { + // addrHash := crypto.Keccak256Hash(addr.Bytes()) + // subset, exists := sc.storages[addrHash] + // if !exists { + // return nil, fmt.Errorf("storage %x not found", addr) + // } + // storageChanges := make(map[common.Hash]*tracing.StorageChange, len(slots)) + // + // for key, encPrev := range slots { + // // Get new value - handle both raw and hashed key formats + // var ( + // exists bool + // encNew []byte + // decPrev []byte + // decNew []byte + // err error + // ) + // if sc.rawStorageKey { + // encNew, exists = subset[crypto.Keccak256Hash(key.Bytes())] + // } else { + // encNew, exists = subset[key] + // } + // if !exists { + // return nil, fmt.Errorf("storage slot %x-%x not found", addr, key) + // } + // + // // Decode the prev and new values + // if len(encPrev) > 0 { + // _, decPrev, _, err = rlp.Split(encPrev) + // if err != nil { + // return nil, fmt.Errorf("failed to decode prevValue: %v", err) + // } + // } + // if len(encNew) > 0 { + // _, decNew, _, err = rlp.Split(encNew) + // if err != nil { + // return nil, fmt.Errorf("failed to decode newValue: %v", err) + // } + // } + // storageChanges[key] = &tracing.StorageChange{ + // Prev: common.BytesToHash(decPrev), + // New: common.BytesToHash(decNew), + // } + // } + // update.StorageChanges[addr] = storageChanges + //} + // + //// Gather all contract code changes + //for addr, code := range sc.codes { + // change := &tracing.CodeChange{ + // New: &tracing.ContractCode{ + // Hash: code.hash, + // Code: code.blob, + // Exists: code.duplicate, + // }, + // } + // if code.originHash != types.EmptyCodeHash { + // change.Prev = &tracing.ContractCode{ + // Hash: code.originHash, + // Code: code.originBlob, + // Exists: true, + // } + // } + // update.CodeChanges[addr] = change + //} + // + //// Gather all trie node changes + //if sc.nodes != nil { + // for owner, subset := range sc.nodes.Sets { + // nodeChanges := make(map[string]*tracing.TrieNodeChange, len(subset.Origins)) + // for path, oldNode := range subset.Origins { + // newNode, exists := subset.Nodes[path] + // if !exists { + // return nil, fmt.Errorf("node %x-%v not found", owner, path) + // } + // nodeChanges[path] = &tracing.TrieNodeChange{ + // Prev: &trienode.Node{ + // Hash: crypto.Keccak256Hash(oldNode), + // Blob: oldNode, + // }, + // New: &trienode.Node{ + // Hash: newNode.Hash, + // Blob: newNode.Blob, + // }, + // } + // } + // update.TrieChanges[owner] = nodeChanges + // } + //} + //return update, nil } diff --git a/trie/trienode/node.go b/trie/trienode/node.go index 228a64f04c..0bd630c3b3 100644 --- a/trie/trienode/node.go +++ b/trie/trienode/node.go @@ -259,6 +259,16 @@ func (set *MergedNodeSet) Merge(other *NodeSet) error { return nil } +// MergeSet merges the provided set into local one. +func (set *MergedNodeSet) MergeSet(other *MergedNodeSet) error { + for _, subset := range other.Sets { + if err := set.Merge(subset); err != nil { + return err + } + } + return nil +} + // Nodes returns a two-dimensional map for internal nodes. func (set *MergedNodeSet) Nodes() map[common.Hash]map[string]*Node { nodes := make(map[common.Hash]map[string]*Node, len(set.Sets))