core/state: use journaling approach

This commit is contained in:
MariusVanDerWijden 2026-04-28 19:29:17 +02:00
parent 46be549b00
commit c1b1faed8b
6 changed files with 47 additions and 60 deletions

View file

@ -216,33 +216,15 @@ func (j *journal) length() int {
return len(j.entries)
}
// stateChangedBytes computes the state bytes created by the call frame
// identified by snapshotId. Since subcalls always compute their results
// before the parent (innermost-first), this only scans journal entries
// between this snapshot and the next one — the frame's own entries.
// Subcall results are summed from the cache and subtracted.
//
// The result is cached in stateBytesCharged[snapshotId] so the parent
// frame can look it up instead of re-scanning.
func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Address]*stateObject) int64 {
// TODO (MariusVanDerWijden): this is a bit slop-py needs to be cleaned up
// Resolve snapshot index.
idx := sort.Search(len(j.validRevisions), func(i int) bool {
return j.validRevisions[i].id >= snapshotId
})
if idx == len(j.validRevisions) || j.validRevisions[idx].id != snapshotId {
panic(fmt.Errorf("snapshot id %v not found for stateChangedBytes", snapshotId))
// stateChangedBytes computes the state bytes created by the current (topmost)
// call frame, walking only entries that belong directly to this frame and
// skipping over closed child frame ranges.
func (j *journal) stateChangedBytes(stateObjects map[common.Address]*stateObject) int64 {
if len(j.validRevisions) == 0 {
return 0
}
start := j.validRevisions[idx].journalIndex
rev := j.validRevisions[len(j.validRevisions)-1]
// Our range is [start, end) where end is the next revision's start,
// or the end of the journal if we're the last revision.
end := len(j.entries)
if idx+1 < len(j.validRevisions) {
end = j.validRevisions[idx+1].journalIndex
}
// Walk only our own entries.
type slotKey struct {
addr common.Address
key common.Hash
@ -255,8 +237,11 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr
created := make(map[common.Address]bool)
codeChanged := make(map[common.Address]bool)
for i := start; i < end; i++ {
switch e := j.entries[i].(type) {
// Walk only this frame's own entries, skipping closed child ranges.
// Add cached subcall costs from closedChildren.
var subcallBytes int64
visit := func(e journalEntry) {
switch e := e.(type) {
case createContractChange:
created[e.account] = true
case codeChange:
@ -268,6 +253,18 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr
}
}
}
idx := rev.journalIndex
for _, child := range rev.closedChildren {
for ; idx < child.start; idx++ {
visit(j.entries[idx])
}
// Add the cached cost for this subcall.
subcallBytes += j.stateBytesCharged[child.start]
idx = child.end
}
for ; idx < len(j.entries); idx++ {
visit(j.entries[idx])
}
var totalBytes int64
for range created {
@ -296,7 +293,7 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr
// cleared in earlier frame, re-set here — no charge.
// - X → Y (non-zero to non-zero): no charge.
// - zero → zero: no change.
// - !prevZero && curZero && !origZero: pre-exising slot was
// - !prevZero && curZero && !origZero: pre-existing slot was
// cleared now, don't refund to not enable gas tokens.
}
for addr := range codeChanged {
@ -306,8 +303,11 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr
}
}
// Cache our result so the parent can look it up.
j.stateBytesCharged[snapshotId] = totalBytes
// Add subcall costs to get the total for this frame (own + children).
totalBytes += subcallBytes
// Cache so the parent can look up this frame's total cost.
j.stateBytesCharged[rev.journalIndex] = totalBytes
return totalBytes
}

View file

@ -772,11 +772,10 @@ const (
CostPerSlot = 32
)
// StateChangedBytes computes the state bytes created since the given snapshot,
// excluding bytes already charged by subcalls. See journal.stateChangedBytes
// for the detailed accounting.
func (s *StateDB) StateChangedBytes(snapshotId int) int64 {
return s.journal.stateChangedBytes(snapshotId, s.stateObjects)
// StateChangedBytes computes the state bytes created by the current (topmost)
// call frame, excluding entries from closed child frames.
func (s *StateDB) StateChangedBytes() int64 {
return s.journal.stateChangedBytes(s.stateObjects)
}
type removedAccountWithBalance struct {

View file

@ -293,6 +293,6 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
return s.inner.Finalise(deleteEmptyObjects)
}
func (s *hookedStateDB) StateChangedBytes(snapshotId int) int64 {
return s.inner.StateChangedBytes(snapshotId)
func (s *hookedStateDB) StateChangedBytes() int64 {
return s.inner.StateChangedBytes()
}

View file

@ -598,10 +598,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
ret []byte
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
)
// EIP-8037: Take a snapshot for the outer call frame so we can compute
// state gas for state changes made at the transaction level (nonce,
// value transfer, authorizations, and contract creation overhead).
outerSnapshot := st.state.Snapshot()
// Take a snapshot for gas calculation
st.state.Snapshot()
var execGasUsed vm.GasUsed
if contractCreation {
@ -634,7 +633,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// EIP-8037: charge state gas for the outer call frame's own state changes.
if rules.IsAmsterdam {
if vmerr == nil {
outerBytes := st.state.StateChangedBytes(outerSnapshot)
outerBytes := st.state.StateChangedBytes()
st.gasRemaining.Charge(vm.GasCosts{StateGas: outerBytes * int64(st.evm.Context.CostPerStateByte)})
} else {
if execGasUsed.StateGas > 0 {

View file

@ -281,11 +281,6 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
}
evm.StateDB.CreateAccount(addr)
}
if evm.chainRules.IsAmsterdam {
// Compute state changed bytes for account creation.
evm.StateDB.StateChangedBytes(snapshot)
}
innerSnapshot := evm.StateDB.Snapshot()
// Perform the value transfer only in non-syscall mode.
// Calling this is required even for zero-value transfers,
// to ensure the state clearing mechanism is applied.
@ -327,7 +322,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam {
// Charge state costs
bytesCharged := evm.StateDB.StateChangedBytes(innerSnapshot)
bytesCharged := evm.StateDB.StateChangedBytes()
stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)}
if !gas.CanAfford(stateGasCost) {
gas.Exhaust()
@ -389,7 +384,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
} else {
evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam {
bytesCharged := evm.StateDB.StateChangedBytes(snapshot)
bytesCharged := evm.StateDB.StateChangedBytes()
stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)}
if !gas.CanAfford(stateGasCost) {
gas.Exhaust()
@ -444,7 +439,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
} else {
evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam {
bytesCharged := evm.StateDB.StateChangedBytes(snapshot)
bytesCharged := evm.StateDB.StateChangedBytes()
stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)}
if !gas.CanAfford(stateGasCost) {
gas.Exhaust()
@ -599,12 +594,6 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
}
evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules)
if evm.chainRules.IsAmsterdam {
// Compute the state changed for the contract init.
evm.StateDB.StateChangedBytes(snapshot)
}
initSnapshot := evm.StateDB.Snapshot()
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
contract := NewContract(caller, address, value, gas, evm.jumpDests)
@ -624,7 +613,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam {
// Charge initcode's state changes to the created contract's gas.
bytesCharged := evm.StateDB.StateChangedBytes(initSnapshot)
bytesCharged := evm.StateDB.StateChangedBytes()
stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)}
if !contract.Gas.CanAfford(stateGasCost) {
contract.Gas.Exhaust()

View file

@ -106,7 +106,7 @@ type StateDB interface {
// Finalise must be invoked at the end of a transaction
Finalise(bool) *bal.StateAccessList
// StateChangedBytes returns the number of state bytes created since the
// given snapshot. Used by EIP-8037 for state gas metering.
StateChangedBytes(snapshotId int) int64
// StateChangedBytes returns the number of state bytes created by the
// current call frame. Used by EIP-8037 for state gas metering.
StateChangedBytes() int64
}