package state import ( "fmt" "sort" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) const ( // storageDeleteLimit denotes the highest permissible memory allocation // employed for contract storage deletion. storageDeleteLimit = 512 * 1024 * 1024 ) /* The portions of the EVM we want to leverage only use the following methods (The rest can be left with panics for now): * Exist * CreateAccount * GetCodeHash * AddAddressToAccessList * {Get,Set}Nonce * {Get,Set,Add,Sub}Balance * {Get,Set}Code * {Get,Set}State * {Get,Set}TransientState * Snapshot * RevertToSnapshot */ var _ vm.StateDB = &StateDB{} type revision struct { id int journalIndex int } // StateDB structs within the ethereum protocol are used to store anything // within the merkle trie. StateDBs take care of caching and storing // nested states. It's the general query interface to retrieve: // // * Contracts // * Accounts // // Once the state is committed, tries cached in stateDB (including account // trie, storage tries) will no longer be functional. A new state instance // must be created with new root and updated database for accessing post- // commit states. type StateDB struct { db Database hasher crypto.KeccakState // originBlockHash is the blockhash for the state we are working on top of originBlockHash common.Hash // originalRoot is the pre-state root, before any changes were made. // It will be updated when the Commit is called. originalRoot common.Hash // These maps hold the state changes (including the corresponding // original value) that occurred in this **block**. accounts map[common.Hash][]byte // The mutated accounts in 'slim RLP' encoding storages map[common.Hash]map[common.Hash][]byte // The mutated slots in prefix-zero trimmed rlp format accountsOrigin map[common.Address][]byte // The original value of mutated accounts in 'slim RLP' encoding storagesOrigin map[common.Address]map[common.Hash][]byte // The original value of mutated slots in prefix-zero trimmed rlp format // This map holds 'live' objects, which will get modified while processing // a state transition. stateObjects map[common.Address]*stateObject stateObjectsPending map[common.Address]struct{} // State objects finalized but not yet written to the trie stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution stateObjectsDestruct map[common.Address]*types.StateAccount // State objects destructed in the block along with its previous value // DB error. // State objects are used by the consensus core and VM which are // unable to deal with database-level errors. Any error that occurs // during a database read is memoized here and will eventually be returned // by StateDB.Commit. dbErr error // The refund counter, also used by state transitioning. refund uint64 // The tx context and all occurred logs in the scope of transaction. thash common.Hash txIndex int logs map[common.Hash][]*types.Log logSize uint // Preimages occurred seen by VM in the scope of block. preimages map[common.Hash][]byte // Per-transaction access list accessList *accessList // Transient storage transientStorage transientStorage // Journal of state modifications. This is the backbone of // Snapshot and RevertToSnapshot. journal *journal validRevisions []revision nextRevisionId int // Measurements gathered during execution for debugging purposes AccountReads time.Duration StorageReads time.Duration } // New creates a new StateDB on the state for the provided blockHash func New(blockHash common.Hash, db Database) (*StateDB, error) { sdb := &StateDB{ db: db, originBlockHash: blockHash, stateObjects: make(map[common.Address]*stateObject), stateObjectsPending: make(map[common.Address]struct{}), stateObjectsDirty: make(map[common.Address]struct{}), stateObjectsDestruct: make(map[common.Address]*types.StateAccount), logs: make(map[common.Hash][]*types.Log), preimages: make(map[common.Hash][]byte), journal: newJournal(), accessList: newAccessList(), transientStorage: newTransientStorage(), hasher: crypto.NewKeccakState(), } return sdb, nil } // setError remembers the first non-nil error it is called with. func (s *StateDB) setError(err error) { if s.dbErr == nil { s.dbErr = err } } func (s *StateDB) AddLog(log *types.Log) { s.journal.append(addLogChange{txhash: s.thash}) log.TxHash = s.thash log.TxIndex = uint(s.txIndex) log.Index = s.logSize s.logs[s.thash] = append(s.logs[s.thash], log) s.logSize++ } // AddPreimage records a SHA3 preimage seen by the VM. func (s *StateDB) AddPreimage(hash common.Hash, preimage []byte) { if _, ok := s.preimages[hash]; !ok { s.journal.append(addPreimageChange{hash: hash}) pi := make([]byte, len(preimage)) copy(pi, preimage) s.preimages[hash] = pi } } // AddRefund adds gas to the refund counter func (s *StateDB) AddRefund(gas uint64) { s.journal.append(refundChange{prev: s.refund}) s.refund += gas } // SubRefund removes gas from the refund counter. // This method will panic if the refund counter goes below zero func (s *StateDB) SubRefund(gas uint64) { s.journal.append(refundChange{prev: s.refund}) if gas > s.refund { panic(fmt.Sprintf("Refund counter below zero (gas: %d > refund: %d)", gas, s.refund)) } s.refund -= gas } // Exist reports whether the given account address exists in the state. // Notably this also returns true for self-destructed accounts. func (s *StateDB) Exist(addr common.Address) bool { return s.getStateObject(addr) != nil } // Empty returns whether the state object is either non-existent // or empty according to the EIP161 specification (balance = nonce = code = 0) func (s *StateDB) Empty(addr common.Address) bool { so := s.getStateObject(addr) return so == nil || so.empty() } // GetBalance retrieves the balance from the given address or 0 if object not found func (s *StateDB) GetBalance(addr common.Address) *uint256.Int { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Balance() } return common.U2560 } // 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 { return stateObject.Nonce() } 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 } func (s *StateDB) GetCode(addr common.Address) []byte { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Code() } return nil } func (s *StateDB) GetCodeSize(addr common.Address) int { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.CodeSize() } return 0 } func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { stateObject := s.getStateObject(addr) if stateObject != nil { return common.BytesToHash(stateObject.CodeHash()) } return common.Hash{} } // GetState retrieves a value from the given account's storage trie. func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.GetState(hash) } return common.Hash{} } // 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) if stateObject != nil { return stateObject.GetCommittedState(hash) } return common.Hash{} } // Database retrieves the low level database supporting the lower level trie ops. func (s *StateDB) Database() Database { return s.db } func (s *StateDB) HasSelfDestructed(addr common.Address) bool { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.selfDestructed } return false } /* * SETTERS */ // AddBalance adds amount to the account associated with addr. func (s *StateDB) AddBalance(addr common.Address, amount *uint256.Int) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { stateObject.AddBalance(amount) } } // SubBalance subtracts amount from the account associated with addr. func (s *StateDB) SubBalance(addr common.Address, amount *uint256.Int) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { stateObject.SubBalance(amount) } } func (s *StateDB) SetBalance(addr common.Address, amount *uint256.Int) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { stateObject.SetBalance(amount) } } func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { stateObject.SetNonce(nonce) } } func (s *StateDB) SetCode(addr common.Address, code []byte) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { stateObject.SetCode(crypto.Keccak256Hash(code), code) } } func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { stateObject.SetState(key, value) } } // SetStorage replaces the entire storage for the specified account with given // storage. This function should only be used for debugging and the mutations // must be discarded afterwards. func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common.Hash) { // SetStorage needs to wipe existing storage. We achieve this by pretending // that the account self-destructed earlier in this block, by flagging // it in stateObjectsDestruct. The effect of doing so is that storage lookups // will not hit disk, since it is assumed that the disk-data is belonging // to a previous incarnation of the object. // // TODO(rjl493456442) this function should only be supported by 'unwritable' // state and all mutations made should all be discarded afterwards. if _, ok := s.stateObjectsDestruct[addr]; !ok { s.stateObjectsDestruct[addr] = nil } stateObject := s.getOrNewStateObject(addr) for k, v := range storage { stateObject.SetState(k, v) } } // SelfDestruct marks the given account as selfdestructed. // This clears the account balance. // // The account's state object is still available until the state is committed, // getStateObject will return a non-nil account after SelfDestruct. func (s *StateDB) SelfDestruct(addr common.Address) { stateObject := s.getStateObject(addr) if stateObject == nil { return } s.journal.append(selfDestructChange{ account: &addr, prev: stateObject.selfDestructed, prevbalance: new(uint256.Int).Set(stateObject.Balance()), }) stateObject.markSelfdestructed() stateObject.data.Balance = new(uint256.Int) } func (s *StateDB) Selfdestruct6780(addr common.Address) { stateObject := s.getStateObject(addr) if stateObject == nil { return } if stateObject.created { s.SelfDestruct(addr) } } // SetTransientState sets transient storage for a given account. It // adds the change to the journal so that it can be rolled back // to its previous value if there is a revert. func (s *StateDB) SetTransientState(addr common.Address, key, value common.Hash) { prev := s.GetTransientState(addr, key) if prev == value { return } s.journal.append(transientStorageChange{ account: &addr, key: key, prevalue: prev, }) s.setTransientState(addr, key, value) } // setTransientState is a lower level setter for transient storage. It // is called during a revert to prevent modifications to the journal. func (s *StateDB) setTransientState(addr common.Address, key, value common.Hash) { s.transientStorage.Set(addr, key, value) } // GetTransientState gets transient storage for a given account. func (s *StateDB) GetTransientState(addr common.Address, key common.Hash) common.Hash { return s.transientStorage.Get(addr, key) } // // Setting, updating & deleting state object methods. // // getStateObject retrieves a state object given by the address, returning nil if // the object is not found or was deleted in this execution context. If you need // to differentiate between non-existent/just-deleted, use getDeletedStateObject. func (s *StateDB) getStateObject(addr common.Address) *stateObject { if obj := s.getDeletedStateObject(addr); obj != nil && !obj.deleted { return obj } return nil } // getDeletedStateObject is similar to getStateObject, but instead of returning // nil for a deleted state object, it returns the actual object with the deleted // flag set. This is needed by the state journal to revert to the correct s- // destructed object instead of wiping all knowledge about the state object. // TODO: func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { // Prefer live objects if any is available if obj := s.stateObjects[addr]; obj != nil { return obj } // If no live objects are available, load from the database // TODO: REPLACE TRIE ACCESS HERE // can add a fallback option to use ipfsethdb to do the trie access if direct access fails start := time.Now() addrHash := crypto.Keccak256Hash(addr.Bytes()) data, err := s.db.StateAccount(addrHash, s.originBlockHash) if metrics.EnabledExpensive { s.AccountReads += time.Since(start) } if err != nil { s.setError(fmt.Errorf("getDeletedStateObject (%x) error: %w", addr.Bytes(), err)) return nil } if data == nil { return nil } // Insert into the live set obj := newObject(s, addr, data, s.originBlockHash) s.setStateObject(obj) return obj } func (s *StateDB) setStateObject(object *stateObject) { s.stateObjects[object.Address()] = object } // getOrNewStateObject retrieves a state object or create a new state object if nil. func (s *StateDB) getOrNewStateObject(addr common.Address) *stateObject { stateObject := s.getStateObject(addr) if stateObject == nil { stateObject, _ = s.createObject(addr) } return stateObject } // createObject creates a new state object. If there is an existing account with // the given address, it is overwritten and returned as the second return value. func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) { prev = s.getDeletedStateObject(addr) // Note, prev might have been deleted, we need that! newobj = newObject(s, addr, nil, s.originBlockHash) if prev == nil { s.journal.append(createObjectChange{account: &addr}) } else { // The original account should be marked as destructed and all cached // account and storage data should be cleared as well. Note, it must // be done here, otherwise the destruction event of "original account" // will be lost. _, prevdestruct := s.stateObjectsDestruct[prev.address] if !prevdestruct { s.stateObjectsDestruct[prev.address] = prev.origin } // There may be some cached account/storage data already since IntermediateRoot // will be called for each transaction before byzantium fork which will always // cache the latest account/storage data. prevAccount, ok := s.accountsOrigin[prev.address] s.journal.append(resetObjectChange{ account: &addr, prev: prev, prevdestruct: prevdestruct, prevAccount: s.accounts[prev.addrHash], prevStorage: s.storages[prev.addrHash], prevAccountOriginExist: ok, prevAccountOrigin: prevAccount, prevStorageOrigin: s.storagesOrigin[prev.address], }) delete(s.accounts, prev.addrHash) delete(s.storages, prev.addrHash) delete(s.accountsOrigin, prev.address) delete(s.storagesOrigin, prev.address) } s.setStateObject(newobj) if prev != nil && !prev.deleted { return newobj, prev } return newobj, nil } // CreateAccount explicitly creates a state object. If a state object with the address // already exists the balance is carried over to the new account. // // CreateAccount is called during the EVM CREATE operation. The situation might arise that // a contract does the following: // // 1. sends funds to sha(account ++ (nonce + 1)) // 2. tx_create(sha(account ++ nonce)) (note that this gets the address of 1) // // Carrying over the balance ensures that Ether doesn't disappear. func (s *StateDB) CreateAccount(addr common.Address) { newObj, prev := s.createObject(addr) if prev != nil { newObj.setBalance(prev.data.Balance) } } // Snapshot returns an identifier for the current revision of the state. func (s *StateDB) Snapshot() int { id := s.nextRevisionId s.nextRevisionId++ s.validRevisions = append(s.validRevisions, revision{id, s.journal.length()}) return id } // RevertToSnapshot reverts all state changes made since the given revision. func (s *StateDB) RevertToSnapshot(revid int) { // Find the snapshot in the stack of valid snapshots. idx := sort.Search(len(s.validRevisions), func(i int) bool { return s.validRevisions[i].id >= revid }) if idx == len(s.validRevisions) || s.validRevisions[idx].id != revid { panic(fmt.Errorf("revision id %v cannot be reverted", revid)) } snp := s.validRevisions[idx].journalIndex // Replay the journal to undo changes and remove invalidated snapshots s.journal.revert(s, snp) s.validRevisions = s.validRevisions[:idx] } // GetRefund returns the current value of the refund counter. func (s *StateDB) GetRefund() uint64 { return s.refund } // Finalise finalises the state by removing the destructed objects and clears // the journal as well as the refunds. Finalise, however, will not push any updates // into the tries just yet. Only IntermediateRoot or Commit will do that. func (s *StateDB) Finalise(deleteEmptyObjects bool) { addressesToPrefetch := make([][]byte, 0, len(s.journal.dirties)) for addr := range s.journal.dirties { obj, exist := s.stateObjects[addr] if !exist { // ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2 // That tx goes out of gas, and although the notion of 'touched' does not exist there, the // touch-event will still be recorded in the journal. Since ripeMD is a special snowflake, // it will persist in the journal even though the journal is reverted. In this special circumstance, // it may exist in `s.journal.dirties` but not in `s.stateObjects`. // Thus, we can safely ignore it here continue } if obj.selfDestructed || (deleteEmptyObjects && obj.empty()) { obj.deleted = true // We need to maintain account deletions explicitly (will remain // set indefinitely). Note only the first occurred self-destruct // event is tracked. if _, ok := s.stateObjectsDestruct[obj.address]; !ok { s.stateObjectsDestruct[obj.address] = obj.origin } // Note, we can't do this only at the end of a block because multiple // transactions within the same block might self destruct and then // resurrect an account; but the snapshotter needs both events. delete(s.accounts, obj.addrHash) // Clear out any previously updated account data (may be recreated via a resurrect) delete(s.storages, obj.addrHash) // Clear out any previously updated storage data (may be recreated via a resurrect) delete(s.accountsOrigin, obj.address) // Clear out any previously updated account data (may be recreated via a resurrect) delete(s.storagesOrigin, obj.address) // Clear out any previously updated storage data (may be recreated via a resurrect) } else { obj.finalise(true) // Prefetch slots in the background } obj.created = false s.stateObjectsPending[addr] = struct{}{} s.stateObjectsDirty[addr] = struct{}{} // At this point, also ship the address off to the precacher. The precacher // will start loading tries, and when the change is eventually committed, // the commit-phase will be a lot faster addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure } // Invalidate journal because reverting across transactions is not allowed. s.clearJournalAndRefund() } // SetTxContext sets the current transaction hash and index which are // used when the EVM emits new state logs. It should be invoked before // transaction execution. func (s *StateDB) SetTxContext(thash common.Hash, ti int) { s.thash = thash s.txIndex = ti } func (s *StateDB) clearJournalAndRefund() { if len(s.journal.entries) > 0 { s.journal = newJournal() s.refund = 0 } s.validRevisions = s.validRevisions[:0] // Snapshots can be created without journal entries } // Prepare handles the preparatory steps for executing a state transition with. // This method must be invoked before state transition. // // Berlin fork: // - Add sender to access list (2929) // - Add destination to access list (2929) // - Add precompiles to access list (2929) // - Add the contents of the optional tx access list (2930) // // Potential EIPs: // - Reset access list (Berlin) // - Add coinbase to access list (EIP-3651) // - Reset transient storage (EIP-1153) func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, dst *common.Address, precompiles []common.Address, list types.AccessList) { if rules.IsBerlin { // Clear out any leftover from previous executions al := newAccessList() s.accessList = al al.AddAddress(sender) if dst != nil { al.AddAddress(*dst) // If it's a create-tx, the destination will be added inside evm.create } for _, addr := range precompiles { al.AddAddress(addr) } for _, el := range list { al.AddAddress(el.Address) for _, key := range el.StorageKeys { al.AddSlot(el.Address, key) } } if rules.IsShanghai { // EIP-3651: warm coinbase al.AddAddress(coinbase) } } // Reset transient storage at the beginning of transaction execution s.transientStorage = newTransientStorage() } // AddAddressToAccessList adds the given address to the access list func (s *StateDB) AddAddressToAccessList(addr common.Address) { if s.accessList.AddAddress(addr) { s.journal.append(accessListAddAccountChange{&addr}) } } // AddSlotToAccessList adds the given (address, slot)-tuple to the access list func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { addrMod, slotMod := s.accessList.AddSlot(addr, slot) if addrMod { // In practice, this should not happen, since there is no way to enter the // scope of 'address' without having the 'address' become already added // to the access list (via call-variant, create, etc). // Better safe than sorry, though s.journal.append(accessListAddAccountChange{&addr}) } if slotMod { s.journal.append(accessListAddSlotChange{ address: &addr, slot: &slot, }) } } // AddressInAccessList returns true if the given address is in the access list. func (s *StateDB) AddressInAccessList(addr common.Address) bool { return s.accessList.ContainsAddress(addr) } // SlotInAccessList returns true if the given (address, slot)-tuple is in the access list. func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { return s.accessList.Contains(addr, slot) } // Copy creates a deep, independent copy of the state. // Snapshots of the copied state cannot be applied to the copy. func (s *StateDB) Copy() *StateDB { // Copy all the basic fields, initialize the memory ones state := &StateDB{ db: s.db, originalRoot: s.originalRoot, accounts: make(map[common.Hash][]byte), storages: make(map[common.Hash]map[common.Hash][]byte), accountsOrigin: make(map[common.Address][]byte), storagesOrigin: make(map[common.Address]map[common.Hash][]byte), stateObjects: make(map[common.Address]*stateObject, len(s.journal.dirties)), stateObjectsPending: make(map[common.Address]struct{}, len(s.stateObjectsPending)), stateObjectsDirty: make(map[common.Address]struct{}, len(s.journal.dirties)), stateObjectsDestruct: make(map[common.Address]*types.StateAccount, len(s.stateObjectsDestruct)), refund: s.refund, logs: make(map[common.Hash][]*types.Log, len(s.logs)), logSize: s.logSize, preimages: make(map[common.Hash][]byte, len(s.preimages)), journal: newJournal(), hasher: crypto.NewKeccakState(), } // Copy the dirty states, logs, and preimages for addr := range s.journal.dirties { // As documented [here](https://github.com/ethereum/go-ethereum/pull/16485#issuecomment-380438527), // and in the Finalise-method, there is a case where an object is in the journal but not // in the stateObjects: OOG after touch on ripeMD prior to Byzantium. Thus, we need to check for // nil if object, exist := s.stateObjects[addr]; exist { // Even though the original object is dirty, we are not copying the journal, // so we need to make sure that any side-effect the journal would have caused // during a commit (or similar op) is already applied to the copy. state.stateObjects[addr] = object.deepCopy(state) state.stateObjectsDirty[addr] = struct{}{} // Mark the copy dirty to force internal (code/state) commits state.stateObjectsPending[addr] = struct{}{} // Mark the copy pending to force external (account) commits } } // Above, we don't copy the actual journal. This means that if the copy // is copied, the loop above will be a no-op, since the copy's journal // is empty. Thus, here we iterate over stateObjects, to enable copies // of copies. for addr := range s.stateObjectsPending { if _, exist := state.stateObjects[addr]; !exist { state.stateObjects[addr] = s.stateObjects[addr].deepCopy(state) } state.stateObjectsPending[addr] = struct{}{} } for addr := range s.stateObjectsDirty { if _, exist := state.stateObjects[addr]; !exist { state.stateObjects[addr] = s.stateObjects[addr].deepCopy(state) } state.stateObjectsDirty[addr] = struct{}{} } // Deep copy the destruction markers. for addr, value := range s.stateObjectsDestruct { state.stateObjectsDestruct[addr] = value } // Deep copy the state changes made in the scope of block // along with their original values. state.accounts = copySet(s.accounts) state.storages = copy2DSet(s.storages) state.accountsOrigin = copySet(state.accountsOrigin) state.storagesOrigin = copy2DSet(state.storagesOrigin) // Deep copy the logs occurred in the scope of block for hash, logs := range s.logs { cpy := make([]*types.Log, len(logs)) for i, l := range logs { cpy[i] = new(types.Log) *cpy[i] = *l } state.logs[hash] = cpy } // Deep copy the preimages occurred in the scope of block for hash, preimage := range s.preimages { state.preimages[hash] = preimage } // Do we need to copy the access list and transient storage? // In practice: No. At the start of a transaction, these two lists are empty. // In practice, we only ever copy state _between_ transactions/blocks, never // in the middle of a transaction. However, it doesn't cost us much to copy // empty lists, so we do it anyway to not blow up if we ever decide copy them // in the middle of a transaction. state.accessList = s.accessList.Copy() state.transientStorage = s.transientStorage.Copy() return state } // copySet returns a deep-copied set. func copySet[k comparable](set map[k][]byte) map[k][]byte { copied := make(map[k][]byte, len(set)) for key, val := range set { copied[key] = common.CopyBytes(val) } return copied } // copy2DSet returns a two-dimensional deep-copied set. func copy2DSet[k comparable](set map[k]map[common.Hash][]byte) map[k]map[common.Hash][]byte { copied := make(map[k]map[common.Hash][]byte, len(set)) for addr, subset := range set { copied[addr] = make(map[common.Hash][]byte, len(subset)) for key, val := range subset { copied[addr][key] = common.CopyBytes(val) } } return copied }