mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-29 17:57:36 +00:00
Implements spec change https://github.com/ethereum/EIPs/pull/11807 This PR resolves the conflict between the EIP-7928 and EIP-8037. Specifically in contract deployment, EIP-7928 requires to not resolve the deployed account until it's accessed, while in EIP-8037, the early access is required to determine if the account-creation should be charged or not. This PR addresses this conflict by changing the EIP-8037 a bit, unconditionally charge the account creation in CREATE Family (CreateTx, Create/Create2 opcode) and refunds the associated gas cost if the account creation doesn't happen ultimately. Checkout https://hackmd.io/@bFEBbZiVSAO0IURh9qzEFg/BJmFYqCeGl for more details What's more, now the LIFO mechanism is used for refilling the state cost in frame revert, frame halt, state opcode refunds.
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)
|
|
}
|
|
}
|