core/state: charge account creation to parent

This commit is contained in:
MariusVanDerWijden 2026-04-28 21:06:05 +02:00
parent c1b1faed8b
commit 87641c7265
6 changed files with 73 additions and 43 deletions

View file

@ -216,14 +216,23 @@ func (j *journal) length() int {
return len(j.entries) return len(j.entries)
} }
// stateChangedBytes computes the state bytes created by the current (topmost) // stateChangedBytes computes the state bytes created by the call frame
// call frame, walking only entries that belong directly to this frame and // identified by revid, walking only entries that belong directly to this
// skipping over closed child frame ranges. // frame and skipping over closed child frame ranges. The result is cached
func (j *journal) stateChangedBytes(stateObjects map[common.Address]*stateObject) int64 { // in stateBytesCharged so that parent frames can look it up.
if len(j.validRevisions) == 0 { //
return 0 // When excludeSubcalls is true, cached subcall costs are not added to the
// total. This is useful when subcalls have already been charged to their
// own gas budgets and shouldn't bubble up to the ancestor frames.
func (j *journal) stateChangedBytes(revid int, stateObjects map[common.Address]*stateObject, excludeSubcalls bool) int64 {
// Find the revision by ID.
idx := sort.Search(len(j.validRevisions), func(i int) bool {
return j.validRevisions[i].id >= revid
})
if idx == len(j.validRevisions) || j.validRevisions[idx].id != revid {
panic(fmt.Errorf("revision id %v not found for stateChangedBytes", revid))
} }
rev := j.validRevisions[len(j.validRevisions)-1] rev := j.validRevisions[idx]
type slotKey struct { type slotKey struct {
addr common.Address addr common.Address
@ -253,17 +262,19 @@ func (j *journal) stateChangedBytes(stateObjects map[common.Address]*stateObject
} }
} }
} }
idx := rev.journalIndex pos := rev.journalIndex
for _, child := range rev.closedChildren { for _, child := range rev.closedChildren {
for ; idx < child.start; idx++ { for ; pos < child.start; pos++ {
visit(j.entries[idx]) visit(j.entries[pos])
} }
// Add the cached cost for this subcall. if !excludeSubcalls {
subcallBytes += j.stateBytesCharged[child.start] // Add the cached cost for this subcall.
idx = child.end subcallBytes += j.stateBytesCharged[child.start]
}
pos = child.end
} }
for ; idx < len(j.entries); idx++ { for ; pos < len(j.entries); pos++ {
visit(j.entries[idx]) visit(j.entries[pos])
} }
var totalBytes int64 var totalBytes int64

View file

@ -772,10 +772,11 @@ const (
CostPerSlot = 32 CostPerSlot = 32
) )
// StateChangedBytes computes the state bytes created by the current (topmost) // StateChangedBytes computes the state bytes created by the call frame
// call frame, excluding entries from closed child frames. // identified by revid, excluding entries from closed child frames. When
func (s *StateDB) StateChangedBytes() int64 { // excludeSubcalls is true, cached subcall costs are not added to the total.
return s.journal.stateChangedBytes(s.stateObjects) func (s *StateDB) StateChangedBytes(revid int, excludeSubcalls bool) int64 {
return s.journal.stateChangedBytes(revid, s.stateObjects, excludeSubcalls)
} }
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() int64 { func (s *hookedStateDB) StateChangedBytes(revid int, excludeSubcalls bool) int64 {
return s.inner.StateChangedBytes() return s.inner.StateChangedBytes(revid, excludeSubcalls)
} }

View file

