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 12:59:53 +02:00
parent 6aa174111e
commit ff799721ff
No known key found for this signature in database
8 changed files with 123 additions and 74 deletions

View file

@ -5,10 +5,10 @@
# https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0
a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz
# version:spec-tests-bal v7.2.0
# version:spec-tests-bal v7.3.1
# https://github.com/ethereum/execution-specs/releases
# https://github.com/ethereum/execution-specs/releases/download/tests-bal%40v7.2.0
fc1d9ae174cdd5db789068839999e6f83666cc79f7dac36e973d7616d9a2e2cf fixtures_bal.tar.gz
# https://github.com/ethereum/execution-specs/releases/download/tests-bal%40v7.3.1
3c9bd8799a506a96f74162863efdf5eaa00226e645db6523346fdb7c5ba0bf62 fixtures_bal.tar.gz
# version:golang 1.25.10
# https://go.dev/dl/

View file

@ -674,11 +674,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
ret []byte
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
result vm.GasBudget
// Capture the forwarded regular-gas amount BEFORE ForwardAll consumes
// it, so Absorb can back out state-gas spillover from UsedRegularGas
// per EIP-8037.
forwarded = st.gasRemaining.RegularGas
)
if contractCreation {
// Check whether the init code size has been exceeded.
@ -687,7 +682,7 @@ 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, forwarded)
st.gasRemaining.Absorb(result)
// If the contract creation failed, refund the account-creation state
// gas pre-charged in IntrinsicGas.
@ -711,7 +706,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
// Execute the transaction's call.
ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value)
st.gasRemaining.Absorb(result, forwarded)
st.gasRemaining.Absorb(result)
}
// Settle down the gas usage and refund the ETH back if any remaining
@ -821,7 +816,13 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g
}
if rules.IsAmsterdam {
if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil {
// EIP-7623/7976: the calldata floor applies to the block-level regular
// gas dimension as well, mirroring its effect on the receipt gas. The
// spec accumulates max(tx_regular_gas, calldata_floor) into
// block_gas_used, so the block must never count fewer regular units
// than the floor the sender was charged.
blockRegularGas := max(txRegularGas, floorDataGas)
if err = st.gp.ChargeGasAmsterdam(blockRegularGas, txStateGas, gasUsed); err != nil {
return 0, 0, err
}
} else {

View file

@ -153,9 +153,9 @@ func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.G
}
// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state.
func (c *Contract) refundGas(child GasBudget, forwarded uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) {
func (c *Contract) refundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) {
prior := c.Gas
c.Gas.Absorb(child, forwarded)
c.Gas.Absorb(child)
if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}

View file

@ -596,6 +596,8 @@ func enable7843(jt *JumpTable) {
func enable8037(jt *JumpTable) {
jt[CREATE].constantGas = params.CreateGasAmsterdam
jt[CREATE2].constantGas = params.CreateGasAmsterdam
jt[CREATE].dynamicGas = gasCreateEip8037
jt[CREATE2].dynamicGas = gasCreate2Eip8037
jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037
jt[SSTORE].dynamicGas = gasSStore8037
}

View file

@ -271,7 +271,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, reservoir := evm.StateDB.Snapshot(), gas.StateGas
snapshot := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr)
if !evm.StateDB.Exist(addr) {
if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) {
@ -285,7 +285,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false)
if _, ok := gas.ChargeRegular(wgas); !ok {
evm.StateDB.RevertToSnapshot(snapshot)
return nil, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, gas.ExitHalt(), ErrOutOfGas
}
}
@ -299,7 +299,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
if !ok {
evm.StateDB.RevertToSnapshot(snapshot)
return nil, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, gas.ExitHalt(), ErrOutOfGas
}
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
@ -330,7 +330,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
}
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
@ -372,7 +372,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance
}
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
snapshot := evm.StateDB.Snapshot()
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
@ -387,7 +387,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
}
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
@ -418,7 +418,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
snapshot := evm.StateDB.Snapshot()
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
@ -431,7 +431,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
}
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
@ -465,7 +465,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
// after all empty accounts were deleted, so this is not required. However, if we omit this,
// then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json.
// We could change this, but for now it's left for legacy reasons
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
snapshot := evm.StateDB.Snapshot()
// We do an AddBalance of zero here, just in order to trigger a touch.
// This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium,
@ -483,7 +483,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
}
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
@ -521,14 +521,13 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
}
// Increment the caller's nonce after passing all validations
evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator)
reservoir := gas.StateGas
// Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 {
statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas)
prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas})
if !ok {
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas
}
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
@ -549,7 +548,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if evm.StateDB.GetNonce(address) != 0 ||
(contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code
isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) {
halt := gas.ExitHalt(reservoir)
halt := gas.ExitHalt()
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution)
}
@ -563,18 +562,9 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
snapshot := evm.StateDB.Snapshot()
if !evm.StateDB.Exist(address) {
evm.StateDB.CreateAccount(address)
if evm.chainRules.IsAmsterdam && evm.depth > 0 {
// Only charge state gas if we are not doing a create transaction.
// Prevents double charging with IntrinsicGas.
prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
if !ok {
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
}
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
}
}
// EIP-8037: the account-creation state gas is charged before the
// opcode runs (gasCreateEip8037) for CREATE/CREATE2 opcodes, and in
// IntrinsicGas for creation transactions, so there is no charge here.
}
// CreateContract means that regardless of whether the account previously existed
// in the state trie or not, it _now_ becomes created as a _contract_ account.
@ -589,7 +579,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if evm.chainRules.IsEIP4762 {
consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas)
if consumed < wanted {
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas
}
prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
if evm.Config.Tracer.HasGasHook() {
@ -614,7 +604,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot)
exit := contract.Gas.Exit(err, reservoir)
exit := contract.Gas.Exit(err)
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution)

