diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index f595a9bf4d..6985b94528 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -250,17 +250,18 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } statedb.SetTxContext(tx.Hash(), len(receipts)) var ( - snapshot = statedb.Snapshot() - prevGas = gaspool.Gas() + prevGas = gaspool.Gas() ) + statedb.Snapshot() receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, &gasUsed, evm) if err != nil { - statedb.RevertToSnapshot(snapshot) + statedb.RevertSnapshot() log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err) rejectedTxs = append(rejectedTxs, &rejectedTx{i, err.Error()}) gaspool.SetGas(prevGas) continue } + statedb.DiscardSnapshot() includedTxs = append(includedTxs, tx) if hashError != nil { return nil, nil, nil, NewError(ErrorMissingBlockhash, hashError) diff --git a/cmd/evm/runner.go b/cmd/evm/runner.go index ebb3e04461..2b9fb2fcbc 100644 --- a/cmd/evm/runner.go +++ b/cmd/evm/runner.go @@ -230,7 +230,6 @@ func runCmd(ctx *cli.Context) error { sdb := state.NewDatabase(triedb, nil) prestate, _ = state.New(genesis.Root(), sdb) chainConfig = genesisConfig.Config - if ctx.String(SenderFlag.Name) != "" { sender = common.HexToAddress(ctx.String(SenderFlag.Name)) } diff --git a/core/state/journal.go b/core/state/journal.go index f3f976f24f..1f5f056aec 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -1,4 +1,4 @@ -// Copyright 2016 The go-ethereum Authors +// Copyright 2024 the go-ethereum Authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify @@ -17,485 +17,80 @@ package state import ( - "fmt" - "maps" - "slices" - "sort" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/core/types" ) -type revision struct { - id int - journalIndex int -} - -// journalEntry is a modification entry in the state change journal that can be -// reverted on demand. -type journalEntry interface { - // revert undoes the changes introduced by this journal entry. - revert(*StateDB) - - // dirtied returns the Ethereum address modified by this journal entry. - dirtied() *common.Address - - // copy returns a deep-copied journal entry. - copy() journalEntry -} - -// journal contains the list of state modifications applied since the last state -// commit. These are tracked to be able to be reverted in the case of an execution -// exception or request for reversal. -type journal struct { - entries []journalEntry // Current changes tracked by the journal - dirties map[common.Address]int // Dirty accounts and the number of changes - - validRevisions []revision - nextRevisionId int -} - -// newJournal creates a new initialized journal. -func newJournal() *journal { - return &journal{ - dirties: make(map[common.Address]int), - } -} - -// reset clears the journal, after this operation the journal can be used anew. -// It is semantically similar to calling 'newJournal', but the underlying slices -// can be reused. -func (j *journal) reset() { - j.entries = j.entries[:0] - j.validRevisions = j.validRevisions[:0] - clear(j.dirties) - j.nextRevisionId = 0 -} - -// snapshot returns an identifier for the current revision of the state. -func (j *journal) snapshot() int { - id := j.nextRevisionId - j.nextRevisionId++ - j.validRevisions = append(j.validRevisions, revision{id, j.length()}) - return id -} - -// revertToSnapshot reverts all state changes made since the given revision. -func (j *journal) revertToSnapshot(revid int, s *StateDB) { - // Find the snapshot in the stack of valid snapshots. - idx := sort.Search(len(j.validRevisions), func(i int) bool { - return j.validRevisions[i].id >= revid - }) - if idx == len(j.validRevisions) || j.validRevisions[idx].id != revid { - panic(fmt.Errorf("revision id %v cannot be reverted", revid)) - } - snapshot := j.validRevisions[idx].journalIndex - - // Replay the journal to undo changes and remove invalidated snapshots - j.revert(s, snapshot) - j.validRevisions = j.validRevisions[:idx] -} - -// append inserts a new modification entry to the end of the change journal. -func (j *journal) append(entry journalEntry) { - j.entries = append(j.entries, entry) - if addr := entry.dirtied(); addr != nil { - j.dirties[*addr]++ - } -} - -// revert undoes a batch of journalled modifications along with any reverted -// dirty handling too. -func (j *journal) revert(statedb *StateDB, snapshot int) { - for i := len(j.entries) - 1; i >= snapshot; i-- { - // Undo the changes made by the operation - j.entries[i].revert(statedb) - - // Drop any dirty tracking induced by the change - if addr := j.entries[i].dirtied(); addr != nil { - if j.dirties[*addr]--; j.dirties[*addr] == 0 { - delete(j.dirties, *addr) - } - } - } - j.entries = j.entries[:snapshot] -} - -// dirty explicitly sets an address to dirty, even if the change entries would -// otherwise suggest it as clean. This method is an ugly hack to handle the RIPEMD -// precompile consensus exception. -func (j *journal) dirty(addr common.Address) { - j.dirties[addr]++ -} - -// length returns the current number of entries in the journal. -func (j *journal) length() int { - return len(j.entries) -} - -// copy returns a deep-copied journal. -func (j *journal) copy() *journal { - entries := make([]journalEntry, 0, j.length()) - for i := 0; i < j.length(); i++ { - entries = append(entries, j.entries[i].copy()) - } - return &journal{ - entries: entries, - dirties: maps.Clone(j.dirties), - validRevisions: slices.Clone(j.validRevisions), - nextRevisionId: j.nextRevisionId, - } -} - -func (j *journal) logChange(txHash common.Hash) { - j.append(addLogChange{txhash: txHash}) -} - -func (j *journal) createObject(addr common.Address) { - j.append(createObjectChange{account: addr}) -} - -func (j *journal) createContract(addr common.Address) { - j.append(createContractChange{account: addr}) -} - -func (j *journal) destruct(addr common.Address) { - j.append(selfDestructChange{account: addr}) -} - -func (j *journal) storageChange(addr common.Address, key, prev, origin common.Hash) { - j.append(storageChange{ - account: addr, - key: key, - prevvalue: prev, - origvalue: origin, - }) -} - -func (j *journal) transientStateChange(addr common.Address, key, prev common.Hash) { - j.append(transientStorageChange{ - account: addr, - key: key, - prevalue: prev, - }) -} - -func (j *journal) refundChange(previous uint64) { - j.append(refundChange{prev: previous}) -} - -func (j *journal) balanceChange(addr common.Address, previous *uint256.Int) { - j.append(balanceChange{ - account: addr, - prev: previous.Clone(), - }) -} - -func (j *journal) setCode(address common.Address, prevCode []byte) { - j.append(codeChange{ - account: address, - prevCode: prevCode, - }) -} - -func (j *journal) nonceChange(address common.Address, prev uint64) { - j.append(nonceChange{ - account: address, - prev: prev, - }) -} - -func (j *journal) touchChange(address common.Address) { - j.append(touchChange{ - account: address, - }) - if address == ripemd { - // Explicitly put it in the dirty-cache, which is otherwise generated from - // flattened journals. - j.dirty(address) - } -} - -func (j *journal) accessListAddAccount(addr common.Address) { - j.append(accessListAddAccountChange{addr}) -} - -func (j *journal) accessListAddSlot(addr common.Address, slot common.Hash) { - j.append(accessListAddSlotChange{ - address: addr, - slot: slot, - }) -} - -type ( - // Changes to the account trie. - createObjectChange struct { - account common.Address - } - // createContractChange represents an account becoming a contract-account. - // This event happens prior to executing initcode. The journal-event simply - // manages the created-flag, in order to allow same-tx destruction. - createContractChange struct { - account common.Address - } - selfDestructChange struct { - account common.Address - } - - // Changes to individual accounts. - balanceChange struct { - account common.Address - prev *uint256.Int - } - nonceChange struct { - account common.Address - prev uint64 - } - storageChange struct { - account common.Address - key common.Hash - prevvalue common.Hash - origvalue common.Hash - } - codeChange struct { - account common.Address - prevCode []byte - } - - // Changes to other state values. - refundChange struct { - prev uint64 - } - addLogChange struct { - txhash common.Hash - } - touchChange struct { - account common.Address - } - - // Changes to the access list - accessListAddAccountChange struct { - address common.Address - } - accessListAddSlotChange struct { - address common.Address - slot common.Hash - } - - // Changes to transient storage - transientStorageChange struct { - account common.Address - key, prevalue common.Hash - } -) - -func (ch createObjectChange) revert(s *StateDB) { - delete(s.stateObjects, ch.account) -} - -func (ch createObjectChange) dirtied() *common.Address { - return &ch.account -} - -func (ch createObjectChange) copy() journalEntry { - return createObjectChange{ - account: ch.account, - } -} - -func (ch createContractChange) revert(s *StateDB) { - s.getStateObject(ch.account).newContract = false -} - -func (ch createContractChange) dirtied() *common.Address { - return nil -} - -func (ch createContractChange) copy() journalEntry { - return createContractChange{ - account: ch.account, - } -} - -func (ch selfDestructChange) revert(s *StateDB) { - obj := s.getStateObject(ch.account) - if obj != nil { - obj.selfDestructed = false - } -} - -func (ch selfDestructChange) dirtied() *common.Address { - return &ch.account -} - -func (ch selfDestructChange) copy() journalEntry { - return selfDestructChange{ - account: ch.account, - } -} - -var ripemd = common.HexToAddress("0000000000000000000000000000000000000003") - -func (ch touchChange) revert(s *StateDB) { -} - -func (ch touchChange) dirtied() *common.Address { - return &ch.account -} - -func (ch touchChange) copy() journalEntry { - return touchChange{ - account: ch.account, - } -} - -func (ch balanceChange) revert(s *StateDB) { - s.getStateObject(ch.account).setBalance(ch.prev) -} - -func (ch balanceChange) dirtied() *common.Address { - return &ch.account -} - -func (ch balanceChange) copy() journalEntry { - return balanceChange{ - account: ch.account, - prev: new(uint256.Int).Set(ch.prev), - } -} - -func (ch nonceChange) revert(s *StateDB) { - s.getStateObject(ch.account).setNonce(ch.prev) -} - -func (ch nonceChange) dirtied() *common.Address { - return &ch.account -} - -func (ch nonceChange) copy() journalEntry { - return nonceChange{ - account: ch.account, - prev: ch.prev, - } -} - -func (ch codeChange) revert(s *StateDB) { - s.getStateObject(ch.account).setCode(crypto.Keccak256Hash(ch.prevCode), ch.prevCode) -} - -func (ch codeChange) dirtied() *common.Address { - return &ch.account -} - -func (ch codeChange) copy() journalEntry { - return codeChange{ - account: ch.account, - prevCode: ch.prevCode, - } -} - -func (ch storageChange) revert(s *StateDB) { - s.getStateObject(ch.account).setState(ch.key, ch.prevvalue, ch.origvalue) -} - -func (ch storageChange) dirtied() *common.Address { - return &ch.account -} - -func (ch storageChange) copy() journalEntry { - return storageChange{ - account: ch.account, - key: ch.key, - prevvalue: ch.prevvalue, - origvalue: ch.origvalue, - } -} - -func (ch transientStorageChange) revert(s *StateDB) { - s.setTransientState(ch.account, ch.key, ch.prevalue) -} - -func (ch transientStorageChange) dirtied() *common.Address { - return nil -} - -func (ch transientStorageChange) copy() journalEntry { - return transientStorageChange{ - account: ch.account, - key: ch.key, - prevalue: ch.prevalue, - } -} - -func (ch refundChange) revert(s *StateDB) { - s.refund = ch.prev -} - -func (ch refundChange) dirtied() *common.Address { - return nil -} - -func (ch refundChange) copy() journalEntry { - return refundChange{ - prev: ch.prev, - } -} - -func (ch addLogChange) revert(s *StateDB) { - logs := s.logs[ch.txhash] - if len(logs) == 1 { - delete(s.logs, ch.txhash) - } else { - s.logs[ch.txhash] = logs[:len(logs)-1] - } - s.logSize-- -} - -func (ch addLogChange) dirtied() *common.Address { - return nil -} - -func (ch addLogChange) copy() journalEntry { - return addLogChange{ - txhash: ch.txhash, - } -} - -func (ch accessListAddAccountChange) revert(s *StateDB) { - /* - One important invariant here, is that whenever a (addr, slot) is added, if the - addr is not already present, the add causes two journal entries: - - one for the address, - - one for the (address,slot) - Therefore, when unrolling the change, we can always blindly delete the - (addr) at this point, since no storage adds can remain when come upon - a single (addr) change. - */ - s.accessList.DeleteAddress(ch.address) -} - -func (ch accessListAddAccountChange) dirtied() *common.Address { - return nil -} - -func (ch accessListAddAccountChange) copy() journalEntry { - return accessListAddAccountChange{ - address: ch.address, - } -} - -func (ch accessListAddSlotChange) revert(s *StateDB) { - s.accessList.DeleteSlot(ch.address, ch.slot) -} - -func (ch accessListAddSlotChange) dirtied() *common.Address { - return nil -} - -func (ch accessListAddSlotChange) copy() journalEntry { - return accessListAddSlotChange{ - address: ch.address, - slot: ch.slot, - } +type journal interface { + // snapshot starts a new journal scope which can be reverted or discarded. + // The lifeycle of journalling is as follows: + // - snapshot() starts a 'scope'. + // - The method snapshot() may be called any number of times. + // - For each call to snapshot, there should be a corresponding call to end + // the scope via either of: + // - revertToSnapshot, which undoes the changes in the scope, or + // - discardSnapshot, which discards the ability to revert the changes in the scope. + snapshot() + + // revertSnapshot reverts all state changes made since the last call to snapshot(). + revertSnapshot(s *StateDB) + + // discardSnapshot removes the latest snapshot; after calling this + // method, it is no longer possible to revert to that particular snapshot, the + // changes are considered part of the parent scope. + discardSnapshot() + + // reset clears the journal so it can be reused. + reset() + + // dirtyAccounts returns a list of all accounts modified in this journal + dirtyAccounts() []common.Address + + // accessListAddAccount journals the adding of addr to the access list + accessListAddAccount(addr common.Address) + + // accessListAddSlot journals the adding of addr/slot to the access list + accessListAddSlot(addr common.Address, slot common.Hash) + + // logChange journals the adding of a log related to the txHash + logChange(txHash common.Hash) + + // createObject journals the event of a new account created in the trie. + createObject(addr common.Address) + + // createContract journals the creation of a new contract at addr. + // OBS: This method must not be applied twice, it assumes that the pre-state + // (i.e the rollback-state) is non-created. + createContract(addr common.Address, account *types.StateAccount) + + // destruct journals the destruction of an account in the trie. + // pre-state (i.e the rollback-state) is non-destructed (and, for the purpose + // of EIP-XXX (TODO lookup), created in this tx). + destruct(addr common.Address, account *types.StateAccount) + + // storageChange journals a change in the storage data related to addr. + // It records the key and previous value of the slot. + storageChange(addr common.Address, key, prev, origin common.Hash) + + // transientStateChange journals a change in the t-storage data related to addr. + // It records the key and previous value of the slot. + transientStateChange(addr common.Address, key, prev common.Hash) + + // refundChange journals that the refund has been changed, recording the previous value. + refundChange(previous uint64) + + // balanceChange journals that the balance of addr has been changed, recording the previous value + balanceChange(addr common.Address, account *types.StateAccount, destructed, newContract bool) + + // setCode journals that the code of addr has been set. + setCode(addr common.Address, account *types.StateAccount, prevCode []byte) + + // nonceChange journals that the nonce of addr was changed, recording the previous value. + nonceChange(addr common.Address, account *types.StateAccount, destructed, newContract bool) + + // touchChange journals that the account at addr was touched during execution. + touchChange(addr common.Address, account *types.StateAccount, destructed, newContract bool) + + // copy returns a deep-copied journal. + copy() journal } diff --git a/core/state/journal_linear.go b/core/state/journal_linear.go new file mode 100644 index 0000000000..9d46e545dd --- /dev/null +++ b/core/state/journal_linear.go @@ -0,0 +1,522 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "maps" + "slices" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" +) + +// journalEntry is a modification entry in the state change linear journal that can be +// reverted on demand. +type journalEntry interface { + // revert undoes the changes introduced by this entry. + revert(*StateDB) + + // dirtied returns the Ethereum address modified by this entry. + dirtied() *common.Address + + // copy returns a deep-copied entry. + copy() journalEntry +} + +// linearJournal contains the list of state modifications applied since the last state +// commit. These are tracked to be able to be reverted in the case of an execution +// exception or request for reversal. +type linearJournal struct { + entries []journalEntry // Current changes tracked by the linearJournal + dirties map[common.Address]int // Dirty accounts and the number of changes + + revisions []int // sequence of indexes to points in time designating snapshots +} + +// compile-time interface check +var _ journal = (*linearJournal)(nil) + +// newLinearJournal creates a new initialized linearJournal. +func newLinearJournal() *linearJournal { + s := &linearJournal{ + dirties: make(map[common.Address]int), + } + s.snapshot() // create snaphot zero + return s +} + +// reset clears the journal, after this operation the journal can be used anew. +// It is semantically similar to calling 'newJournal', but the underlying slices +// can be reused. +func (j *linearJournal) reset() { + j.entries = j.entries[:0] + j.revisions = j.revisions[:0] + clear(j.dirties) + j.snapshot() +} + +func (j linearJournal) dirtyAccounts() []common.Address { + dirty := make([]common.Address, 0, len(j.dirties)) + // flatten into list + for addr := range j.dirties { + dirty = append(dirty, addr) + } + return dirty +} + +// snapshot starts a new journal scope which can be reverted or discarded. +func (j *linearJournal) snapshot() { + j.revisions = append(j.revisions, len(j.entries)) +} + +// revertSnapshot reverts all state changes made since the last call to snapshot(). +func (j *linearJournal) revertSnapshot(s *StateDB) { + id := len(j.revisions) - 1 + if id < 0 { + j.snapshot() + return + } + revision := j.revisions[id] + // Replay the linearJournal to undo changes and remove invalidated snapshots + j.revertTo(s, revision) + j.revisions = j.revisions[:id] + if id == 0 { + j.snapshot() + } +} + +// discardSnapshot removes the latest snapshot; after calling this +// method, it is no longer possible to revert to that particular snapshot, the +// changes are considered part of the parent scope. +func (j *linearJournal) discardSnapshot() { + id := len(j.revisions) - 1 + if id <= 0 { + // If a transaction is applied successfully, the statedb.Finalize will + // end by clearing and resetting the journal. Invoking a discardSnapshot + // afterwards will land here: calling discard on an empty journal. + // This is fine + return + } + j.revisions = j.revisions[:id] +} + +// append inserts a new modification entry to the end of the change linearJournal. +func (j *linearJournal) append(entry journalEntry) { + j.entries = append(j.entries, entry) + if addr := entry.dirtied(); addr != nil { + j.dirties[*addr]++ + } +} + +// revert undoes a batch of journalled modifications along with any reverted +// dirty handling too. +func (j *linearJournal) revertTo(statedb *StateDB, snapshot int) { + for i := len(j.entries) - 1; i >= snapshot; i-- { + // Undo the changes made by the operation + j.entries[i].revert(statedb) + + // Drop any dirty tracking induced by the change + if addr := j.entries[i].dirtied(); addr != nil { + if j.dirties[*addr]--; j.dirties[*addr] == 0 { + delete(j.dirties, *addr) + } + } + } + j.entries = j.entries[:snapshot] +} + +// dirty explicitly sets an address to dirty, even if the change entries would +// otherwise suggest it as clean. This method is an ugly hack to handle the RIPEMD +// precompile consensus exception. +func (j *linearJournal) dirty(addr common.Address) { + j.dirties[addr]++ +} + +// length returns the current number of entries in the linearJournal. +func (j *linearJournal) length() int { + return len(j.entries) +} + +// copy returns a deep-copied journal. +func (j *linearJournal) copy() journal { + entries := make([]journalEntry, 0, j.length()) + for i := 0; i < j.length(); i++ { + entries = append(entries, j.entries[i].copy()) + } + return &linearJournal{ + entries: entries, + dirties: maps.Clone(j.dirties), + revisions: slices.Clone(j.revisions), + } +} + +func (j *linearJournal) logChange(txHash common.Hash) { + j.append(addLogChange{txhash: txHash}) +} + +func (j *linearJournal) createObject(addr common.Address) { + j.append(createObjectChange{account: addr}) +} + +func (j *linearJournal) createContract(addr common.Address, account *types.StateAccount) { + j.append(createContractChange{account: addr}) +} + +func (j *linearJournal) destruct(addr common.Address, account *types.StateAccount) { + j.append(selfDestructChange{account: addr}) +} + +func (j *linearJournal) storageChange(addr common.Address, key, prev, origin common.Hash) { + j.append(storageChange{ + account: addr, + key: key, + prevvalue: prev, + origvalue: origin, + }) +} + +func (j *linearJournal) transientStateChange(addr common.Address, key, prev common.Hash) { + j.append(transientStorageChange{ + account: addr, + key: key, + prevalue: prev, + }) +} + +func (j *linearJournal) refundChange(previous uint64) { + j.append(refundChange{prev: previous}) +} + +func (j *linearJournal) balanceChange(addr common.Address, account *types.StateAccount, destructed, newContract bool) { + j.append(balanceChange{ + account: addr, + prev: account.Balance.Clone(), + }) +} + +func (j *linearJournal) setCode(address common.Address, account *types.StateAccount, prevCode []byte) { + j.append(codeChange{ + account: address, + prevCode: prevCode, + }) +} + +func (j *linearJournal) nonceChange(address common.Address, account *types.StateAccount, destructed, newContract bool) { + j.append(nonceChange{ + account: address, + prev: account.Nonce, + }) +} + +func (j *linearJournal) touchChange(address common.Address, account *types.StateAccount, destructed, newContract bool) { + j.append(touchChange{ + account: address, + }) + if address == ripemd { + // Explicitly put it in the dirty-cache, which is otherwise generated from + // flattened journals. + j.dirty(address) + } +} + +func (j *linearJournal) accessListAddAccount(addr common.Address) { + j.append(accessListAddAccountChange{addr}) +} + +func (j *linearJournal) accessListAddSlot(addr common.Address, slot common.Hash) { + j.append(accessListAddSlotChange{ + address: addr, + slot: slot, + }) +} + +type ( + // Changes to the account trie. + createObjectChange struct { + account common.Address + } + // createContractChange represents an account becoming a contract-account. + // This event happens prior to executing initcode. The linearJournal-event simply + // manages the created-flag, in order to allow same-tx destruction. + createContractChange struct { + account common.Address + } + selfDestructChange struct { + account common.Address + } + + // Changes to individual accounts. + balanceChange struct { + account common.Address + prev *uint256.Int + } + nonceChange struct { + account common.Address + prev uint64 + } + storageChange struct { + account common.Address + key common.Hash + prevvalue common.Hash + origvalue common.Hash + } + codeChange struct { + account common.Address + prevCode []byte + } + + // Changes to other state values. + refundChange struct { + prev uint64 + } + addLogChange struct { + txhash common.Hash + } + touchChange struct { + account common.Address + } + + // Changes to the access list + accessListAddAccountChange struct { + address common.Address + } + accessListAddSlotChange struct { + address common.Address + slot common.Hash + } + + // Changes to transient storage + transientStorageChange struct { + account common.Address + key, prevalue common.Hash + } +) + +func (ch createObjectChange) revert(s *StateDB) { + delete(s.stateObjects, ch.account) +} + +func (ch createObjectChange) dirtied() *common.Address { + return &ch.account +} + +func (ch createObjectChange) copy() journalEntry { + return createObjectChange{ + account: ch.account, + } +} + +func (ch createContractChange) revert(s *StateDB) { + s.getStateObject(ch.account).newContract = false +} + +func (ch createContractChange) dirtied() *common.Address { + // This method returns nil, since the transformation from non-contract to + // contract is not an operation which has an effect on the trie: + // it does not make the account part of the dirty-set. + // Creating the account (createObject) or setting the code (setCode) + // however, do, and are. + return nil +} + +func (ch createContractChange) copy() journalEntry { + return createContractChange{ + account: ch.account, + } +} + +func (ch selfDestructChange) revert(s *StateDB) { + obj := s.getStateObject(ch.account) + if obj != nil { + obj.selfDestructed = false + } +} + +func (ch selfDestructChange) dirtied() *common.Address { + return &ch.account +} + +func (ch selfDestructChange) copy() journalEntry { + return selfDestructChange{ + account: ch.account, + } +} + +var ripemd = common.HexToAddress("0000000000000000000000000000000000000003") + +func (ch touchChange) revert(s *StateDB) { +} + +func (ch touchChange) dirtied() *common.Address { + return &ch.account +} + +func (ch touchChange) copy() journalEntry { + return touchChange{ + account: ch.account, + } +} + +func (ch balanceChange) revert(s *StateDB) { + s.getStateObject(ch.account).setBalance(ch.prev) +} + +func (ch balanceChange) dirtied() *common.Address { + return &ch.account +} + +func (ch balanceChange) copy() journalEntry { + return balanceChange{ + account: ch.account, + prev: new(uint256.Int).Set(ch.prev), + } +} + +func (ch nonceChange) revert(s *StateDB) { + s.getStateObject(ch.account).setNonce(ch.prev) +} + +func (ch nonceChange) dirtied() *common.Address { + return &ch.account +} + +func (ch nonceChange) copy() journalEntry { + return nonceChange{ + account: ch.account, + prev: ch.prev, + } +} + +func (ch codeChange) revert(s *StateDB) { + s.getStateObject(ch.account).setCode(crypto.Keccak256Hash(ch.prevCode), ch.prevCode) +} + +func (ch codeChange) dirtied() *common.Address { + return &ch.account +} + +func (ch codeChange) copy() journalEntry { + return codeChange{ + account: ch.account, + prevCode: ch.prevCode} +} + +func (ch storageChange) revert(s *StateDB) { + s.getStateObject(ch.account).setState(ch.key, ch.prevvalue, ch.origvalue) +} + +func (ch storageChange) dirtied() *common.Address { + return &ch.account +} + +func (ch storageChange) copy() journalEntry { + return storageChange{ + account: ch.account, + key: ch.key, + prevvalue: ch.prevvalue, + } +} + +func (ch transientStorageChange) revert(s *StateDB) { + s.setTransientState(ch.account, ch.key, ch.prevalue) +} + +func (ch transientStorageChange) dirtied() *common.Address { + return nil +} + +func (ch transientStorageChange) copy() journalEntry { + return transientStorageChange{ + account: ch.account, + key: ch.key, + prevalue: ch.prevalue, + } +} + +func (ch refundChange) revert(s *StateDB) { + s.refund = ch.prev +} + +func (ch refundChange) dirtied() *common.Address { + return nil +} + +func (ch refundChange) copy() journalEntry { + return refundChange{ + prev: ch.prev, + } +} + +func (ch addLogChange) revert(s *StateDB) { + logs := s.logs[ch.txhash] + if len(logs) == 1 { + delete(s.logs, ch.txhash) + } else { + s.logs[ch.txhash] = logs[:len(logs)-1] + } + s.logSize-- +} + +func (ch addLogChange) dirtied() *common.Address { + return nil +} + +func (ch addLogChange) copy() journalEntry { + return addLogChange{ + txhash: ch.txhash, + } +} + +func (ch accessListAddAccountChange) revert(s *StateDB) { + /* + One important invariant here, is that whenever a (addr, slot) is added, if the + addr is not already present, the add causes two linearJournal entries: + - one for the address, + - one for the (address,slot) + Therefore, when unrolling the change, we can always blindly delete the + (addr) at this point, since no storage adds can remain when come upon + a single (addr) change. + */ + s.accessList.DeleteAddress(ch.address) +} + +func (ch accessListAddAccountChange) dirtied() *common.Address { + return nil +} + +func (ch accessListAddAccountChange) copy() journalEntry { + return accessListAddAccountChange{ + address: ch.address, + } +} + +func (ch accessListAddSlotChange) revert(s *StateDB) { + s.accessList.DeleteSlot(ch.address, ch.slot) +} + +func (ch accessListAddSlotChange) dirtied() *common.Address { + return nil +} + +func (ch accessListAddSlotChange) copy() journalEntry { + return accessListAddSlotChange{ + address: ch.address, + slot: ch.slot, + } +} diff --git a/core/state/journal_set.go b/core/state/journal_set.go new file mode 100644 index 0000000000..23e648a781 --- /dev/null +++ b/core/state/journal_set.go @@ -0,0 +1,506 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "bytes" + "maps" + "slices" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" +) + +var ( + _ journal = (*sparseJournal)(nil) +) + +// journalAccount represents the 'journable state' of a types.Account. +// Which means, all the normal fields except storage root, but also with a +// destruction-flag. +type journalAccount struct { + nonce uint64 + balance uint256.Int + codeHash []byte // nil == emptyCodeHAsh + destructed bool + newContract bool +} + +type addrSlot struct { + addr common.Address + slot common.Hash +} + +type doubleHash struct { + origin common.Hash + prev common.Hash +} + +// scopedJournal represents all changes within a single callscope. These changes +// are either all reverted, or all committed -- they cannot be partially applied. +type scopedJournal struct { + accountChanges map[common.Address]*journalAccount + refund int64 + logs []common.Hash + + accessListAddresses []common.Address + accessListAddrSlots []addrSlot + + storageChanges map[common.Address]map[common.Hash]doubleHash + tStorageChanges map[common.Address]map[common.Hash]common.Hash + + // code is a lookup-table for code. + code map[common.Hash][]byte +} + +func newScopedJournal() *scopedJournal { + return &scopedJournal{ + refund: -1, + } +} + +func (j *scopedJournal) deepCopy() *scopedJournal { + var cpy = &scopedJournal{ + // The accountChanges copy will copy the pointers to + // journalAccount objects: thus not actually deep copy those + // objects. That is fine: we never mutate journalAccount. + accountChanges: maps.Clone(j.accountChanges), + refund: j.refund, + logs: slices.Clone(j.logs), + accessListAddresses: slices.Clone(j.accessListAddresses), + accessListAddrSlots: slices.Clone(j.accessListAddrSlots), + code: maps.Clone(j.code), + } + if j.storageChanges != nil { + cpy.storageChanges = make(map[common.Address]map[common.Hash]doubleHash) + for addr, changes := range j.storageChanges { + cpy.storageChanges[addr] = maps.Clone(changes) + } + } + if j.tStorageChanges != nil { + cpy.tStorageChanges = make(map[common.Address]map[common.Hash]common.Hash) + for addr, changes := range j.tStorageChanges { + cpy.tStorageChanges[addr] = maps.Clone(changes) + } + } + return cpy +} + +// stashCode remembers a piece of code for later. The code itself is not part of +// the account (only the codeHash is). +func (j *scopedJournal) stashCode(prev []byte) { + if j.code == nil { + j.code = make(map[common.Hash][]byte) + } + j.code[crypto.Keccak256Hash(prev)] = prev +} + +func (j *scopedJournal) lookupCode(hash common.Hash) []byte { + return j.code[hash] +} + +func (j *scopedJournal) journalRefundChange(prev uint64) { + if j.refund == -1 { + // We convert from uint64 to int64 here, so that we can use -1 + // to represent "no previous value set". + // Treating refund as int64 is fine, there's no possibility for + // refund to ever exceed maxInt64. + j.refund = int64(prev) + } +} + +// journalAccountChange is the common shared implementation for all account-changes. +// These changes all fall back to this method: +// - balance change +// - nonce change +// - destruct-change +// - code change +// - touch change +// - creation change (in this case, the account is nil) +func (j *scopedJournal) journalAccountChange(address common.Address, account *types.StateAccount, destructed, newContract bool) { + if j.accountChanges == nil { + j.accountChanges = make(map[common.Address]*journalAccount) + } + // If the account has already been journalled, we're done here + if _, ok := j.accountChanges[address]; ok { + return + } + if account == nil { + j.accountChanges[address] = nil // created now, previously non-existent + return + } + ja := &journalAccount{ + nonce: account.Nonce, + balance: *account.Balance, + destructed: destructed, + newContract: newContract, + } + if !bytes.Equal(account.CodeHash, types.EmptyCodeHash[:]) { + ja.codeHash = account.CodeHash + } + j.accountChanges[address] = ja +} + +func (j *scopedJournal) journalLog(txHash common.Hash) { + j.logs = append(j.logs, txHash) +} + +func (j *scopedJournal) journalAccessListAddAccount(addr common.Address) { + j.accessListAddresses = append(j.accessListAddresses, addr) +} + +func (j *scopedJournal) journalAccessListAddSlot(addr common.Address, slot common.Hash) { + j.accessListAddrSlots = append(j.accessListAddrSlots, addrSlot{addr, slot}) +} + +func (j *scopedJournal) journalSetState(addr common.Address, key, prev, origin common.Hash) { + if j.storageChanges == nil { + j.storageChanges = make(map[common.Address]map[common.Hash]doubleHash) + } + changes, ok := j.storageChanges[addr] + if !ok { + changes = make(map[common.Hash]doubleHash) + j.storageChanges[addr] = changes + } + // Do not overwrite a previous value! + if _, ok := changes[key]; !ok { + changes[key] = doubleHash{origin: origin, prev: prev} + } +} + +func (j *scopedJournal) journalSetTransientState(addr common.Address, key, prev common.Hash) { + if j.tStorageChanges == nil { + j.tStorageChanges = make(map[common.Address]map[common.Hash]common.Hash) + } + changes, ok := j.tStorageChanges[addr] + if !ok { + changes = make(map[common.Hash]common.Hash) + j.tStorageChanges[addr] = changes + } + // Do not overwrite a previous value! + if _, ok := changes[key]; !ok { + changes[key] = prev + } +} + +func (j *scopedJournal) revert(s *StateDB) { + // Revert refund + if j.refund != -1 { + s.refund = uint64(j.refund) + } + // Revert storage changes + for addr, changes := range j.storageChanges { + obj := s.getStateObject(addr) + for key, val := range changes { + obj.setState(key, val.prev, val.origin) + } + } + // Revert t-store changes + for addr, changes := range j.tStorageChanges { + for key, val := range changes { + s.setTransientState(addr, key, val) + } + } + + // Revert changes to accounts + for addr, data := range j.accountChanges { + if data == nil { // Reverting a create + delete(s.stateObjects, addr) + continue + } + obj := s.getStateObject(addr) + obj.setNonce(data.nonce) + // Setting 'code' to nil means it will be loaded from disk + // next time it is needed. We avoid nilling it unless required + journalHash := data.codeHash + if data.codeHash == nil { + if !bytes.Equal(obj.CodeHash(), types.EmptyCodeHash[:]) { + obj.setCode(types.EmptyCodeHash, nil) + } + } else { + if !bytes.Equal(obj.CodeHash(), journalHash) { + h := common.BytesToHash(journalHash) + obj.setCode(h, j.lookupCode(h)) + } + } + obj.setBalance(&data.balance) + obj.selfDestructed = data.destructed + obj.newContract = data.newContract + } + // Revert logs + for _, txhash := range j.logs { + logs := s.logs[txhash] + if len(logs) == 1 { + delete(s.logs, txhash) + } else { + s.logs[txhash] = logs[:len(logs)-1] + } + s.logSize-- + } + // Revert access list additions + for i := len(j.accessListAddrSlots) - 1; i >= 0; i-- { + item := j.accessListAddrSlots[i] + s.accessList.DeleteSlot(item.addr, item.slot) + } + for i := len(j.accessListAddresses) - 1; i >= 0; i-- { + s.accessList.DeleteAddress(j.accessListAddresses[i]) + } +} + +func (j *scopedJournal) merge(parent *scopedJournal) { + if parent.refund == -1 { + parent.refund = j.refund + } + // Merge changes to accounts + if parent.accountChanges == nil { + parent.accountChanges = j.accountChanges + } else { + for addr, data := range j.accountChanges { + if _, present := parent.accountChanges[addr]; present { + // Nothing to do here, it's already stored in parent scope + continue + } + parent.accountChanges[addr] = data + } + } + // Merge logs + parent.logs = append(parent.logs, j.logs...) + + // Merge access list additions + parent.accessListAddrSlots = append(parent.accessListAddrSlots, j.accessListAddrSlots...) + parent.accessListAddresses = append(parent.accessListAddresses, j.accessListAddresses...) + + if parent.storageChanges == nil { + parent.storageChanges = j.storageChanges + } else { + // Merge storage changes + for addr, changes := range j.storageChanges { + prevChanges, ok := parent.storageChanges[addr] + if !ok { + parent.storageChanges[addr] = changes + continue + } + for k, v := range changes { + if _, ok := prevChanges[k]; !ok { + prevChanges[k] = v + } + } + } + } + if parent.tStorageChanges == nil { + parent.tStorageChanges = j.tStorageChanges + } else { + // Merge t-store changes + for addr, changes := range j.tStorageChanges { + prevChanges, ok := parent.tStorageChanges[addr] + if !ok { + parent.tStorageChanges[addr] = changes + continue + } + for k, v := range changes { + if _, ok := prevChanges[k]; !ok { + prevChanges[k] = v + } + } + } + } + // Copy the code lookup + if j.code != nil { + if parent.code == nil { + parent.code = j.code + } else { + maps.Copy(parent.code, j.code) + } + } +} + +func (j *scopedJournal) addDirtyAccounts(set map[common.Address]any) { + // Changes due to account changes + for addr := range j.accountChanges { + set[addr] = []interface{}{} + } + // Changes due to storage changes + for addr := range j.storageChanges { + set[addr] = []interface{}{} + } +} + +// sparseJournal contains the list of state modifications applied since the last state +// commit. These are tracked to be able to be reverted in the case of an execution +// exception or request for reversal. +type sparseJournal struct { + entries []*scopedJournal // Current changes tracked by the journal + ripeMagic bool +} + +// newJournal creates a new initialized journal. +func newSparseJournal() *sparseJournal { + s := new(sparseJournal) + s.snapshot() // create snaphot zero + return s +} + +// reset clears the journal, after this operation the journal can be used +// anew. It is semantically similar to calling 'newJournal', but the underlying +// slices can be reused +func (j *sparseJournal) reset() { + j.entries = j.entries[:0] + j.ripeMagic = false + j.snapshot() +} + +func (j *sparseJournal) copy() journal { + cp := &sparseJournal{ + entries: make([]*scopedJournal, 0, len(j.entries)), + ripeMagic: j.ripeMagic, + } + for _, entry := range j.entries { + cp.entries = append(cp.entries, entry.deepCopy()) + } + return cp +} + +// snapshot starts a new journal scope which can be reverted or discarded. +// OBS: A call to Snapshot is _required_ in order to initialize the journalling, +// invoking the journal-methods without having invoked Snapshot will lead to +// panic. +func (j *sparseJournal) snapshot() { + j.entries = append(j.entries, newScopedJournal()) +} + +// revertSnapshot reverts all state changes made since the last call to snapshot(). +func (j *sparseJournal) revertSnapshot(s *StateDB) { + id := len(j.entries) - 1 + j.entries[id].revert(s) + j.entries = j.entries[:id] + if id == 0 { + j.snapshot() + } +} + +// discardSnapshot removes the latest snapshot; after calling this +// method, it is no longer possible to revert to that particular snapshot, the +// changes are considered part of the parent scope. +func (j *sparseJournal) discardSnapshot() { + id := len(j.entries) - 1 + // here we must merge the 'id' with it's parent. + if id == 0 { + // If a transaction is applied successfully, the statedb.Finalize will + // end by clearing and resetting the journal. Invoking a discardSnapshot + // afterwards will land here: calling discard on an empty journal. + // This is fine + return + } + entry := j.entries[id] + parent := j.entries[id-1] + entry.merge(parent) + j.entries = j.entries[:id] +} + +func (j *sparseJournal) journalAccountChange(addr common.Address, account *types.StateAccount, destructed, newContract bool) { + j.entries[len(j.entries)-1].journalAccountChange(addr, account, destructed, newContract) +} + +func (j *sparseJournal) nonceChange(addr common.Address, account *types.StateAccount, destructed, newContract bool) { + j.journalAccountChange(addr, account, destructed, newContract) +} + +func (j *sparseJournal) balanceChange(addr common.Address, account *types.StateAccount, destructed, newContract bool) { + j.journalAccountChange(addr, account, destructed, newContract) +} + +func (j *sparseJournal) setCode(addr common.Address, account *types.StateAccount, prev []byte) { + j.journalAccountChange(addr, account, false, true) + if len(prev) > 0 { + // Keep the code in a lookup + j.entries[len(j.entries)-1].stashCode(prev) + } +} + +func (j *sparseJournal) createObject(addr common.Address) { + // Creating an account which is destructed, hence already exists, is not + // allowed, hence we know destructed == 'false'. + // Also, if we are creating the account now, it cannot yet be a + // newContract (that might come later) + j.journalAccountChange(addr, nil, false, false) +} + +func (j *sparseJournal) createContract(addr common.Address, account *types.StateAccount) { + // Creating an account which is destructed, hence already exists, is not + // allowed, hence we know it to be 'false'. + // Also: if we create the contract now, it cannot be previously created + j.journalAccountChange(addr, account, false, false) +} + +func (j *sparseJournal) destruct(addr common.Address, account *types.StateAccount) { + // destructing an already destructed account must not be journalled. Hence we + // know it to be 'false'. + // Also: if we're allowed to destruct it, it must be `newContract:true`, OR + // the concept of newContract is unused and moot. + j.journalAccountChange(addr, account, false, true) +} + +// var ripemd = common.HexToAddress("0000000000000000000000000000000000000003") +func (j *sparseJournal) touchChange(addr common.Address, account *types.StateAccount, destructed, newContract bool) { + j.journalAccountChange(addr, account, destructed, newContract) + if addr == ripemd { + // Explicitly put it in the dirty-cache one extra time. Ripe magic. + j.ripeMagic = true + } +} + +func (j *sparseJournal) logChange(txHash common.Hash) { + j.entries[len(j.entries)-1].journalLog(txHash) +} + +func (j *sparseJournal) refundChange(prev uint64) { + j.entries[len(j.entries)-1].journalRefundChange(prev) +} + +func (j *sparseJournal) accessListAddAccount(addr common.Address) { + j.entries[len(j.entries)-1].journalAccessListAddAccount(addr) +} + +func (j *sparseJournal) accessListAddSlot(addr common.Address, slot common.Hash) { + j.entries[len(j.entries)-1].journalAccessListAddSlot(addr, slot) +} + +func (j *sparseJournal) storageChange(addr common.Address, key, prev, origin common.Hash) { + j.entries[len(j.entries)-1].journalSetState(addr, key, prev, origin) +} + +func (j *sparseJournal) transientStateChange(addr common.Address, key, prev common.Hash) { + j.entries[len(j.entries)-1].journalSetTransientState(addr, key, prev) +} + +func (j *sparseJournal) dirtyAccounts() []common.Address { + // The dirty-set should encompass all layers + var dirty = make(map[common.Address]any) + for _, scope := range j.entries { + scope.addDirtyAccounts(dirty) + } + if j.ripeMagic { + dirty[ripemd] = []interface{}{} + } + var dirtyList = make([]common.Address, 0, len(dirty)) + for addr := range dirty { + dirtyList = append(dirtyList, addr) + } + return dirtyList +} diff --git a/core/state/journal_test.go b/core/state/journal_test.go new file mode 100644 index 0000000000..70eeb7538f --- /dev/null +++ b/core/state/journal_test.go @@ -0,0 +1,397 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package state provides a caching layer atop the Ethereum state trie. +package state + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "fmt" + "io" + "slices" + "testing" + + "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/holiman/uint256" +) + +func TestLinearJournalDirty(t *testing.T) { + testJournalDirty(t, newLinearJournal()) +} + +func TestSparseJournalDirty(t *testing.T) { + testJournalDirty(t, newSparseJournal()) +} + +// This test verifies some basics around journalling: the ability to +// deliver a dirty-set. +func testJournalDirty(t *testing.T, j journal) { + acc := &types.StateAccount{ + Nonce: 1, + Balance: new(uint256.Int), + Root: common.Hash{}, + CodeHash: nil, + } + { + j.nonceChange(common.Address{0x1}, acc, false, false) + if have, want := len(j.dirtyAccounts()), 1; have != want { + t.Errorf("wrong size of dirty accounts, have %v want %v", have, want) + } + } + { + j.storageChange(common.Address{0x2}, common.Hash{0x1}, common.Hash{0x1}, common.Hash{}) + if have, want := len(j.dirtyAccounts()), 2; have != want { + t.Errorf("wrong size of dirty accounts, have %v want %v", have, want) + } + } + { // The previous scopes should also be accounted for + j.snapshot() + if have, want := len(j.dirtyAccounts()), 2; have != want { + t.Errorf("wrong size of dirty accounts, have %v want %v", have, want) + } + } +} + +func TestLinearJournalAccessList(t *testing.T) { + testJournalAccessList(t, newLinearJournal()) +} + +func TestSparseJournalAccessList(t *testing.T) { + testJournalAccessList(t, newSparseJournal()) +} + +func testJournalAccessList(t *testing.T, j journal) { + var statedb = &StateDB{} + statedb.accessList = newAccessList() + statedb.journal = j + + j.snapshot() + { + // If the journal performs the rollback in the wrong order, this + // will cause a panic. + statedb.AddSlotToAccessList(common.Address{0x1}, common.Hash{0x4}) + statedb.AddSlotToAccessList(common.Address{0x3}, common.Hash{0x4}) + } + statedb.RevertSnapshot() + j.snapshot() + { + statedb.AddAddressToAccessList(common.Address{0x2}) + statedb.AddAddressToAccessList(common.Address{0x3}) + statedb.AddAddressToAccessList(common.Address{0x4}) + } + statedb.RevertSnapshot() + if statedb.accessList.ContainsAddress(common.Address{0x2}) { + t.Fatal("should be missing") + } +} + +func TestLinearJournalRefunds(t *testing.T) { + testJournalRefunds(t, newLinearJournal()) +} + +func TestSparseJournalRefunds(t *testing.T) { + testJournalRefunds(t, newSparseJournal()) +} + +func testJournalRefunds(t *testing.T, j journal) { + var statedb = &StateDB{} + statedb.accessList = newAccessList() + statedb.journal = j + j.snapshot() + { + j.refundChange(0) + j.refundChange(1) + j.snapshot() + { + j.refundChange(2) + j.refundChange(3) + } + j.revertSnapshot(statedb) + if have, want := statedb.refund, uint64(2); have != want { + t.Fatalf("have %d want %d", have, want) + } + j.snapshot() + { + j.refundChange(2) + j.refundChange(3) + } + j.discardSnapshot() + } + j.revertSnapshot(statedb) + if have, want := statedb.refund, uint64(0); have != want { + t.Fatalf("have %d want %d", have, want) + } +} + +type fuzzReader struct { + input io.Reader + exhausted bool +} + +func (f *fuzzReader) byte() byte { + return f.bytes(1)[0] +} + +func (f *fuzzReader) bytes(n int) []byte { + r := make([]byte, n) + if _, err := f.input.Read(r); err != nil { + f.exhausted = true + } + return r +} + +func newEmptyState() *StateDB { + s, _ := New(types.EmptyRootHash, NewDatabaseForTesting()) + return s +} + +// fuzzJournals is pretty similar to `TestSnapshotRandom`/ `newTestAction` in +// statedb_test.go. They both execute a sequence of state-actions, however, they +// test for different aspects. +// This test compares two differing journal-implementations. +// The other test compares every point in time, whether it is identical when going +// forward as when going backwards through the journal entries. +func fuzzJournals(t *testing.T, data []byte) { + var ( + reader = fuzzReader{input: bytes.NewReader(data)} + stateDbs = []*StateDB{ + newEmptyState(), + newEmptyState(), + } + ) + apply := func(action func(stateDbs *StateDB)) { + for _, sdb := range stateDbs { + action(sdb) + } + } + stateDbs[0].journal = newLinearJournal() + stateDbs[1].journal = newSparseJournal() + + for !reader.exhausted { + op := reader.byte() % 18 + switch op { + case 0: // Add account to access lists + addr := common.BytesToAddress(reader.bytes(1)) + t.Logf("Op %d: Add to access list %#x", op, addr) + apply(func(sdb *StateDB) { + sdb.accessList.AddAddress(addr) + }) + case 1: // Add slot to access list + addr := common.BytesToAddress(reader.bytes(1)) + slot := common.BytesToHash(reader.bytes(1)) + t.Logf("Op %d: Add addr:slot to access list %#x : %#x", op, addr, slot) + apply(func(sdb *StateDB) { + sdb.AddSlotToAccessList(addr, slot) + }) + case 2: + var ( + addr = common.BytesToAddress(reader.bytes(1)) + value = uint64(reader.byte()) + ) + t.Logf("Op %d: Add balance %#x %d", op, addr, value) + apply(func(sdb *StateDB) { + sdb.AddBalance(addr, uint256.NewInt(value), 0) + }) + case 3: + t.Logf("Op %d: Copy journals[0]", op) + stateDbs[0].journal = stateDbs[0].journal.copy() + case 4: + t.Logf("Op %d: Copy journals[1]", op) + stateDbs[1].journal = stateDbs[1].journal.copy() + case 5: + var ( + addr = common.BytesToAddress(reader.bytes(1)) + code = reader.bytes(2) + ) + t.Logf("Op %d: (Create and) set code 0x%x", op, addr) + apply(func(s *StateDB) { + if !s.Exist(addr) { + s.CreateAccount(addr) + } + storageRoot := s.GetStorageRoot(addr) + emptyStorage := storageRoot == (common.Hash{}) || storageRoot == types.EmptyRootHash + + if obj := s.getStateObject(addr); obj != nil { + if obj.selfDestructed { + // If it's selfdestructed, we cannot create into it + return + } + } + if emptyStorage { + s.CreateContract(addr) + // We also set some code here, to prevent the + // CreateContract action from being performed twice in a row, + // which would cause a difference in state when unrolling + // the linearJournal. (CreateContact assumes created was false prior to + // invocation, and the linearJournal rollback sets it to false). + s.SetCode(addr, code, tracing.CodeChangeUnspecified) + } + }) + case 6: + addr := common.BytesToAddress(reader.bytes(1)) + t.Logf("Op %d: Create 0x%x", op, addr) + apply(func(sdb *StateDB) { + if !sdb.Exist(addr) { + sdb.CreateAccount(addr) + } + }) + case 7: + addr := common.BytesToAddress(reader.bytes(1)) + t.Logf("Op %d: (Create and) destruct 0x%x", op, addr) + apply(func(s *StateDB) { + if !s.Exist(addr) { + s.CreateAccount(addr) + } + s.SelfDestruct(addr) + }) + case 8: + txHash := common.BytesToHash(reader.bytes(1)) + t.Logf("Op %d: Add log %#x", op, txHash) + apply(func(sdb *StateDB) { + sdb.logs[txHash] = append(sdb.logs[txHash], new(types.Log)) + sdb.logSize++ + sdb.journal.logChange(txHash) + }) + case 9: + var ( + addr = common.BytesToAddress(reader.bytes(1)) + nonce = binary.BigEndian.Uint64(reader.bytes(8)) + ) + t.Logf("Op %d: Set nonce %#x %d", op, addr, nonce) + apply(func(sdb *StateDB) { + sdb.SetNonce(addr, nonce, tracing.NonceChangeAuthorization) + }) + case 10: + refund := uint64(reader.byte()) + t.Logf("Op %d: Set refund %d", op, refund) + apply(func(sdb *StateDB) { + sdb.journal.refundChange(refund) + }) + case 11: + var ( + addr = common.BytesToAddress(reader.bytes(1)) + key = common.BytesToHash(reader.bytes(1)) + val = common.BytesToHash(reader.bytes(1)) + ) + t.Logf("Op %d: Set storage %#x [%#x]=%#x", op, addr, key, val) + apply(func(sdb *StateDB) { + sdb.SetState(addr, key, val) + }) + case 12: + var ( + addr = common.BytesToAddress(reader.bytes(1)) + ) + t.Logf("Op %d: Zero-balance transfer (touch) %#x", op, addr) + apply(func(sdb *StateDB) { + sdb.AddBalance(addr, uint256.NewInt(0), 0) + }) + case 13: + var ( + addr = common.BytesToAddress(reader.bytes(1)) + key = common.BytesToHash(reader.bytes(1)) + value = common.BytesToHash(reader.bytes(1)) + ) + t.Logf("Op %d: Set t-storage %#x [%#x]=%#x", op, addr, key, value) + apply(func(sdb *StateDB) { + sdb.SetTransientState(addr, key, value) + }) + case 14: + t.Logf("Op %d: Reset journal", op) + apply(func(sdb *StateDB) { + sdb.journal.reset() + }) + case 15: + t.Logf("Op %d: Snapshot", op) + apply(func(sdb *StateDB) { + sdb.Snapshot() + }) + case 16: + t.Logf("Op %d: Discard snapshot", op) + apply(func(sdb *StateDB) { + sdb.DiscardSnapshot() + }) + + case 17: + t.Logf("Op %d: Revert snapshot", op) + apply(func(sdb *StateDB) { + sdb.RevertSnapshot() + }) + } + // Cross-check the dirty-sets + accs1 := stateDbs[0].journal.dirtyAccounts() + slices.SortFunc(accs1, func(a, b common.Address) int { + return bytes.Compare(a.Bytes(), b.Bytes()) + }) + accs2 := stateDbs[1].journal.dirtyAccounts() + slices.SortFunc(accs2, func(a, b common.Address) int { + return bytes.Compare(a.Bytes(), b.Bytes()) + }) + if !slices.Equal(accs1, accs2) { + t.Fatalf("mismatched dirty-sets:\n%v\n%v", accs1, accs2) + } + + for _, addr := range accs1 { + if cHash := stateDbs[0].GetCodeHash(addr); cHash != types.EmptyCodeHash && cHash != (common.Hash{}) { + have := crypto.Keccak256Hash(stateDbs[0].GetCode(addr)) + if have != cHash { + t.Fatalf("0: mismatched codehash <-> code.\ncodehash: %x\nhash(code): %x\n", cHash, have) + } + have = crypto.Keccak256Hash(stateDbs[1].GetCode(addr)) + if have != cHash { + t.Fatalf("1: mismatched codehash <-> code.\ncodehash: %x\nhash(code): %x\n", cHash, have) + } + } + } + } + h1, err1 := stateDbs[0].Commit(0, false, false) + h2, err2 := stateDbs[1].Commit(0, false, false) + if err1 != err2 { + t.Fatalf("Mismatched errors: %v %v", err1, err2) + } + if h1 != h2 { + t.Fatalf("Mismatched roots: %v %v", h1, h2) + } +} + +// FuzzJournals fuzzes the journals. +func FuzzJournals(f *testing.F) { + f.Fuzz(fuzzJournals) +} + +// TestFuzzJournals runs 200 fuzz-tests +func TestFuzzJournals(t *testing.T) { + input := make([]byte, 200) + for i := 0; i < 200; i++ { + rand.Read(input) + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + t.Logf("input: %x", input) + fuzzJournals(t, input) + }) + } +} + +// TestFuzzJournalsSpecific can be used to test a specific input +func TestFuzzJournalsSpecific(t *testing.T) { + t.Skip("example") + input := common.FromHex("71d598d781f65eb7c047fed5d09b1e4e0c1ecad5c447a2149e7d1137fcb1b1d63f4ba6f761918a441a98eb61d69fe011cabfbce00d74bb78539ca9946a602e94d6eabc43c0924ba65ce3e171b476208059d81f33e81d90607e0b6e59d6016840b5c4e9b1a8e9798a5a40be909930658eea351d7a312dba0b1c7199c7e5f62a908a80f7faf29bc0108faae0cf0f497d0f4cd228b7600ef0d88532dfafa6349ea7782f28ad7426eeffc155282a9e58a606d25acd8a730dde61a6e5e887d1ba1fea813bb7f2c6caff25") + fuzzJournals(t, input) +} diff --git a/core/state/state_object.go b/core/state/state_object.go index 767f469bfd..a97c97802f 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -114,7 +114,7 @@ func (s *stateObject) markSelfdestructed() { } func (s *stateObject) touch() { - s.db.journal.touchChange(s.address) + s.db.journal.touchChange(s.address, &s.data, s.selfDestructed, s.newContract) } // getTrie returns the associated storage trie. The trie will be opened if it's @@ -469,7 +469,7 @@ func (s *stateObject) AddBalance(amount *uint256.Int) uint256.Int { // SetBalance sets the balance for the object, and returns the previous balance. func (s *stateObject) SetBalance(amount *uint256.Int) uint256.Int { prev := *s.data.Balance - s.db.journal.balanceChange(s.address, s.data.Balance) + s.db.journal.balanceChange(s.address, &s.data, s.selfDestructed, s.newContract) s.setBalance(amount) return prev } @@ -550,7 +550,7 @@ func (s *stateObject) CodeSize() int { func (s *stateObject) SetCode(codeHash common.Hash, code []byte) (prev []byte) { prev = slices.Clone(s.code) - s.db.journal.setCode(s.address, prev) + s.db.journal.setCode(s.address, &s.data, prev) s.setCode(codeHash, code) return prev } @@ -562,7 +562,7 @@ func (s *stateObject) setCode(codeHash common.Hash, code []byte) { } func (s *stateObject) SetNonce(nonce uint64) { - s.db.journal.nonceChange(s.address, s.data.Nonce) + s.db.journal.nonceChange(s.address, &s.data, s.selfDestructed, s.newContract) s.setNonce(nonce) } diff --git a/core/state/state_test.go b/core/state/state_test.go index eeeb7fa2df..640fe9d7cc 100644 --- a/core/state/state_test.go +++ b/core/state/state_test.go @@ -156,25 +156,26 @@ func TestSnapshot(t *testing.T) { s := newStateEnv() // snapshot the genesis state - genesis := s.state.Snapshot() + s.state.Snapshot() + { + // set initial state object value + s.state.SetState(stateobjaddr, storageaddr, data1) + s.state.Snapshot() + { + // set a new state object value, revert it and ensure correct content + s.state.SetState(stateobjaddr, storageaddr, data2) + } + s.state.RevertSnapshot() - // set initial state object value - s.state.SetState(stateobjaddr, storageaddr, data1) - snapshot := s.state.Snapshot() - - // set a new state object value, revert it and ensure correct content - s.state.SetState(stateobjaddr, storageaddr, data2) - s.state.RevertToSnapshot(snapshot) - - if v := s.state.GetState(stateobjaddr, storageaddr); v != data1 { - t.Errorf("wrong storage value %v, want %v", v, data1) + if v := s.state.GetState(stateobjaddr, storageaddr); v != data1 { + t.Errorf("wrong storage value %v, want %v", v, data1) + } + if v := s.state.GetCommittedState(stateobjaddr, storageaddr); v != (common.Hash{}) { + t.Errorf("wrong committed storage value %v, want %v", v, common.Hash{}) + } } - if v := s.state.GetCommittedState(stateobjaddr, storageaddr); v != (common.Hash{}) { - t.Errorf("wrong committed storage value %v, want %v", v, common.Hash{}) - } - // revert up to the genesis state and ensure correct content - s.state.RevertToSnapshot(genesis) + s.state.RevertSnapshot() if v := s.state.GetState(stateobjaddr, storageaddr); v != (common.Hash{}) { t.Errorf("wrong storage value %v, want %v", v, common.Hash{}) } @@ -185,22 +186,23 @@ func TestSnapshot(t *testing.T) { func TestSnapshotEmpty(t *testing.T) { s := newStateEnv() - s.state.RevertToSnapshot(s.state.Snapshot()) + s.state.Snapshot() + s.state.RevertSnapshot() } func TestCreateObjectRevert(t *testing.T) { state, _ := New(types.EmptyRootHash, NewDatabaseForTesting()) addr := common.BytesToAddress([]byte("so0")) - snap := state.Snapshot() - - state.CreateAccount(addr) - so0 := state.getStateObject(addr) - so0.SetBalance(uint256.NewInt(42)) - so0.SetNonce(43) - so0.SetCode(crypto.Keccak256Hash([]byte{'c', 'a', 'f', 'e'}), []byte{'c', 'a', 'f', 'e'}) - state.setStateObject(so0) - - state.RevertToSnapshot(snap) + state.Snapshot() + { + state.CreateAccount(addr) + so0 := state.getStateObject(addr) + so0.SetBalance(uint256.NewInt(42)) + so0.SetNonce(43) + so0.SetCode(crypto.Keccak256Hash([]byte{'c', 'a', 'f', 'e'}), []byte{'c', 'a', 'f', 'e'}) + state.setStateObject(so0) + } + state.RevertSnapshot() if state.Exist(addr) { t.Error("Unexpected account after revert") } diff --git a/core/state/statedb.go b/core/state/statedb.go index b770698255..a850738e39 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -132,8 +132,8 @@ type StateDB struct { transientStorage transientStorage // Journal of state modifications. This is the backbone of - // Snapshot and RevertToSnapshot. - journal *journal + // Snapshot and RevertSnapshot. + journal journal // State witness if cross validation is needed witness *stateless.Witness @@ -179,7 +179,7 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro mutations: make(map[common.Address]*mutation), logs: make(map[common.Hash][]*types.Log), preimages: make(map[common.Hash][]byte), - journal: newJournal(), + journal: newSparseJournal(), accessList: newAccessList(), transientStorage: newTransientStorage(), } @@ -522,7 +522,7 @@ func (s *StateDB) SelfDestruct(addr common.Address) uint256.Int { // If it is already marked as self-destructed, we do not need to add it // for journalling a second time. if !stateObject.selfDestructed { - s.journal.destruct(addr) + s.journal.destruct(addr, &stateObject.data) stateObject.markSelfdestructed() } return prevBalance @@ -660,7 +660,7 @@ func (s *StateDB) CreateContract(addr common.Address) { obj := s.getStateObject(addr) if !obj.newContract { obj.newContract = true - s.journal.createContract(addr) + s.journal.createContract(addr, &obj.data) } } @@ -726,14 +726,21 @@ func (s *StateDB) Copy() *StateDB { return state } -// Snapshot returns an identifier for the current revision of the state. -func (s *StateDB) Snapshot() int { - return s.journal.snapshot() +// Snapshot starts a new journalled scope. +func (s *StateDB) Snapshot() { + s.journal.snapshot() } -// RevertToSnapshot reverts all state changes made since the given revision. -func (s *StateDB) RevertToSnapshot(revid int) { - s.journal.revertToSnapshot(revid, s) +// DiscardSnapshot removes the ability to roll back the changes in the most +// recent journalled scope. After calling this method, the changes are considered +// part of the parent scope. +func (s *StateDB) DiscardSnapshot() { + s.journal.discardSnapshot() +} + +// RevertSnapshot reverts all state changes made in the most recent journalled scope. +func (s *StateDB) RevertSnapshot() { + s.journal.revertSnapshot(s) } // GetRefund returns the current value of the refund counter. @@ -745,8 +752,9 @@ func (s *StateDB) GetRefund() uint64 { // 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([]common.Address, 0, len(s.journal.dirties)) - for addr := range s.journal.dirties { + dirties := s.journal.dirtyAccounts() + addressesToPrefetch := make([]common.Address, 0, len(dirties)) + for _, addr := range dirties { obj, exist := s.stateObjects[addr] if !exist { // ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2 diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index d2595bcefe..4998f1b6c8 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -141,12 +141,16 @@ func (s *hookedStateDB) Prepare(rules params.Rules, sender, coinbase common.Addr s.inner.Prepare(rules, sender, coinbase, dest, precompiles, txAccesses) } -func (s *hookedStateDB) RevertToSnapshot(i int) { - s.inner.RevertToSnapshot(i) +func (s *hookedStateDB) DiscardSnapshot() { + s.inner.DiscardSnapshot() } -func (s *hookedStateDB) Snapshot() int { - return s.inner.Snapshot() +func (s *hookedStateDB) RevertSnapshot() { + s.inner.RevertSnapshot() +} + +func (s *hookedStateDB) Snapshot() { + s.inner.Snapshot() } func (s *hookedStateDB) AddPreimage(hash common.Hash, bytes []byte) { @@ -280,7 +284,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) { if s.hooks.OnBalanceChange == nil { return } - for addr := range s.inner.journal.dirties { + for _, addr := range s.inner.journal.dirtyAccounts() { obj := s.inner.stateObjects[addr] if obj != nil && obj.selfDestructed { // If ether was sent to account post-selfdestruct it is burnt. diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 147546a3c7..14a4c5bdb4 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -55,7 +55,7 @@ func TestUpdateLeaks(t *testing.T) { sdb = NewDatabase(tdb, nil) ) state, _ := New(types.EmptyRootHash, sdb) - + state.Snapshot() // Update it with some accounts for i := byte(0); i < 255; i++ { addr := common.BytesToAddress([]byte{i}) @@ -111,7 +111,7 @@ func TestIntermediateLeaks(t *testing.T) { } // Write modifications to trie. transState.IntermediateRoot(false) - + transState.journal.snapshot() // Overwrite all the data with new values in the transient database. for i := byte(0); i < 255; i++ { modify(transState, common.Address{i}, i, 99) @@ -223,7 +223,7 @@ func TestCopy(t *testing.T) { } // TestCopyWithDirtyJournal tests if Copy can correct create a equal copied -// stateDB with dirty journal present. +// stateDB with dirty linearJournal present. func TestCopyWithDirtyJournal(t *testing.T) { db := NewDatabaseForTesting() orig, _ := New(types.EmptyRootHash, db) @@ -355,6 +355,12 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction { { name: "SetStorage", fn: func(a testAction, s *StateDB) { + contractHash := s.GetCodeHash(addr) + emptyCode := contractHash == (common.Hash{}) || contractHash == types.EmptyCodeHash + if emptyCode { + // no-op + return + } var key, val common.Hash binary.BigEndian.PutUint16(key[:], uint16(a.args[0])) binary.BigEndian.PutUint16(val[:], uint16(a.args[1])) @@ -365,12 +371,26 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction { { name: "SetCode", fn: func(a testAction, s *StateDB) { - // SetCode can only be performed in case the addr does - // not already hold code + // SetCode cannot be performed if the addr already has code if c := s.GetCode(addr); len(c) > 0 { // no-op return } + // SetCode cannot be performed if the addr has just selfdestructed + if obj := s.getStateObject(addr); obj != nil { + if obj.selfDestructed { + // If it's selfdestructed, we cannot create into it + return + } + } + // SetCode requires the contract to be account + contract to be created first + if obj := s.getStateObject(addr); obj == nil { + s.createObject(addr) + } + obj := s.getStateObject(addr) + if !obj.newContract { + s.CreateContract(addr) + } code := make([]byte, 16) binary.BigEndian.PutUint64(code, uint64(a.args[0])) binary.BigEndian.PutUint64(code[8:], uint64(a.args[1])) @@ -396,13 +416,20 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction { emptyCode := contractHash == (common.Hash{}) || contractHash == types.EmptyCodeHash storageRoot := s.GetStorageRoot(addr) emptyStorage := storageRoot == (common.Hash{}) || storageRoot == types.EmptyRootHash + + if obj := s.getStateObject(addr); obj != nil { + if obj.selfDestructed { + // If it's selfdestructed, we cannot create into it + return + } + } if s.GetNonce(addr) == 0 && emptyCode && emptyStorage { s.CreateContract(addr) // We also set some code here, to prevent the // CreateContract action from being performed twice in a row, // which would cause a difference in state when unrolling - // the journal. (CreateContact assumes created was false prior to - // invocation, and the journal rollback sets it to false). + // the linearJournal. (CreateContact assumes created was false prior to + // invocation, and the linearJournal rollback sets it to false). s.SetCode(addr, []byte{1}, tracing.CodeChangeUnspecified) } }, @@ -410,6 +437,15 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction { { name: "SelfDestruct", fn: func(a testAction, s *StateDB) { + obj := s.getStateObject(addr) + // SelfDestruct requires the object to first exist + if obj == nil { + s.createObject(addr) + } + obj = s.getStateObject(addr) + if !obj.newContract { + s.CreateContract(addr) + } s.SelfDestruct(addr) }, }, @@ -430,15 +466,6 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction { }, args: make([]int64, 1), }, - { - name: "AddPreimage", - fn: func(a testAction, s *StateDB) { - preimage := []byte{1} - hash := common.BytesToHash(preimage) - s.AddPreimage(hash, preimage) - }, - args: make([]int64, 1), - }, { name: "AddAddressToAccessList", fn: func(a testAction, s *StateDB) { @@ -456,6 +483,13 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction { { name: "SetTransientState", fn: func(a testAction, s *StateDB) { + contractHash := s.GetCodeHash(addr) + emptyCode := contractHash == (common.Hash{}) || contractHash == types.EmptyCodeHash + if emptyCode { + // no-op + return + } + var key, val common.Hash binary.BigEndian.PutUint16(key[:], uint16(a.args[0])) binary.BigEndian.PutUint16(val[:], uint16(a.args[1])) @@ -520,14 +554,13 @@ func (test *snapshotTest) String() string { func (test *snapshotTest) run() bool { // Run all actions and create snapshots. var ( - state, _ = New(types.EmptyRootHash, NewDatabaseForTesting()) - snapshotRevs = make([]int, len(test.snapshots)) - sindex = 0 - checkstates = make([]*StateDB, len(test.snapshots)) + state, _ = New(types.EmptyRootHash, NewDatabaseForTesting()) + sindex = 0 + checkstates = make([]*StateDB, len(test.snapshots)) ) for i, action := range test.actions { if len(test.snapshots) > sindex && i == test.snapshots[sindex] { - snapshotRevs[sindex] = state.Snapshot() + state.Snapshot() checkstates[sindex] = state.Copy() sindex++ } @@ -536,7 +569,7 @@ func (test *snapshotTest) run() bool { // Revert all snapshots in reverse order. Each revert must yield a state // that is equivalent to fresh state with all actions up the snapshot applied. for sindex--; sindex >= 0; sindex-- { - state.RevertToSnapshot(snapshotRevs[sindex]) + state.RevertSnapshot() if err := test.checkEqual(state, checkstates[sindex]); err != nil { test.err = fmt.Errorf("state mismatch after revert to snapshot %d\n%v", sindex, err) return false @@ -664,22 +697,23 @@ func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error { return fmt.Errorf("got GetLogs(common.Hash{}) == %v, want GetLogs(common.Hash{}) == %v", state.GetLogs(common.Hash{}, 0, common.Hash{}, 0), checkstate.GetLogs(common.Hash{}, 0, common.Hash{}, 0)) } - if !maps.Equal(state.journal.dirties, checkstate.journal.dirties) { - getKeys := func(dirty map[common.Address]int) string { - var keys []common.Address - out := new(strings.Builder) - for key := range dirty { - keys = append(keys, key) + { // Check the dirty-accounts + have := state.journal.dirtyAccounts() + want := checkstate.journal.dirtyAccounts() + slices.SortFunc(have, common.Address.Cmp) + slices.SortFunc(want, common.Address.Cmp) + if !slices.Equal(have, want) { + getKeys := func(keys []common.Address) string { + out := new(strings.Builder) + for i, key := range keys { + fmt.Fprintf(out, " %d. %v\n", i, key) + } + return out.String() } - slices.SortFunc(keys, common.Address.Cmp) - for i, key := range keys { - fmt.Fprintf(out, " %d. %v\n", i, key) - } - return out.String() + haveK := getKeys(have) + wantK := getKeys(want) + return fmt.Errorf("dirty-journal set mismatch.\nhave:\n%v\nwant:\n%v\n", haveK, wantK) } - have := getKeys(state.journal.dirties) - want := getKeys(checkstate.journal.dirties) - return fmt.Errorf("dirty-journal set mismatch.\nhave:\n%v\nwant:\n%v\n", have, want) } return nil } @@ -690,14 +724,14 @@ func TestTouchDelete(t *testing.T) { root, _ := s.state.Commit(0, false, false) s.state, _ = New(root, s.state.db) - snapshot := s.state.Snapshot() + s.state.Snapshot() s.state.AddBalance(common.Address{}, new(uint256.Int), tracing.BalanceChangeUnspecified) - if len(s.state.journal.dirties) != 1 { + if len(s.state.journal.dirtyAccounts()) != 1 { t.Fatal("expected one dirty state object") } - s.state.RevertToSnapshot(snapshot) - if len(s.state.journal.dirties) != 0 { + s.state.RevertSnapshot() + if len(s.state.journal.dirtyAccounts()) != 0 { t.Fatal("expected no dirty state object") } } @@ -941,9 +975,11 @@ func TestDeleteCreateRevert(t *testing.T) { state.SelfDestruct(addr) state.Finalise(true) - id := state.Snapshot() - state.SetBalance(addr, uint256.NewInt(2), tracing.BalanceChangeUnspecified) - state.RevertToSnapshot(id) + state.Snapshot() + { + state.SetBalance(addr, uint256.NewInt(2), tracing.BalanceChangeUnspecified) + } + state.RevertSnapshot() // Commit the entire state and make sure we don't crash and have the correct state root, _ = state.Commit(0, true, false) @@ -1091,33 +1127,34 @@ func TestStateDBAccessList(t *testing.T) { } } + state.journal.snapshot() // journal id 0 state.AddAddressToAccessList(addr("aa")) // 1 - state.AddSlotToAccessList(addr("bb"), slot("01")) // 2,3 + state.journal.snapshot() // journal id 1 + state.AddAddressToAccessList(addr("bb")) // 2 + state.journal.snapshot() // journal id 2 + state.AddSlotToAccessList(addr("bb"), slot("01")) // 3 + state.journal.snapshot() // journal id 3 state.AddSlotToAccessList(addr("bb"), slot("02")) // 4 + state.journal.snapshot() // journal id 4 verifyAddrs("aa", "bb") verifySlots("bb", "01", "02") // Make a copy stateCopy1 := state.Copy() - if exp, got := 4, state.journal.length(); exp != got { - t.Fatalf("journal length mismatch: have %d, want %d", got, exp) - } - // same again, should cause no journal entries + // same again, should cause no linearJournal entries state.AddSlotToAccessList(addr("bb"), slot("01")) state.AddSlotToAccessList(addr("bb"), slot("02")) state.AddAddressToAccessList(addr("aa")) - if exp, got := 4, state.journal.length(); exp != got { - t.Fatalf("journal length mismatch: have %d, want %d", got, exp) - } + // some new ones state.AddSlotToAccessList(addr("bb"), slot("03")) // 5 + state.journal.snapshot() // journal id 5 state.AddSlotToAccessList(addr("aa"), slot("01")) // 6 - state.AddSlotToAccessList(addr("cc"), slot("01")) // 7,8 - state.AddAddressToAccessList(addr("cc")) - if exp, got := 8, state.journal.length(); exp != got { - t.Fatalf("journal length mismatch: have %d, want %d", got, exp) - } + state.journal.snapshot() // journal id 6 + state.AddAddressToAccessList(addr("cc")) // 7 + state.journal.snapshot() // journal id 7 + state.AddSlotToAccessList(addr("cc"), slot("01")) // 8 verifyAddrs("aa", "bb", "cc") verifySlots("aa", "01") @@ -1125,7 +1162,7 @@ func TestStateDBAccessList(t *testing.T) { verifySlots("cc", "01") // now start rolling back changes - state.journal.revert(state, 7) + state.journal.revertSnapshot(state) // revert to 6 if _, ok := state.SlotInAccessList(addr("cc"), slot("01")); ok { t.Fatalf("slot present, expected missing") } @@ -1133,7 +1170,7 @@ func TestStateDBAccessList(t *testing.T) { verifySlots("aa", "01") verifySlots("bb", "01", "02", "03") - state.journal.revert(state, 6) + state.journal.revertSnapshot(state) // revert to 5 if state.AddressInAccessList(addr("cc")) { t.Fatalf("addr present, expected missing") } @@ -1141,40 +1178,40 @@ func TestStateDBAccessList(t *testing.T) { verifySlots("aa", "01") verifySlots("bb", "01", "02", "03") - state.journal.revert(state, 5) + state.journal.revertSnapshot(state) // revert to 4 if _, ok := state.SlotInAccessList(addr("aa"), slot("01")); ok { t.Fatalf("slot present, expected missing") } verifyAddrs("aa", "bb") verifySlots("bb", "01", "02", "03") - state.journal.revert(state, 4) + state.journal.revertSnapshot(state) // revert to 3 if _, ok := state.SlotInAccessList(addr("bb"), slot("03")); ok { t.Fatalf("slot present, expected missing") } verifyAddrs("aa", "bb") verifySlots("bb", "01", "02") - state.journal.revert(state, 3) + state.journal.revertSnapshot(state) // revert to 2 if _, ok := state.SlotInAccessList(addr("bb"), slot("02")); ok { t.Fatalf("slot present, expected missing") } verifyAddrs("aa", "bb") verifySlots("bb", "01") - state.journal.revert(state, 2) + state.journal.revertSnapshot(state) // revert to 1 if _, ok := state.SlotInAccessList(addr("bb"), slot("01")); ok { t.Fatalf("slot present, expected missing") } verifyAddrs("aa", "bb") - state.journal.revert(state, 1) + state.journal.revertSnapshot(state) // revert to 0 if state.AddressInAccessList(addr("bb")) { t.Fatalf("addr present, expected missing") } verifyAddrs("aa") - state.journal.revert(state, 0) + state.journal.revertSnapshot(state) if state.AddressInAccessList(addr("aa")) { t.Fatalf("addr present, expected missing") } @@ -1245,11 +1282,9 @@ func TestStateDBTransientStorage(t *testing.T) { key := common.Hash{0x01} value := common.Hash{0x02} addr := common.Address{} - + state.journal.snapshot() state.SetTransientState(addr, key, value) - if exp, got := 1, state.journal.length(); exp != got { - t.Fatalf("journal length mismatch: have %d, want %d", got, exp) - } + // the retrieved value should equal what was set if got := state.GetTransientState(addr, key); got != value { t.Fatalf("transient storage mismatch: have %x, want %x", got, value) @@ -1257,7 +1292,7 @@ func TestStateDBTransientStorage(t *testing.T) { // revert the transient state being set and then check that the // value is now the empty hash - state.journal.revert(state, 0) + state.journal.revertSnapshot(state) if got, exp := state.GetTransientState(addr, key), (common.Hash{}); exp != got { t.Fatalf("transient storage mismatch: have %x, want %x", got, exp) } @@ -1350,21 +1385,75 @@ func TestStorageDirtiness(t *testing.T) { checkDirty(common.Hash{0x1}, common.Hash{}, false) // the storage change is valid, dirty marker is expected - snap := state.Snapshot() - state.SetState(addr, common.Hash{0x1}, common.Hash{0x1}) - checkDirty(common.Hash{0x1}, common.Hash{0x1}, true) - + state.Snapshot() + { + state.SetState(addr, common.Hash{0x1}, common.Hash{0x1}) + checkDirty(common.Hash{0x1}, common.Hash{0x1}, true) + } // the storage change is reverted, dirtiness should be revoked - state.RevertToSnapshot(snap) + state.RevertSnapshot() checkDirty(common.Hash{0x1}, common.Hash{}, false) // the storage is reset back to its original value, dirtiness should be revoked state.SetState(addr, common.Hash{0x1}, common.Hash{0x1}) - snap = state.Snapshot() - state.SetState(addr, common.Hash{0x1}, common.Hash{}) - checkDirty(common.Hash{0x1}, common.Hash{}, false) - + state.Snapshot() + { + state.SetState(addr, common.Hash{0x1}, common.Hash{}) + checkDirty(common.Hash{0x1}, common.Hash{}, false) + } // the storage change is reverted, dirty value should be set back - state.RevertToSnapshot(snap) + state.RevertSnapshot() checkDirty(common.Hash{0x1}, common.Hash{0x1}, true) } + +func TestStorageDirtiness2(t *testing.T) { + var ( + disk = rawdb.NewMemoryDatabase() + tdb = triedb.NewDatabase(disk, nil) + db = NewDatabase(tdb, nil) + state, _ = New(types.EmptyRootHash, db) + addr = common.HexToAddress("0x1") + checkDirty = func(key common.Hash, value common.Hash, dirty bool) { + t.Helper() + obj := state.getStateObject(addr) + v, exist := obj.dirtyStorage[key] + if exist != dirty { + t.Fatalf("unexpected dirty marker, want: %v, have: %v", dirty, exist) + } + if !exist { + return + } + if v != value { + t.Fatalf("unexpected storage slot, want: %x, have: %x", value, v) + } + } + ) + + { // Initiate a state, where an account has SLOT(1) = 0xA, +nonzero balance + state.CreateAccount(addr) + state.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified) // Prevent empty-delete + state.SetState(addr, common.Hash{0x1}, common.Hash{0xa}) + root, err := state.Commit(0, true, false) + if err != nil { + t.Fatal(err) + } + // Init phase done, load it again + if state, err = New(root, NewDatabase(tdb, nil)); err != nil { + t.Fatal(err) + } + } + // A no-op storage change, no dirty marker + state.SetState(addr, common.Hash{0x1}, common.Hash{0xa}) + checkDirty(common.Hash{0x1}, common.Hash{0xa}, false) + + // Enter new scope + state.Snapshot() + { + state.SetState(addr, common.Hash{0x1}, common.Hash{0xb}) // SLOT(1) = 0xB + checkDirty(common.Hash{0x1}, common.Hash{0xb}, true) // Should be flagged dirty + } + state.RevertSnapshot() // Revert scope + + // the storage change has been set back to original, dirtiness should be revoked + checkDirty(common.Hash{0x1}, common.Hash{0x1}, false) +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 88ef1cf121..9ee7913774 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -254,7 +254,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance } - snapshot := evm.StateDB.Snapshot() + evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) if !evm.StateDB.Exist(addr) { @@ -268,7 +268,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // Thus, only pay for the creation of the code hash leaf here. wgas := evm.AccessEvents.CodeHashGas(addr, true, gas, false) if gas < wgas { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertSnapshot() return nil, 0, ErrOutOfGas } gas -= wgas @@ -276,6 +276,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { // Calling a non-existing account, don't do anything. + evm.StateDB.DiscardSnapshot() return nil, gas, nil } evm.StateDB.CreateAccount(addr) @@ -302,7 +303,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // above we revert to the snapshot and consume any gas remaining. Additionally, // when we're in homestead this also counts for code storage gas errors. if err != nil { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertSnapshot() if err != ErrExecutionReverted { if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) @@ -310,9 +311,8 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g gas = 0 } - // TODO: consider clearing up unused snapshots: - //} else { - // evm.StateDB.DiscardSnapshot(snapshot) + } else { + evm.StateDB.DiscardSnapshot() } return ret, gas, err } @@ -343,7 +343,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance } - var snapshot = evm.StateDB.Snapshot() + evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { @@ -357,13 +357,15 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt gas = contract.Gas } if err != nil { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertSnapshot() if err != ErrExecutionReverted { if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) } gas = 0 } + } else { + evm.StateDB.DiscardSnapshot() } return ret, gas, err } @@ -386,7 +388,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth } - var snapshot = evm.StateDB.Snapshot() + evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { @@ -401,13 +403,15 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, gas = contract.Gas } if err != nil { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertSnapshot() if err != ErrExecutionReverted { if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) } gas = 0 } + } else { + evm.StateDB.DiscardSnapshot() } return ret, gas, err } @@ -433,7 +437,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b // after all empty accounts were deleted, so this is not required. However, if we omit this, // then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json. // We could change this, but for now it's left for legacy reasons - var snapshot = evm.StateDB.Snapshot() + evm.StateDB.Snapshot() // We do an AddBalance of zero here, just in order to trigger a touch. // This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium, @@ -456,7 +460,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b gas = contract.Gas } if err != nil { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertSnapshot() if err != ErrExecutionReverted { if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) @@ -464,6 +468,8 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b gas = 0 } + } else { + evm.StateDB.DiscardSnapshot() } return ret, gas, err } @@ -525,7 +531,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui // Create a new account on the state only if the object was not present. // It might be possible the contract code is deployed to a pre-existent // account with non-zero balance. - snapshot := evm.StateDB.Snapshot() + evm.StateDB.Snapshot() if !evm.StateDB.Exist(address) { evm.StateDB.CreateAccount(address) } @@ -562,10 +568,12 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui ret, err = evm.initNewContract(contract, address) if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertSnapshot() if err != ErrExecutionReverted { contract.UseGas(contract.Gas, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) } + } else { + evm.StateDB.DiscardSnapshot() } return ret, address, contract.Gas, err } diff --git a/core/vm/interface.go b/core/vm/interface.go index d7f4c10e1f..0a2e133258 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -89,8 +89,14 @@ type StateDB interface { Prepare(rules params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) - RevertToSnapshot(int) - Snapshot() int + // Snapshot starts a new journalled scope. + Snapshot() + // RevertSnapshot reverts all state changes made in the most recent journalled scope. + RevertSnapshot() + // DiscardSnapshot removes the ability to roll back the changes in the most + // recent journalled scope. After calling this method, the changes are considered + // part of the parent scope. + DiscardSnapshot() AddLog(*types.Log) AddPreimage(common.Hash, []byte) diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index b454522978..a5800aa5e7 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -216,7 +216,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { evm := vm.NewEVM(context, state.StateDB, test.Genesis.Config, vm.Config{}) for i := 0; i < b.N; i++ { - snap := state.StateDB.Snapshot() + state.StateDB.Snapshot() tracer, err := tracers.DefaultDirectory.New(tracerName, new(tracers.Context), nil, test.Genesis.Config) if err != nil { b.Fatalf("failed to create call tracer: %v", err) @@ -235,7 +235,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { if _, err = tracer.GetResult(); err != nil { b.Fatal(err) } - state.StateDB.RevertToSnapshot(snap) + state.StateDB.RevertSnapshot() } } diff --git a/eth/tracers/tracers_test.go b/eth/tracers/tracers_test.go index a72dbf6ee6..31b01ebf16 100644 --- a/eth/tracers/tracers_test.go +++ b/eth/tracers/tracers_test.go @@ -92,11 +92,11 @@ func BenchmarkTransactionTraceV2(b *testing.B) { tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) evm.Config.Tracer = tracer - snap := state.StateDB.Snapshot() + state.StateDB.Snapshot() _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) if err != nil { b.Fatal(err) } - state.StateDB.RevertToSnapshot(snap) + state.StateDB.RevertSnapshot() } } diff --git a/miner/worker.go b/miner/worker.go index c0574eac23..bc2b6e7208 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -330,13 +330,11 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio // applyTransaction runs the transaction. If execution fails, state and gas pool are reverted. func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) { - var ( - snap = env.state.Snapshot() - gp = env.gasPool.Gas() - ) + gp := env.gasPool.Gas() + env.state.Snapshot() receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx, &env.header.GasUsed) if err != nil { - env.state.RevertToSnapshot(snap) + env.state.RevertSnapshot() env.gasPool.SetGas(gp) } return receipt, err diff --git a/tests/state_test.go b/tests/state_test.go index 301bc3a7a9..b5c6a1bfbb 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -310,7 +310,7 @@ func runBenchmark(b *testing.B, t *StateTest) { ) b.ResetTimer() for n := 0; n < b.N; n++ { - snapshot := state.StateDB.Snapshot() + state.StateDB.Snapshot() state.StateDB.Prepare(rules, msg.From, context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) b.StartTimer() start := time.Now() @@ -327,7 +327,7 @@ func runBenchmark(b *testing.B, t *StateTest) { refund += state.StateDB.GetRefund() gasUsed += msg.GasLimit - leftOverGas - state.StateDB.RevertToSnapshot(snapshot) + state.StateDB.RevertSnapshot() } if elapsed < 1 { elapsed = 1 diff --git a/tests/state_test_util.go b/tests/state_test_util.go index ec7eec1f39..dd734a7e87 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -327,17 +327,18 @@ func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config, snapsh tracer.OnTxStart(evm.GetVMContext(), nil, msg.From) } // Execute the message. - snapshot := st.StateDB.Snapshot() + st.StateDB.Snapshot() gaspool := new(core.GasPool) gaspool.AddGas(block.GasLimit()) vmRet, err := core.ApplyMessage(evm, msg, gaspool) if err != nil { - st.StateDB.RevertToSnapshot(snapshot) + st.StateDB.RevertSnapshot() if tracer := evm.Config.Tracer; tracer != nil && tracer.OnTxEnd != nil { evm.Config.Tracer.OnTxEnd(nil, err) } return st, common.Hash{}, 0, err } + st.StateDB.DiscardSnapshot() // Add 0-value mining reward. This only makes a difference in the cases // where // - the coinbase self-destructed, or