go-ethereum/core/eip8037_test.go
2026-06-17 16:42:27 +08:00

614 lines
24 KiB
Go

// 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 <http://www.gnu.org/licenses/>.
// 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())
}
}