From ed7c83a28cd1b31a8acc9220007119ab5bd4467d Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Sun, 14 Jun 2026 13:50:22 +0200 Subject: [PATCH] core/vm: align with current spec (claude) All these should be removed once we updated the spec --- core/state_transition.go | 34 ++++++++++++++++++++------ core/vm/evm.go | 52 ++++++++++++++++++++++++++++------------ core/vm/gas_table.go | 16 +++++++++++++ core/vm/gascosts.go | 20 ++++++++++++++++ 4 files changed, 100 insertions(+), 22 deletions(-) diff --git a/core/state_transition.go b/core/state_transition.go index 8dae706c8b..0b0b34d59f 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -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: diff --git a/core/vm/evm.go b/core/vm/evm.go index 7133047d6f..74b51716e0 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -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. diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 5cf64a9844..2e8128e108 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -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) { diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 98130b9ce5..f4469804f0 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -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: //