@ -600,7 +600,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
) )
// Take a snapshot for gas calculation // Take a snapshot for gas calculation
st.state.Snapshot() outerSnapshot := st.state.Snapshot()
var execGasUsed vm.GasUsed var execGasUsed vm.GasUsed
if contractCreation { if contractCreation {
@ -633,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() outerBytes := st.state.StateChangedBytes(outerSnapshot, false)
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

@ -255,7 +255,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance return nil, gas, ErrInsufficientBalance
} }
snapshot := evm.StateDB.Snapshot() snapshot1 := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr) p, isPrecompile := evm.precompile(addr)
if !evm.StateDB.Exist(addr) { if !evm.StateDB.Exist(addr) {
if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) { if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) {
@ -268,7 +268,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
// Thus, only pay for the creation of the code hash leaf here. // Thus, only pay for the creation of the code hash leaf here.
wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false)
if _, ok := gas.Charge(GasCosts{RegularGas: wgas}); !ok { if _, ok := gas.Charge(GasCosts{RegularGas: wgas}); !ok {
evm.StateDB.RevertToSnapshot(snapshot) evm.StateDB.RevertToSnapshot(snapshot1)
gas.Exhaust() gas.Exhaust()
return nil, gas, ErrOutOfGas return nil, gas, ErrOutOfGas
} }
@ -276,7 +276,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() {
// Calling a non-existing account, don't do anything. // Calling a non-existing account, don't do anything.
evm.StateDB.CloseSnapshot(snapshot) evm.StateDB.CloseSnapshot(snapshot1)
return nil, gas, nil return nil, gas, nil
} }
evm.StateDB.CreateAccount(addr) evm.StateDB.CreateAccount(addr)
@ -288,6 +288,9 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
evm.Context.Transfer(evm.StateDB, caller, addr, value, &evm.chainRules) evm.Context.Transfer(evm.StateDB, caller, addr, value, &evm.chainRules)
} }
// Second snapshot: callee execution frame.
snapshot2 := evm.StateDB.Snapshot()
if isPrecompile { if isPrecompile {
ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else { } else {
@ -308,28 +311,31 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
// above we revert to the snapshot and consume any gas remaining. Additionally, // above we revert to the snapshot and consume any gas remaining. Additionally,
// when we're in homestead this also counts for code storage gas errors. // when we're in homestead this also counts for code storage gas errors.
if err != nil { if err != nil {
evm.StateDB.RevertToSnapshot(snapshot) evm.StateDB.RevertToSnapshot(snapshot1)
if err != ErrExecutionReverted { if err != ErrExecutionReverted {
if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
} }
gas.Exhaust() gas.Exhaust()
} }
// TODO: consider clearing up unused snapshots:
//} else {
// evm.StateDB.DiscardSnapshot(snapshot)
} else { } else {
evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam { if evm.chainRules.IsAmsterdam {
// Charge state costs // Charge callee's state changes to the callee's gas.
bytesCharged := evm.StateDB.StateChangedBytes() bytesCharged := evm.StateDB.StateChangedBytes(snapshot2, false)
stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)}
if !gas.CanAfford(stateGasCost) { if !gas.CanAfford(stateGasCost) {
evm.StateDB.RevertToSnapshot(snapshot1)
gas.Exhaust() gas.Exhaust()
return ret, gas, ErrOutOfGas return ret, gas, ErrOutOfGas
} }
gas.Charge(stateGasCost) gas.Charge(stateGasCost)
} }
evm.StateDB.CloseSnapshot(snapshot2)
if evm.chainRules.IsAmsterdam {
// Cache parents costs (excluding subcalls)
evm.StateDB.StateChangedBytes(snapshot1, true)
}
evm.StateDB.CloseSnapshot(snapshot1)
} }
return ret, gas, err return ret, gas, err
} }
@ -382,9 +388,8 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
gas.Exhaust() gas.Exhaust()
} }
} else { } else {
evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam { if evm.chainRules.IsAmsterdam {
bytesCharged := evm.StateDB.StateChangedBytes() bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false)
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()
@ -392,6 +397,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
} }
gas.Charge(stateGasCost) gas.Charge(stateGasCost)
} }
evm.StateDB.CloseSnapshot(snapshot)
} }
return ret, gas, err return ret, gas, err
} }
@ -437,9 +443,8 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
gas.Exhaust() gas.Exhaust()
} }
} else { } else {
evm.StateDB.CloseSnapshot(snapshot)
if evm.chainRules.IsAmsterdam { if evm.chainRules.IsAmsterdam {
bytesCharged := evm.StateDB.StateChangedBytes() bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false)
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()
@ -447,6 +452,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
} }
gas.Charge(stateGasCost) gas.Charge(stateGasCost)
} }
evm.StateDB.CloseSnapshot(snapshot)
} }
return ret, gas, err return ret, gas, err
@ -567,7 +573,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
// Create a new account on the state only if the object was not present. // Create a new account on the state only if the object was not present.
// It might be possible the contract code is deployed to a pre-existent // It might be possible the contract code is deployed to a pre-existent
// account with non-zero balance. // account with non-zero balance.
snapshot := evm.StateDB.Snapshot() snapshot1 := evm.StateDB.Snapshot()
if !evm.StateDB.Exist(address) { if !evm.StateDB.Exist(address) {
evm.StateDB.CreateAccount(address) evm.StateDB.CreateAccount(address)
} }
@ -594,6 +600,9 @@ 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)
// Second snapshot: initcode execution frame.
snapshot2 := 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)
@ -605,22 +614,29 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
ret, err = evm.initNewContract(contract, address) ret, err = evm.initNewContract(contract, address)
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot) // Revert to snapshot1 to undo both account creation and initcode changes.
evm.StateDB.RevertToSnapshot(snapshot1)
if err != ErrExecutionReverted { if err != ErrExecutionReverted {
contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution)
} }
} else { } else {
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() bytesCharged := evm.StateDB.StateChangedBytes(snapshot2, false)
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) {
evm.StateDB.RevertToSnapshot(snapshot1)
contract.Gas.Exhaust() contract.Gas.Exhaust()
return ret, address, contract.Gas, ErrOutOfGas return ret, address, contract.Gas, ErrOutOfGas
} }
contract.Gas.Charge(stateGasCost) contract.Gas.Charge(stateGasCost)
} }
evm.StateDB.CloseSnapshot(snapshot2)
if evm.chainRules.IsAmsterdam {
// Cache snapshot1's state bytes (exclude subcalls)
evm.StateDB.StateChangedBytes(snapshot1, true)
}
evm.StateDB.CloseSnapshot(snapshot1)
} }
return ret, address, contract.Gas, err return ret, address, contract.Gas, err
} }

View file

@ -107,6 +107,8 @@ type StateDB interface {
Finalise(bool) *bal.StateAccessList Finalise(bool) *bal.StateAccessList
// StateChangedBytes returns the number of state bytes created by the // StateChangedBytes returns the number of state bytes created by the
// current call frame. Used by EIP-8037 for state gas metering. // call frame identified by the given snapshot ID. When excludeSubcalls
StateChangedBytes() int64 // is true, cached subcall costs are not added to the total. Used by
// EIP-8037 for state gas metering.
StateChangedBytes(revid int, excludeSubcalls bool) int64
} }