mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-23 23:24:30 +00:00
739 lines
28 KiB
Go
739 lines
28 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/>.
|
|
|
|
// 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)
|
|
}
|
|
}
|