core/vm: align with current spec (claude)

All these should be removed once we updated the spec
This commit is contained in:
MariusVanDerWijden 2026-06-14 13:50:22 +02:00
parent ff799721ff
commit ed7c83a28c
No known key found for this signature in database
4 changed files with 100 additions and 22 deletions

View file

@ -352,6 +352,7 @@ type stateTransition struct {
gp *GasPool
msg *Message
gasRemaining vm.GasBudget
initReservoir uint64 // initial state-gas reservoir carved out of GasLimit (EIP-8037)
state vm.StateDB
evm *vm.EVM
}
@ -462,7 +463,8 @@ func (st *stateTransition) buyGas() error {
if isAmsterdam {
limit = min(st.msg.GasLimit, params.MaxTxGas)
}
st.gasRemaining = vm.NewGasBudget(limit, st.msg.GasLimit-limit)
st.initReservoir = st.msg.GasLimit - limit
st.gasRemaining = vm.NewGasBudget(limit, st.initReservoir)
if st.evm.Config.Tracer.HasGasHook() {
st.evm.Config.Tracer.EmitGasChange(tracing.Gas{}, st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance)
@ -683,12 +685,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// Execute the transaction's creation.
ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value)
st.gasRemaining.Absorb(result)
// If the contract creation failed, refund the account-creation state
// gas pre-charged in IntrinsicGas.
if rules.IsAmsterdam && vmerr != nil {
st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
}
} else {
// Increment the nonce for the next transaction.
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)
@ -709,6 +705,29 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
st.gasRemaining.Absorb(result)
}
// EIP-8037 (fork.py:1086): on any transaction error, the state gas
// consumed during *execution* is discarded — those state changes are
// reverted, so the charge is restored to the reservoir and not counted
// toward block_state_gas_used. The intrinsic state gas (CREATE new-account
// and EIP-7702 authorization charges) is tracked separately by the spec and
// is NOT discarded here; the CREATE new-account portion is refunded above
// via its dedicated RefundState. The frame-level Exit forms already refund
// state gas on a reverting/halting sub-call, but a top-level frame that
// ends in a code-deposit halt (or any other tx-level vmerr) can leave
// accumulated execution UsedStateGas that must be discarded here.
if rules.IsAmsterdam && vmerr != nil {
executionStateGas := st.gasRemaining.UsedStateGas - int64(cost.StateGas)
if executionStateGas > 0 {
st.gasRemaining.RefundState(uint64(executionStateGas))
}
// Additionally, a failed CREATE transaction refunds the intrinsic
// account-creation state gas pre-charged in IntrinsicGas (fork.py:1093:
// when tx.to is Bytes0 the NEW_ACCOUNT charge is added to state_refund).
if contractCreation {
st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
}
}
// Settle down the gas usage and refund the ETH back if any remaining
gasUsed, peakUsed, err := st.settleGas(rules, floorDataGas)
if err != nil {
@ -781,6 +800,7 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g
if st.gasRemaining.UsedStateGas < 0 {
return 0, 0, fmt.Errorf("negative topmost frame state gas usage, %d", st.gasRemaining.UsedStateGas)
}
txStateGas := uint64(st.gasRemaining.UsedStateGas)
// EIP-8037:

View file

@ -295,7 +295,14 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
}
evm.StateDB.CreateAccount(addr)
}
if evm.chainRules.IsAmsterdam && !value.IsZero() && evm.StateDB.Empty(addr) {
// EIP-8037: a value-bearing CALL to an empty account pays NEW_ACCOUNT state
// gas. For nested calls this is charged on the caller frame by the dynamic
// gas table (gasCallIntrinsic), matching the spec's inline charge_state_gas
// in system.call. Only the top-most call (depth 0) — which is dispatched
// straight to evm.Call without passing through that gas table — needs the
// charge applied here, against the forwarded budget. Charging in both places
// would double-count the new account.
if evm.depth == 0 && evm.chainRules.IsAmsterdam && !value.IsZero() && evm.StateDB.Empty(addr) {
prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
if !ok {
evm.StateDB.RevertToSnapshot(snapshot)
@ -597,14 +604,23 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
contract.SetCallCode(common.Hash{}, code)
contract.IsDeployment = true
ret, err = evm.initNewContract(contract, address)
var depositHalt bool
ret, depositHalt, err = evm.initNewContract(contract, address)
// Special case: ErrCodeStoreOutOfGas pre-Homestead does NOT roll back
// state and gas is preserved (i.e., treated as success).
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot)
exit := contract.Gas.Exit(err)
// EIP-8037: a code-deposit halt (initcode body succeeded, deposit step
// failed) keeps the state gas the body consumed for discard at the tx
// level, rather than refunding the reservoir like a mid-execution halt.
var exit GasBudget
if depositHalt && evm.chainRules.IsAmsterdam {
exit = contract.Gas.ExitCodeDepositHalt()
} else {
exit = contract.Gas.Exit(err)
}
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution)
@ -619,54 +635,60 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
// initNewContract runs a new contract's creation code, performs checks on the
// resulting code that is to be deployed, and consumes necessary gas.
func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]byte, error) {
ret, err := evm.Run(contract, nil, false)
//
// The returned depositHalt flag is true when the initcode body itself ran to
// completion successfully but a subsequent code-deposit check failed (oversized
// code, 0xEF prefix, or insufficient gas for the hash/deposit charge). Under
// EIP-8037 this halt is metered differently from a mid-execution halt: the
// state gas consumed by the (successful) body is kept rather than refunded.
func (evm *EVM) initNewContract(contract *Contract, address common.Address) (ret []byte, depositHalt bool, err error) {
ret, err = evm.Run(contract, nil, false)
if err != nil {
return ret, err
return ret, false, err
}
// Check prefix before gas calculation.
// Reject code starting with 0xEF if EIP-3541 is enabled.
if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon {
return ret, ErrInvalidCode
return ret, true, ErrInvalidCode
}
if evm.chainRules.IsEIP4762 {
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas)
contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if len(ret) > 0 && (consumed < wanted) {
return ret, ErrCodeStoreOutOfGas
return ret, true, ErrCodeStoreOutOfGas
}
if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
return ret, err
return ret, true, err
}
} else if evm.chainRules.IsAmsterdam {
// Check max code size BEFORE charging gas so over-max code
// does not consume state gas (which would inflate tx_state).
if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
return ret, err
return ret, true, err
}
// Charge regular gas (hash cost) before state gas.
regularCost := toWordSize(uint64(len(ret))) * params.Keccak256WordGas
if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
return ret, ErrCodeStoreOutOfGas
return ret, true, ErrCodeStoreOutOfGas
}
// Charge state gas (code-deposit) afterwards.
stateCost := uint64(len(ret)) * evm.Context.CostPerStateByte
if !contract.chargeState(stateCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
return ret, ErrCodeStoreOutOfGas
return ret, true, ErrCodeStoreOutOfGas
}
} else {
createDataCost := uint64(len(ret)) * params.CreateDataGas
if !contract.chargeRegular(createDataCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
return ret, ErrCodeStoreOutOfGas
return ret, true, ErrCodeStoreOutOfGas
}
if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
return ret, err
return ret, true, err
}
}
if len(ret) > 0 {
evm.StateDB.SetCode(address, ret, tracing.CodeChangeContractCreation)
}
return ret, nil
return ret, false, nil
}
// Create creates a new contract using code as deployment code.

