diff --git a/build/checksums.txt b/build/checksums.txt index 7751011ce1..acfc11f623 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -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 v5.6.1 +# version:spec-tests-bal v8037.0.0 # https://github.com/ethereum/execution-spec-tests/releases -# https://github.com/ethereum/execution-spec-tests/releases/download/bal%40v5.6.1 -741530c88f6a48c15184d1504316c02c3a76c2322c410a04b643a85185dc62e9 fixtures_bal.tar.gz +# https://github.com/ethereum/execution-spec-tests/releases/download/snobal-devnet-5%40v8037.0.0 +0b0d5b14e9f2b7d3a7037f42703d76c2baf2b20cb550bd6b34ea761b7fd407df fixtures_snobal-devnet-5.tar.gz # version:golang 1.25.9 # https://go.dev/dl/ diff --git a/build/ci.go b/build/ci.go index d6e3af1ac0..d1045d0131 100644 --- a/build/ci.go +++ b/build/ci.go @@ -455,7 +455,7 @@ func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string func downloadBALSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string { ext := ".tar.gz" - base := "fixtures_bal" + base := "fixtures_snobal-devnet-5" archivePath := filepath.Join(cachedir, base+ext) if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { log.Fatal(err) diff --git a/core/state/journal.go b/core/state/journal.go index b3f97655dd..cb54858305 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -287,7 +287,20 @@ func (j *journal) stateChangedBytes(revid int, stateObjects map[common.Address]* } var totalBytes int64 - for range created { + // Per EIP-8037 spec compute_state_byte_diff: only count +112 for accounts + // that exist at frame exit. Per EIP-161, accounts that end up with + // nonce=0, balance=0, and empty code are considered non-existent and + // pruned — these don't count even though we created the object record + // (e.g. AddBalance(addr, 0) for a zero-value SELFDESTRUCT to a + // non-existent beneficiary). + for addr := range created { + obj := stateObjects[addr] + if obj == nil { + continue + } + if obj.empty() { + continue + } totalBytes += CostPerAccount } for sk, si := range slots { diff --git a/core/state_transition.go b/core/state_transition.go index a03be12b00..61663e26d4 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -650,8 +650,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if rules.IsAmsterdam { if vmerr == nil { outerBytes := st.state.StateChangedBytes(outerSnapshot, false) - // Refund state gas for selfdestructed accounts. - outerBytes -= st.state.SelfDestructRefundBytes() // For contract-creation txs the intrinsic already paid for the // account creation; subtract it so we don't double-charge. if contractCreation { @@ -661,6 +659,20 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { alreadyPaid := st.gasRemaining.StateGasUsed thisCallCost := outerBytes*int64(st.evm.Context.CostPerStateByte) - alreadyPaid st.gasRemaining.Charge(vm.GasCosts{StateGas: thisCallCost}) + + // EIP-8037 + EIP-6780: refund state gas for accounts created and + // selfdestructed in the same tx. Per spec interpreter.py:200-201, + // this runs at the top-level after the frame's apply_frame_state_gas: + // reservoir is credited and state_gas_used is decremented. The + // decrement may go negative when account-creation was paid via the + // intrinsic (CREATE tx) rather than execution state-gas; the + // negative offsets the intrinsic at tx-level state-gas accounting. + refundBytes := st.state.SelfDestructRefundBytes() + if refundBytes > 0 { + refundCost := refundBytes * int64(st.evm.Context.CostPerStateByte) + st.gasRemaining.StateGas += uint64(refundCost) + st.gasRemaining.StateGasUsed -= refundCost + } } else { // On top-level error, restore state-gas reservoir and reset // the state-gas-used counter; state changes are reverted, no diff --git a/core/vm/evm.go b/core/vm/evm.go index ea10591a36..d78e0b0d81 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -346,8 +346,16 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g bytesCharged, alreadyPaid, thisCallCost, gas.CanAfford(stateGasCost)) } if !gas.CanAfford(stateGasCost) { + // EIP-8037 spec apply_frame_state_gas OOG branch: roll back + // state, set OOG, preserve reservoir + remaining regular gas. + // Per incorporate_child_on_error: child's reservoir + max(0, + // state_gas_used) flows to caller's reservoir; state_gas_used + // is dropped. Convert here so RefundGas does the right thing. evm.StateDB.RevertToSnapshot(snapshot1) - gas.Exhaust() + if gas.StateGasUsed > 0 { + gas.StateGas += uint64(gas.StateGasUsed) + } + gas.StateGasUsed = 0 return ret, gas, ErrOutOfGas } gas.Charge(stateGasCost) @@ -432,7 +440,17 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt bytesCharged, alreadyPaid, thisCallCost, gas.CanAfford(stateGasCost)) } if !gas.CanAfford(stateGasCost) { - gas.Exhaust() + // EIP-8037 spec apply_frame_state_gas OOG branch: roll back + // state, set OOG, preserve reservoir and remaining regular + // gas. Per incorporate_child_on_error: child's reservoir + // AND max(0, state_gas_used) both flow to the caller's + // reservoir; state_gas_used itself is dropped (not + // propagated as credit). Convert here. + evm.StateDB.RevertToSnapshot(snapshot1) + if gas.StateGasUsed > 0 { + gas.StateGas += uint64(gas.StateGasUsed) + } + gas.StateGasUsed = 0 return ret, gas, ErrOutOfGas } gas.Charge(stateGasCost) @@ -512,7 +530,17 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, bytesCharged, alreadyPaid, thisCallCost, gas.CanAfford(stateGasCost)) } if !gas.CanAfford(stateGasCost) { - gas.Exhaust() + // EIP-8037 spec apply_frame_state_gas OOG branch: roll back + // state, set OOG, preserve reservoir and remaining regular + // gas. Per incorporate_child_on_error: child's reservoir + // AND max(0, state_gas_used) both flow to the caller's + // reservoir; state_gas_used itself is dropped (not + // propagated as credit). Convert here. + evm.StateDB.RevertToSnapshot(snapshot1) + if gas.StateGasUsed > 0 { + gas.StateGas += uint64(gas.StateGasUsed) + } + gas.StateGasUsed = 0 return ret, gas, ErrOutOfGas } gas.Charge(stateGasCost) @@ -710,8 +738,15 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value thisCallCost := bytesCharged*int64(evm.Context.CostPerStateByte) - alreadyPaid stateGasCost := GasCosts{StateGas: thisCallCost} if !contract.Gas.CanAfford(stateGasCost) { + // EIP-8037 spec: state-gas OOG rolls back state, preserves + // reservoir+regular, converts max(0, state_gas_used) into + // reservoir credit (incorporate_child_on_error semantics), + // then drops state_gas_used. evm.StateDB.RevertToSnapshot(snapshot1) - contract.Gas.Exhaust() + if contract.Gas.StateGasUsed > 0 { + contract.Gas.StateGas += uint64(contract.Gas.StateGasUsed) + } + contract.Gas.StateGasUsed = 0 return ret, address, contract.Gas, ErrOutOfGas } contract.Gas.Charge(stateGasCost) diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 48752cad52..4f9c8c9294 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -127,6 +127,12 @@ func (g GasBudget) CanAfford(cost GasCosts) bool { // Charge deducts the given gas cost from the budget. It returns the // pre-charge regular gas value and false if the budget does not have // sufficient gas to cover the cost. +// +// Per EIP-8037 spec apply_frame_state_gas: a negative state-gas cost +// (refund from net state shrinkage) credits only the reservoir, not the +// per-frame state_gas_used tracker. Otherwise the negative would +// propagate to the parent's already_paid and cause the parent's +// frame-end charge to be over-counted. func (g *GasBudget) Charge(cost GasCosts) (uint64, bool) { prior := g.RegularGas if !g.CanAfford(cost) { @@ -134,11 +140,11 @@ func (g *GasBudget) Charge(cost GasCosts) (uint64, bool) { } g.RegularGas -= cost.RegularGas g.RegularGasUsed += cost.RegularGas - g.StateGasUsed += cost.StateGas if cost.StateGas < 0 { g.StateGas -= uint64(cost.StateGas) return prior, true } + g.StateGasUsed += cost.StateGas if uint64(cost.StateGas) > g.StateGas { spillover := uint64(cost.StateGas) - g.StateGas g.StateGas = 0 diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 85755b96f3..0759bb2da5 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -17,7 +17,9 @@ package vm import ( + "fmt" "math" + "os" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" @@ -771,7 +773,15 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // reset parent's reservoir to zero. Leftover comes back via RefundGas. childStateGas := scope.Contract.Gas.StateGas scope.Contract.Gas.StateGas = 0 + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, " opCall depth=%d childStateGas=%d gas=%d to=%v\n", + evm.depth, childStateGas, gas, toAddr) + } ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudget(gas, childStateGas), &value) + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, " opCall depth=%d returnGas=<%d,%d>su=%d err=%v\n", + evm.depth, returnGas.RegularGas, returnGas.StateGas, returnGas.StateGasUsed, err) + } if err != nil { temp.Clear()