View file

@ -367,6 +367,62 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
return GasCosts{RegularGas: gas}, nil
}
// gasCreateEip8037 is the CREATE gas calculator for Amsterdam. It charges the
// account-creation cost as state gas (EIP-8037) here, before the opcode runs,
// so the 63/64 gas-forwarding split sees the post-charge regular gas. The
// charge is refunded to the reservoir in opCreate on any failure path that
// does not create an account.
func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
}
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
}
size, overflow := stack.back(2).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
return GasCosts{}, err
}
// Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow.
wordGas := params.InitCodeWordGas * ((size + 31) / 32)
stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte
return GasCosts{
RegularGas: gas + wordGas,
StateGas: stateGas,
}, nil
}
// gasCreate2Eip8037 is the CREATE2 gas calculator for Amsterdam. See
// gasCreateEip8037; CREATE2 additionally charges Keccak256WordGas for hashing
// the init code.
func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
}
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
}
size, overflow := stack.back(2).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
return GasCosts{}, err
}
// Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow.
wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * ((size + 31) / 32)
stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte
return GasCosts{
RegularGas: gas + wordGas,
StateGas: stateGas,
}, nil
}
func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
expByteLen := uint64((stack.back(1).BitLen() + 7) / 8)

View file

@ -226,9 +226,11 @@ func (g GasBudget) ExitRevert() GasBudget {
// ExitHalt produces the leftover for an exceptional halt.
//
// - state_gas_reservoir is reset back to its value at the start of the child frame
// - the gas_left initially given to the child is consumed (set to zero)
func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
// Per the updated EIP-8037, only the regular gas_left is burned (folded into
// UsedRegularGas); the entire state-gas reservoir — including any portion that
// spilled into the regular pool during execution — is refunded to the caller's
// reservoir rather than reclassified as burned regular gas.
func (g GasBudget) ExitHalt() GasBudget {
reservoir := int64(g.StateGas) + g.UsedStateGas
if reservoir < 0 {
// Reservoir should never be negative. By construction it equals
@ -237,17 +239,10 @@ func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
reservoir = 0
log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas)
}
// The portion of state gas charged from regular gas is also burned
// together with the regular gas, rather than being returned to the
// parent's state-gas reservoir.
var spilled uint64
if uint64(reservoir) > initStateReservoir {
spilled = uint64(reservoir) - initStateReservoir
}
return GasBudget{
RegularGas: 0,
StateGas: initStateReservoir,
UsedRegularGas: g.UsedRegularGas + g.RegularGas + spilled,
StateGas: uint64(reservoir),
UsedRegularGas: g.UsedRegularGas + g.RegularGas,
UsedStateGas: 0,
}
}
@ -261,33 +256,24 @@ func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
//
// Soft validation failures (occurring BEFORE evm.Run) should call Preserved
// directly instead of going through this dispatcher.
func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget {
func (g GasBudget) Exit(err error) GasBudget {
switch {
case err == nil:
return g.ExitSuccess()
case err == ErrExecutionReverted:
return g.ExitRevert()
default:
return g.ExitHalt(initStateReservoir)
return g.ExitHalt()
}
}
// Absorb merges a sub-call's leftover GasBudget into this (caller's) running
// budget. Additionally, it does an EIP-8037 spillover correction:
// state-gas that spilled into the regular pool inside the child frame is
// excluded from the UsedRegularGas.
//
// spillover = forwarded - child.RegularGas - child.UsedRegularGas
//
// forwarded is the regular-gas amount that was passed to the child at call
// entry (i.e., the regular initial of the child's GasBudget).
func (g *GasBudget) Absorb(child GasBudget, forwarded uint64) {
spillover := forwarded - child.RegularGas - child.UsedRegularGas
g.UsedRegularGas -= child.RegularGas
// budget. Under the updated EIP-8037, state-gas no longer spills into the
// child's burned regular gas on halt, so the child's UsedRegularGas can be
// folded in directly without a spillover correction.
func (g *GasBudget) Absorb(child GasBudget) {
g.RegularGas += child.RegularGas
g.UsedRegularGas += child.UsedRegularGas
g.StateGas = child.StateGas
g.UsedStateGas += child.UsedStateGas
g.UsedRegularGas -= spillover
}

View file

@ -677,7 +677,14 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Stack.push(&stackvalue)
// Refund the leftover gas back to current frame
scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
// EIP-8037: no account was created on any failure path, so refund the
// account-creation state gas charged before the opcode ran (gasCreateEip8037)
// back to the reservoir.
if evm.chainRules.IsAmsterdam && suberr != nil {
scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte)
}
if suberr == ErrExecutionReverted {
evm.returnData = res // set REVERT data to return data buffer
@ -711,7 +718,14 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Stack.push(&stackvalue)
// Refund the leftover gas back to current frame
scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
// EIP-8037: no account was created on any failure path, so refund the
// account-creation state gas charged before the opcode ran (gasCreate2Eip8037)
// back to the reservoir.
if evm.chainRules.IsAmsterdam && suberr != nil {
scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte)
}
if suberr == ErrExecutionReverted {
evm.returnData = res // set REVERT data to return data buffer
@ -756,7 +770,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@ -792,7 +806,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@ -824,7 +838,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@ -857,7 +871,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil