mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-16 21:16:37 +00:00
core/state: track the block-level accessList (#34803)
This PR extends the journal to track the pre-transaction values of mutated balances, nonces, and code. At the end of the transaction, these values are used to filter out no-op changes, such as balance transitions from a-> b->a. These changes are excluded from the block-level access list. Additionally, there is a dedicated `bal.ConstructionBlockAccessList` objects for gathering the state reads and writes within the current transaction. These state writes will be keyed by the block accessList index. --------- Co-authored-by: jwasinger <j-wasinger@hotmail.com>
This commit is contained in:
parent
0494cdce23
commit
b2aa6987de
18 changed files with 624 additions and 220 deletions
|
|
@ -270,7 +270,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
statedb.SetTxContext(tx.Hash(), len(receipts))
|
statedb.SetTxContext(tx.Hash(), len(receipts), uint32(len(receipts)+1))
|
||||||
var (
|
var (
|
||||||
snapshot = statedb.Snapshot()
|
snapshot = statedb.Snapshot()
|
||||||
gp = gaspool.Snapshot()
|
gp = gaspool.Snapshot()
|
||||||
|
|
@ -336,7 +336,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
|
||||||
for _, receipt := range receipts {
|
for _, receipt := range receipts {
|
||||||
allLogs = append(allLogs, receipt.Logs...)
|
allLogs = append(allLogs, receipt.Logs...)
|
||||||
}
|
}
|
||||||
requests, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm)
|
requests, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err))
|
return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti
|
||||||
blockContext = NewEVMBlockContext(b.header, bc, &b.header.Coinbase)
|
blockContext = NewEVMBlockContext(b.header, bc, &b.header.Coinbase)
|
||||||
evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig)
|
evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig)
|
||||||
)
|
)
|
||||||
b.statedb.SetTxContext(tx.Hash(), len(b.txs))
|
b.statedb.SetTxContext(tx.Hash(), len(b.txs), uint32(len(b.txs)+1))
|
||||||
receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx)
|
receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -323,7 +323,7 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) {
|
||||||
blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
|
blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
|
||||||
evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{})
|
evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{})
|
||||||
|
|
||||||
requests, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm)
|
requests, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("failed to run post-execution: %v", err))
|
panic(fmt.Sprintf("failed to run post-execution: %v", err))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
|
@ -32,26 +31,163 @@ type revision struct {
|
||||||
journalIndex int
|
journalIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// journalMutationKind indicates the type of account mutation.
|
||||||
|
type journalMutationKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// journalMutationKindNone is the zero value returned by mutation() for
|
||||||
|
// entries that don't carry a tracked account mutation. The accompanying
|
||||||
|
// bool is false in that case; callers must gate on it before using the
|
||||||
|
// kind.
|
||||||
|
journalMutationKindNone journalMutationKind = iota
|
||||||
|
journalMutationKindTouch
|
||||||
|
journalMutationKindCreate
|
||||||
|
journalMutationKindSelfDestruct
|
||||||
|
journalMutationKindBalance
|
||||||
|
journalMutationKindNonce
|
||||||
|
journalMutationKindCode
|
||||||
|
journalMutationKindStorage
|
||||||
|
journalMutationKindCount // sentinel, must stay last
|
||||||
|
)
|
||||||
|
|
||||||
|
type journalMutationCounts [journalMutationKindCount]int
|
||||||
|
|
||||||
|
// journalMutationState tracks, per account, both the per-kind count of mutation
|
||||||
|
// entries currently present in the journal and the pre-tx value of each
|
||||||
|
// metadata field captured on its first touch (balance/nonce/code).
|
||||||
|
// The *Set flags indicate whether the corresponding field has been mutated
|
||||||
|
// at least once in the current tx window; they are cleared when all entries
|
||||||
|
// of that kind are reverted. Storage slots are tracked elsewhere.
|
||||||
|
type journalMutationState struct {
|
||||||
|
counts journalMutationCounts
|
||||||
|
|
||||||
|
balance *uint256.Int
|
||||||
|
balanceSet bool
|
||||||
|
nonce uint64
|
||||||
|
nonceSet bool
|
||||||
|
code []byte
|
||||||
|
codeSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *journalMutationState) add(kind journalMutationKind) {
|
||||||
|
s.counts.add(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove drops one occurrence of the given mutation kind. It returns a flag
|
||||||
|
// indicating whether no entries of any kind remain.
|
||||||
|
func (s *journalMutationState) remove(kind journalMutationKind) bool {
|
||||||
|
if s.counts.remove(kind) {
|
||||||
|
// No entries of this kind remain for this account; drop the
|
||||||
|
// corresponding stashed original so the state mirrors the
|
||||||
|
// live mutation set.
|
||||||
|
s.clearKind(kind)
|
||||||
|
}
|
||||||
|
return s.counts == (journalMutationCounts{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearKind drops the stashed original for the given mutation kind. It is
|
||||||
|
// invoked during revert once no journal entries of that kind remain for the
|
||||||
|
// account. Kinds that don't correspond to a tracked metadata field are no-ops.
|
||||||
|
func (s *journalMutationState) clearKind(kind journalMutationKind) {
|
||||||
|
switch kind {
|
||||||
|
case journalMutationKindBalance:
|
||||||
|
s.balance = nil
|
||||||
|
s.balanceSet = false
|
||||||
|
case journalMutationKindNonce:
|
||||||
|
s.nonce = 0
|
||||||
|
s.nonceSet = false
|
||||||
|
case journalMutationKindCode:
|
||||||
|
s.code = nil
|
||||||
|
s.codeSet = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *journalMutationState) copy() *journalMutationState {
|
||||||
|
cpy := *s
|
||||||
|
if s.balance != nil {
|
||||||
|
cpy.balance = new(uint256.Int).Set(s.balance)
|
||||||
|
}
|
||||||
|
if s.code != nil {
|
||||||
|
cpy.code = slices.Clone(s.code)
|
||||||
|
}
|
||||||
|
return &cpy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *journalMutationCounts) add(kind journalMutationKind) {
|
||||||
|
c[kind]++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *journalMutationCounts) remove(kind journalMutationKind) bool {
|
||||||
|
c[kind]--
|
||||||
|
return c[kind] == 0
|
||||||
|
}
|
||||||
|
|
||||||
// journalEntry is a modification entry in the state change journal that can be
|
// journalEntry is a modification entry in the state change journal that can be
|
||||||
// reverted on demand.
|
// reverted on demand.
|
||||||
type journalEntry interface {
|
type journalEntry interface {
|
||||||
// revert undoes the changes introduced by this journal entry.
|
// revert undoes the changes introduced by this journal entry.
|
||||||
revert(*StateDB)
|
revert(*StateDB)
|
||||||
|
|
||||||
// dirtied returns the Ethereum address modified by this journal entry.
|
// mutation returns the account mutation introduced by this entry.
|
||||||
// indicates false if no address was changed.
|
// It indicates false if no tracked account mutation was made.
|
||||||
dirtied() (common.Address, bool)
|
mutation() (common.Address, journalMutationKind, bool)
|
||||||
|
|
||||||
// copy returns a deep-copied journal entry.
|
// copy returns a deep-copied journal entry.
|
||||||
copy() journalEntry
|
copy() journalEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stashBalance records prev as the pre-tx balance of addr, iff this is the
|
||||||
|
// first balance touch seen in the current tx. Subsequent balance writes are
|
||||||
|
// ignored so the stored value remains the true pre-tx original.
|
||||||
|
func (j *journal) stashBalance(addr common.Address, prev *uint256.Int) {
|
||||||
|
s := j.mutationStateFor(addr)
|
||||||
|
if s.balanceSet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// The balance is already deep-copied and safe to hold the object here.
|
||||||
|
s.balance = prev
|
||||||
|
s.balanceSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// stashNonce records prev as the pre-tx nonce of addr on first touch.
|
||||||
|
func (j *journal) stashNonce(addr common.Address, prev uint64) {
|
||||||
|
s := j.mutationStateFor(addr)
|
||||||
|
if s.nonceSet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.nonce = prev
|
||||||
|
s.nonceSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// stashCode records prev as the pre-tx code of addr on first touch.
|
||||||
|
func (j *journal) stashCode(addr common.Address, prev []byte) {
|
||||||
|
s := j.mutationStateFor(addr)
|
||||||
|
if s.codeSet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// The code is already deep-copied in the StateDB, safe to
|
||||||
|
// hold the reference here.
|
||||||
|
s.code = prev
|
||||||
|
s.codeSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// mutationStateFor returns the mutation state for addr, creating an empty one
|
||||||
|
// if absent.
|
||||||
|
func (j *journal) mutationStateFor(addr common.Address) *journalMutationState {
|
||||||
|
s := j.mutations[addr]
|
||||||
|
if s == nil {
|
||||||
|
s = new(journalMutationState)
|
||||||
|
j.mutations[addr] = s
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// journal contains the list of state modifications applied since the last state
|
// 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
|
// commit. These are tracked to be able to be reverted in the case of an execution
|
||||||
// exception or request for reversal.
|
// exception or request for reversal.
|
||||||
type journal struct {
|
type journal struct {
|
||||||
entries []journalEntry // Current changes tracked by the journal
|
entries []journalEntry // Current changes tracked by the journal
|
||||||
dirties map[common.Address]int // Dirty accounts and the number of changes
|
mutations map[common.Address]*journalMutationState // Per-account mutation kinds and pre-tx originals
|
||||||
|
|
||||||
validRevisions []revision
|
validRevisions []revision
|
||||||
nextRevisionId int
|
nextRevisionId int
|
||||||
|
|
@ -60,7 +196,7 @@ type journal struct {
|
||||||
// newJournal creates a new initialized journal.
|
// newJournal creates a new initialized journal.
|
||||||
func newJournal() *journal {
|
func newJournal() *journal {
|
||||||
return &journal{
|
return &journal{
|
||||||
dirties: make(map[common.Address]int),
|
mutations: make(map[common.Address]*journalMutationState),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +206,7 @@ func newJournal() *journal {
|
||||||
func (j *journal) reset() {
|
func (j *journal) reset() {
|
||||||
j.entries = j.entries[:0]
|
j.entries = j.entries[:0]
|
||||||
j.validRevisions = j.validRevisions[:0]
|
j.validRevisions = j.validRevisions[:0]
|
||||||
clear(j.dirties)
|
clear(j.mutations)
|
||||||
j.nextRevisionId = 0
|
j.nextRevisionId = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,33 +237,52 @@ func (j *journal) revertToSnapshot(revid int, s *StateDB) {
|
||||||
// append inserts a new modification entry to the end of the change journal.
|
// append inserts a new modification entry to the end of the change journal.
|
||||||
func (j *journal) append(entry journalEntry) {
|
func (j *journal) append(entry journalEntry) {
|
||||||
j.entries = append(j.entries, entry)
|
j.entries = append(j.entries, entry)
|
||||||
if addr, dirty := entry.dirtied(); dirty {
|
if addr, kind, dirty := entry.mutation(); dirty {
|
||||||
j.dirties[addr]++
|
state := j.mutations[addr]
|
||||||
|
if state == nil {
|
||||||
|
state = new(journalMutationState)
|
||||||
|
j.mutations[addr] = state
|
||||||
|
}
|
||||||
|
state.add(kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// revert undoes a batch of journalled modifications along with any reverted
|
// revert undoes a batch of journalled modifications along with any reverted
|
||||||
// dirty handling too.
|
// mutation tracking too.
|
||||||
func (j *journal) revert(statedb *StateDB, snapshot int) {
|
func (j *journal) revert(statedb *StateDB, snapshot int) {
|
||||||
for i := len(j.entries) - 1; i >= snapshot; i-- {
|
for i := len(j.entries) - 1; i >= snapshot; i-- {
|
||||||
// Undo the changes made by the operation
|
// Undo the changes made by the operation
|
||||||
j.entries[i].revert(statedb)
|
j.entries[i].revert(statedb)
|
||||||
|
|
||||||
// Drop any dirty tracking induced by the change
|
// Drop any mutation tracking induced by the change.
|
||||||
if addr, dirty := j.entries[i].dirtied(); dirty {
|
if addr, kind, dirty := j.entries[i].mutation(); dirty {
|
||||||
if j.dirties[addr]--; j.dirties[addr] == 0 {
|
state := j.mutations[addr]
|
||||||
delete(j.dirties, addr)
|
if state == nil {
|
||||||
|
panic(fmt.Errorf("journal mutation tracking missing for %x", addr[:]))
|
||||||
|
}
|
||||||
|
if state.remove(kind) {
|
||||||
|
delete(j.mutations, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
j.entries = j.entries[:snapshot]
|
j.entries = j.entries[:snapshot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// dirty explicitly sets an address to dirty, even if the change entries would
|
// ripemdMagic explicitly keeps RIPEMD160 in the mutation set with a touch change.
|
||||||
// otherwise suggest it as clean. This method is an ugly hack to handle the RIPEMD
|
//
|
||||||
// precompile consensus exception.
|
// Ethereum Mainnet contains an old empty-account touch/revert quirk for address
|
||||||
func (j *journal) dirty(addr common.Address) {
|
// 0x03. If we only relied on the journal entry above, the revert path would
|
||||||
j.dirties[addr]++
|
// remove the account from the mutation set together with the touch.
|
||||||
|
//
|
||||||
|
// Keep an explicit touch marker so tx finalisation still sees RIPEMD160
|
||||||
|
// on the mutation pass when replaying that historical case.
|
||||||
|
func (j *journal) ripemdMagic() {
|
||||||
|
state := j.mutations[ripemd]
|
||||||
|
if state == nil {
|
||||||
|
state = new(journalMutationState)
|
||||||
|
j.mutations[ripemd] = state
|
||||||
|
}
|
||||||
|
state.add(journalMutationKindTouch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// length returns the current number of entries in the journal.
|
// length returns the current number of entries in the journal.
|
||||||
|
|
@ -141,9 +296,13 @@ func (j *journal) copy() *journal {
|
||||||
for i := 0; i < j.length(); i++ {
|
for i := 0; i < j.length(); i++ {
|
||||||
entries = append(entries, j.entries[i].copy())
|
entries = append(entries, j.entries[i].copy())
|
||||||
}
|
}
|
||||||
|
mutations := make(map[common.Address]*journalMutationState, len(j.mutations))
|
||||||
|
for addr, state := range j.mutations {
|
||||||
|
mutations[addr] = state.copy()
|
||||||
|
}
|
||||||
return &journal{
|
return &journal{
|
||||||
entries: entries,
|
entries: entries,
|
||||||
dirties: maps.Clone(j.dirties),
|
mutations: mutations,
|
||||||
validRevisions: slices.Clone(j.validRevisions),
|
validRevisions: slices.Clone(j.validRevisions),
|
||||||
nextRevisionId: j.nextRevisionId,
|
nextRevisionId: j.nextRevisionId,
|
||||||
}
|
}
|
||||||
|
|
@ -187,13 +346,16 @@ func (j *journal) refundChange(previous uint64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *journal) balanceChange(addr common.Address, previous *uint256.Int) {
|
func (j *journal) balanceChange(addr common.Address, previous *uint256.Int) {
|
||||||
|
prev := previous.Clone()
|
||||||
|
j.stashBalance(addr, prev)
|
||||||
j.append(balanceChange{
|
j.append(balanceChange{
|
||||||
account: addr,
|
account: addr,
|
||||||
prev: previous.Clone(),
|
prev: prev,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *journal) setCode(address common.Address, prevCode []byte) {
|
func (j *journal) setCode(address common.Address, prevCode []byte) {
|
||||||
|
j.stashCode(address, prevCode)
|
||||||
j.append(codeChange{
|
j.append(codeChange{
|
||||||
account: address,
|
account: address,
|
||||||
prevCode: prevCode,
|
prevCode: prevCode,
|
||||||
|
|
@ -201,6 +363,7 @@ func (j *journal) setCode(address common.Address, prevCode []byte) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *journal) nonceChange(address common.Address, prev uint64) {
|
func (j *journal) nonceChange(address common.Address, prev uint64) {
|
||||||
|
j.stashNonce(address, prev)
|
||||||
j.append(nonceChange{
|
j.append(nonceChange{
|
||||||
account: address,
|
account: address,
|
||||||
prev: prev,
|
prev: prev,
|
||||||
|
|
@ -212,9 +375,18 @@ func (j *journal) touchChange(address common.Address) {
|
||||||
account: address,
|
account: address,
|
||||||
})
|
})
|
||||||
if address == ripemd {
|
if address == ripemd {
|
||||||
// Explicitly put it in the dirty-cache, which is otherwise generated from
|
// Preserve the historical RIPEMD160 precompile consensus exception.
|
||||||
// flattened journals.
|
//
|
||||||
j.dirty(address)
|
// Mainnet contains an old empty-account touch/revert quirk for address
|
||||||
|
// 0x03. If we only relied on the journal entry above, the revert path
|
||||||
|
// would remove the account from the dirty set together with the touch.
|
||||||
|
// Keep an explicit dirty marker so tx finalisation still sees the
|
||||||
|
// account on the dirty pass when replaying that historical case.
|
||||||
|
//
|
||||||
|
// This does not force deletion by itself: Finalise will still delete the
|
||||||
|
// account only if the state object is present at tx end and qualifies for
|
||||||
|
// deletion there.
|
||||||
|
j.ripemdMagic()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,8 +467,8 @@ func (ch createObjectChange) revert(s *StateDB) {
|
||||||
delete(s.stateObjects, ch.account)
|
delete(s.stateObjects, ch.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch createObjectChange) dirtied() (common.Address, bool) {
|
func (ch createObjectChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return ch.account, true
|
return ch.account, journalMutationKindCreate, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch createObjectChange) copy() journalEntry {
|
func (ch createObjectChange) copy() journalEntry {
|
||||||
|
|
@ -309,8 +481,8 @@ func (ch createContractChange) revert(s *StateDB) {
|
||||||
s.getStateObject(ch.account).newContract = false
|
s.getStateObject(ch.account).newContract = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch createContractChange) dirtied() (common.Address, bool) {
|
func (ch createContractChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return common.Address{}, false
|
return common.Address{}, journalMutationKindNone, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch createContractChange) copy() journalEntry {
|
func (ch createContractChange) copy() journalEntry {
|
||||||
|
|
@ -326,8 +498,8 @@ func (ch selfDestructChange) revert(s *StateDB) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch selfDestructChange) dirtied() (common.Address, bool) {
|
func (ch selfDestructChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return ch.account, true
|
return ch.account, journalMutationKindSelfDestruct, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch selfDestructChange) copy() journalEntry {
|
func (ch selfDestructChange) copy() journalEntry {
|
||||||
|
|
@ -341,8 +513,8 @@ var ripemd = common.HexToAddress("0000000000000000000000000000000000000003")
|
||||||
func (ch touchChange) revert(s *StateDB) {
|
func (ch touchChange) revert(s *StateDB) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch touchChange) dirtied() (common.Address, bool) {
|
func (ch touchChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return ch.account, true
|
return ch.account, journalMutationKindTouch, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch touchChange) copy() journalEntry {
|
func (ch touchChange) copy() journalEntry {
|
||||||
|
|
@ -355,8 +527,8 @@ func (ch balanceChange) revert(s *StateDB) {
|
||||||
s.getStateObject(ch.account).setBalance(ch.prev)
|
s.getStateObject(ch.account).setBalance(ch.prev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch balanceChange) dirtied() (common.Address, bool) {
|
func (ch balanceChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return ch.account, true
|
return ch.account, journalMutationKindBalance, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch balanceChange) copy() journalEntry {
|
func (ch balanceChange) copy() journalEntry {
|
||||||
|
|
@ -370,8 +542,8 @@ func (ch nonceChange) revert(s *StateDB) {
|
||||||
s.getStateObject(ch.account).setNonce(ch.prev)
|
s.getStateObject(ch.account).setNonce(ch.prev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch nonceChange) dirtied() (common.Address, bool) {
|
func (ch nonceChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return ch.account, true
|
return ch.account, journalMutationKindNonce, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch nonceChange) copy() journalEntry {
|
func (ch nonceChange) copy() journalEntry {
|
||||||
|
|
@ -385,8 +557,8 @@ func (ch codeChange) revert(s *StateDB) {
|
||||||
s.getStateObject(ch.account).setCode(crypto.Keccak256Hash(ch.prevCode), ch.prevCode)
|
s.getStateObject(ch.account).setCode(crypto.Keccak256Hash(ch.prevCode), ch.prevCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch codeChange) dirtied() (common.Address, bool) {
|
func (ch codeChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return ch.account, true
|
return ch.account, journalMutationKindCode, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch codeChange) copy() journalEntry {
|
func (ch codeChange) copy() journalEntry {
|
||||||
|
|
@ -400,8 +572,8 @@ func (ch storageChange) revert(s *StateDB) {
|
||||||
s.getStateObject(ch.account).setState(ch.key, ch.prevvalue, ch.origvalue)
|
s.getStateObject(ch.account).setState(ch.key, ch.prevvalue, ch.origvalue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch storageChange) dirtied() (common.Address, bool) {
|
func (ch storageChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return ch.account, true
|
return ch.account, journalMutationKindStorage, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch storageChange) copy() journalEntry {
|
func (ch storageChange) copy() journalEntry {
|
||||||
|
|
@ -417,8 +589,8 @@ func (ch transientStorageChange) revert(s *StateDB) {
|
||||||
s.setTransientState(ch.account, ch.key, ch.prevalue)
|
s.setTransientState(ch.account, ch.key, ch.prevalue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch transientStorageChange) dirtied() (common.Address, bool) {
|
func (ch transientStorageChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return common.Address{}, false
|
return common.Address{}, journalMutationKindNone, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch transientStorageChange) copy() journalEntry {
|
func (ch transientStorageChange) copy() journalEntry {
|
||||||
|
|
@ -433,8 +605,8 @@ func (ch refundChange) revert(s *StateDB) {
|
||||||
s.refund = ch.prev
|
s.refund = ch.prev
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch refundChange) dirtied() (common.Address, bool) {
|
func (ch refundChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return common.Address{}, false
|
return common.Address{}, journalMutationKindNone, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch refundChange) copy() journalEntry {
|
func (ch refundChange) copy() journalEntry {
|
||||||
|
|
@ -453,8 +625,8 @@ func (ch addLogChange) revert(s *StateDB) {
|
||||||
s.logSize--
|
s.logSize--
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch addLogChange) dirtied() (common.Address, bool) {
|
func (ch addLogChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return common.Address{}, false
|
return common.Address{}, journalMutationKindNone, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch addLogChange) copy() journalEntry {
|
func (ch addLogChange) copy() journalEntry {
|
||||||
|
|
@ -476,8 +648,8 @@ func (ch accessListAddAccountChange) revert(s *StateDB) {
|
||||||
s.accessList.DeleteAddress(ch.address)
|
s.accessList.DeleteAddress(ch.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch accessListAddAccountChange) dirtied() (common.Address, bool) {
|
func (ch accessListAddAccountChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return common.Address{}, false
|
return common.Address{}, journalMutationKindNone, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch accessListAddAccountChange) copy() journalEntry {
|
func (ch accessListAddAccountChange) copy() journalEntry {
|
||||||
|
|
@ -490,8 +662,8 @@ func (ch accessListAddSlotChange) revert(s *StateDB) {
|
||||||
s.accessList.DeleteSlot(ch.address, ch.slot)
|
s.accessList.DeleteSlot(ch.address, ch.slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch accessListAddSlotChange) dirtied() (common.Address, bool) {
|
func (ch accessListAddSlotChange) mutation() (common.Address, journalMutationKind, bool) {
|
||||||
return common.Address{}, false
|
return common.Address{}, journalMutationKindNone, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch accessListAddSlotChange) copy() journalEntry {
|
func (ch accessListAddSlotChange) copy() journalEntry {
|
||||||
|
|
|
||||||
219
core/state/journal_test.go
Normal file
219
core/state/journal_test.go
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
// Copyright 2026 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/tracing"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fuzzJournalAddrs is a small fixed pool used by the fuzz harness to force
|
||||||
|
// repeated collisions on the same account, which exercises the multi-entry
|
||||||
|
// path in the journal's mutation tracking and originals cleanup on revert.
|
||||||
|
// It deliberately excludes the RIPEMD-160 precompile (0x03), which has a
|
||||||
|
// consensus-level touch/revert exception that would complicate invariants.
|
||||||
|
var fuzzJournalAddrs = []common.Address{
|
||||||
|
common.BytesToAddress([]byte{0x11}),
|
||||||
|
common.BytesToAddress([]byte{0x22}),
|
||||||
|
common.BytesToAddress([]byte{0x44}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkJournalInvariants validates that:
|
||||||
|
// - journal.mutations exactly reflects the dirty entries currently in
|
||||||
|
// journal.entries (per-kind counts and mask match what you'd get by
|
||||||
|
// walking the entries from scratch).
|
||||||
|
// - journal.originals mirrors that set for the three tracked metadata kinds
|
||||||
|
// (balance/nonce/code): a *Set flag is true iff the account currently has
|
||||||
|
// at least one corresponding entry in the journal.
|
||||||
|
// - An address is present in originals only if it also has at least one
|
||||||
|
// tracked-kind mutation in the journal.
|
||||||
|
func checkJournalInvariants(t *testing.T, j *journal) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Reconstruct the expected per-address counts from the live entries.
|
||||||
|
expected := make(map[common.Address]*journalMutationCounts)
|
||||||
|
for _, e := range j.entries {
|
||||||
|
addr, kind, dirty := e.mutation()
|
||||||
|
if !dirty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c := expected[addr]
|
||||||
|
if c == nil {
|
||||||
|
c = &journalMutationCounts{}
|
||||||
|
expected[addr] = c
|
||||||
|
}
|
||||||
|
c.add(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(j.mutations) != len(expected) {
|
||||||
|
t.Fatalf("mutations size %d, want %d", len(j.mutations), len(expected))
|
||||||
|
}
|
||||||
|
for addr, state := range j.mutations {
|
||||||
|
want, ok := expected[addr]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("mutations has extra address %x", addr)
|
||||||
|
}
|
||||||
|
if state.counts != *want {
|
||||||
|
t.Fatalf("addr %x: counts=%+v want=%+v", addr, state.counts, *want)
|
||||||
|
}
|
||||||
|
// First-touch *Set flags must mirror the live per-kind counts.
|
||||||
|
if state.balanceSet != (want[journalMutationKindBalance] > 0) {
|
||||||
|
t.Fatalf("addr %x: balanceSet=%v want=%v (balance count=%d)",
|
||||||
|
addr, state.balanceSet, want[journalMutationKindBalance] > 0, want[journalMutationKindBalance])
|
||||||
|
}
|
||||||
|
if state.nonceSet != (want[journalMutationKindNonce] > 0) {
|
||||||
|
t.Fatalf("addr %x: nonceSet=%v want=%v (nonce count=%d)",
|
||||||
|
addr, state.nonceSet, want[journalMutationKindNonce] > 0, want[journalMutationKindNonce])
|
||||||
|
}
|
||||||
|
if state.codeSet != (want[journalMutationKindCode] > 0) {
|
||||||
|
t.Fatalf("addr %x: codeSet=%v want=%v (code count=%d)",
|
||||||
|
addr, state.codeSet, want[journalMutationKindCode] > 0, want[journalMutationKindCode])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzJournal drives a randomised sequence of state mutations, snapshots and
|
||||||
|
// reverts against a fresh StateDB and validates the journal's internal
|
||||||
|
// bookkeeping invariants after every step. It also asserts that reverting
|
||||||
|
// back to the root snapshot empties mutations, originals and entries
|
||||||
|
// completely. The seed corpus ensures the test also runs as a regular unit
|
||||||
|
// test via `go test -run FuzzJournal`.
|
||||||
|
func FuzzJournal(f *testing.F) {
|
||||||
|
seeds := [][]byte{
|
||||||
|
// balance then full revert (simplest a→b→a case).
|
||||||
|
{0x00, 0x00, 0x05, 0x05, 0x00},
|
||||||
|
// balance+nonce+code mixed, then revert to root.
|
||||||
|
{0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x03, 0x05, 0x00},
|
||||||
|
// snapshot, mutate, revert, mutate again.
|
||||||
|
{0x04, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, 0x01, 0x05},
|
||||||
|
// storage interleaved with metadata.
|
||||||
|
{0x03, 0x00, 0x01, 0x00, 0x01, 0x05, 0x03, 0x02, 0x02, 0x04, 0x03, 0x01, 0x07},
|
||||||
|
// many ops, no explicit revert — exercises steady-state invariants.
|
||||||
|
{0x00, 0x01, 0x02, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02,
|
||||||
|
0x03, 0x04, 0x00, 0x01, 0x02, 0x00, 0x06, 0x08, 0x0a, 0x0c},
|
||||||
|
}
|
||||||
|
for _, s := range seeds {
|
||||||
|
f.Add(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
sdb, err := New(types.EmptyRootHash, NewDatabaseForTesting())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root := sdb.Snapshot()
|
||||||
|
|
||||||
|
// Stack of snapshot IDs taken during the fuzz loop.
|
||||||
|
var pending []int
|
||||||
|
|
||||||
|
// readByte returns the next byte and advances the cursor. Returns
|
||||||
|
// (0, false) if exhausted.
|
||||||
|
i := 0
|
||||||
|
readByte := func() (byte, bool) {
|
||||||
|
if i >= len(data) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
b := data[i]
|
||||||
|
i++
|
||||||
|
return b, true
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
op, ok := readByte()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch op % 6 {
|
||||||
|
case 0: // SetBalance
|
||||||
|
a, ok1 := readByte()
|
||||||
|
v, ok2 := readByte()
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
|
||||||
|
sdb.SetBalance(addr, uint256.NewInt(uint64(v)), tracing.BalanceChangeUnspecified)
|
||||||
|
case 1: // SetNonce
|
||||||
|
a, ok1 := readByte()
|
||||||
|
n, ok2 := readByte()
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
|
||||||
|
sdb.SetNonce(addr, uint64(n), tracing.NonceChangeUnspecified)
|
||||||
|
case 2: // SetCode
|
||||||
|
a, ok1 := readByte()
|
||||||
|
l, ok2 := readByte()
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
|
||||||
|
code := make([]byte, int(l)%8)
|
||||||
|
for k := range code {
|
||||||
|
b, ok := readByte()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
code[k] = b
|
||||||
|
}
|
||||||
|
sdb.SetCode(addr, code, tracing.CodeChangeUnspecified)
|
||||||
|
case 3: // SetState (storage; tracked as mutation kind, no original)
|
||||||
|
a, ok1 := readByte()
|
||||||
|
k, ok2 := readByte()
|
||||||
|
v, ok3 := readByte()
|
||||||
|
if !ok1 || !ok2 || !ok3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
|
||||||
|
sdb.SetState(addr,
|
||||||
|
common.BytesToHash([]byte{k}),
|
||||||
|
common.BytesToHash([]byte{v}))
|
||||||
|
case 4: // Snapshot
|
||||||
|
pending = append(pending, sdb.Snapshot())
|
||||||
|
case 5: // RevertToSnapshot
|
||||||
|
if len(pending) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sel, ok := readByte()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
idx := int(sel) % len(pending)
|
||||||
|
sdb.RevertToSnapshot(pending[idx])
|
||||||
|
pending = pending[:idx]
|
||||||
|
}
|
||||||
|
checkJournalInvariants(t, sdb.journal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After reverting to the root snapshot, the journal must be fully
|
||||||
|
// drained: no entries, no mutations, no originals. This is the core
|
||||||
|
// guarantee the user cares about — "all mutations against a single
|
||||||
|
// account reverted" taken to its limit across every account.
|
||||||
|
sdb.RevertToSnapshot(root)
|
||||||
|
checkJournalInvariants(t, sdb.journal)
|
||||||
|
|
||||||
|
if n := len(sdb.journal.entries); n != 0 {
|
||||||
|
t.Fatalf("entries not drained after revert-to-root: %d remain", n)
|
||||||
|
}
|
||||||
|
if n := len(sdb.journal.mutations); n != 0 {
|
||||||
|
t.Fatalf("mutations not drained after revert-to-root: %d remain", n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -184,8 +184,9 @@ func (s *stateObject) getState(key common.Hash) (common.Hash, common.Hash) {
|
||||||
// without any mutations caused in the current execution.
|
// without any mutations caused in the current execution.
|
||||||
func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
|
func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
|
||||||
// Record slot access regardless of whether the storage slot exists.
|
// Record slot access regardless of whether the storage slot exists.
|
||||||
s.db.stateReadList.AddState(s.address, key)
|
if s.db.stateAccessList != nil {
|
||||||
|
s.db.stateAccessList.StorageRead(s.address, key)
|
||||||
|
}
|
||||||
// If we have a pending write or clean cached, return that
|
// If we have a pending write or clean cached, return that
|
||||||
if value, pending := s.pendingStorage[key]; pending {
|
if value, pending := s.pendingStorage[key]; pending {
|
||||||
return value
|
return value
|
||||||
|
|
@ -274,6 +275,13 @@ func (s *stateObject) finalise() {
|
||||||
// map as the dirty slot might have been committed already (before the
|
// map as the dirty slot might have been committed already (before the
|
||||||
// byzantium fork) and entry is necessary to modify the value back.
|
// byzantium fork) and entry is necessary to modify the value back.
|
||||||
s.pendingStorage[key] = value
|
s.pendingStorage[key] = value
|
||||||
|
|
||||||
|
// Aggregate storage writes into the block-level access list.
|
||||||
|
// All slots in the dirtyStorage set must have post-transaction
|
||||||
|
// values that differ from their pre-transaction values.
|
||||||
|
if s.db.stateAccessList != nil {
|
||||||
|
s.db.stateAccessList.StorageWrite(s.db.blockAccessIndex, s.address, key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if s.db.prefetcher != nil && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash {
|
if s.db.prefetcher != nil && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash {
|
||||||
if err := s.db.prefetcher.prefetch(s.addrHash(), s.data.Root, s.address, nil, slotsToPrefetch, false); err != nil {
|
if err := s.db.prefetcher.prefetch(s.addrHash(), s.data.Root, s.address, nil, slotsToPrefetch, false); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
|
@ -128,7 +129,10 @@ type StateDB struct {
|
||||||
accessEvents *AccessEvents
|
accessEvents *AccessEvents
|
||||||
|
|
||||||
// Per-transaction state access footprint for EIP-7928
|
// Per-transaction state access footprint for EIP-7928
|
||||||
stateReadList *bal.StateAccessList
|
stateAccessList *bal.ConstructionBlockAccessList
|
||||||
|
|
||||||
|
// Block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution)
|
||||||
|
blockAccessIndex uint32
|
||||||
|
|
||||||
// Transient storage
|
// Transient storage
|
||||||
transientStorage transientStorage
|
transientStorage transientStorage
|
||||||
|
|
@ -589,8 +593,9 @@ func (s *StateDB) deleteStateObject(addr common.Address) {
|
||||||
// the object is not found or was deleted in this execution context.
|
// the object is not found or was deleted in this execution context.
|
||||||
func (s *StateDB) getStateObject(addr common.Address) *stateObject {
|
func (s *StateDB) getStateObject(addr common.Address) *stateObject {
|
||||||
// Record state access regardless of whether the account exists.
|
// Record state access regardless of whether the account exists.
|
||||||
s.stateReadList.AddAccount(addr)
|
if s.stateAccessList != nil {
|
||||||
|
s.stateAccessList.AccountRead(addr)
|
||||||
|
}
|
||||||
// Prefer live objects if any is available
|
// Prefer live objects if any is available
|
||||||
if obj := s.stateObjects[addr]; obj != nil {
|
if obj := s.stateObjects[addr]; obj != nil {
|
||||||
return obj
|
return obj
|
||||||
|
|
@ -693,6 +698,7 @@ func (s *StateDB) Copy() *StateDB {
|
||||||
refund: s.refund,
|
refund: s.refund,
|
||||||
thash: s.thash,
|
thash: s.thash,
|
||||||
txIndex: s.txIndex,
|
txIndex: s.txIndex,
|
||||||
|
blockAccessIndex: s.blockAccessIndex,
|
||||||
logs: make(map[common.Hash][]*types.Log, len(s.logs)),
|
logs: make(map[common.Hash][]*types.Log, len(s.logs)),
|
||||||
logSize: s.logSize,
|
logSize: s.logSize,
|
||||||
preimages: maps.Clone(s.preimages),
|
preimages: maps.Clone(s.preimages),
|
||||||
|
|
@ -716,9 +722,6 @@ func (s *StateDB) Copy() *StateDB {
|
||||||
if s.accessEvents != nil {
|
if s.accessEvents != nil {
|
||||||
state.accessEvents = s.accessEvents.Copy()
|
state.accessEvents = s.accessEvents.Copy()
|
||||||
}
|
}
|
||||||
if s.stateReadList != nil {
|
|
||||||
state.stateReadList = s.stateReadList.Copy()
|
|
||||||
}
|
|
||||||
// Deep copy cached state objects.
|
// Deep copy cached state objects.
|
||||||
for addr, obj := range s.stateObjects {
|
for addr, obj := range s.stateObjects {
|
||||||
state.stateObjects[addr] = obj.deepCopy(state)
|
state.stateObjects[addr] = obj.deepCopy(state)
|
||||||
|
|
@ -740,6 +743,9 @@ func (s *StateDB) Copy() *StateDB {
|
||||||
}
|
}
|
||||||
state.logs[hash] = cpy
|
state.logs[hash] = cpy
|
||||||
}
|
}
|
||||||
|
if s.stateAccessList != nil {
|
||||||
|
state.stateAccessList = s.stateAccessList.Copy()
|
||||||
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -775,7 +781,7 @@ type removedAccountWithBalance struct {
|
||||||
// before the Finalise.
|
// before the Finalise.
|
||||||
func (s *StateDB) LogsForBurnAccounts() []*types.Log {
|
func (s *StateDB) LogsForBurnAccounts() []*types.Log {
|
||||||
var list []removedAccountWithBalance
|
var list []removedAccountWithBalance
|
||||||
for addr := range s.journal.dirties {
|
for addr := range s.journal.mutations {
|
||||||
if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() {
|
if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() {
|
||||||
list = append(list, removedAccountWithBalance{
|
list = append(list, removedAccountWithBalance{
|
||||||
address: obj.address,
|
address: obj.address,
|
||||||
|
|
@ -799,17 +805,20 @@ func (s *StateDB) LogsForBurnAccounts() []*types.Log {
|
||||||
// Finalise finalises the state by removing the destructed objects and clears
|
// Finalise finalises the state by removing the destructed objects and clears
|
||||||
// the journal as well as the refunds. Finalise, however, will not push any updates
|
// 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.
|
// into the tries just yet. Only IntermediateRoot or Commit will do that.
|
||||||
func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
|
func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList {
|
||||||
addressesToPrefetch := make([]common.Address, 0, len(s.journal.dirties))
|
addressesToPrefetch := make([]common.Address, 0, len(s.journal.mutations))
|
||||||
for addr := range s.journal.dirties {
|
for addr, state := range s.journal.mutations {
|
||||||
obj, exist := s.stateObjects[addr]
|
obj, exist := s.stateObjects[addr]
|
||||||
if !exist {
|
if !exist {
|
||||||
// ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2
|
// RIPEMD160 (0x03) gets an extra dirty marker for a historical
|
||||||
// That tx goes out of gas, and although the notion of 'touched' does not exist there, the
|
// mainnet consensus exception (at block 1714175, in tx
|
||||||
// touch-event will still be recorded in the journal. Since ripeMD is a special snowflake,
|
// 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2)
|
||||||
// it will persist in the journal even though the journal is reverted. In this special circumstance,
|
// around empty-account touch/revert handling.
|
||||||
// it may exist in `s.journal.dirties` but not in `s.stateObjects`.
|
//
|
||||||
// Thus, we can safely ignore it here
|
// That marker survives journal revert, so the account may remain in
|
||||||
|
// s.journal.mutations even though its state object was rolled
|
||||||
|
// back and no longer exists. In that case there is nothing to
|
||||||
|
// finalise or delete, so ignore it here.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if obj.selfDestructed || (deleteEmptyObjects && obj.empty()) {
|
if obj.selfDestructed || (deleteEmptyObjects && obj.empty()) {
|
||||||
|
|
@ -822,7 +831,43 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
|
||||||
if _, ok := s.stateObjectsDestruct[obj.address]; !ok {
|
if _, ok := s.stateObjectsDestruct[obj.address]; !ok {
|
||||||
s.stateObjectsDestruct[obj.address] = obj
|
s.stateObjectsDestruct[obj.address] = obj
|
||||||
}
|
}
|
||||||
|
// Aggregate the account mutation into the block-level accessList
|
||||||
|
// if Amsterdam has been activated.
|
||||||
|
if s.stateAccessList != nil {
|
||||||
|
// Notably, if the account is deleted during the transaction,
|
||||||
|
// its pre-transaction nonce, code, and storage must be empty.
|
||||||
|
//
|
||||||
|
// EIP-6780 restricts self-destruct to contracts deployed within
|
||||||
|
// the same transaction, while EIP-7610 rejects deployments to
|
||||||
|
// destinations with non-empty storage, non-zero nonce and non-empty
|
||||||
|
// code.
|
||||||
|
//
|
||||||
|
// Therefore, when an account is deleted, its pre-transaction nonce
|
||||||
|
// code and storage is guaranteed to be empty, leaving nothing to
|
||||||
|
// clean up here.
|
||||||
|
balance := uint256.NewInt(0)
|
||||||
|
if state.balanceSet && balance.Cmp(state.balance) != 0 {
|
||||||
|
s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Aggregate the account mutation into the block-level accessList
|
||||||
|
// if Amsterdam has been activated.
|
||||||
|
if s.stateAccessList != nil {
|
||||||
|
balance := obj.Balance()
|
||||||
|
if state.balanceSet && balance.Cmp(state.balance) != 0 {
|
||||||
|
s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance)
|
||||||
|
}
|
||||||
|
nonce := obj.Nonce()
|
||||||
|
if state.nonceSet && nonce != state.nonce {
|
||||||
|
s.stateAccessList.NonceChange(addr, s.blockAccessIndex, nonce)
|
||||||
|
}
|
||||||
|
if state.codeSet {
|
||||||
|
if code := obj.Code(); !bytes.Equal(code, state.code) {
|
||||||
|
s.stateAccessList.CodeChange(addr, s.blockAccessIndex, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
obj.finalise()
|
obj.finalise()
|
||||||
s.markUpdate(addr)
|
s.markUpdate(addr)
|
||||||
}
|
}
|
||||||
|
|
@ -839,7 +884,7 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
|
||||||
// Invalidate journal because reverting across transactions is not allowed.
|
// Invalidate journal because reverting across transactions is not allowed.
|
||||||
s.clearJournalAndRefund()
|
s.clearJournalAndRefund()
|
||||||
|
|
||||||
return s.stateReadList
|
return s.stateAccessList
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntermediateRoot computes the current root hash of the state trie.
|
// IntermediateRoot computes the current root hash of the state trie.
|
||||||
|
|
@ -1052,9 +1097,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
|
||||||
// SetTxContext sets the current transaction hash and index which are
|
// SetTxContext sets the current transaction hash and index which are
|
||||||
// used when the EVM emits new state logs. It should be invoked before
|
// used when the EVM emits new state logs. It should be invoked before
|
||||||
// transaction execution.
|
// transaction execution.
|
||||||
func (s *StateDB) SetTxContext(thash common.Hash, ti int) {
|
func (s *StateDB) SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) {
|
||||||
s.thash = thash
|
s.thash = thash
|
||||||
s.txIndex = ti
|
s.txIndex = ti
|
||||||
|
s.blockAccessIndex = blockAccessIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StateDB) clearJournalAndRefund() {
|
func (s *StateDB) clearJournalAndRefund() {
|
||||||
|
|
@ -1435,7 +1481,7 @@ func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, d
|
||||||
s.transientStorage = newTransientStorage()
|
s.transientStorage = newTransientStorage()
|
||||||
|
|
||||||
if rules.IsAmsterdam {
|
if rules.IsAmsterdam {
|
||||||
s.stateReadList = bal.NewStateAccessList()
|
s.stateAccessList = bal.NewConstructionBlockAccessList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log {
|
||||||
return s.inner.LogsForBurnAccounts()
|
return s.inner.LogsForBurnAccounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
|
func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList {
|
||||||
if s.hooks.OnBalanceChange == nil && s.hooks.OnNonceChangeV2 == nil && s.hooks.OnNonceChange == nil && s.hooks.OnCodeChangeV2 == nil && s.hooks.OnCodeChange == nil {
|
if s.hooks.OnBalanceChange == nil && s.hooks.OnNonceChangeV2 == nil && s.hooks.OnNonceChange == nil && s.hooks.OnCodeChangeV2 == nil && s.hooks.OnCodeChange == nil {
|
||||||
// Short circuit if no relevant hooks are set.
|
// Short circuit if no relevant hooks are set.
|
||||||
return s.inner.Finalise(deleteEmptyObjects)
|
return s.inner.Finalise(deleteEmptyObjects)
|
||||||
|
|
@ -244,7 +244,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
|
||||||
// that state change hooks will be invoked in deterministic
|
// that state change hooks will be invoked in deterministic
|
||||||
// order when the accounts are deleted below
|
// order when the accounts are deleted below
|
||||||
var selfDestructedAddrs []common.Address
|
var selfDestructedAddrs []common.Address
|
||||||
for addr := range s.inner.journal.dirties {
|
for addr := range s.inner.journal.mutations {
|
||||||
obj := s.inner.stateObjects[addr]
|
obj := s.inner.stateObjects[addr]
|
||||||
if obj == nil || !obj.selfDestructed {
|
if obj == nil || !obj.selfDestructed {
|
||||||
// Not self-destructed, keep searching.
|
// Not self-destructed, keep searching.
|
||||||
|
|
@ -288,3 +288,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
|
||||||
}
|
}
|
||||||
return s.inner.Finalise(deleteEmptyObjects)
|
return s.inner.Finalise(deleteEmptyObjects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *hookedStateDB) SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) {
|
||||||
|
s.inner.SetTxContext(thash, ti, blockAccessIndex)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ func TestBurn(t *testing.T) {
|
||||||
// TestHooks is a basic sanity-check of all hooks
|
// TestHooks is a basic sanity-check of all hooks
|
||||||
func TestHooks(t *testing.T) {
|
func TestHooks(t *testing.T) {
|
||||||
inner, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
|
inner, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
|
||||||
inner.SetTxContext(common.Hash{0x11}, 100) // For the log
|
inner.SetTxContext(common.Hash{0x11}, 100, 101) // For the log
|
||||||
var result []string
|
var result []string
|
||||||
var wants = []string{
|
var wants = []string{
|
||||||
"0xaa00000000000000000000000000000000000000.balance: 0->100 (Unspecified)",
|
"0xaa00000000000000000000000000000000000000.balance: 0->100 (Unspecified)",
|
||||||
|
|
|
||||||
|
|
@ -662,26 +662,30 @@ func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error {
|
||||||
return fmt.Errorf("got GetLogs(common.Hash{}) == %v, want GetLogs(common.Hash{}) == %v",
|
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))
|
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) {
|
if !equalMutationSets(state.journal.mutations, checkstate.journal.mutations) {
|
||||||
getKeys := func(dirty map[common.Address]int) string {
|
return fmt.Errorf("journal mutation set mismatch.\nhave:\n%v\nwant:\n%v\n", state.journal.mutations, checkstate.journal.mutations)
|
||||||
var keys []common.Address
|
|
||||||
out := new(strings.Builder)
|
|
||||||
for key := range dirty {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
slices.SortFunc(keys, common.Address.Cmp)
|
|
||||||
for i, key := range keys {
|
|
||||||
fmt.Fprintf(out, " %d. %v\n", i, key)
|
|
||||||
}
|
|
||||||
return out.String()
|
|
||||||
}
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// equalMutationSets checks that two journal mutation maps have the same set of
|
||||||
|
// addresses and, for each address, the same per-kind counts. The stashed
|
||||||
|
// original values are ignored because comparing them across two independent
|
||||||
|
// state databases (with distinct pointer identities) isn't the point of this
|
||||||
|
// check — we only care that the two journals agree on what was touched.
|
||||||
|
func equalMutationSets(a, b map[common.Address]*journalMutationState) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for addr, sa := range a {
|
||||||
|
sb, ok := b[addr]
|
||||||
|
if !ok || sa.counts != sb.counts {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func TestTouchDelete(t *testing.T) {
|
func TestTouchDelete(t *testing.T) {
|
||||||
s := newStateEnv()
|
s := newStateEnv()
|
||||||
s.state.getOrNewStateObject(common.Address{})
|
s.state.getOrNewStateObject(common.Address{})
|
||||||
|
|
@ -691,12 +695,54 @@ func TestTouchDelete(t *testing.T) {
|
||||||
snapshot := s.state.Snapshot()
|
snapshot := s.state.Snapshot()
|
||||||
s.state.AddBalance(common.Address{}, new(uint256.Int), tracing.BalanceChangeUnspecified)
|
s.state.AddBalance(common.Address{}, new(uint256.Int), tracing.BalanceChangeUnspecified)
|
||||||
|
|
||||||
if len(s.state.journal.dirties) != 1 {
|
if len(s.state.journal.mutations) != 1 {
|
||||||
t.Fatal("expected one dirty state object")
|
t.Fatal("expected one mutated state object")
|
||||||
}
|
}
|
||||||
s.state.RevertToSnapshot(snapshot)
|
s.state.RevertToSnapshot(snapshot)
|
||||||
if len(s.state.journal.dirties) != 0 {
|
if len(s.state.journal.mutations) != 0 {
|
||||||
t.Fatal("expected no dirty state object")
|
t.Fatal("expected no journal mutations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJournalMutationTracking(t *testing.T) {
|
||||||
|
state, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
|
||||||
|
addr := common.HexToAddress("0x01")
|
||||||
|
key := common.HexToHash("0x02")
|
||||||
|
|
||||||
|
if _, ok := state.journal.mutations[addr]; ok {
|
||||||
|
t.Fatal("unexpected initial mutation entry")
|
||||||
|
}
|
||||||
|
snapshot := state.Snapshot()
|
||||||
|
|
||||||
|
state.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
|
||||||
|
state.SetNonce(addr, 2, tracing.NonceChangeUnspecified)
|
||||||
|
state.SetCode(addr, []byte{0x1}, tracing.CodeChangeUnspecified)
|
||||||
|
state.SetState(addr, key, common.Hash{0x3})
|
||||||
|
|
||||||
|
want := journalMutationCounts{
|
||||||
|
journalMutationKindCreate: 1,
|
||||||
|
journalMutationKindBalance: 1,
|
||||||
|
journalMutationKindNonce: 1,
|
||||||
|
journalMutationKindCode: 1,
|
||||||
|
journalMutationKindStorage: 1,
|
||||||
|
}
|
||||||
|
checkCounts := func(got *journalMutationState, label string) {
|
||||||
|
t.Helper()
|
||||||
|
if got == nil {
|
||||||
|
t.Fatalf("%s: missing mutation entry for %x", label, addr)
|
||||||
|
}
|
||||||
|
if got.counts != want {
|
||||||
|
t.Fatalf("%s: counts=%+v, want=%+v", label, got.counts, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkCounts(state.journal.mutations[addr], "state")
|
||||||
|
|
||||||
|
copy := state.Copy()
|
||||||
|
checkCounts(copy.journal.mutations[addr], "copy")
|
||||||
|
|
||||||
|
state.RevertToSnapshot(snapshot)
|
||||||
|
if _, ok := state.journal.mutations[addr]; ok {
|
||||||
|
t.Fatalf("unexpected mutation entry after revert")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c
|
||||||
// Disable the nonce check
|
// Disable the nonce check
|
||||||
msg.SkipNonceChecks = true
|
msg.SkipNonceChecks = true
|
||||||
|
|
||||||
stateCpy.SetTxContext(tx.Hash(), i)
|
stateCpy.SetTxContext(tx.Hash(), i, uint32(i+1))
|
||||||
|
|
||||||
// We attempt to apply a transaction. The goal is not to execute
|
// We attempt to apply a transaction. The goal is not to execute
|
||||||
// the transaction successfully, rather to warm up touched data slots.
|
// the transaction successfully, rather to warm up touched data slots.
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
|
return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
|
||||||
}
|
}
|
||||||
statedb.SetTxContext(tx.Hash(), i)
|
statedb.SetTxContext(tx.Hash(), i, uint32(i+1))
|
||||||
_, _, spanEnd := telemetry.StartSpan(ctx, "core.ApplyTransactionWithEVM",
|
_, _, spanEnd := telemetry.StartSpan(ctx, "core.ApplyTransactionWithEVM",
|
||||||
telemetry.StringAttribute("tx.hash", tx.Hash().Hex()),
|
telemetry.StringAttribute("tx.hash", tx.Hash().Hex()),
|
||||||
telemetry.Int64Attribute("tx.index", int64(i)),
|
telemetry.Int64Attribute("tx.index", int64(i)),
|
||||||
|
|
@ -109,8 +109,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
|
||||||
allLogs = append(allLogs, receipt.Logs...)
|
allLogs = append(allLogs, receipt.Logs...)
|
||||||
spanEnd(nil)
|
spanEnd(nil)
|
||||||
}
|
}
|
||||||
// Run the post-execution system calls
|
requests, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1))
|
||||||
requests, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -143,7 +142,7 @@ func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Ha
|
||||||
// PostExecution processes post-execution system calls when Prague is enabled.
|
// PostExecution processes post-execution system calls when Prague is enabled.
|
||||||
// If Prague is not activated, it returns null requests to differentiate from
|
// If Prague is not activated, it returns null requests to differentiate from
|
||||||
// empty requests.
|
// empty requests.
|
||||||
func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM) (requests [][]byte, err error) {
|
func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, err error) {
|
||||||
_, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution")
|
_, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution")
|
||||||
defer spanEnd(&err)
|
defer spanEnd(&err)
|
||||||
|
|
||||||
|
|
@ -155,11 +154,11 @@ func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.
|
||||||
return nil, fmt.Errorf("failed to parse deposit logs: %w", err)
|
return nil, fmt.Errorf("failed to parse deposit logs: %w", err)
|
||||||
}
|
}
|
||||||
// EIP-7002
|
// EIP-7002
|
||||||
if err := ProcessWithdrawalQueue(&requests, evm); err != nil {
|
if err := ProcessWithdrawalQueue(&requests, evm, blockAccessIndex); err != nil {
|
||||||
return nil, fmt.Errorf("failed to process withdrawal queue: %w", err)
|
return nil, fmt.Errorf("failed to process withdrawal queue: %w", err)
|
||||||
}
|
}
|
||||||
// EIP-7251
|
// EIP-7251
|
||||||
if err := ProcessConsolidationQueue(&requests, evm); err != nil {
|
if err := ProcessConsolidationQueue(&requests, evm, blockAccessIndex); err != nil {
|
||||||
return nil, fmt.Errorf("failed to process consolidation queue: %w", err)
|
return nil, fmt.Errorf("failed to process consolidation queue: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -268,6 +267,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) {
|
||||||
Data: beaconRoot[:],
|
Data: beaconRoot[:],
|
||||||
}
|
}
|
||||||
evm.SetTxContext(NewEVMTxContext(msg))
|
evm.SetTxContext(NewEVMTxContext(msg))
|
||||||
|
evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
|
||||||
evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress)
|
evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress)
|
||||||
_, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
|
_, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
|
||||||
if evm.StateDB.AccessEvents() != nil {
|
if evm.StateDB.AccessEvents() != nil {
|
||||||
|
|
@ -295,6 +295,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
|
||||||
Data: prevHash.Bytes(),
|
Data: prevHash.Bytes(),
|
||||||
}
|
}
|
||||||
evm.SetTxContext(NewEVMTxContext(msg))
|
evm.SetTxContext(NewEVMTxContext(msg))
|
||||||
|
evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
|
||||||
evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress)
|
evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress)
|
||||||
_, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
|
_, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -308,17 +309,17 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
|
||||||
|
|
||||||
// ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract.
|
// ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract.
|
||||||
// It returns the opaque request data returned by the contract.
|
// It returns the opaque request data returned by the contract.
|
||||||
func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM) error {
|
func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error {
|
||||||
return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress)
|
return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract.
|
// ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract.
|
||||||
// It returns the opaque request data returned by the contract.
|
// It returns the opaque request data returned by the contract.
|
||||||
func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM) error {
|
func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error {
|
||||||
return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress)
|
return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address) error {
|
func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32) error {
|
||||||
if tracer := evm.Config.Tracer; tracer != nil {
|
if tracer := evm.Config.Tracer; tracer != nil {
|
||||||
onSystemCallStart(tracer, evm.GetVMContext())
|
onSystemCallStart(tracer, evm.GetVMContext())
|
||||||
if tracer.OnSystemCallEnd != nil {
|
if tracer.OnSystemCallEnd != nil {
|
||||||
|
|
@ -334,6 +335,7 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte
|
||||||
To: &addr,
|
To: &addr,
|
||||||
}
|
}
|
||||||
evm.SetTxContext(NewEVMTxContext(msg))
|
evm.SetTxContext(NewEVMTxContext(msg))
|
||||||
|
evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex)
|
||||||
evm.StateDB.AddAddressToAccessList(addr)
|
evm.StateDB.AddAddressToAccessList(addr)
|
||||||
ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
|
ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
|
||||||
if evm.StateDB.AccessEvents() != nil {
|
if evm.StateDB.AccessEvents() != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
// Copyright 2026 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 bal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"maps"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StorageAccessList represents a set of storage slots accessed within an account.
|
|
||||||
type StorageAccessList map[common.Hash]struct{}
|
|
||||||
|
|
||||||
// StateAccessList records the set of accounts and storage slots that have been
|
|
||||||
// accessed. An entry with an empty StorageAccessList denotes an account access
|
|
||||||
// without any storage slot access.
|
|
||||||
type StateAccessList struct {
|
|
||||||
list map[common.Address]StorageAccessList
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStateAccessList returns an empty StateAccessList ready for use.
|
|
||||||
func NewStateAccessList() *StateAccessList {
|
|
||||||
return &StateAccessList{
|
|
||||||
list: make(map[common.Address]StorageAccessList),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddAccount records an access to the given account. It is a no-op if the
|
|
||||||
// account is already present.
|
|
||||||
func (s *StateAccessList) AddAccount(addr common.Address) {
|
|
||||||
if s == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, exists := s.list[addr]; !exists {
|
|
||||||
s.list[addr] = make(StorageAccessList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddState records an access to the given storage slot. The owning account is
|
|
||||||
// implicitly recorded as well.
|
|
||||||
func (s *StateAccessList) AddState(addr common.Address, slot common.Hash) {
|
|
||||||
if s == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
slots, exists := s.list[addr]
|
|
||||||
if !exists {
|
|
||||||
slots = make(StorageAccessList)
|
|
||||||
s.list[addr] = slots
|
|
||||||
}
|
|
||||||
slots[slot] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge merges the entries from other into the receiver.
|
|
||||||
func (s *StateAccessList) Merge(other *StateAccessList) {
|
|
||||||
if s == nil || other == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for addr, otherSlots := range other.list {
|
|
||||||
slots, exists := s.list[addr]
|
|
||||||
if !exists {
|
|
||||||
s.list[addr] = otherSlots
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
maps.Copy(slots, otherSlots)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy returns a deep copy of the StateAccessList.
|
|
||||||
func (s *StateAccessList) Copy() *StateAccessList {
|
|
||||||
if s == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cpy := &StateAccessList{
|
|
||||||
list: make(map[common.Address]StorageAccessList, len(s.list)),
|
|
||||||
}
|
|
||||||
for addr, slots := range s.list {
|
|
||||||
cpy.list[addr] = maps.Clone(slots)
|
|
||||||
}
|
|
||||||
return cpy
|
|
||||||
}
|
|
||||||
|
|
@ -71,8 +71,8 @@ type ConstructionBlockAccessList struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConstructionBlockAccessList instantiates an empty access list.
|
// NewConstructionBlockAccessList instantiates an empty access list.
|
||||||
func NewConstructionBlockAccessList() ConstructionBlockAccessList {
|
func NewConstructionBlockAccessList() *ConstructionBlockAccessList {
|
||||||
return ConstructionBlockAccessList{
|
return &ConstructionBlockAccessList{
|
||||||
Accounts: make(map[common.Address]*ConstructionAccountAccess),
|
Accounts: make(map[common.Address]*ConstructionAccountAccess),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,5 +169,5 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList {
|
||||||
aaCopy.CodeChange = codes
|
aaCopy.CodeChange = codes
|
||||||
res.Accounts[addr] = &aaCopy
|
res.Accounts[addr] = &aaCopy
|
||||||
}
|
}
|
||||||
return &res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,5 +98,6 @@ type StateDB interface {
|
||||||
AccessEvents() *state.AccessEvents
|
AccessEvents() *state.AccessEvents
|
||||||
|
|
||||||
// Finalise must be invoked at the end of a transaction
|
// Finalise must be invoked at the end of a transaction
|
||||||
Finalise(bool) *bal.StateAccessList
|
Finalise(bool) *bal.ConstructionBlockAccessList
|
||||||
|
SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block,
|
||||||
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
|
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
|
||||||
|
|
||||||
// Not yet the searched for transaction, execute on top of the current state
|
// Not yet the searched for transaction, execute on top of the current state
|
||||||
statedb.SetTxContext(tx.Hash(), idx)
|
statedb.SetTxContext(tx.Hash(), idx, uint32(idx+1))
|
||||||
if _, err := core.ApplyMessage(evm, msg, nil); err != nil {
|
if _, err := core.ApplyMessage(evm, msg, nil); err != nil {
|
||||||
return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err)
|
return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -530,7 +530,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
|
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
|
||||||
statedb.SetTxContext(tx.Hash(), i)
|
statedb.SetTxContext(tx.Hash(), i, uint32(i+1))
|
||||||
if _, err := core.ApplyMessage(evm, msg, nil); err != nil {
|
if _, err := core.ApplyMessage(evm, msg, nil); err != nil {
|
||||||
log.Warn("Tracing intermediate roots did not complete", "txindex", i, "txhash", tx.Hash(), "err", err)
|
log.Warn("Tracing intermediate roots did not complete", "txindex", i, "txhash", tx.Hash(), "err", err)
|
||||||
// We intentionally don't return the error here: if we do, then the RPC server will not
|
// We intentionally don't return the error here: if we do, then the RPC server will not
|
||||||
|
|
@ -681,7 +681,7 @@ txloop:
|
||||||
|
|
||||||
// Generate the next state snapshot fast without tracing
|
// Generate the next state snapshot fast without tracing
|
||||||
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
|
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
|
||||||
statedb.SetTxContext(tx.Hash(), i)
|
statedb.SetTxContext(tx.Hash(), i, uint32(i+1))
|
||||||
if _, err := core.ApplyMessage(evm, msg, nil); err != nil {
|
if _, err := core.ApplyMessage(evm, msg, nil); err != nil {
|
||||||
failed = err
|
failed = err
|
||||||
break txloop
|
break txloop
|
||||||
|
|
@ -793,7 +793,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
// Execute the transaction and flush any traces to disk
|
// Execute the transaction and flush any traces to disk
|
||||||
statedb.SetTxContext(tx.Hash(), i)
|
statedb.SetTxContext(tx.Hash(), i, uint32(i+1))
|
||||||
if tracer.OnTxStart != nil {
|
if tracer.OnTxStart != nil {
|
||||||
tracer.OnTxStart(evm.GetVMContext(), tx, msg.From)
|
tracer.OnTxStart(evm.GetVMContext(), tx, msg.From)
|
||||||
}
|
}
|
||||||
|
|
@ -1016,7 +1016,7 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Call Prepare to clear out the statedb access list
|
// Call Prepare to clear out the statedb access list
|
||||||
statedb.SetTxContext(txctx.TxHash, txctx.TxIndex)
|
statedb.SetTxContext(txctx.TxHash, txctx.TxIndex, uint32(txctx.TxIndex+1))
|
||||||
|
|
||||||
_, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm)
|
_, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -340,7 +340,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
|
||||||
tracer.reset(txHash, uint(i))
|
tracer.reset(txHash, uint(i))
|
||||||
|
|
||||||
// EoA check is always skipped, even in validation mode.
|
// EoA check is always skipped, even in validation mode.
|
||||||
sim.state.SetTxContext(txHash, i)
|
sim.state.SetTxContext(txHash, i, uint32(i+1))
|
||||||
msg := call.ToMessage(header.BaseFee, !sim.validate)
|
msg := call.ToMessage(header.BaseFee, !sim.validate)
|
||||||
result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp)
|
result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -390,8 +390,8 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
|
||||||
header.BlobGasUsed = &blobGasUsed
|
header.BlobGasUsed = &blobGasUsed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run post-execution system calls
|
// Process EIP-7685 requests
|
||||||
requests, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm)
|
requests, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams,
|
||||||
// otherwise, fill the block with the current transactions from the txpool
|
// otherwise, fill the block with the current transactions from the txpool
|
||||||
if genParam.forceOverrides && len(genParam.overrideTxs) > 0 {
|
if genParam.forceOverrides && len(genParam.overrideTxs) > 0 {
|
||||||
for _, tx := range genParam.overrideTxs {
|
for _, tx := range genParam.overrideTxs {
|
||||||
work.state.SetTxContext(tx.Hash(), work.tcount)
|
work.state.SetTxContext(tx.Hash(), work.tcount, uint32(work.tcount+1))
|
||||||
if err := miner.commitTransaction(ctx, work, tx); err != nil {
|
if err := miner.commitTransaction(ctx, work, tx); err != nil {
|
||||||
// all passed transactions HAVE to be valid at this point
|
// all passed transactions HAVE to be valid at this point
|
||||||
return &newPayloadResult{err: err}
|
return &newPayloadResult{err: err}
|
||||||
|
|
@ -208,7 +208,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect consensus-layer requests if Prague is enabled.
|
// Collect consensus-layer requests if Prague is enabled.
|
||||||
requests, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm)
|
requests, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &newPayloadResult{err: err}
|
return &newPayloadResult{err: err}
|
||||||
}
|
}
|
||||||
|
|
@ -502,7 +502,7 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Start executing the transaction
|
// Start executing the transaction
|
||||||
env.state.SetTxContext(tx.Hash(), env.tcount)
|
env.state.SetTxContext(tx.Hash(), env.tcount, uint32(env.tcount+1))
|
||||||
|
|
||||||
err := miner.commitTransaction(ctx, env, tx)
|
err := miner.commitTransaction(ctx, env, tx)
|
||||||
switch {
|
switch {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue