diff --git a/core/eip8037_test.go b/core/eip8037_test.go
new file mode 100644
index 0000000000..c7e94ec629
--- /dev/null
+++ b/core/eip8037_test.go
@@ -0,0 +1,614 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Transaction- and block-level tests for EIP-8037 (multidimensional state-gas
+// metering). They apply whole transactions and inspect the 2D block gas pool
+// (cumulativeRegular / cumulativeState) and the receipt/peak figures.
+
+package core
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/consensus/beacon"
+ "github.com/ethereum/go-ethereum/consensus/ethash"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
+)
+
+var (
+ cfg8037 = balChainConfig()
+ signer8037 = types.LatestSigner(cfg8037)
+ rules8037 = cfg8037.Rules(big.NewInt(0), true, 0)
+ senderKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ senderAddr = crypto.PubkeyToAddress(senderKey.PublicKey)
+
+ // state-gas charges in units (CPSB applied).
+ newAccountState = uint64(params.AccountCreationSize * params.CostPerStateByte) // 183,600
+ newSlotState = uint64(params.StorageCreationSize * params.CostPerStateByte) // 97,920
+ authBaseState = uint64(params.AuthorizationCreationSize * params.CostPerStateByte) // 35,190
+ authWorstState = newAccountState + authBaseState // 218,790
+)
+
+// mkState builds an in-memory StateDB from a genesis allocation.
+func mkState(alloc types.GenesisAlloc) *state.StateDB {
+ sdb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
+ for addr, acc := range alloc {
+ sdb.CreateAccount(addr)
+ if acc.Balance != nil {
+ sdb.AddBalance(addr, uint256.MustFromBig(acc.Balance), tracing.BalanceChangeUnspecified)
+ }
+ if acc.Nonce != 0 {
+ sdb.SetNonce(addr, acc.Nonce, tracing.NonceChangeGenesis)
+ }
+ if len(acc.Code) != 0 {
+ sdb.SetCode(addr, acc.Code, tracing.CodeChangeUnspecified)
+ }
+ for k, v := range acc.Storage {
+ sdb.SetState(addr, k, v)
+ }
+ }
+ sdb.Finalise(true)
+ return sdb
+}
+
+// amsterdamCoreEVM builds an Amsterdam EVM over statedb with fees disabled.
+func amsterdamCoreEVM(sdb *state.StateDB) *vm.EVM {
+ ctx := vm.BlockContext{
+ CanTransfer: CanTransfer,
+ Transfer: Transfer,
+ GetHash: func(uint64) common.Hash { return common.Hash{} },
+ BlockNumber: big.NewInt(0),
+ Random: &common.Hash{},
+ Difficulty: big.NewInt(0),
+ BaseFee: big.NewInt(0),
+ BlobBaseFee: big.NewInt(0),
+ GasLimit: 60_000_000,
+ CostPerStateByte: params.CostPerStateByte,
+ }
+ return vm.NewEVM(ctx, sdb, cfg8037, vm.Config{NoBaseFee: true})
+}
+
+// applyMsg applies one transaction with a fresh block gas pool and returns the
+// execution result, the gas pool (for the 2D split) and any consensus error.
+func applyMsg(t *testing.T, sdb *state.StateDB, tx *types.Transaction) (*ExecutionResult, *GasPool, error) {
+ t.Helper()
+ evm := amsterdamCoreEVM(sdb)
+ msg, err := TransactionToMessage(tx, signer8037, evm.Context.BaseFee)
+ if err != nil {
+ t.Fatalf("to message: %v", err)
+ }
+ gp := NewGasPool(evm.Context.GasLimit)
+ // Drive the stateTransition directly (as ApplyMessage does) so the test can
+ // inspect the final tx-level GasBudget vector via st.gasRemaining.
+ evm.SetTxContext(NewEVMTxContext(msg))
+ st := newStateTransition(evm, msg, gp)
+ res, err := st.execute()
+ if err == nil && res != nil {
+ assertPoolSane(t, res, gp)
+ limit := min(msg.GasLimit, params.MaxTxGas)
+ assertBudgetSane(t, vm.NewGasBudget(limit, msg.GasLimit-limit), st.gasRemaining)
+ }
+ return res, gp, err
+}
+
+// assertBudgetSane validates the final tx-level GasBudget vector:
+//
+// regular: RegularGas + UsedRegularGas + Spilled == initial.RegularGas
+// state: StateGas + UsedStateGas == initial.StateGas + Spilled
+// scalar: Used(initial) == UsedRegularGas + UsedStateGas
+func assertBudgetSane(t *testing.T, initial, got vm.GasBudget) {
+ t.Helper()
+ if got.RegularGas+got.UsedRegularGas+got.Spilled != initial.RegularGas {
+ t.Fatalf("regular not conserved: R=%d usedR=%d spilled=%d, want sum %d",
+ got.RegularGas, got.UsedRegularGas, got.Spilled, initial.RegularGas)
+ }
+ if int64(got.StateGas)+got.UsedStateGas != int64(initial.StateGas)+int64(got.Spilled) {
+ t.Fatalf("state not conserved: S=%d usedS=%d spilled=%d, want %d+spilled",
+ got.StateGas, got.UsedStateGas, got.Spilled, initial.StateGas)
+ }
+ if int64(got.Used(initial)) != int64(got.UsedRegularGas)+got.UsedStateGas {
+ t.Fatalf("scalar mismatch: used=%d, usedR=%d usedS=%d",
+ got.Used(initial), got.UsedRegularGas, got.UsedStateGas)
+ }
+}
+
+// assertPoolSane validates the whole 2D block-gas-pool vector after a single tx.
+//
+// receipt: cumulativeUsed == res.UsedGas <= res.MaxUsedGas
+// pre-refund: cumulativeRegular + cumulativeState <= res.MaxUsedGas (peak)
+// bottleneck: Used() == max(cumulativeRegular, cumulativeState) <= initial
+func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool) {
+ t.Helper()
+ if gp.cumulativeUsed != res.UsedGas {
+ t.Fatalf("receipt scalar = %d, want UsedGas %d", gp.cumulativeUsed, res.UsedGas)
+ }
+ if res.UsedGas > res.MaxUsedGas {
+ t.Fatalf("post-refund gas %d exceeds peak %d", res.UsedGas, res.MaxUsedGas)
+ }
+ if sum := gp.cumulativeRegular + gp.cumulativeState; sum > res.MaxUsedGas {
+ t.Fatalf("regular+state %d exceeds peak %d", sum, res.MaxUsedGas)
+ }
+ if gp.Used() != max(gp.cumulativeRegular, gp.cumulativeState) {
+ t.Fatalf("block used %d != max(%d,%d)", gp.Used(), gp.cumulativeRegular, gp.cumulativeState)
+ }
+ if gp.Used() > gp.initial {
+ t.Fatalf("block used %d exceeds limit %d", gp.Used(), gp.initial)
+ }
+}
+
+// senderAlloc funds the sender with the given extra accounts merged in.
+func senderAlloc(extra types.GenesisAlloc) types.GenesisAlloc {
+ alloc := types.GenesisAlloc{senderAddr: {Balance: big.NewInt(1e18)}}
+ for a, acc := range extra {
+ alloc[a] = acc
+ }
+ return alloc
+}
+
+// callTx builds a signed dynamic-fee call to `to` with zero fees.
+func callTx(nonce uint64, to common.Address, value int64, gas uint64, data []byte) *types.Transaction {
+ return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
+ ChainID: cfg8037.ChainID, Nonce: nonce, To: &to, Value: big.NewInt(value),
+ Gas: gas, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), Data: data,
+ })
+}
+
+// createTx builds a signed contract-creation transaction.
+func createTx(nonce, gas uint64, initCode []byte) *types.Transaction {
+ return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
+ ChainID: cfg8037.ChainID, Nonce: nonce, To: nil, Value: big.NewInt(0),
+ Gas: gas, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), Data: initCode,
+ })
+}
+
+var (
+ deploy3 = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // init: return 3 bytes of code
+ revertI = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // init: REVERT
+)
+
+// ===================== Top-level create transaction ======================
+
+// A creation tx's intrinsic gas pre-charges one account creation as state gas.
+func TestCreateTxIntrinsicChargesAccountUnconditionally(t *testing.T) {
+ cost, err := IntrinsicGas(nil, nil, nil, true, rules8037, params.CostPerStateByte)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cost.StateGas != newAccountState {
+ t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, newAccountState)
+ }
+}
+
+// Creating onto a pre-existing (balance-only) address refills the account
+// portion; only the code deposit is charged as state gas.
+func TestCreateTxPreexistingDestRefill(t *testing.T) {
+ derived := crypto.CreateAddress(senderAddr, 0)
+ sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Balance: big.NewInt(1)}}))
+ _, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, deploy3))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := uint64(3 * params.CostPerStateByte); gp.cumulativeState != want {
+ t.Fatalf("state gas = %d, want %d", gp.cumulativeState, want)
+ }
+}
+
+// A creation tx that reverts refills the account-creation charge.
+func TestCreateTxRevertRefill(t *testing.T) {
+ sdb := mkState(senderAlloc(nil))
+ res, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, revertI))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !res.Failed() {
+ t.Fatal("expected failed creation")
+ }
+ if gp.cumulativeState != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
+ }
+}
+
+// An address collision burns gas_left while refilling the account charge.
+func TestCreateTxCollisionConsumesGasLeft(t *testing.T) {
+ const gas = 1_000_000
+ derived := crypto.CreateAddress(senderAddr, 0)
+ sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Nonce: 1}}))
+ res, gp, err := applyMsg(t, sdb, createTx(0, gas, deploy3))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !res.Failed() {
+ t.Fatal("expected collision failure")
+ }
+ if gp.cumulativeState != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
+ }
+ // All forwarded gas_left is burned; only the refilled account charge (which
+ // had spilled into regular) returns to gas_left. So regular gas consumed is
+ // exactly tx.gas - newAccountState, with no other refund.
+ if want := uint64(gas) - newAccountState; gp.cumulativeRegular != want {
+ t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want)
+ }
+}
+
+// ======================== Transaction validation =========================
+
+// The regular dimension must have room for min(tx.gas, MaxTxGas).
+func TestValidationRegularGasAvailable(t *testing.T) {
+ gp := NewGasPool(30_000_000)
+ gp.cumulativeRegular = 29_000_000
+ if gp.CheckGasAmsterdam(2_000_000, 0) == nil {
+ t.Fatal("expected regular dimension full")
+ }
+ if err := gp.CheckGasAmsterdam(1_000_000, 0); err != nil {
+ t.Fatalf("regular fits but rejected: %v", err)
+ }
+}
+
+// The state dimension must have room for the whole tx.gas.
+func TestValidationStateGasAvailable(t *testing.T) {
+ gp := NewGasPool(30_000_000)
+ gp.cumulativeState = 29_000_000
+ if gp.CheckGasAmsterdam(0, 2_000_000) == nil {
+ t.Fatal("expected state dimension full")
+ }
+ if err := gp.CheckGasAmsterdam(0, 1_000_000); err != nil {
+ t.Fatalf("state fits but rejected: %v", err)
+ }
+}
+
+// tx.gas may exceed MaxTxGas: regular is capped at MaxTxGas while the state
+// dimension reserves the full tx.gas (the excess lands in the reservoir).
+func TestValidationStateGasOverflowAllowed(t *testing.T) {
+ gas := uint64(params.MaxTxGas) + 5_000_000
+ gp := NewGasPool(40_000_000)
+ if err := gp.CheckGasAmsterdam(min(gas, params.MaxTxGas), gas); err != nil {
+ t.Fatalf("overflow tx rejected at pool: %v", err)
+ }
+ // A real transfer with gas above MaxTxGas is accepted under Amsterdam.
+ sdb := mkState(senderAlloc(nil))
+ to := common.HexToAddress("0xc0ffee")
+ if _, _, err := applyMsg(t, sdb, callTx(0, to, 1, gas, nil)); err != nil {
+ t.Fatalf("tx with gas > MaxTxGas rejected: %v", err)
+ }
+}
+
+// Intrinsic regular gas above MaxTxGas (EIP-7825 cap) is rejected.
+func TestValidationIntrinsicRegularCap(t *testing.T) {
+ al := make(types.AccessList, 8000) // ~19.2M regular, over the 16.77M cap
+ for i := range al {
+ al[i].Address = common.BigToAddress(big.NewInt(int64(i + 1)))
+ }
+ tx := types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
+ ChainID: cfg8037.ChainID, Nonce: 0, To: &senderAddr, Value: big.NewInt(0),
+ Gas: 25_000_000, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), AccessList: al,
+ })
+ if _, _, err := applyMsg(t, mkState(senderAlloc(nil)), tx); err == nil {
+ t.Fatal("expected rejection for intrinsic regular over MaxTxGas")
+ }
+}
+
+// ========================= Refund and gas used ===========================
+
+// clearSlots deploys a contract that zeroes slots 1..n, each preset to 1.
+func clearSlots(addr common.Address, n int) (types.GenesisAlloc, []byte) {
+ var code []byte
+ storage := make(map[common.Hash]common.Hash, n)
+ for s := 1; s <= n; s++ {
+ code = append(code, 0x60, 0x00, 0x60, byte(s), 0x55) // PUSH1 0; PUSH1 s; SSTORE
+ storage[common.BytesToHash([]byte{byte(s)})] = common.BytesToHash([]byte{1})
+ }
+ return types.GenesisAlloc{addr: {Code: append(code, 0x00), Storage: storage}}, nil
+}
+
+// tx_gas_used_before_refund (peak) exceeds the post-refund gas used.
+func TestGasUsedBeforeRefund(t *testing.T) {
+ c := common.HexToAddress("0xc1ea0")
+ alloc, _ := clearSlots(c, 1)
+ res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, nil))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.MaxUsedGas <= res.UsedGas {
+ t.Fatalf("peak %d must exceed post-refund %d", res.MaxUsedGas, res.UsedGas)
+ }
+}
+
+// The refund is capped at 20% of gas used before refund.
+func TestRefundCappedAt20Percent(t *testing.T) {
+ c := common.HexToAddress("0xc1ea3")
+ alloc, _ := clearSlots(c, 3) // refund (3x4800) exceeds the 20% cap
+ res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, nil))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := res.MaxUsedGas - res.MaxUsedGas/5; res.UsedGas != want {
+ t.Fatalf("gas used = %d, want capped %d", res.UsedGas, want)
+ }
+}
+
+// The EIP-7623 calldata floor is applied after the refund.
+func TestRefundCalldataFloorAfterRefund(t *testing.T) {
+ data := make([]byte, 1000) // all-zero calldata: floor dominates a bare call
+ floor, _ := FloorDataGas(rules8037, data, nil)
+ to := common.HexToAddress("0xeeee")
+ res, _, err := applyMsg(t, mkState(senderAlloc(nil)), callTx(0, to, 0, 1_000_000, data))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedGas != floor {
+ t.Fatalf("gas used = %d, want floor %d", res.UsedGas, floor)
+ }
+}
+
+// When the floor exceeds the post-refund gas, it negates part of the refund.
+func TestRefundFloorNegatesRefund(t *testing.T) {
+ c := common.HexToAddress("0xc1ea1")
+ alloc, _ := clearSlots(c, 1)
+ data := make([]byte, 1000)
+ floor, _ := FloorDataGas(rules8037, data, nil)
+ res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, data))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedGas != floor {
+ t.Fatalf("gas used = %d, want floor %d (refund negated)", res.UsedGas, floor)
+ }
+}
+
+// ========================= Block-level accounting ========================
+
+// The pool tracks regular and state cumulatively in separate counters.
+func TestBlockTracksTwoCounters(t *testing.T) {
+ gp := NewGasPool(60_000_000)
+ if err := gp.ChargeGasAmsterdam(100, 200, 300); err != nil {
+ t.Fatal(err)
+ }
+ if gp.cumulativeRegular != 100 || gp.cumulativeState != 200 {
+ t.Fatalf("counters = (%d,%d), want (100,200)", gp.cumulativeRegular, gp.cumulativeState)
+ }
+}
+
+// Block gas used is the max of the two dimensions.
+func TestBlockGasUsedIsMax(t *testing.T) {
+ gp := NewGasPool(60_000_000)
+ gp.ChargeGasAmsterdam(100, 200, 300)
+ if gp.Used() != 200 {
+ t.Fatalf("block used = %d, want 200", gp.Used())
+ }
+}
+
+// Block validity is checked against the max dimension, not the sum.
+func TestBlockValidityAgainstMax(t *testing.T) {
+ gp := NewGasPool(150)
+ // regular 100 + state 120: sum 220 > 150 but max 120 <= 150 is valid.
+ if err := gp.ChargeGasAmsterdam(100, 120, 0); err != nil {
+ t.Fatalf("max within limit but rejected: %v", err)
+ }
+ // state 200 alone exceeds the limit.
+ if err := gp.ChargeGasAmsterdam(0, 200, 0); err == nil {
+ t.Fatal("expected block overflow on state dimension")
+ }
+}
+
+// The block header gas_used reflects the bottleneck dimension (here, state),
+// which the base-fee update then equilibrates against.
+func TestBlockBaseFeeUsesMax(t *testing.T) {
+ c := common.HexToAddress("0x5707e5")
+ var code []byte
+ for s := 1; s <= 5; s++ {
+ code = append(code, 0x60, byte(s), 0x60, byte(s), 0x55) // SSTORE new slot s
+ }
+ env := newBALTestEnv(types.GenesisAlloc{c: {Code: append(code, 0x00)}})
+ engine := beacon.New(ethash.NewFaker())
+ _, blocks, _ := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, b *BlockGen) {
+ b.AddTx(env.tx(0, &c, big.NewInt(0), 1_000_000, 0, nil))
+ })
+ if want := uint64(5 * newSlotState); blocks[0].GasUsed() != want {
+ t.Fatalf("block gas used = %d, want %d (state bottleneck)", blocks[0].GasUsed(), want)
+ }
+}
+
+// Receipt cumulative_gas_used is the running sum of per-tx gas (post-refund,
+// post-floor), so consecutive receipts differ by exactly that tx's gas.
+func TestReceiptCumulativeGasUsed(t *testing.T) {
+ env := newBALTestEnv(nil)
+ a, b := common.HexToAddress("0xaaaa"), common.HexToAddress("0xbbbb")
+ engine := beacon.New(ethash.NewFaker())
+ _, _, receipts := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, g *BlockGen) {
+ g.AddTx(env.tx(0, &a, big.NewInt(1), txGasNewAccount, 0, nil))
+ g.AddTx(env.tx(1, &b, big.NewInt(1), txGasNewAccount, 0, nil))
+ })
+ r := receipts[0]
+ if got := r[1].CumulativeGasUsed - r[0].CumulativeGasUsed; got != r[1].GasUsed {
+ t.Fatalf("cumulative delta = %d, want tx gas %d", got, r[1].GasUsed)
+ }
+}
+
+// ======================= EIP-7702 authorizations =========================
+
+// signAuth signs an authorization from authKey for the given delegate and nonce.
+func signAuth(t *testing.T, authKey string, delegate common.Address, nonce uint64) (types.SetCodeAuthorization, common.Address) {
+ t.Helper()
+ k, _ := crypto.HexToECDSA(authKey)
+ auth, err := types.SignSetCode(k, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate, Nonce: nonce,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+ return auth, crypto.PubkeyToAddress(k.PublicKey)
+}
+
+func setCodeTx(nonce uint64, to common.Address, auths []types.SetCodeAuthorization) *types.Transaction {
+ return types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{
+ ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: nonce, To: to, Value: new(uint256.Int),
+ Gas: 1_000_000, GasFeeCap: new(uint256.Int), GasTipCap: new(uint256.Int), AuthList: auths,
+ })
+}
+
+const authKeyA = "0202020202020202020202020202020202020202020202020202002020202020"
+
+var delegate8037 = common.HexToAddress("0xde1e8a7e")
+
+// Intrinsic gas pre-charges the worst-case (account + indicator) per auth.
+func TestAuthIntrinsicWorstCase(t *testing.T) {
+ cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, false, rules8037, params.CostPerStateByte)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cost.StateGas != authWorstState {
+ t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, authWorstState)
+ }
+}
+
+// An invalid authorization refills its entire intrinsic state-gas charge.
+func TestAuthInvalidRefillFull(t *testing.T) {
+ k, _ := crypto.HexToECDSA(authKeyA)
+ bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{
+ ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id
+ })
+ sdb := mkState(senderAlloc(nil))
+ _, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{bad}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if gp.cumulativeState != 0 {
+ t.Fatalf("state gas = %d, want 0 (fully refilled)", gp.cumulativeState)
+ }
+}
+
+// A pre-existing authority refills the account portion (indicator stands).
+func TestAuthAccountExistsRefill(t *testing.T) {
+ auth, authority := signAuth(t, authKeyA, delegate8037, 0)
+ sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))
+ _, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if gp.cumulativeState != authBaseState {
+ t.Fatalf("state gas = %d, want %d (account refilled)", gp.cumulativeState, authBaseState)
+ }
+}
+
+// Setting a delegation on an already-delegated authority refills the indicator
+// portion (and the account portion, since the authority already exists).
+func TestAuthSetOnDelegatedRefillBase(t *testing.T) {
+ auth, authority := signAuth(t, authKeyA, delegate8037, 0)
+ pre := types.AddressToDelegation(common.HexToAddress("0xabcd"))
+ sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Code: pre}}))
+ _, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if gp.cumulativeState != 0 {
+ t.Fatalf("state gas = %d, want 0 (account+indicator refilled)", gp.cumulativeState)
+ }
+}
+
+// A net-new delegation on a fresh authority keeps the full worst-case charge.
+func TestAuthSetNetNewNoRefill(t *testing.T) {
+ auth, _ := signAuth(t, authKeyA, delegate8037, 0)
+ sdb := mkState(senderAlloc(nil))
+ _, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if gp.cumulativeState != authWorstState {
+ t.Fatalf("state gas = %d, want %d (no refill)", gp.cumulativeState, authWorstState)
+ }
+}
+
+// Clearing a delegation writes no indicator, so the indicator portion refills.
+func TestAuthClearRefillBase(t *testing.T) {
+ auth, _ := signAuth(t, authKeyA, common.Address{}, 0) // clear (address ZERO)
+ sdb := mkState(senderAlloc(nil))
+ _, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := uint64(newAccountState); gp.cumulativeState != want {
+ t.Fatalf("state gas = %d, want %d (indicator refilled)", gp.cumulativeState, want)
+ }
+}
+
+// 0->a->0 in one tx: the indicator created by an earlier auth and cleared by a
+// later one writes zero net bytes, so both indicator charges refill.
+func TestAuthClearSameTxDoubleRefill(t *testing.T) {
+ set, authority := signAuth(t, authKeyA, delegate8037, 0)
+ clr, _ := signAuth(t, authKeyA, common.Address{}, 1)
+ sdb := mkState(senderAlloc(nil))
+ _, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{set, clr}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ _ = authority
+ if want := uint64(newAccountState); gp.cumulativeState != want {
+ t.Fatalf("state gas = %d, want %d (net-zero delegation)", gp.cumulativeState, want)
+ }
+}
+
+// The same authority across two auths is charged for its account only once.
+func TestAuthDuplicateAuthorityOnce(t *testing.T) {
+ a0, _ := signAuth(t, authKeyA, delegate8037, 0)
+ a1, _ := signAuth(t, authKeyA, delegate8037, 1)
+ sdb := mkState(senderAlloc(nil))
+ _, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{a0, a1}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if gp.cumulativeState != authWorstState {
+ t.Fatalf("state gas = %d, want %d (leaf+indicator once)", gp.cumulativeState, authWorstState)
+ }
+}
+
+// ===================== System contracts / system calls ===================
+
+// System call gas limit keeps 30M regular plus a state reservoir for new slots.
+func TestSystemCallGasLimit(t *testing.T) {
+ limit, budget := systemCallGasBudget(amsterdamCoreEVM(mkState(nil)))
+ if limit != 30_000_000 || budget.RegularGas != 30_000_000 {
+ t.Fatalf("limit/regular = %d/%d, want 30M/30M", limit, budget.RegularGas)
+ }
+}
+
+// The extra system budget is placed in the state reservoir (16 new slots).
+func TestSystemCallExtraInReservoir(t *testing.T) {
+ _, budget := systemCallGasBudget(amsterdamCoreEVM(mkState(nil)))
+ want := uint64(params.SystemMaxSStoresPerCall * params.CostPerStateByte * params.StorageCreationSize)
+ if budget.StateGas != want {
+ t.Fatalf("reservoir = %d, want %d", budget.StateGas, want)
+ }
+}
+
+// System calls do not contribute to either block dimension: an empty block
+// (whose system calls still write state) reports zero gas used.
+func TestSystemCallNotCountedInBlock(t *testing.T) {
+ env := newBALTestEnv(nil)
+ engine := beacon.New(ethash.NewFaker())
+ _, blocks, _ := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, b *BlockGen) {})
+ if blocks[0].GasUsed() != 0 {
+ t.Fatalf("block gas used = %d, want 0 (system calls excluded)", blocks[0].GasUsed())
+ }
+}
diff --git a/core/vm/eip8037_test.go b/core/vm/eip8037_test.go
new file mode 100644
index 0000000000..658096d228
--- /dev/null
+++ b/core/vm/eip8037_test.go
@@ -0,0 +1,739 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Opcode-level tests for EIP-8037 (multidimensional state-gas metering).
+// They drive a single frame via evm.Call and assert the state-gas accounting
+// exposed by the returned GasBudget (UsedStateGas / StateGas / Spilled).
+
+package vm
+
+import (
+ "math"
+ "math/big"
+ "math/rand"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
+)
+
+// state-gas charges in units (CPSB applied).
+var (
+ stateGasNewAccount = int64(params.AccountCreationSize * params.CostPerStateByte) // 183,600
+ stateGasNewSlot = int64(params.StorageCreationSize * params.CostPerStateByte) // 97,920
+)
+
+// amsterdam8037Config clones MergedTestChainConfig with Amsterdam (EIP-8037) live.
+func amsterdam8037Config() *params.ChainConfig {
+ cfg := *params.MergedTestChainConfig
+ cfg.AmsterdamTime = new(uint64)
+ blob := *cfg.BlobScheduleConfig
+ blob.Amsterdam = blob.Osaka
+ cfg.BlobScheduleConfig = &blob
+ return &cfg
+}
+
+// amsterdam8037EVM builds an EVM with real value transfers and CPSB wired in.
+func amsterdam8037EVM(statedb StateDB) *EVM {
+ ctx := BlockContext{
+ CanTransfer: func(db StateDB, addr common.Address, amount *uint256.Int) bool {
+ return db.GetBalance(addr).Cmp(amount) >= 0
+ },
+ Transfer: func(db StateDB, sender, recipient common.Address, amount *uint256.Int, _ *params.Rules) {
+ db.SubBalance(sender, amount, tracing.BalanceChangeTransfer)
+ db.AddBalance(recipient, amount, tracing.BalanceChangeTransfer)
+ },
+ BlockNumber: big.NewInt(0),
+ Random: &common.Hash{},
+ CostPerStateByte: params.CostPerStateByte,
+ }
+ return NewEVM(ctx, statedb, amsterdam8037Config(), Config{})
+}
+
+// run8037 executes code at a contract address and returns the call's return
+// data and the resulting budget. setup mutates the pre-state (before Finalise)
+// and may fund the contract.
+func run8037(t *testing.T, code []byte, gas GasBudget, value *uint256.Int, setup func(db *state.StateDB, self common.Address)) ([]byte, GasBudget, error) {
+ t.Helper()
+ self := common.BytesToAddress([]byte("self"))
+ statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
+ statedb.CreateAccount(self)
+ statedb.SetCode(self, code, tracing.CodeChangeUnspecified)
+ if setup != nil {
+ setup(statedb, self)
+ }
+ statedb.Finalise(true)
+ ret, result, err := amsterdam8037EVM(statedb).Call(common.Address{}, self, nil, gas, value)
+ assertBudgetSane(t, gas, result)
+ return ret, result, err
+}
+
+// assertBudgetSane verifies the GasBudget conservation identities that must hold
+// for any frame exit (success, revert or halt), validating the whole vector.
+//
+// regular: RegularGas + UsedRegularGas + Spilled == initial.RegularGas
+// state: StateGas + UsedStateGas == initial.StateGas + Spilled
+// scalar: Used(initial) == UsedRegularGas + UsedStateGas
+func assertBudgetSane(t *testing.T, initial, got GasBudget) {
+ t.Helper()
+ if got.RegularGas+got.UsedRegularGas+got.Spilled != initial.RegularGas {
+ t.Fatalf("regular not conserved: R=%d usedR=%d spilled=%d, want sum %d",
+ got.RegularGas, got.UsedRegularGas, got.Spilled, initial.RegularGas)
+ }
+ if int64(got.StateGas)+got.UsedStateGas != int64(initial.StateGas)+int64(got.Spilled) {
+ t.Fatalf("state not conserved: S=%d usedS=%d spilled=%d, want %d+spilled",
+ got.StateGas, got.UsedStateGas, got.Spilled, initial.StateGas)
+ }
+ if int64(got.Used(initial)) != int64(got.UsedRegularGas)+got.UsedStateGas {
+ t.Fatalf("scalar mismatch: used=%d, usedR=%d usedS=%d",
+ got.Used(initial), got.UsedRegularGas, got.UsedStateGas)
+ }
+}
+
+// hugeBudget is a budget that never runs out, with a separate state reservoir.
+func hugeBudget() GasBudget { return NewGasBudget(math.MaxUint64/2, math.MaxUint64/2) }
+
+// sstore returns "PUSH val; PUSH slot; SSTORE" bytecode.
+func sstore(slot, val byte) []byte { return []byte{0x60, val, 0x60, slot, 0x55} }
+
+// setSlot commits an original (pre-tx) value into a storage slot.
+func setSlot(slot, val byte) func(*state.StateDB, common.Address) {
+ return func(db *state.StateDB, self common.Address) {
+ db.SetState(self, common.BytesToHash([]byte{slot}), common.BytesToHash([]byte{val}))
+ }
+}
+
+// ============================ SSTORE state-gas =============================
+
+// 0 -> 0 -> x: brand-new slot is charged one storage-creation.
+func TestSStoreNewSlot(t *testing.T) {
+ _, res, err := run8037(t, sstore(0, 1), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != stateGasNewSlot {
+ t.Fatalf("state gas = %d, want %d", res.UsedStateGas, stateGasNewSlot)
+ }
+}
+
+// 0 -> x -> 0: slot created then cleared in-tx, net charge refilled to zero.
+func TestSStoreClearZeroAtStart(t *testing.T) {
+ code := append(sstore(0, 1), sstore(0, 0)...)
+ _, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
+ }
+}
+
+// x -> x -> 0: clearing a slot non-zero at tx start makes no state adjustment.
+func TestSStoreClearOriginalNonzero(t *testing.T) {
+ _, res, err := run8037(t, sstore(0, 0), hugeBudget(), new(uint256.Int), setSlot(0, 1))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
+ }
+}
+
+// x -> 0 -> x: clearing then restoring the original value makes no adjustment.
+func TestSStoreRestoreOriginal(t *testing.T) {
+ code := append(sstore(0, 0), sstore(0, 1)...)
+ _, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), setSlot(0, 1))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
+ }
+}
+
+// x -> y: overwriting an existing slot with another value makes no adjustment.
+func TestSStoreOtherWrite(t *testing.T) {
+ _, res, err := run8037(t, sstore(0, 2), hugeBudget(), new(uint256.Int), setSlot(0, 1))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
+ }
+}
+
+// New-slot charge is metered at the opcode: with a reservoir smaller than the
+// charge it spills into regular gas exactly at the SSTORE.
+func TestSStoreChargedAtOpcodeEnd(t *testing.T) {
+ _, res, err := run8037(t, sstore(0, 1), NewGasBudget(1_000_000, 100), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := uint64(stateGasNewSlot) - 100; res.Spilled != want {
+ t.Fatalf("spilled = %d, want %d", res.Spilled, want)
+ }
+}
+
+// The SSTORE reentrancy sentry checks gas_left only; the reservoir is excluded.
+// Uses a noop write (1->1->1) so the sentry is the sole gate.
+func TestSStoreStipendExcludesReservoir(t *testing.T) {
+ // regular at the sentry, huge reservoir: must still fail.
+ if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2306, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err == nil {
+ t.Fatal("expected sentry failure with regular gas at the limit")
+ }
+ // one more regular gas clears the sentry.
+ if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2307, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err != nil {
+ t.Fatalf("unexpected failure above sentry: %v", err)
+ }
+}
+
+// ---- CALL / CREATE bytecode helpers ----
+
+var (
+ freshAddr = common.BytesToAddress([]byte("fresh-target"))
+ existAddr = common.BytesToAddress([]byte("exist-target"))
+ balanceAddr = common.BytesToAddress([]byte("balance-only"))
+ childAddr = common.BytesToAddress([]byte("child-frame"))
+ revertInit = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // PUSH1 0; PUSH1 0; REVERT
+ invalidInit = []byte{0xfe} // INVALID
+ deploy3Init = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // return 3 bytes of code
+ deploy0Init = []byte{0x60, 0x00, 0x60, 0x00, 0xf3} // return 0 bytes of code
+ stop = []byte{0x00}
+ revertTail = []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
+ invalidTail = []byte{0xfe}
+ stateDeposit = int64(3 * params.CostPerStateByte) // 3-byte code deposit (4,590)
+)
+
+// callCode builds bytecode that CALLs `to` forwarding `value` wei and all gas,
+// followed by `tail`.
+func callCode(to common.Address, value byte, tail []byte) []byte {
+ b := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, value, 0x73}
+ b = append(b, to.Bytes()...)
+ b = append(b, 0x5a, 0xf1) // GAS; CALL
+ return append(b, tail...)
+}
+
+// deployCode builds bytecode that MSTOREs init and runs CREATE/CREATE2 with value.
+func deployCode(init []byte, create2 bool, value byte) []byte {
+ word := make([]byte, 32)
+ copy(word[32-len(init):], init)
+ off, sz := byte(32-len(init)), byte(len(init))
+ b := append([]byte{0x7f}, word...) // PUSH32 init-word
+ b = append(b, 0x60, 0x00, 0x52) // PUSH1 0; MSTORE
+ if create2 {
+ b = append(b, 0x60, 0x00, 0x60, sz, 0x60, off, 0x60, value, 0xf5) // salt,size,off,value; CREATE2
+ } else {
+ b = append(b, 0x60, sz, 0x60, off, 0x60, value, 0xf0) // size,off,value; CREATE
+ }
+ return append(b, 0x00) // STOP
+}
+
+func fund(addr common.Address, wei int64) func(*state.StateDB, common.Address) {
+ return func(db *state.StateDB, _ common.Address) {
+ db.AddBalance(addr, uint256.NewInt(uint64(wei)), tracing.BalanceChangeUnspecified)
+ }
+}
+
+// ====================== CALL* new-account state-gas =======================
+
+// CALL with value to a non-existent account charges one account creation.
+func TestCallValueToNewAccount(t *testing.T) {
+ _, res, err := run8037(t, callCode(freshAddr, 1, stop), hugeBudget(), new(uint256.Int), fund(common.BytesToAddress([]byte("self")), 10))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != stateGasNewAccount {
+ t.Fatalf("state gas = %d, want %d", res.UsedStateGas, stateGasNewAccount)
+ }
+}
+
+// CALL with value to an existing (code-bearing) account is not charged.
+func TestCallValueToExistingAccount(t *testing.T) {
+ setup := func(db *state.StateDB, self common.Address) {
+ db.CreateAccount(existAddr)
+ db.SetCode(existAddr, stop, tracing.CodeChangeUnspecified)
+ db.AddBalance(self, uint256.NewInt(10), tracing.BalanceChangeUnspecified)
+ }
+ _, res, err := run8037(t, callCode(existAddr, 1, stop), hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
+ }
+}
+
+// CALL with zero value creates no account, so nothing is charged.
+func TestCallZeroValueToNewAccount(t *testing.T) {
+ _, res, err := run8037(t, callCode(freshAddr, 0, stop), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
+ }
+}
+
+// CALL that fails before the child frame (insufficient balance) refills the charge.
+func TestCallInsufficientBalanceRefill(t *testing.T) {
+ // self has no balance, so the value transfer fails the CanTransfer check.
+ _, res, err := run8037(t, callCode(freshAddr, 1, stop), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
+ }
+}
+
+// A new-account charge is refilled when its frame reverts.
+func TestCallChildRevertRefill(t *testing.T) {
+ code := callCode(freshAddr, 1, revertTail)
+ _, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), fund(common.BytesToAddress([]byte("self")), 10))
+ if err != ErrExecutionReverted {
+ t.Fatalf("err = %v, want revert", err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
+ }
+}
+
+// A new-account charge is refilled when its frame halts exceptionally.
+func TestCallChildExceptionalHaltRefill(t *testing.T) {
+ code := callCode(freshAddr, 1, invalidTail)
+ _, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), fund(common.BytesToAddress([]byte("self")), 10))
+ if err == nil || err == ErrExecutionReverted {
+ t.Fatalf("err = %v, want exceptional halt", err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
+ }
+}
+
+// An account with balance but no code/nonce is existent: no account charge.
+func TestCallBalanceOnlyAccountIsExistent(t *testing.T) {
+ setup := func(db *state.StateDB, self common.Address) {
+ db.AddBalance(balanceAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
+ db.AddBalance(self, uint256.NewInt(10), tracing.BalanceChangeUnspecified)
+ }
+ _, res, err := run8037(t, callCode(balanceAddr, 1, stop), hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
+ }
+}
+
+// ===================== CREATE / CREATE2 state-gas =========================
+
+// CREATE to a fresh address charges account creation plus code deposit.
+func TestCreateNewAccount(t *testing.T) {
+ _, res, err := run8037(t, deployCode(deploy3Init, false, 0), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := stateGasNewAccount + stateDeposit; res.UsedStateGas != want {
+ t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want)
+ }
+}
+
+// CREATE onto a pre-existing (balance-only) leaf refills the account portion;
+// only the code deposit is charged.
+func TestCreatePreexistingTarget(t *testing.T) {
+ setup := func(db *state.StateDB, self common.Address) {
+ derived := crypto.CreateAddress(self, db.GetNonce(self))
+ db.AddBalance(derived, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
+ }
+ _, res, err := run8037(t, deployCode(deploy3Init, false, 0), hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != stateDeposit {
+ t.Fatalf("state gas = %d, want %d", res.UsedStateGas, stateDeposit)
+ }
+}
+
+// CREATE whose init code reverts refills the account charge and deposits nothing.
+func TestCreateInitRevertRefill(t *testing.T) {
+ _, res, err := run8037(t, deployCode(revertInit, false, 0), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
+ }
+}
+
+// CREATE whose init code halts exceptionally refills the account charge.
+func TestCreateInitOOGRefill(t *testing.T) {
+ _, res, err := run8037(t, deployCode(invalidInit, false, 0), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
+ }
+}
+
+// CREATE onto an address collision (existing nonce) refills the account charge.
+func TestCreateAddressCollisionRefill(t *testing.T) {
+ setup := func(db *state.StateDB, self common.Address) {
+ derived := crypto.CreateAddress(self, db.GetNonce(self))
+ db.SetNonce(derived, 1, tracing.NonceChangeUnspecified)
+ }
+ _, res, err := run8037(t, deployCode(deploy3Init, false, 0), hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
+ }
+}
+
+// CREATE with value exceeding balance fails before the frame and is refilled.
+func TestCreateInsufficientBalanceRefill(t *testing.T) {
+ // self has no balance; CREATE forwards value 1.
+ _, res, err := run8037(t, deployCode(deploy3Init, false, 1), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
+ }
+}
+
+// CREATE2 charges account creation plus code deposit identically to CREATE.
+func TestCreate2SameSemantics(t *testing.T) {
+ _, res, err := run8037(t, deployCode(deploy3Init, true, 0), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := stateGasNewAccount + stateDeposit; res.UsedStateGas != want {
+ t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want)
+ }
+}
+
+// The code-deposit portion is charged per byte independently of the account
+// charge: the delta between a 3-byte and 0-byte deploy is exactly 3 x CPSB.
+func TestCreateCodeDepositChargedSeparately(t *testing.T) {
+ _, big3, err := run8037(t, deployCode(deploy3Init, false, 0), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, big0, err := run8037(t, deployCode(deploy0Init, false, 0), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := big3.UsedStateGas - big0.UsedStateGas; got != stateDeposit {
+ t.Fatalf("deposit delta = %d, want %d", got, stateDeposit)
+ }
+}
+
+// ========================= SELFDESTRUCT state-gas =========================
+
+// selfdestruct sending balance to a non-existent beneficiary creates it.
+func TestSelfdestructCreatesNewAccount(t *testing.T) {
+ code := append([]byte{0x73}, freshAddr.Bytes()...) // PUSH20 beneficiary
+ code = append(code, 0xff) // SELFDESTRUCT
+ _, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), fund(common.BytesToAddress([]byte("self")), 10))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != stateGasNewAccount {
+ t.Fatalf("state gas = %d, want %d", res.UsedStateGas, stateGasNewAccount)
+ }
+}
+
+// selfdestruct to an existing beneficiary creates no account.
+func TestSelfdestructToExistingAccount(t *testing.T) {
+ setup := func(db *state.StateDB, self common.Address) {
+ db.AddBalance(existAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
+ db.AddBalance(self, uint256.NewInt(10), tracing.BalanceChangeUnspecified)
+ }
+ code := append([]byte{0x73}, existAddr.Bytes()...)
+ code = append(code, 0xff)
+ _, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
+ }
+}
+
+// A contract created and self-destructed in the same tx gets no refill: the
+// account-creation charge stands.
+func TestSelfdestructSameTxAccountNoRefill(t *testing.T) {
+ // init code selfdestructs to self (existing), so only the create charges.
+ self := common.BytesToAddress([]byte("self"))
+ init := append([]byte{0x73}, self.Bytes()...)
+ init = append(init, 0xff)
+ _, res, err := run8037(t, deployCode(init, false, 0), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != stateGasNewAccount {
+ t.Fatalf("state gas = %d, want %d (no refill)", res.UsedStateGas, stateGasNewAccount)
+ }
+}
+
+// selfdestruct of a pre-existing account refills nothing (EIP-6780: not removed).
+func TestSelfdestructPreexistingNoRefill(t *testing.T) {
+ setup := func(db *state.StateDB, self common.Address) {
+ db.AddBalance(existAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
+ }
+ code := append([]byte{0x73}, existAddr.Bytes()...)
+ code = append(code, 0xff)
+ _, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
+ }
+}
+
+// ===================== Reservoir / gas_left mechanics =====================
+
+// State-gas is drawn from the reservoir first: a charge within reservoir size
+// does not spill into regular gas.
+func TestReservoirDrawnFirst(t *testing.T) {
+ _, res, err := run8037(t, sstore(0, 1), NewGasBudget(1_000_000, 200_000), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.Spilled != 0 {
+ t.Fatalf("spilled = %d, want 0", res.Spilled)
+ }
+ if want := uint64(200_000 - stateGasNewSlot); res.StateGas != want {
+ t.Fatalf("reservoir left = %d, want %d", res.StateGas, want)
+ }
+}
+
+// The GAS opcode returns gas_left only, excluding the reservoir.
+func TestGasOpcodeExcludesReservoir(t *testing.T) {
+ code := []byte{0x5a, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3} // GAS; MSTORE; RETURN(32)
+ ret, _, err := run8037(t, code, NewGasBudget(1_000_000, 500_000), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := new(uint256.Int).SetBytes(ret).Uint64(); got != 1_000_000-GasQuickStep {
+ t.Fatalf("GAS = %d, want %d (reservoir excluded)", got, 1_000_000-GasQuickStep)
+ }
+}
+
+// Refills are LIFO: borrowed regular gas is repaid before the reservoir. With a
+// zero reservoir, a 0->x->0 SSTORE repays the spill and leaves the reservoir at 0.
+func TestLIFORefillOrder(t *testing.T) {
+ code := append(sstore(0, 1), sstore(0, 0)...)
+ _, res, err := run8037(t, code, NewGasBudget(1_000_000, 0), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.Spilled != 0 || res.StateGas != 0 || res.UsedStateGas != 0 {
+ t.Fatalf("after LIFO refill: spilled=%d reservoir=%d used=%d, want 0/0/0", res.Spilled, res.StateGas, res.UsedStateGas)
+ }
+}
+
+// State-gas charged inside a child frame is refilled at the frame boundary when
+// the child reverts or halts.
+func TestStateGasMeteredAtFrameBoundary(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ tail []byte
+ }{
+ {"revert", revertTail},
+ {"halt", invalidTail},
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ childCode := append(sstore(0, 1), tt.tail...)
+ setup := func(db *state.StateDB, self common.Address) {
+ db.CreateAccount(childAddr)
+ db.SetCode(childAddr, childCode, tracing.CodeChangeUnspecified)
+ }
+ _, res, err := run8037(t, callCode(childAddr, 0, stop), hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedStateGas != 0 {
+ t.Fatalf("state gas = %d, want 0 (refilled at boundary)", res.UsedStateGas)
+ }
+ })
+ }
+}
+
+// ===================== LIFO refill vector invariant =========================
+
+// Charge A then B (both spilling into regular because the reservoir is too
+// small), then refill only A. The refill must repay the borrowed regular gas
+// first (Spilled -> 0) before crediting the reservoir, leaving B outstanding.
+func TestLIFORefillRepaysRegularBeforeReservoir(t *testing.T) {
+ initial := NewGasBudget(1000, 100) // reservoir covers only 100 of state gas
+ b := initial
+
+ b.ChargeState(150) // A: 100 from reservoir, 50 spills into regular
+ b.ChargeState(30) // B: reservoir empty, all 30 spills
+ if b.Spilled != 80 || b.StateGas != 0 {
+ t.Fatalf("after A+B: spilled=%d reservoir=%d, want 80/0", b.Spilled, b.StateGas)
+ }
+
+ b.RefundState(150) // refill A: repay 80 to regular first, 70 tops reservoir
+ if b.Spilled != 0 {
+ t.Fatalf("spilled=%d, want 0 (regular repaid before reservoir)", b.Spilled)
+ }
+ if b.StateGas != 70 {
+ t.Fatalf("reservoir=%d, want 70 (remainder after repaying regular)", b.StateGas)
+ }
+ assertBudgetSane(t, initial, b)
+}
+
+// Fuzz arbitrary sequences of state/regular charges and LIFO refills around the
+// reservoir/spill boundary: the GasBudget vector must stay self-consistent after
+// every op and across all three frame-exit forms, and refilling every charge
+// must restore the state side exactly (reservoir to initial, nothing borrowed).
+func TestLIFOVectorInvariantUnderRandomOps(t *testing.T) {
+ rng := rand.New(rand.NewSource(8037))
+ for trial := 0; trial < 2000; trial++ {
+ initial := NewGasBudget(1_000_000, uint64(rng.Intn(1000)))
+ b := initial
+ outstanding := int64(0) // state-gas charged but not yet refilled
+ for step := 0; step < 40; step++ {
+ switch rng.Intn(3) {
+ case 0: // state charge (may spill into regular)
+ if s := uint64(rng.Intn(400)); b.CanAfford(GasCosts{StateGas: s}) {
+ b.ChargeState(s)
+ outstanding += int64(s)
+ }
+ case 1: // regular charge
+ if r := uint64(rng.Intn(400)); b.CanAfford(GasCosts{RegularGas: r}) {
+ b.ChargeRegular(r)
+ }
+ case 2: // LIFO refill of part of the outstanding state gas
+ if outstanding > 0 {
+ s := uint64(rng.Int63n(outstanding) + 1)
+ b.RefundState(s)
+ outstanding -= int64(s)
+ }
+ }
+ assertBudgetSane(t, initial, b)
+ assertBudgetSane(t, initial, b.ExitSuccess())
+ assertBudgetSane(t, initial, b.ExitRevert())
+ assertBudgetSane(t, initial, b.ExitHalt())
+ }
+ if outstanding > 0 {
+ b.RefundState(uint64(outstanding))
+ }
+ if b.Spilled != 0 || b.StateGas != initial.StateGas || b.UsedStateGas != 0 {
+ t.Fatalf("trial %d: after full refill spilled=%d reservoir=%d used=%d, want 0/%d/0",
+ trial, b.Spilled, b.StateGas, b.UsedStateGas, initial.StateGas)
+ }
+ }
+}
+
+// ================== Halting frame terminal state (nested) ===================
+
+func concat(parts ...[]byte) []byte {
+ var b []byte
+ for _, p := range parts {
+ b = append(b, p...)
+ }
+ return b
+}
+
+// assertHalted checks the predictable terminal budget of an exceptionally
+// halted frame: regular gas fully consumed, state restored to the frame's
+// initial reservoir, and no net state-gas used.
+func assertHalted(t *testing.T, initial, got GasBudget) {
+ t.Helper()
+ if got.RegularGas != 0 {
+ t.Fatalf("RegularGas = %d, want 0 (gas_left consumed on halt)", got.RegularGas)
+ }
+ if got.StateGas != initial.StateGas {
+ t.Fatalf("StateGas = %d, want %d (reservoir restored)", got.StateGas, initial.StateGas)
+ }
+ if got.UsedStateGas != 0 {
+ t.Fatalf("UsedStateGas = %d, want 0 (all refilled)", got.UsedStateGas)
+ }
+}
+
+var (
+ haltGrandchild = common.BytesToAddress([]byte("grandchild"))
+ haltOKChild = common.BytesToAddress([]byte("child-ok")) // succeeds, calls grandchild
+ haltBadChild = common.BytesToAddress([]byte("child-halt")) // SSTOREs then INVALID
+)
+
+// haltFrameChildren is a run8037 setup that funds self and deploys a 3-level
+// child set: a success child that itself calls a grandchild, and a halting child.
+func haltFrameChildren(db *state.StateDB, self common.Address) {
+ db.AddBalance(self, uint256.NewInt(1000), tracing.BalanceChangeUnspecified)
+ db.CreateAccount(haltGrandchild)
+ db.SetCode(haltGrandchild, concat(sstore(5, 5), []byte{0x00}), tracing.CodeChangeUnspecified) // new slot; STOP
+ db.CreateAccount(haltOKChild)
+ db.SetCode(haltOKChild, concat(sstore(1, 1), callCode(haltGrandchild, 0, nil), []byte{0x00}), tracing.CodeChangeUnspecified)
+ db.CreateAccount(haltBadChild)
+ db.SetCode(haltBadChild, concat(sstore(3, 3), []byte{0xfe}), tracing.CodeChangeUnspecified) // new slot; INVALID
+}
+
+// A frame that charges state, drives a successful child (with a grandchild), a
+// halting child and a new-account call, then halts, returns the predictable
+// terminal budget regardless of all the descendant activity.
+func TestHaltFrameTerminalState(t *testing.T) {
+ top := concat(
+ sstore(0, 1), // self: new slot
+ callCode(haltOKChild, 0, nil), // child + grandchild succeed
+ callCode(haltBadChild, 0, nil), // descendant halts (contained)
+ callCode(freshAddr, 1, nil), // new-account charge
+ []byte{0xfe}, // this frame halts
+ )
+ initial := NewGasBudget(2_000_000, 300_000)
+ _, res, err := run8037(t, top, initial, new(uint256.Int), haltFrameChildren)
+ if err == nil || err == ErrExecutionReverted {
+ t.Fatalf("err = %v, want exceptional halt", err)
+ }
+ assertHalted(t, initial, res)
+}
+
+// Fuzz: arbitrary sequences of state writes, child calls (success / halting) and
+// new-account calls, always terminated by INVALID. However the descendants
+// behave, a halted frame's terminal budget is always (0, initial reservoir, 0).
+func TestHaltFrameTerminalStateFuzz(t *testing.T) {
+ rng := rand.New(rand.NewSource(80371))
+ for trial := 0; trial < 400; trial++ {
+ steps := [][]byte{
+ sstore(byte(1+rng.Intn(20)), 1),
+ callCode(haltOKChild, 0, nil),
+ callCode(haltBadChild, 0, nil),
+ callCode(freshAddr, 1, nil),
+ }
+ var code []byte
+ for n := 1 + rng.Intn(8); n > 0; n-- {
+ code = append(code, steps[rng.Intn(len(steps))]...)
+ }
+ code = append(code, 0xfe) // halt
+ initial := NewGasBudget(3_000_000, uint64(rng.Intn(400_000)))
+ _, res, err := run8037(t, code, initial, new(uint256.Int), haltFrameChildren)
+ if err == nil || err == ErrExecutionReverted {
+ t.Fatalf("trial %d: err = %v, want halt", trial, err)
+ }
+ assertHalted(t, initial, res)
+ }
+}