mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-20 11:46:44 +00:00
Implement the spec changes of EIP-2780 and EIP-8037.
See the spec diffs in
- https://github.com/ethereum/EIPs/pull/11844
- https://github.com/ethereum/EIPs/pull/11891
- https://github.com/ethereum/EIPs/pull/11906
-
a4801f3bb1
---------
Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
904 lines
35 KiB
Go
904 lines
35 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 (
|
|
"errors"
|
|
"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
|
|
}
|
|
|
|
// mkCommittedState is mkState with the allocation committed to disk and
|
|
// reloaded. EIP-161-empty accounts carrying only storage do not survive an
|
|
// in-memory Finalise; committing without empty-account deletion reproduces
|
|
// the synthesized prestate an EIP-7610 fixture would load from disk.
|
|
func mkCommittedState(t *testing.T, alloc types.GenesisAlloc) *state.StateDB {
|
|
t.Helper()
|
|
db := state.NewDatabaseForTesting()
|
|
sdb, _ := state.New(types.EmptyRootHash, db)
|
|
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)
|
|
}
|
|
}
|
|
root, err := sdb.Commit(0, false, false)
|
|
if err != nil {
|
|
t.Fatalf("commit prestate: %v", err)
|
|
}
|
|
sdb, err = state.New(root, db)
|
|
if err != nil {
|
|
t.Fatalf("reopen prestate: %v", err)
|
|
}
|
|
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)
|
|
|
|
evm.SetTxContext(NewEVMTxContext(msg))
|
|
st := newStateTransition(evm, msg, gp)
|
|
res, err := st.execute()
|
|
if err == nil && res != nil {
|
|
floor, ferr := FloorDataGas(rules8037, msg.From, msg.To, msg.Value, msg.Data, msg.AccessList)
|
|
if ferr != nil {
|
|
t.Fatalf("floor data gas: %v", ferr)
|
|
}
|
|
assertPoolSane(t, res, gp, floor)
|
|
|
|
intrinsic, ierr := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules8037)
|
|
if ierr != nil {
|
|
t.Fatalf("intrinsic gas: %v", ierr)
|
|
}
|
|
executionGas := msg.GasLimit - intrinsic
|
|
gasLeft := min(params.MaxTxGas-intrinsic, executionGas)
|
|
assertBudgetSane(t, vm.NewGasBudget(gasLeft, executionGas-gasLeft), 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
|
|
// regular: cumulativeRegular <= max(res.MaxUsedGas - cumulativeState, floor)
|
|
// (the calldata floor pads the regular dimension alone, so the
|
|
// dimension sum may exceed the pre-refund peak when it binds)
|
|
// bottleneck: Used() == max(cumulativeRegular, cumulativeState) <= initial
|
|
func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool, floor uint64) {
|
|
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 gp.cumulativeState > res.MaxUsedGas {
|
|
t.Fatalf("state %d exceeds peak %d", gp.cumulativeState, res.MaxUsedGas)
|
|
}
|
|
if cap := max(res.MaxUsedGas-gp.cumulativeState, floor); gp.cumulativeRegular > cap {
|
|
t.Fatalf("regular %d exceeds pre-refund cap %d (peak %d, state %d, floor %d)",
|
|
gp.cumulativeRegular, cap, res.MaxUsedGas, gp.cumulativeState, floor)
|
|
}
|
|
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
|
|
haltI = []byte{0xfe, 0x00, 0x00, 0x00, 0x00} // init: INVALID, exceptional halt
|
|
)
|
|
|
|
// ===================== Top-level create transaction ======================
|
|
|
|
// A creation tx's intrinsic gas is state-independent: the new-account state
|
|
// charge depends on whether the deployment target exists and is charged at
|
|
// runtime (EIP-2780), not intrinsically.
|
|
func TestCreateTxIntrinsicNoStateGas(t *testing.T) {
|
|
cost, err := IntrinsicGas(nil, nil, nil, common.Address{}, nil, nil, rules8037)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := params.TxBaseCost2780 + params.CreateAccessAmsterdam; cost != want {
|
|
t.Fatalf("intrinsic gas = %d, want %d", cost, want)
|
|
}
|
|
}
|
|
|
|
// Creating onto a pre-existing (balance-only) address incurs no new-account
|
|
// runtime charge; 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 applied at
|
|
// runtime.
|
|
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. The colliding target exists, so no
|
|
// new-account state gas is charged at runtime in the first place.
|
|
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 (never charged)", gp.cumulativeState)
|
|
}
|
|
// All forwarded gas_left is burned: the whole gas limit is consumed as
|
|
// regular gas.
|
|
if want := uint64(gas); gp.cumulativeRegular != want {
|
|
t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want)
|
|
}
|
|
}
|
|
|
|
// An account can exist yet be EIP-161-empty in the middle of a transaction,
|
|
// e.g. after being touched as the zero-balance beneficiary of a SELFDESTRUCT.
|
|
// Deploying onto such an account should charge account-creation cost.
|
|
func TestCreate2TransientEmptyDestNoRefill(t *testing.T) {
|
|
var (
|
|
orchestrator = common.HexToAddress("0xc0de000000000000000000000000000000000002")
|
|
destructor = common.HexToAddress("0xc0de000000000000000000000000000000000003")
|
|
target = crypto.CreateAddress2(orchestrator, [32]byte{}, crypto.Keccak256(deploy3))
|
|
)
|
|
// destructor: SELFDESTRUCT with zero balance to the future CREATE2 target,
|
|
// leaving it existing but EIP-161-empty for the rest of the transaction.
|
|
destructorCode := append(append([]byte{0x73}, target.Bytes()...), 0xff) // PUSH20 target, SELFDESTRUCT
|
|
|
|
// orchestrator: CALL destructor (persist the success flag in slot 0),
|
|
// then CREATE2 deploy3 with salt 0, targeting the touched address.
|
|
code := []byte{
|
|
0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, // ret/arg sizes and offsets, value = 0
|
|
0x73, // PUSH20 destructor
|
|
}
|
|
code = append(code, destructor.Bytes()...)
|
|
code = append(code,
|
|
0x62, 0x03, 0x0d, 0x40, // PUSH3 200,000 call gas
|
|
0xf1, // CALL
|
|
0x60, 0x00, 0x55, // SSTORE the call result at slot 0
|
|
0x64, 0x60, 0x03, 0x60, 0x00, 0xf3, // PUSH5 deploy3 init code
|
|
0x60, 0x00, 0x52, // MSTORE at word 0 (right-aligned, code at offset 27)
|
|
0x60, 0x00, // salt = 0
|
|
0x60, 0x05, // size = 5
|
|
0x60, 0x1b, // offset = 27
|
|
0x60, 0x00, // endowment = 0
|
|
0xf5, 0x50, // CREATE2, POP
|
|
0x00, // STOP
|
|
)
|
|
sdb := mkState(senderAlloc(types.GenesisAlloc{
|
|
orchestrator: {Code: code},
|
|
destructor: {Code: destructorCode},
|
|
}))
|
|
res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, 2_000_000, nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Failed() {
|
|
t.Fatalf("execution failed: %v", res.Err)
|
|
}
|
|
// The inner call must have succeeded, so the target was touched into an
|
|
// existing-but-empty account before the CREATE2 executed.
|
|
if flag := sdb.GetState(orchestrator, common.Hash{}); flag != common.BigToHash(big.NewInt(1)) {
|
|
t.Fatalf("destructor call flag = %v, want 1", flag)
|
|
}
|
|
if code := sdb.GetCode(target); len(code) != 3 {
|
|
t.Fatalf("deployed code length = %d, want 3", len(code))
|
|
}
|
|
// State gas: the orchestrator's flag slot, the created contract account
|
|
// (charged, not refilled) and the 3-byte code deposit.
|
|
want := newSlotState + newAccountState + uint64(3*params.CostPerStateByte)
|
|
if gp.cumulativeState != want {
|
|
t.Fatalf("state gas = %d, want %d (account creation must not be refilled)", gp.cumulativeState, want)
|
|
}
|
|
}
|
|
|
|
// ========== Storage-only (EIP-7610-shaped) deployment destination ===========
|
|
//
|
|
// A destination carrying storage while having zero nonce, zero balance and
|
|
// empty code is EIP-161-empty, so the account-creation state gas is
|
|
// pre-charged in the parent frame.
|
|
|
|
// create2Orchestrator returns runtime code that CREATE2-deploys the given
|
|
// 5-byte init code with salt 0 and stores the result address at slot 0.
|
|
func create2Orchestrator(initCode []byte) []byte {
|
|
code := append([]byte{0x64}, initCode...) // PUSH5 init code
|
|
return append(code,
|
|
0x60, 0x00, 0x52, // MSTORE at word 0 (right-aligned, code at offset 27)
|
|
0x60, 0x00, // salt = 0
|
|
0x60, 0x05, // size = 5
|
|
0x60, 0x1b, // offset = 27
|
|
0x60, 0x00, // endowment = 0
|
|
0xf5, // CREATE2
|
|
0x60, 0x00, 0x55, // SSTORE the result address at slot 0
|
|
0x00, // STOP
|
|
)
|
|
}
|
|
|
|
// storageOnlyAlloc allocates the orchestrator and its CREATE2 target, the
|
|
// latter carrying a single storage slot while remaining EIP-161-empty.
|
|
func storageOnlyAlloc(orchestrator common.Address, initCode []byte) (types.GenesisAlloc, common.Address) {
|
|
target := crypto.CreateAddress2(orchestrator, [32]byte{}, crypto.Keccak256(initCode))
|
|
return types.GenesisAlloc{
|
|
orchestrator: {Code: create2Orchestrator(initCode)},
|
|
target: {Storage: map[common.Hash]common.Hash{{}: common.BigToHash(big.NewInt(1))}},
|
|
}, target
|
|
}
|
|
|
|
// Deploying onto a storage-only destination pre-charges the account creation.
|
|
// Under the registry-based EIP-7610 check the creation proceeds, so the
|
|
// charge is consumed like any other creation.
|
|
func TestCreate2StorageOnlyDestCharged(t *testing.T) {
|
|
orchestrator := common.HexToAddress("0xc0de000000000000000000000000000000000004")
|
|
alloc, target := storageOnlyAlloc(orchestrator, deploy3)
|
|
sdb := mkCommittedState(t, senderAlloc(alloc))
|
|
res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, 1_000_000, nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Failed() {
|
|
t.Fatalf("execution failed: %v", res.Err)
|
|
}
|
|
if code := sdb.GetCode(target); len(code) != 3 {
|
|
t.Fatalf("deployed code length = %d, want 3", len(code))
|
|
}
|
|
// The created account (charged, consumed), the orchestrator's result slot
|
|
// and the 3-byte code deposit.
|
|
want := newAccountState + newSlotState + uint64(3*params.CostPerStateByte)
|
|
if gp.cumulativeState != want {
|
|
t.Fatalf("state gas = %d, want %d", gp.cumulativeState, want)
|
|
}
|
|
}
|
|
|
|
// If the pre-charge succeeds and the create frame then fails, only the create
|
|
// frame halts: the forwarded regular gas is burnt, the account-creation
|
|
// charge is refilled, and the parent frame continues.
|
|
func TestCreate2StorageOnlyDestRefillOnFrameHalt(t *testing.T) {
|
|
const gas = 1_000_000
|
|
orchestrator := common.HexToAddress("0xc0de000000000000000000000000000000000005")
|
|
alloc, target := storageOnlyAlloc(orchestrator, haltI)
|
|
sdb := mkCommittedState(t, senderAlloc(alloc))
|
|
res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, gas, nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Failed() {
|
|
t.Fatalf("parent frame must survive the create-frame halt: %v", res.Err)
|
|
}
|
|
// The CREATE2 pushed zero and nothing was deployed.
|
|
if flag := sdb.GetState(orchestrator, common.Hash{}); flag != (common.Hash{}) {
|
|
t.Fatalf("create result = %v, want 0", flag)
|
|
}
|
|
if code := sdb.GetCode(target); len(code) != 0 {
|
|
t.Fatalf("deployed code length = %d, want 0", len(code))
|
|
}
|
|
// The account-creation charge was refilled in full.
|
|
if gp.cumulativeState != 0 {
|
|
t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
|
|
}
|
|
if res.UsedGas > gas-newAccountState {
|
|
t.Fatalf("used gas = %d, want at most %d (charge not refilled?)", res.UsedGas, gas-newAccountState)
|
|
}
|
|
}
|
|
|
|
// If the remaining gas cannot cover the account-creation pre-charge, the
|
|
// parent frame itself halts with out-of-gas instead of the create frame.
|
|
func TestCreate2StorageOnlyDestPrechargeOOG(t *testing.T) {
|
|
// Enough for the CREATE2 constant cost, short of the 183,600 pre-charge.
|
|
const gas = 150_000
|
|
orchestrator := common.HexToAddress("0xc0de000000000000000000000000000000000006")
|
|
alloc, _ := storageOnlyAlloc(orchestrator, deploy3)
|
|
sdb := mkCommittedState(t, senderAlloc(alloc))
|
|
res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, gas, nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !res.Failed() || !errors.Is(res.Err, vm.ErrOutOfGas) {
|
|
t.Fatalf("err = %v, want out of gas in the parent frame", res.Err)
|
|
}
|
|
if gp.cumulativeState != 0 {
|
|
t.Fatalf("state gas = %d, want 0 (charge never applied)", gp.cumulativeState)
|
|
}
|
|
// The parent is the topmost frame, so its halt burns the whole gas limit.
|
|
if gp.cumulativeRegular != gas {
|
|
t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, gas)
|
|
}
|
|
}
|
|
|
|
// ======================== 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 := 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")
|
|
}
|
|
}
|
|
|
|
// The EIP-7623/7976 calldata floor is capped by MaxTxGas even when the gas
|
|
// limit covers it: a transaction whose floor cost exceeds the cap is rejected
|
|
// regardless of its (much smaller) intrinsic gas.
|
|
func TestValidationFloorCostCap(t *testing.T) {
|
|
// All-zero calldata: the floor charges 64/byte while the intrinsic
|
|
// charges only 4/byte, so the floor crosses the cap long before the
|
|
// intrinsic does.
|
|
data := make([]byte, 300_000) // floor ~19.2M > 16.77M cap, intrinsic ~1.2M
|
|
floor, err := FloorDataGas(rules8037, senderAddr, &senderAddr, new(uint256.Int), data, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
intrinsic, err := IntrinsicGas(data, nil, nil, senderAddr, &senderAddr, new(uint256.Int), rules8037)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if floor <= params.MaxTxGas || intrinsic > params.MaxTxGas {
|
|
t.Fatalf("setup: floor %d must exceed cap %d while intrinsic %d stays below",
|
|
floor, params.MaxTxGas, intrinsic)
|
|
}
|
|
// The gas limit covers the floor, so the rejection can only come from
|
|
// the MaxTxGas cap on the floor cost.
|
|
tx := callTx(0, senderAddr, 0, floor+1_000_000, data)
|
|
if _, _, err := applyMsg(t, mkState(senderAlloc(nil)), tx); !errors.Is(err, ErrFloorDataGas) {
|
|
t.Fatalf("expected ErrFloorDataGas, got %v", err)
|
|
}
|
|
}
|
|
|
|
// ========================= 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, 4)
|
|
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
|
|
to := common.HexToAddress("0xeeee")
|
|
floor, _ := FloorDataGas(rules8037, senderAddr, &to, new(uint256.Int), data, nil)
|
|
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, senderAddr, &c, new(uint256.Int), 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 := 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 charges only the state-independent per-authorization base;
|
|
// the state-dependent charges are applied at runtime (EIP-2780).
|
|
func TestAuthIntrinsicBaseOnly(t *testing.T) {
|
|
cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, common.Address{}, &delegate8037, nil, rules8037)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The recipient touch and the per-authorization authority access (priced
|
|
// into RegularPerAuthBaseCost) are both charged at the cold rate
|
|
// unconditionally at the intrinsic phase (EIP-2780).
|
|
want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + params.RegularPerAuthBaseCost
|
|
if cost != want {
|
|
t.Fatalf("intrinsic gas = %d, want %d", cost, want)
|
|
}
|
|
}
|
|
|
|
// An invalid authorization incurs no runtime 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 is not charged for an account leaf; only the
|
|
// net-new indicator bytes are charged at runtime.
|
|
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 (indicator only)", gp.cumulativeState, authBaseState)
|
|
}
|
|
}
|
|
|
|
// Setting a delegation on an already-delegated authority writes no net-new
|
|
// bytes (and no account leaf, since the authority exists): no state charge.
|
|
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 (nothing net-new)", gp.cumulativeState)
|
|
}
|
|
}
|
|
|
|
// A net-new delegation on a fresh authority is charged the account leaf plus
|
|
// the indicator bytes at runtime.
|
|
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 (leaf + indicator)", gp.cumulativeState, authWorstState)
|
|
}
|
|
}
|
|
|
|
// Clearing a delegation writes no indicator, so only the (new) account leaf is
|
|
// charged at runtime.
|
|
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 := newAccountState; gp.cumulativeState != want {
|
|
t.Fatalf("state gas = %d, want %d (account leaf only)", gp.cumulativeState, want)
|
|
}
|
|
}
|
|
|
|
// 0->a->0 in one tx: the indicator charge applies when the delegation is set
|
|
// and is never credited back when a later auth clears it in the same
|
|
// transaction.
|
|
func TestAuthClearSameTxNoRefill(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 := authWorstState; gp.cumulativeState != want {
|
|
t.Fatalf("state gas = %d, want %d (indicator charge kept on clear)", gp.cumulativeState, want)
|
|
}
|
|
}
|
|
|
|
// 0->a->0->b in one tx: the indicator charge applies at most once per
|
|
// authority — re-installing a delegation after an intra-tx clear is free.
|
|
func TestAuthSetClearSetChargedOnce(t *testing.T) {
|
|
set, _ := signAuth(t, authKeyA, delegate8037, 0)
|
|
clr, _ := signAuth(t, authKeyA, common.Address{}, 1)
|
|
set2, authority := signAuth(t, authKeyA, common.HexToAddress("0xde1e8a7f"), 2)
|
|
sdb := mkState(senderAlloc(nil))
|
|
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{set, clr, set2}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The final delegation is installed and the indicator was paid exactly once.
|
|
if _, delegated := types.ParseDelegation(sdb.GetCode(authority)); !delegated {
|
|
t.Fatal("final delegation not installed")
|
|
}
|
|
if want := authWorstState; gp.cumulativeState != want {
|
|
t.Fatalf("state gas = %d, want %d (leaf + indicator exactly once)", 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())
|
|
}
|
|
}
|