From 87641c7265284ecf69c75e4ddd4400b855df8e74 Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Tue, 28 Apr 2026 21:06:05 +0200 Subject: [PATCH] core/state: charge account creation to parent --- core/state/journal.go | 41 +++++++++++++++++----------- core/state/statedb.go | 9 ++++--- core/state/statedb_hooked.go | 4 +-- core/state_transition.go | 4 +-- core/vm/evm.go | 52 +++++++++++++++++++++++------------- core/vm/interface.go | 6 +++-- 6 files changed, 73 insertions(+), 43 deletions(-) diff --git a/core/state/journal.go b/core/state/journal.go index b189d54427..903deacc79 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -216,14 +216,23 @@ func (j *journal) length() int { return len(j.entries) } -// 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 +// stateChangedBytes computes the state bytes created by the call frame +// identified by revid, walking only entries that belong directly to this +// frame and skipping over closed child frame ranges. The result is cached +// in stateBytesCharged so that parent frames can look it up. +// +// 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 { 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 ; idx < child.start; idx++ { - visit(j.entries[idx]) + for ; pos < child.start; pos++ { + visit(j.entries[pos]) } - // Add the cached cost for this subcall. - subcallBytes += j.stateBytesCharged[child.start] - idx = child.end + if !excludeSubcalls { + // Add the cached cost for this subcall. + subcallBytes += j.stateBytesCharged[child.start] + } + pos = child.end } - for ; idx < len(j.entries); idx++ { - visit(j.entries[idx]) + for ; pos < len(j.entries); pos++ { + visit(j.entries[pos]) } var totalBytes int64 diff --git a/core/state/statedb.go b/core/state/statedb.go index 5624c0c686..56b083e17c 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -772,10 +772,11 @@ const ( CostPerSlot = 32 ) -// 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) +// StateChangedBytes computes the state bytes created by the call frame +// identified by revid, excluding entries from closed child frames. When +// excludeSubcalls is true, cached subcall costs are not added to the total. +func (s *StateDB) StateChangedBytes(revid int, excludeSubcalls bool) int64 { + return s.journal.stateChangedBytes(revid, s.stateObjects, excludeSubcalls) } type removedAccountWithBalance struct { diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index b1d05c30a2..db3d692aee 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -293,6 +293,6 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { return s.inner.Finalise(deleteEmptyObjects) } -func (s *hookedStateDB) StateChangedBytes() int64 { - return s.inner.StateChangedBytes() +func (s *hookedStateDB) StateChangedBytes(revid int, excludeSubcalls bool) int64 { + return s.inner.StateChangedBytes(revid, excludeSubcalls) } diff --git a/core/state_transition.go b/core/state_transition.go index 932699acd1..fa2332106f 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -600,7 +600,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ) // Take a snapshot for gas calculation - st.state.Snapshot() + outerSnapshot := st.state.Snapshot() var execGasUsed vm.GasUsed 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. if rules.IsAmsterdam { 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)}) } else { if execGasUsed.StateGas > 0 { diff --git a/core/vm/evm.go b/core/vm/evm.go index cb14a3743b..8a01205ba6 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -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) { return nil, gas, ErrInsufficientBalance } - snapshot := evm.StateDB.Snapshot() + snapshot1 := evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) if !evm.StateDB.Exist(addr) { 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. wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) if _, ok := gas.Charge(GasCosts{RegularGas: wgas}); !ok { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertToSnapshot(snapshot1) gas.Exhaust() 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() { // Calling a non-existing account, don't do anything. - evm.StateDB.CloseSnapshot(snapshot) + evm.StateDB.CloseSnapshot(snapshot1) return nil, gas, nil } 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) } + // Second snapshot: callee execution frame. + snapshot2 := evm.StateDB.Snapshot() + if isPrecompile { ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) } 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, // when we're in homestead this also counts for code storage gas errors. if err != nil { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertToSnapshot(snapshot1) if err != ErrExecutionReverted { if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } - // TODO: consider clearing up unused snapshots: - //} else { - // evm.StateDB.DiscardSnapshot(snapshot) } else { - evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - // Charge state costs - bytesCharged := evm.StateDB.StateChangedBytes() + // Charge callee's state changes to the callee's gas. + bytesCharged := evm.StateDB.StateChangedBytes(snapshot2, false) stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { + evm.StateDB.RevertToSnapshot(snapshot1) gas.Exhaust() return ret, gas, ErrOutOfGas } 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 } @@ -382,9 +388,8 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt gas.Exhaust() } } else { - evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes() + bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false) stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -392,6 +397,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } gas.Charge(stateGasCost) } + evm.StateDB.CloseSnapshot(snapshot) } return ret, gas, err } @@ -437,9 +443,8 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, gas.Exhaust() } } else { - evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes() + bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false) stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -447,6 +452,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } gas.Charge(stateGasCost) } + evm.StateDB.CloseSnapshot(snapshot) } 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. // It might be possible the contract code is deployed to a pre-existent // account with non-zero balance. - snapshot := evm.StateDB.Snapshot() + snapshot1 := evm.StateDB.Snapshot() if !evm.StateDB.Exist(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) + // 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. // The contract is a scoped environment for this execution context only. 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) 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 { contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) } } else { - evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { // 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)} if !contract.Gas.CanAfford(stateGasCost) { + evm.StateDB.RevertToSnapshot(snapshot1) contract.Gas.Exhaust() return ret, address, contract.Gas, ErrOutOfGas } 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 } diff --git a/core/vm/interface.go b/core/vm/interface.go index 3c5f4acd6c..3e2f6d115c 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -107,6 +107,8 @@ type StateDB interface { Finalise(bool) *bal.StateAccessList // StateChangedBytes returns the number of state bytes created by the - // current call frame. Used by EIP-8037 for state gas metering. - StateChangedBytes() int64 + // call frame identified by the given snapshot ID. When excludeSubcalls + // 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 }