core/state: make journalling set-based
core/state: add handling for DiscardSnapshot core/state: use new journal core/state, genesis: fix flaw re discard/commit. In case the state is committed, the journal is reset, thus it is not correct to Discard/Revert snapshots at that point. core/state: fix nil defer in merge core/state: fix bugs in setjournal core/state: journal api changes core/state: bugfixes in sparse journal core/state: journal tests core/state: improve post-state check in journal-fuzzing test core/state: post-rebase fixups miner: remove discard-snapshot call, it's not needed since journal will be reset in Finalize core/state: fix tests core/state: lint core/state: supply origin-value when reverting storage change Update core/genesis.go core/state: fix erroneous comments core/state: review-nits regarding the journal
This commit is contained in:
parent
56c5d77dae
commit
03d720707b
|
@ -279,6 +279,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
|
|||
}
|
||||
continue
|
||||
}
|
||||
statedb.DiscardSnapshot(snapshot)
|
||||
includedTxs = append(includedTxs, tx)
|
||||
if hashError != nil {
|
||||
return nil, nil, nil, NewError(ErrorMissingBlockhash, hashError)
|
||||
|
|
|
@ -176,7 +176,8 @@ func runCmd(ctx *cli.Context) error {
|
|||
sdb := state.NewDatabase(triedb, nil)
|
||||
statedb, _ = state.New(genesis.Root(), sdb)
|
||||
chainConfig = genesisConfig.Config
|
||||
|
||||
id := statedb.Snapshot()
|
||||
defer statedb.DiscardSnapshot(id)
|
||||
if ctx.String(SenderFlag.Name) != "" {
|
||||
sender = common.HexToAddress(ctx.String(SenderFlag.Name))
|
||||
}
|
||||
|
|
|
@ -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,489 +17,82 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
type revision struct {
|
||||
id int
|
||||
journalIndex int
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
validRevisions []revision
|
||||
nextRevisionId int
|
||||
}
|
||||
|
||||
// compile-time interface check
|
||||
var _ journal = (*linearJournal)(nil)
|
||||
|
||||
// newLinearJournal creates a new initialized linearJournal.
|
||||
func newLinearJournal() *linearJournal {
|
||||
return &linearJournal{
|
||||
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 *linearJournal) reset() {
|
||||
j.entries = j.entries[:0]
|
||||
j.validRevisions = j.validRevisions[:0]
|
||||
clear(j.dirties)
|
||||
j.nextRevisionId = 0
|
||||
}
|
||||
|
||||
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 returns an identifier for the current revision of the state.
|
||||
func (j *linearJournal) 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 *linearJournal) 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 (valid revisions: %d)", revid, len(j.validRevisions)))
|
||||
}
|
||||
snapshot := j.validRevisions[idx].journalIndex
|
||||
|
||||
// Replay the linearJournal 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 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) 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 *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),
|
||||
validRevisions: slices.Clone(j.validRevisions),
|
||||
nextRevisionId: j.nextRevisionId,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
j.append(createContractChange{account: addr})
|
||||
}
|
||||
|
||||
func (j *linearJournal) destruct(addr common.Address) {
|
||||
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, previous *uint256.Int) {
|
||||
j.append(balanceChange{
|
||||
account: addr,
|
||||
prev: previous.Clone(),
|
||||
})
|
||||
}
|
||||
|
||||
func (j *linearJournal) setCode(address common.Address) {
|
||||
j.append(codeChange{account: address})
|
||||
}
|
||||
|
||||
func (j *linearJournal) nonceChange(address common.Address, prev uint64) {
|
||||
j.append(nonceChange{
|
||||
account: address,
|
||||
prev: prev,
|
||||
})
|
||||
}
|
||||
|
||||
func (j *linearJournal) 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 *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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
type journal interface {
|
||||
// snapshot returns an identifier for the current revision of the state.
|
||||
// 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() int
|
||||
|
||||
// Changes to transient storage
|
||||
transientStorageChange struct {
|
||||
account common.Address
|
||||
key, prevalue common.Hash
|
||||
}
|
||||
)
|
||||
// revertToSnapshot reverts all state changes made since the given revision.
|
||||
revertToSnapshot(revid int, s *StateDB)
|
||||
|
||||
func (ch createObjectChange) revert(s *StateDB) {
|
||||
delete(s.stateObjects, ch.account)
|
||||
}
|
||||
|
||||
func (ch createObjectChange) dirtied() *common.Address {
|
||||
return &ch.account
|
||||
}
|
||||
// discardSnapshot removes the snapshot with the given id; 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(revid int)
|
||||
|
||||
func (ch createObjectChange) copy() journalEntry {
|
||||
return createObjectChange{
|
||||
account: ch.account,
|
||||
}
|
||||
}
|
||||
// reset clears the journal so it can be reused.
|
||||
reset()
|
||||
|
||||
func (ch createContractChange) revert(s *StateDB) {
|
||||
s.getStateObject(ch.account).newContract = false
|
||||
}
|
||||
// dirtyAccounts returns a list of all accounts modified in this journal
|
||||
dirtyAccounts() []common.Address
|
||||
|
||||
func (ch createContractChange) dirtied() *common.Address {
|
||||
return nil
|
||||
}
|
||||
// accessListAddAccount journals the adding of addr to the access list
|
||||
accessListAddAccount(addr common.Address)
|
||||
|
||||
func (ch createContractChange) copy() journalEntry {
|
||||
return createContractChange{
|
||||
account: ch.account,
|
||||
}
|
||||
}
|
||||
// accessListAddSlot journals the adding of addr/slot to the access list
|
||||
accessListAddSlot(addr common.Address, slot common.Hash)
|
||||
|
||||
func (ch selfDestructChange) revert(s *StateDB) {
|
||||
obj := s.getStateObject(ch.account)
|
||||
if obj != nil {
|
||||
obj.selfDestructed = false
|
||||
}
|
||||
}
|
||||
// logChange journals the adding of a log related to the txHash
|
||||
logChange(txHash common.Hash)
|
||||
|
||||
func (ch selfDestructChange) dirtied() *common.Address {
|
||||
return &ch.account
|
||||
}
|
||||
// createObject journals the event of a new account created in the trie.
|
||||
createObject(addr common.Address)
|
||||
|
||||
func (ch selfDestructChange) copy() journalEntry {
|
||||
return selfDestructChange{
|
||||
account: ch.account,
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
|
||||
var ripemd = common.HexToAddress("0000000000000000000000000000000000000003")
|
||||
// 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)
|
||||
|
||||
func (ch touchChange) revert(s *StateDB) {
|
||||
}
|
||||
// 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)
|
||||
|
||||
func (ch touchChange) dirtied() *common.Address {
|
||||
return &ch.account
|
||||
}
|
||||
// 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)
|
||||
|
||||
func (ch touchChange) copy() journalEntry {
|
||||
return touchChange{
|
||||
account: ch.account,
|
||||
}
|
||||
}
|
||||
// refundChange journals that the refund has been changed, recording the previous value.
|
||||
refundChange(previous uint64)
|
||||
|
||||
func (ch balanceChange) revert(s *StateDB) {
|
||||
s.getStateObject(ch.account).setBalance(ch.prev)
|
||||
}
|
||||
// balanceChange journals that the balance of addr has been changed, recording the previous value
|
||||
balanceChange(addr common.Address, account *types.StateAccount, destructed, newContract bool)
|
||||
|
||||
func (ch balanceChange) dirtied() *common.Address {
|
||||
return &ch.account
|
||||
}
|
||||
// setCode journals that the code of addr has been set.
|
||||
// OBS: This method must not be applied twice -- it always assumes that the
|
||||
// pre-state (i.e the rollback-state) is "no code".
|
||||
setCode(addr common.Address, account *types.StateAccount)
|
||||
|
||||
func (ch balanceChange) copy() journalEntry {
|
||||
return balanceChange{
|
||||
account: ch.account,
|
||||
prev: new(uint256.Int).Set(ch.prev),
|
||||
}
|
||||
}
|
||||
// nonceChange journals that the nonce of addr was changed, recording the previous value.
|
||||
nonceChange(addr common.Address, account *types.StateAccount, destructed, newContract bool)
|
||||
|
||||
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(types.EmptyCodeHash, nil)
|
||||
}
|
||||
|
||||
func (ch codeChange) dirtied() *common.Address {
|
||||
return &ch.account
|
||||
}
|
||||
|
||||
func (ch codeChange) copy() journalEntry {
|
||||
return codeChange{account: ch.account}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// touchChange journals that the account at addr was touched during execution.
|
||||
touchChange(addr common.Address, account *types.StateAccount, destructed, newContract bool)
|
||||
|
||||
func (ch accessListAddSlotChange) copy() journalEntry {
|
||||
return accessListAddSlotChange{
|
||||
address: ch.address,
|
||||
slot: ch.slot,
|
||||
}
|
||||
// copy returns a deep-copied journal.
|
||||
copy() journal
|
||||
}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
type journal interface {
|
||||
|
||||
// snapshot returns an identifier for the current revision of the state.
|
||||
snapshot() int
|
||||
|
||||
// revertToSnapshot reverts all state changes made since the given revision.
|
||||
revertToSnapshot(revid int, s *StateDB)
|
||||
|
||||
// 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)
|
||||
|
||||
// destruct journals the destruction of an account in the trie.
|
||||
// OBS: This method must not be applied twice -- it always assumes that the
|
||||
// pre-state (i.e the rollback-state) is non-destructed.
|
||||
destruct(addr common.Address)
|
||||
|
||||
// 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 tha the balance of addr has been changed, recording the previous value
|
||||
balanceChange(addr common.Address, previous *uint256.Int)
|
||||
|
||||
// JournalSetCode journals that the code of addr has been set.
|
||||
// OBS: This method must not be applied twice -- it always assumes that the
|
||||
// pre-state (i.e the rollback-state) is "no code".
|
||||
setCode(addr common.Address)
|
||||
|
||||
// nonceChange journals that the nonce of addr was changed, recording the previous value.
|
||||
nonceChange(addr common.Address, prev uint64)
|
||||
|
||||
// touchChange journals that the account at addr was touched during execution.
|
||||
touchChange(addr common.Address)
|
||||
|
||||
// copy returns a deep-copied journal.
|
||||
copy() journal
|
||||
}
|
|
@ -0,0 +1,510 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
type revision struct {
|
||||
id int
|
||||
journalIndex int
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
validRevisions []revision
|
||||
nextRevisionId int
|
||||
}
|
||||
|
||||
// compile-time interface check
|
||||
var _ journal = (*linearJournal)(nil)
|
||||
|
||||
// newLinearJournal creates a new initialized linearJournal.
|
||||
func newLinearJournal() *linearJournal {
|
||||
return &linearJournal{
|
||||
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 *linearJournal) reset() {
|
||||
j.entries = j.entries[:0]
|
||||
j.validRevisions = j.validRevisions[:0]
|
||||
clear(j.dirties)
|
||||
j.nextRevisionId = 0
|
||||
}
|
||||
|
||||
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 returns an identifier for the current revision of the state.
|
||||
func (j *linearJournal) snapshot() int {
|
||||
id := j.nextRevisionId
|
||||
j.nextRevisionId++
|
||||
j.validRevisions = append(j.validRevisions, revision{id, j.length()})
|
||||
return id
|
||||
}
|
||||
|
||||
func (j *linearJournal) 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 (valid revisions: %d)", revid, len(j.validRevisions)))
|
||||
}
|
||||
snapshot := j.validRevisions[idx].journalIndex
|
||||
|
||||
// Replay the linearJournal to undo changes and remove invalidated snapshots
|
||||
j.revert(s, snapshot)
|
||||
j.validRevisions = j.validRevisions[:idx]
|
||||
}
|
||||
|
||||
// discardSnapshot removes the snapshot with the given id; 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 int) {
|
||||
}
|
||||
|
||||
// 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) 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 *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),
|
||||
validRevisions: slices.Clone(j.validRevisions),
|
||||
nextRevisionId: j.nextRevisionId,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
j.append(codeChange{account: address})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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(types.EmptyCodeHash, nil)
|
||||
}
|
||||
|
||||
func (ch codeChange) dirtied() *common.Address {
|
||||
return &ch.account
|
||||
}
|
||||
|
||||
func (ch codeChange) copy() journalEntry {
|
||||
return codeChange{account: ch.account}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,489 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"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
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
obj.setCode(common.BytesToHash(data.codeHash), nil)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.snapshot()
|
||||
}
|
||||
|
||||
func (j *sparseJournal) copy() journal {
|
||||
cp := &sparseJournal{
|
||||
entries: make([]*scopedJournal, 0, len(j.entries)),
|
||||
}
|
||||
for _, entry := range j.entries {
|
||||
cp.entries = append(cp.entries, entry.deepCopy())
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
// snapshot returns an identifier for the current revision of the state.
|
||||
// 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() int {
|
||||
id := len(j.entries)
|
||||
j.entries = append(j.entries, newScopedJournal())
|
||||
return id
|
||||
}
|
||||
|
||||
// revertToSnapshot reverts all state changes made since the given revision.
|
||||
func (j *sparseJournal) revertToSnapshot(id int, s *StateDB) {
|
||||
if id >= len(j.entries) {
|
||||
panic(fmt.Errorf("revision id %v cannot be reverted", id))
|
||||
}
|
||||
// Revert the entries sequentially
|
||||
for i := len(j.entries) - 1; i >= id; i-- {
|
||||
entry := j.entries[i]
|
||||
entry.revert(s)
|
||||
}
|
||||
j.entries = j.entries[:id]
|
||||
}
|
||||
|
||||
// discardSnapshot removes the snapshot with the given id; 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 int) {
|
||||
if id == 0 {
|
||||
return
|
||||
}
|
||||
// here we must merge the 'id' with it's parent.
|
||||
want := len(j.entries) - 1
|
||||
have := id
|
||||
if want != have {
|
||||
if want == 0 && id == 1 {
|
||||
// If a transcation is applied successfully, the statedb.Finalize will
|
||||
// end by clearing and resetting the journal. Invoking a discardSnapshot
|
||||
// afterwards will lead us here.
|
||||
// Let's not panic, but it's ok to complain a bit
|
||||
log.Error("Extraneous invocation to discard snapshot")
|
||||
return
|
||||
} else {
|
||||
panic(fmt.Sprintf("journalling error, want discard(%d), have discard(%d)", want, have))
|
||||
}
|
||||
}
|
||||
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) {
|
||||
j.journalAccountChange(addr, account, false, true)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package state provides a caching layer atop the Ethereum state trie.
|
||||
package state
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"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
|
||||
|
||||
{
|
||||
// If the journal performs the rollback in the wrong order, this
|
||||
// will cause a panic.
|
||||
id := j.snapshot()
|
||||
statedb.AddSlotToAccessList(common.Address{0x1}, common.Hash{0x4})
|
||||
statedb.AddSlotToAccessList(common.Address{0x3}, common.Hash{0x4})
|
||||
statedb.RevertToSnapshot(id)
|
||||
}
|
||||
{
|
||||
id := j.snapshot()
|
||||
statedb.AddAddressToAccessList(common.Address{0x2})
|
||||
statedb.AddAddressToAccessList(common.Address{0x3})
|
||||
statedb.AddAddressToAccessList(common.Address{0x4})
|
||||
statedb.RevertToSnapshot(id)
|
||||
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
|
||||
zero := j.snapshot()
|
||||
j.refundChange(0)
|
||||
j.refundChange(1)
|
||||
{
|
||||
id := j.snapshot()
|
||||
j.refundChange(2)
|
||||
j.refundChange(3)
|
||||
j.revertToSnapshot(id, statedb)
|
||||
if have, want := statedb.refund, uint64(2); have != want {
|
||||
t.Fatalf("have %d want %d", have, want)
|
||||
}
|
||||
}
|
||||
{
|
||||
id := j.snapshot()
|
||||
j.refundChange(2)
|
||||
j.refundChange(3)
|
||||
j.discardSnapshot(id)
|
||||
}
|
||||
j.revertToSnapshot(zero, statedb)
|
||||
if have, want := statedb.refund, uint64(0); have != want {
|
||||
t.Fatalf("have %d want %d", have, want)
|
||||
}
|
||||
}
|
|
@ -113,7 +113,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
|
||||
|
@ -462,7 +462,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
|
||||
}
|
||||
|
@ -536,7 +536,7 @@ func (s *stateObject) CodeSize() int {
|
|||
}
|
||||
|
||||
func (s *stateObject) SetCode(codeHash common.Hash, code []byte) {
|
||||
s.db.journal.setCode(s.address)
|
||||
s.db.journal.setCode(s.address, &s.data)
|
||||
s.setCode(codeHash, code)
|
||||
}
|
||||
|
||||
|
@ -547,7 +547,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)
|
||||
}
|
||||
|
||||
|
|
|
@ -177,7 +177,7 @@ func New(root common.Hash, db Database) (*StateDB, error) {
|
|||
mutations: make(map[common.Address]*mutation),
|
||||
logs: make(map[common.Hash][]*types.Log),
|
||||
preimages: make(map[common.Hash][]byte),
|
||||
journal: newLinearJournal(),
|
||||
journal: newSparseJournal(),
|
||||
accessList: newAccessList(),
|
||||
transientStorage: newTransientStorage(),
|
||||
}
|
||||
|
@ -502,7 +502,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
|
||||
|
@ -642,7 +642,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -711,6 +711,13 @@ func (s *StateDB) Snapshot() int {
|
|||
return s.journal.snapshot()
|
||||
}
|
||||
|
||||
// DiscardSnapshot removes the snapshot with the given id; 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 (s *StateDB) DiscardSnapshot(id int) {
|
||||
s.journal.discardSnapshot(id)
|
||||
}
|
||||
|
||||
// RevertToSnapshot reverts all state changes made since the given revision.
|
||||
func (s *StateDB) RevertToSnapshot(revid int) {
|
||||
s.journal.revertToSnapshot(revid, s)
|
||||
|
|
|
@ -141,6 +141,10 @@ func (s *hookedStateDB) Prepare(rules params.Rules, sender, coinbase common.Addr
|
|||
s.inner.Prepare(rules, sender, coinbase, dest, precompiles, txAccesses)
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) DiscardSnapshot(id int) {
|
||||
s.inner.DiscardSnapshot(id)
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) RevertToSnapshot(i int) {
|
||||
s.inner.RevertToSnapshot(i)
|
||||
}
|
||||
|
@ -254,7 +258,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.
|
||||
|
|
|
@ -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)
|
||||
|
@ -364,6 +364,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]))
|
||||
|
@ -374,12 +380,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]))
|
||||
|
@ -405,6 +425,13 @@ 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
|
||||
|
@ -419,6 +446,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)
|
||||
},
|
||||
},
|
||||
|
@ -439,15 +475,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) {
|
||||
|
@ -465,6 +492,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]))
|
||||
|
@ -690,8 +724,8 @@ func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error {
|
|||
}
|
||||
return out.String()
|
||||
}
|
||||
haveK := getKeys(state.journal.dirtyAccounts())
|
||||
wantK := getKeys(checkstate.journal.dirtyAccounts())
|
||||
haveK := getKeys(have)
|
||||
wantK := getKeys(want)
|
||||
return fmt.Errorf("dirty-journal set mismatch.\nhave:\n%v\nwant:\n%v\n", haveK, wantK)
|
||||
}
|
||||
}
|
||||
|
@ -1124,17 +1158,12 @@ func TestStateDBAccessList(t *testing.T) {
|
|||
|
||||
// Make a copy
|
||||
stateCopy1 := state.Copy()
|
||||
if exp, got := 4, state.journal.(*linearJournal).length(); exp != got {
|
||||
t.Fatalf("linearJournal length mismatch: have %d, want %d", got, exp)
|
||||
}
|
||||
|
||||
// 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.(*linearJournal).length(); exp != got {
|
||||
t.Fatalf("linearJournal length mismatch: have %d, want %d", got, exp)
|
||||
}
|
||||
|
||||
// some new ones
|
||||
state.AddSlotToAccessList(addr("bb"), slot("03")) // 5
|
||||
push(state.journal.snapshot()) // journal id 5
|
||||
|
@ -1143,9 +1172,6 @@ func TestStateDBAccessList(t *testing.T) {
|
|||
state.AddAddressToAccessList(addr("cc")) // 7
|
||||
push(state.journal.snapshot()) // journal id 7
|
||||
state.AddSlotToAccessList(addr("cc"), slot("01")) // 8
|
||||
if exp, got := 8, state.journal.(*linearJournal).length(); exp != got {
|
||||
t.Fatalf("linearJournal length mismatch: have %d, want %d", got, exp)
|
||||
}
|
||||
|
||||
verifyAddrs("aa", "bb", "cc")
|
||||
verifySlots("aa", "01")
|
||||
|
@ -1275,9 +1301,7 @@ func TestStateDBTransientStorage(t *testing.T) {
|
|||
addr := common.Address{}
|
||||
revision := state.journal.snapshot()
|
||||
state.SetTransientState(addr, key, value)
|
||||
if exp, got := 1, state.journal.(*linearJournal).length(); exp != got {
|
||||
t.Fatalf("linearJournal 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)
|
||||
|
|
|
@ -206,6 +206,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
|
|||
|
||||
if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() {
|
||||
// Calling a non-existing account, don't do anything.
|
||||
evm.StateDB.DiscardSnapshot(snapshot)
|
||||
return nil, gas, nil
|
||||
}
|
||||
evm.StateDB.CreateAccount(addr)
|
||||
|
@ -242,9 +243,8 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
|
|||
|
||||
gas = 0
|
||||
}
|
||||
// TODO: consider clearing up unused snapshots:
|
||||
//} else {
|
||||
// evm.StateDB.DiscardSnapshot(snapshot)
|
||||
} else {
|
||||
evm.StateDB.DiscardSnapshot(snapshot)
|
||||
}
|
||||
return ret, gas, err
|
||||
}
|
||||
|
@ -298,6 +298,8 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
|
|||
|
||||
gas = 0
|
||||
}
|
||||
} else {
|
||||
evm.StateDB.DiscardSnapshot(snapshot)
|
||||
}
|
||||
return ret, gas, err
|
||||
}
|
||||
|
@ -344,6 +346,8 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
|
|||
}
|
||||
gas = 0
|
||||
}
|
||||
} else {
|
||||
evm.StateDB.DiscardSnapshot(snapshot)
|
||||
}
|
||||
return ret, gas, err
|
||||
}
|
||||
|
@ -403,6 +407,8 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
|
|||
|
||||
gas = 0
|
||||
}
|
||||
} else {
|
||||
evm.StateDB.DiscardSnapshot(snapshot)
|
||||
}
|
||||
return ret, gas, err
|
||||
}
|
||||
|
@ -514,6 +520,8 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
|
|||
if err != ErrExecutionReverted {
|
||||
contract.UseGas(contract.Gas, evm.Config.Tracer, tracing.GasChangeCallFailedExecution)
|
||||
}
|
||||
} else {
|
||||
evm.StateDB.DiscardSnapshot(snapshot)
|
||||
}
|
||||
return ret, address, contract.Gas, err
|
||||
}
|
||||
|
|
|
@ -88,7 +88,14 @@ type StateDB interface {
|
|||
|
||||
Prepare(rules params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList)
|
||||
|
||||
// RevertToSnapshot reverts all state changes made since the given revision.
|
||||
RevertToSnapshot(int)
|
||||
|
||||
// DiscardSnapshot removes the snapshot with the given id; 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(int)
|
||||
// Snapshot returns an identifier for the current scope of the state.
|
||||
Snapshot() int
|
||||
|
||||
AddLog(*types.Log)
|
||||
|
|
|
@ -311,6 +311,7 @@ func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config, snapsh
|
|||
}
|
||||
return st, common.Hash{}, 0, err
|
||||
}
|
||||
st.StateDB.DiscardSnapshot(snapshot)
|
||||
// Add 0-value mining reward. This only makes a difference in the cases
|
||||
// where
|
||||
// - the coinbase self-destructed, or
|
||||
|
|
Loading…
Reference in New Issue