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