View file

@ -21,6 +21,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/params"
)
@ -502,6 +503,21 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
return 0, ErrOutOfGas
}
// Stateful check
if evm.chainRules.IsAmsterdam {
// EIP-8037: the cost of creating a new account via a value-bearing
// CALL is metered as state gas (NEW_ACCOUNT * CostPerStateByte),
// not the legacy regular CallNewAccountGas. Charge it directly here
// so it drains the state reservoir (spilling into regular gas only
// when the reservoir is exhausted), mirroring the spec's inline
// charge_state_gas call in system.call.
if transfersValue && evm.StateDB.Empty(address) {
stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte
if !contract.chargeState(stateGas, evm.Config.Tracer, tracing.GasChangeAccountCreation) {
return 0, ErrOutOfGas
}
}
return gas, nil
}
var stateGas uint64
if evm.chainRules.IsEIP158 {
if transfersValue && evm.StateDB.Empty(address) {

View file

@ -247,6 +247,26 @@ func (g GasBudget) ExitHalt() GasBudget {
}
}
// ExitCodeDepositHalt produces the leftover for a CREATE/CREATE2 frame whose
// initcode body ran to completion but then failed during the code-deposit step
// (oversized code, 0xEF prefix, or insufficient gas for the hash/deposit
// charge). Per the spec's process_create_message exception handler, this path
// differs from a mid-execution ExceptionalHalt: only the remaining regular gas
// is burned, while the state gas the (successful) body consumed is KEPT in
// UsedStateGas rather than refunded to the reservoir. The state changes are
// reverted by the caller, but the state-gas accounting is propagated upward so
// the top-level tx settlement can discard it as the state dimension (rather
// than folding it back into the combined balance, which would wrongly deflate
// the regular dimension).
func (g GasBudget) ExitCodeDepositHalt() GasBudget {
return GasBudget{
RegularGas: 0,
StateGas: g.StateGas,
UsedRegularGas: g.UsedRegularGas + g.RegularGas,
UsedStateGas: g.UsedStateGas,
}
}
// Exit dispatches on err to the appropriate exit-form constructor
// for the post-evm.Run path:
//