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) return len(j.entries)
} }
// stateChangedBytes computes the state bytes created by the call frame // stateChangedBytes computes the state bytes created by the current (topmost)
// identified by snapshotId. Since subcalls always compute their results // call frame, walking only entries that belong directly to this frame and
// before the parent (innermost-first), this only scans journal entries // skipping over closed child frame ranges.
// between this snapshot and the next one — the frame's own entries. func (j *journal) stateChangedBytes(stateObjects map[common.Address]*stateObject) int64 {
// Subcall results are summed from the cache and subtracted. if len(j.validRevisions) == 0 {
// return 0
// 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))
} }
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 { type slotKey struct {
addr common.Address addr common.Address
key common.Hash key common.Hash
@ -255,8 +237,11 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr
created := make(map[common.Address]bool) created := make(map[common.Address]bool)
codeChanged := make(map[common.Address]bool) codeChanged := make(map[common.Address]bool)
for i := start; i < end; i++ { // Walk only this frame's own entries, skipping closed child ranges.
switch e := j.entries[i].(type) { // Add cached subcall costs from closedChildren.
var subcallBytes int64
visit := func(e journalEntry) {
switch e := e.(type) {
case createContractChange: case createContractChange:
created[e.account] = true created[e.account] = true
case codeChange: 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 var totalBytes int64
for range created { 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. // cleared in earlier frame, re-set here — no charge.
// - X → Y (non-zero to non-zero): no charge. // - X → Y (non-zero to non-zero): no charge.
// - zero → zero: no change. // - 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. // cleared now, don't refund to not enable gas tokens.
} }
for addr := range codeChanged { 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. // Add subcall costs to get the total for this frame (own + children).
j.stateBytesCharged[snapshotId] = totalBytes totalBytes += subcallBytes
// Cache so the parent can look up this frame's total cost.
j.stateBytesCharged[rev.journalIndex] = totalBytes
return totalBytes return totalBytes
} }

View file

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

View file

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

View file

@ -598,10 +598,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
ret []byte ret []byte
vmerr error // vm errors do not effect consensus and are therefore not assigned to err 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, // Take a snapshot for gas calculation
// value transfer, authorizations, and contract creation overhead). st.state.Snapshot()
outerSnapshot := st.state.Snapshot()
var execGasUsed vm.GasUsed var execGasUsed vm.GasUsed
if contractCreation { 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. // EIP-8037: charge state gas for the outer call frame's own state changes.
if rules.IsAmsterdam { if rules.IsAmsterdam {
if vmerr == nil { if vmerr == nil {
outerBytes := st.state.StateChangedBytes(outerSnapshot) outerBytes := st.state.StateChangedBytes()
st.gasRemaining.Charge(vm.GasCosts{StateGas: outerBytes * int64(st.evm.Context.CostPerStateByte)}) st.gasRemaining.Charge(vm.GasCosts{StateGas: outerBytes * int64(st.evm.Context.CostPerStateByte)})
} else { } else {
if execGasUsed.StateGas > 0 { 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) 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. // Perform the value transfer only in non-syscall mode.
// Calling this is required even for zero-value transfers, // Calling this is required even for zero-value transfers,
// to ensure the state clearing mechanism is applied. // 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) evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam { if evm.chainRules.IsAmsterdam {
// Charge state costs // Charge state costs
bytesCharged := evm.StateDB.StateChangedBytes(innerSnapshot) bytesCharged := evm.StateDB.StateChangedBytes()
stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)}
if !gas.CanAfford(stateGasCost) { if !gas.CanAfford(stateGasCost) {
gas.Exhaust() gas.Exhaust()
@ -389,7 +384,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
} else { } else {
evm.StateDB.CloseSnapshot(snapshot) evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam { if evm.chainRules.IsAmsterdam {
bytesCharged := evm.StateDB.StateChangedBytes(snapshot) bytesCharged := evm.StateDB.StateChangedBytes()
stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)}
if !gas.CanAfford(stateGasCost) { if !gas.CanAfford(stateGasCost) {
gas.Exhaust() gas.Exhaust()
@ -444,7 +439,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
} else { } else {
evm.StateDB.CloseSnapshot(snapshot) evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam { if evm.chainRules.IsAmsterdam {
bytesCharged := evm.StateDB.StateChangedBytes(snapshot) bytesCharged := evm.StateDB.StateChangedBytes()
stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)}
if !gas.CanAfford(stateGasCost) { if !gas.CanAfford(stateGasCost) {
gas.Exhaust() 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) 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. // 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. // The contract is a scoped environment for this execution context only.
contract := NewContract(caller, address, value, gas, evm.jumpDests) 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) evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam { if evm.chainRules.IsAmsterdam {
// Charge initcode's state changes to the created contract's gas. // 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)} stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)}
if !contract.Gas.CanAfford(stateGasCost) { if !contract.Gas.CanAfford(stateGasCost) {
contract.Gas.Exhaust() contract.Gas.Exhaust()

View file

@ -106,7 +106,7 @@ type StateDB interface {
// 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.StateAccessList
// StateChangedBytes returns the number of state bytes created since the // StateChangedBytes returns the number of state bytes created by the
// given snapshot. Used by EIP-8037 for state gas metering. // current call frame. Used by EIP-8037 for state gas metering.
StateChangedBytes(snapshotId int) int64 StateChangedBytes() int64
} }