mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-22 14:44:30 +00:00
plan: investigate lint failure in core/eip8037_test.go
This commit is contained in:
commit
ebf459f8f0
14 changed files with 1689 additions and 121 deletions
614
core/eip8037_test.go
Normal file
614
core/eip8037_test.go
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
// Copyright 2026 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Transaction- and block-level tests for EIP-8037 (multidimensional state-gas
|
||||
// metering). They apply whole transactions and inspect the 2D block gas pool
|
||||
// (cumulativeRegular / cumulativeState) and the receipt/peak figures.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus/beacon"
|
||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/tracing"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg8037 = balChainConfig()
|
||||
signer8037 = types.LatestSigner(cfg8037)
|
||||
rules8037 = cfg8037.Rules(big.NewInt(0), true, 0)
|
||||
senderKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
senderAddr = crypto.PubkeyToAddress(senderKey.PublicKey)
|
||||
|
||||
// state-gas charges in units (CPSB applied).
|
||||
newAccountState = uint64(params.AccountCreationSize * params.CostPerStateByte) // 183,600
|
||||
newSlotState = uint64(params.StorageCreationSize * params.CostPerStateByte) // 97,920
|
||||
authBaseState = uint64(params.AuthorizationCreationSize * params.CostPerStateByte) // 35,190
|
||||
authWorstState = newAccountState + authBaseState // 218,790
|
||||
)
|
||||
|
||||
// mkState builds an in-memory StateDB from a genesis allocation.
|
||||
func mkState(alloc types.GenesisAlloc) *state.StateDB {
|
||||
sdb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
|
||||
for addr, acc := range alloc {
|
||||
sdb.CreateAccount(addr)
|
||||
if acc.Balance != nil {
|
||||
sdb.AddBalance(addr, uint256.MustFromBig(acc.Balance), tracing.BalanceChangeUnspecified)
|
||||
}
|
||||
if acc.Nonce != 0 {
|
||||
sdb.SetNonce(addr, acc.Nonce, tracing.NonceChangeGenesis)
|
||||
}
|
||||
if len(acc.Code) != 0 {
|
||||
sdb.SetCode(addr, acc.Code, tracing.CodeChangeUnspecified)
|
||||
}
|
||||
for k, v := range acc.Storage {
|
||||
sdb.SetState(addr, k, v)
|
||||
}
|
||||
}
|
||||
sdb.Finalise(true)
|
||||
return sdb
|
||||
}
|
||||
|
||||
// amsterdamCoreEVM builds an Amsterdam EVM over statedb with fees disabled.
|
||||
func amsterdamCoreEVM(sdb *state.StateDB) *vm.EVM {
|
||||
ctx := vm.BlockContext{
|
||||
CanTransfer: CanTransfer,
|
||||
Transfer: Transfer,
|
||||
GetHash: func(uint64) common.Hash { return common.Hash{} },
|
||||
BlockNumber: big.NewInt(0),
|
||||
Random: &common.Hash{},
|
||||
Difficulty: big.NewInt(0),
|
||||
BaseFee: big.NewInt(0),
|
||||
BlobBaseFee: big.NewInt(0),
|
||||
GasLimit: 60_000_000,
|
||||
CostPerStateByte: params.CostPerStateByte,
|
||||
}
|
||||
return vm.NewEVM(ctx, sdb, cfg8037, vm.Config{NoBaseFee: true})
|
||||
}
|
||||
|
||||
// applyMsg applies one transaction with a fresh block gas pool and returns the
|
||||
// execution result, the gas pool (for the 2D split) and any consensus error.
|
||||
func applyMsg(t *testing.T, sdb *state.StateDB, tx *types.Transaction) (*ExecutionResult, *GasPool, error) {
|
||||
t.Helper()
|
||||
evm := amsterdamCoreEVM(sdb)
|
||||
msg, err := TransactionToMessage(tx, signer8037, evm.Context.BaseFee)
|
||||
if err != nil {
|
||||
t.Fatalf("to message: %v", err)
|
||||
}
|
||||
gp := NewGasPool(evm.Context.GasLimit)
|
||||
// Drive the stateTransition directly (as ApplyMessage does) so the test can
|
||||
// inspect the final tx-level GasBudget vector via st.gasRemaining.
|
||||
evm.SetTxContext(NewEVMTxContext(msg))
|
||||
st := newStateTransition(evm, msg, gp)
|
||||
res, err := st.execute()
|
||||
if err == nil && res != nil {
|
||||
assertPoolSane(t, res, gp)
|
||||
limit := min(msg.GasLimit, params.MaxTxGas)
|
||||
assertBudgetSane(t, vm.NewGasBudget(limit, msg.GasLimit-limit), st.gasRemaining)
|
||||
}
|
||||
return res, gp, err
|
||||
}
|
||||
|
||||
// assertBudgetSane validates the final tx-level GasBudget vector:
|
||||
//
|
||||
// regular: RegularGas + UsedRegularGas + Spilled == initial.RegularGas
|
||||
// state: StateGas + UsedStateGas == initial.StateGas + Spilled
|
||||
// scalar: Used(initial) == UsedRegularGas + UsedStateGas
|
||||
func assertBudgetSane(t *testing.T, initial, got vm.GasBudget) {
|
||||
t.Helper()
|
||||
if got.RegularGas+got.UsedRegularGas+got.Spilled != initial.RegularGas {
|
||||
t.Fatalf("regular not conserved: R=%d usedR=%d spilled=%d, want sum %d",
|
||||
got.RegularGas, got.UsedRegularGas, got.Spilled, initial.RegularGas)
|
||||
}
|
||||
if int64(got.StateGas)+got.UsedStateGas != int64(initial.StateGas)+int64(got.Spilled) {
|
||||
t.Fatalf("state not conserved: S=%d usedS=%d spilled=%d, want %d+spilled",
|
||||
got.StateGas, got.UsedStateGas, got.Spilled, initial.StateGas)
|
||||
}
|
||||
if int64(got.Used(initial)) != int64(got.UsedRegularGas)+got.UsedStateGas {
|
||||
t.Fatalf("scalar mismatch: used=%d, usedR=%d usedS=%d",
|
||||
got.Used(initial), got.UsedRegularGas, got.UsedStateGas)
|
||||
}
|
||||
}
|
||||
|
||||
// assertPoolSane validates the whole 2D block-gas-pool vector after a single tx.
|
||||
//
|
||||
// receipt: cumulativeUsed == res.UsedGas <= res.MaxUsedGas
|
||||
// pre-refund: cumulativeRegular + cumulativeState <= res.MaxUsedGas (peak)
|
||||
// bottleneck: Used() == max(cumulativeRegular, cumulativeState) <= initial
|
||||
func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool) {
|
||||
t.Helper()
|
||||
if gp.cumulativeUsed != res.UsedGas {
|
||||
t.Fatalf("receipt scalar = %d, want UsedGas %d", gp.cumulativeUsed, res.UsedGas)
|
||||
}
|
||||
if res.UsedGas > res.MaxUsedGas {
|
||||
t.Fatalf("post-refund gas %d exceeds peak %d", res.UsedGas, res.MaxUsedGas)
|
||||
}
|
||||
if sum := gp.cumulativeRegular + gp.cumulativeState; sum > res.MaxUsedGas {
|
||||
t.Fatalf("regular+state %d exceeds peak %d", sum, res.MaxUsedGas)
|
||||
}
|
||||
if gp.Used() != max(gp.cumulativeRegular, gp.cumulativeState) {
|
||||
t.Fatalf("block used %d != max(%d,%d)", gp.Used(), gp.cumulativeRegular, gp.cumulativeState)
|
||||
}
|
||||
if gp.Used() > gp.initial {
|
||||
t.Fatalf("block used %d exceeds limit %d", gp.Used(), gp.initial)
|
||||
}
|
||||
}
|
||||
|
||||
// senderAlloc funds the sender with the given extra accounts merged in.
|
||||
func senderAlloc(extra types.GenesisAlloc) types.GenesisAlloc {
|
||||
alloc := types.GenesisAlloc{senderAddr: {Balance: big.NewInt(1e18)}}
|
||||
for a, acc := range extra {
|
||||
alloc[a] = acc
|
||||
}
|
||||
return alloc
|
||||
}
|
||||
|
||||
// callTx builds a signed dynamic-fee call to `to` with zero fees.
|
||||
func callTx(nonce uint64, to common.Address, value int64, gas uint64, data []byte) *types.Transaction {
|
||||
return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
|
||||
ChainID: cfg8037.ChainID, Nonce: nonce, To: &to, Value: big.NewInt(value),
|
||||
Gas: gas, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// createTx builds a signed contract-creation transaction.
|
||||
func createTx(nonce, gas uint64, initCode []byte) *types.Transaction {
|
||||
return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
|
||||
ChainID: cfg8037.ChainID, Nonce: nonce, To: nil, Value: big.NewInt(0),
|
||||
Gas: gas, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), Data: initCode,
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
deploy3 = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // init: return 3 bytes of code
|
||||
revertI = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // init: REVERT
|
||||
)
|
||||
|
||||
// ===================== Top-level create transaction ======================
|
||||
|
||||
// A creation tx's intrinsic gas pre-charges one account creation as state gas.
|
||||
func TestCreateTxIntrinsicChargesAccountUnconditionally(t *testing.T) {
|
||||
cost, err := IntrinsicGas(nil, nil, nil, true, rules8037, params.CostPerStateByte)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cost.StateGas != newAccountState {
|
||||
t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, newAccountState)
|
||||
}
|
||||
}
|
||||
|
||||
// Creating onto a pre-existing (balance-only) address refills the account
|
||||
// portion; only the code deposit is charged as state gas.
|
||||
func TestCreateTxPreexistingDestRefill(t *testing.T) {
|
||||
derived := crypto.CreateAddress(senderAddr, 0)
|
||||
sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Balance: big.NewInt(1)}}))
|
||||
_, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, deploy3))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := uint64(3 * params.CostPerStateByte); gp.cumulativeState != want {
|
||||
t.Fatalf("state gas = %d, want %d", gp.cumulativeState, want)
|
||||
}
|
||||
}
|
||||
|
||||
// A creation tx that reverts refills the account-creation charge.
|
||||
func TestCreateTxRevertRefill(t *testing.T) {
|
||||
sdb := mkState(senderAlloc(nil))
|
||||
res, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, revertI))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.Failed() {
|
||||
t.Fatal("expected failed creation")
|
||||
}
|
||||
if gp.cumulativeState != 0 {
|
||||
t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
|
||||
}
|
||||
}
|
||||
|
||||
// An address collision burns gas_left while refilling the account charge.
|
||||
func TestCreateTxCollisionConsumesGasLeft(t *testing.T) {
|
||||
const gas = 1_000_000
|
||||
derived := crypto.CreateAddress(senderAddr, 0)
|
||||
sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Nonce: 1}}))
|
||||
res, gp, err := applyMsg(t, sdb, createTx(0, gas, deploy3))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.Failed() {
|
||||
t.Fatal("expected collision failure")
|
||||
}
|
||||
if gp.cumulativeState != 0 {
|
||||
t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
|
||||
}
|
||||
// All forwarded gas_left is burned; only the refilled account charge (which
|
||||
// had spilled into regular) returns to gas_left. So regular gas consumed is
|
||||
// exactly tx.gas - newAccountState, with no other refund.
|
||||
if want := uint64(gas) - newAccountState; gp.cumulativeRegular != want {
|
||||
t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want)
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== Transaction validation =========================
|
||||
|
||||
// The regular dimension must have room for min(tx.gas, MaxTxGas).
|
||||
func TestValidationRegularGasAvailable(t *testing.T) {
|
||||
gp := NewGasPool(30_000_000)
|
||||
gp.cumulativeRegular = 29_000_000
|
||||
if gp.CheckGasAmsterdam(2_000_000, 0) == nil {
|
||||
t.Fatal("expected regular dimension full")
|
||||
}
|
||||
if err := gp.CheckGasAmsterdam(1_000_000, 0); err != nil {
|
||||
t.Fatalf("regular fits but rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The state dimension must have room for the whole tx.gas.
|
||||
func TestValidationStateGasAvailable(t *testing.T) {
|
||||
gp := NewGasPool(30_000_000)
|
||||
gp.cumulativeState = 29_000_000
|
||||
if gp.CheckGasAmsterdam(0, 2_000_000) == nil {
|
||||
t.Fatal("expected state dimension full")
|
||||
}
|
||||
if err := gp.CheckGasAmsterdam(0, 1_000_000); err != nil {
|
||||
t.Fatalf("state fits but rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// tx.gas may exceed MaxTxGas: regular is capped at MaxTxGas while the state
|
||||
// dimension reserves the full tx.gas (the excess lands in the reservoir).
|
||||
func TestValidationStateGasOverflowAllowed(t *testing.T) {
|
||||
gas := uint64(params.MaxTxGas) + 5_000_000
|
||||
gp := NewGasPool(40_000_000)
|
||||
if err := gp.CheckGasAmsterdam(min(gas, params.MaxTxGas), gas); err != nil {
|
||||
t.Fatalf("overflow tx rejected at pool: %v", err)
|
||||
}
|
||||
// A real transfer with gas above MaxTxGas is accepted under Amsterdam.
|
||||
sdb := mkState(senderAlloc(nil))
|
||||
to := common.HexToAddress("0xc0ffee")
|
||||
if _, _, err := applyMsg(t, sdb, callTx(0, to, 1, gas, nil)); err != nil {
|
||||
t.Fatalf("tx with gas > MaxTxGas rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Intrinsic regular gas above MaxTxGas (EIP-7825 cap) is rejected.
|
||||
func TestValidationIntrinsicRegularCap(t *testing.T) {
|
||||
al := make(types.AccessList, 8000) // ~19.2M regular, over the 16.77M cap
|
||||
for i := range al {
|
||||
al[i].Address = common.BigToAddress(big.NewInt(int64(i + 1)))
|
||||
}
|
||||
tx := types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
|
||||
ChainID: cfg8037.ChainID, Nonce: 0, To: &senderAddr, Value: big.NewInt(0),
|
||||
Gas: 25_000_000, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), AccessList: al,
|
||||
})
|
||||
if _, _, err := applyMsg(t, mkState(senderAlloc(nil)), tx); err == nil {
|
||||
t.Fatal("expected rejection for intrinsic regular over MaxTxGas")
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= Refund and gas used ===========================
|
||||
|
||||
// clearSlots deploys a contract that zeroes slots 1..n, each preset to 1.
|
||||
func clearSlots(addr common.Address, n int) (types.GenesisAlloc, []byte) {
|
||||
var code []byte
|
||||
storage := make(map[common.Hash]common.Hash, n)
|
||||
for s := 1; s <= n; s++ {
|
||||
code = append(code, 0x60, 0x00, 0x60, byte(s), 0x55) // PUSH1 0; PUSH1 s; SSTORE
|
||||
storage[common.BytesToHash([]byte{byte(s)})] = common.BytesToHash([]byte{1})
|
||||
}
|
||||
return types.GenesisAlloc{addr: {Code: append(code, 0x00), Storage: storage}}, nil
|
||||
}
|
||||
|
||||
// tx_gas_used_before_refund (peak) exceeds the post-refund gas used.
|
||||
func TestGasUsedBeforeRefund(t *testing.T) {
|
||||
c := common.HexToAddress("0xc1ea0")
|
||||
alloc, _ := clearSlots(c, 1)
|
||||
res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.MaxUsedGas <= res.UsedGas {
|
||||
t.Fatalf("peak %d must exceed post-refund %d", res.MaxUsedGas, res.UsedGas)
|
||||
}
|
||||
}
|
||||
|
||||
// The refund is capped at 20% of gas used before refund.
|
||||
func TestRefundCappedAt20Percent(t *testing.T) {
|
||||
c := common.HexToAddress("0xc1ea3")
|
||||
alloc, _ := clearSlots(c, 3) // refund (3x4800) exceeds the 20% cap
|
||||
res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := res.MaxUsedGas - res.MaxUsedGas/5; res.UsedGas != want {
|
||||
t.Fatalf("gas used = %d, want capped %d", res.UsedGas, want)
|
||||
}
|
||||
}
|
||||
|
||||
// The EIP-7623 calldata floor is applied after the refund.
|
||||
func TestRefundCalldataFloorAfterRefund(t *testing.T) {
|
||||
data := make([]byte, 1000) // all-zero calldata: floor dominates a bare call
|
||||
floor, _ := FloorDataGas(rules8037, data, nil)
|
||||
to := common.HexToAddress("0xeeee")
|
||||
res, _, err := applyMsg(t, mkState(senderAlloc(nil)), callTx(0, to, 0, 1_000_000, data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.UsedGas != floor {
|
||||
t.Fatalf("gas used = %d, want floor %d", res.UsedGas, floor)
|
||||
}
|
||||
}
|
||||
|
||||
// When the floor exceeds the post-refund gas, it negates part of the refund.
|
||||
func TestRefundFloorNegatesRefund(t *testing.T) {
|
||||
c := common.HexToAddress("0xc1ea1")
|
||||
alloc, _ := clearSlots(c, 1)
|
||||
data := make([]byte, 1000)
|
||||
floor, _ := FloorDataGas(rules8037, data, nil)
|
||||
res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.UsedGas != floor {
|
||||
t.Fatalf("gas used = %d, want floor %d (refund negated)", res.UsedGas, floor)
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= Block-level accounting ========================
|
||||
|
||||
// The pool tracks regular and state cumulatively in separate counters.
|
||||
func TestBlockTracksTwoCounters(t *testing.T) {
|
||||
gp := NewGasPool(60_000_000)
|
||||
if err := gp.ChargeGasAmsterdam(100, 200, 300); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gp.cumulativeRegular != 100 || gp.cumulativeState != 200 {
|
||||
t.Fatalf("counters = (%d,%d), want (100,200)", gp.cumulativeRegular, gp.cumulativeState)
|
||||
}
|
||||
}
|
||||
|
||||
// Block gas used is the max of the two dimensions.
|
||||
func TestBlockGasUsedIsMax(t *testing.T) {
|
||||
gp := NewGasPool(60_000_000)
|
||||
gp.ChargeGasAmsterdam(100, 200, 300)
|
||||
if gp.Used() != 200 {
|
||||
t.Fatalf("block used = %d, want 200", gp.Used())
|
||||
}
|
||||
}
|
||||
|
||||
// Block validity is checked against the max dimension, not the sum.
|
||||
func TestBlockValidityAgainstMax(t *testing.T) {
|
||||
gp := NewGasPool(150)
|
||||
// regular 100 + state 120: sum 220 > 150 but max 120 <= 150 is valid.
|
||||
if err := gp.ChargeGasAmsterdam(100, 120, 0); err != nil {
|
||||
t.Fatalf("max within limit but rejected: %v", err)
|
||||
}
|
||||
// state 200 alone exceeds the limit.
|
||||
if err := gp.ChargeGasAmsterdam(0, 200, 0); err == nil {
|
||||
t.Fatal("expected block overflow on state dimension")
|
||||
}
|
||||
}
|
||||
|
||||
// The block header gas_used reflects the bottleneck dimension (here, state),
|
||||
// which the base-fee update then equilibrates against.
|
||||
func TestBlockBaseFeeUsesMax(t *testing.T) {
|
||||
c := common.HexToAddress("0x5707e5")
|
||||
var code []byte
|
||||
for s := 1; s <= 5; s++ {
|
||||
code = append(code, 0x60, byte(s), 0x60, byte(s), 0x55) // SSTORE new slot s
|
||||
}
|
||||
env := newBALTestEnv(types.GenesisAlloc{c: {Code: append(code, 0x00)}})
|
||||
engine := beacon.New(ethash.NewFaker())
|
||||
_, blocks, _ := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, b *BlockGen) {
|
||||
b.AddTx(env.tx(0, &c, big.NewInt(0), 1_000_000, 0, nil))
|
||||
})
|
||||
if want := uint64(5 * newSlotState); blocks[0].GasUsed() != want {
|
||||
t.Fatalf("block gas used = %d, want %d (state bottleneck)", blocks[0].GasUsed(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// Receipt cumulative_gas_used is the running sum of per-tx gas (post-refund,
|
||||
// post-floor), so consecutive receipts differ by exactly that tx's gas.
|
||||
func TestReceiptCumulativeGasUsed(t *testing.T) {
|
||||
env := newBALTestEnv(nil)
|
||||
a, b := common.HexToAddress("0xaaaa"), common.HexToAddress("0xbbbb")
|
||||
engine := beacon.New(ethash.NewFaker())
|
||||
_, _, receipts := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, g *BlockGen) {
|
||||
g.AddTx(env.tx(0, &a, big.NewInt(1), txGasNewAccount, 0, nil))
|
||||
g.AddTx(env.tx(1, &b, big.NewInt(1), txGasNewAccount, 0, nil))
|
||||
})
|
||||
r := receipts[0]
|
||||
if got := r[1].CumulativeGasUsed - r[0].CumulativeGasUsed; got != r[1].GasUsed {
|
||||
t.Fatalf("cumulative delta = %d, want tx gas %d", got, r[1].GasUsed)
|
||||
}
|
||||
}
|
||||
|
||||
// ======================= EIP-7702 authorizations =========================
|
||||
|
||||
// signAuth signs an authorization from authKey for the given delegate and nonce.
|
||||
func signAuth(t *testing.T, authKey string, delegate common.Address, nonce uint64) (types.SetCodeAuthorization, common.Address) {
|
||||
t.Helper()
|
||||
k, _ := crypto.HexToECDSA(authKey)
|
||||
auth, err := types.SignSetCode(k, types.SetCodeAuthorization{
|
||||
ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate, Nonce: nonce,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("sign auth: %v", err)
|
||||
}
|
||||
return auth, crypto.PubkeyToAddress(k.PublicKey)
|
||||
}
|
||||
|
||||
func setCodeTx(nonce uint64, to common.Address, auths []types.SetCodeAuthorization) *types.Transaction {
|
||||
return types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{
|
||||
ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: nonce, To: to, Value: new(uint256.Int),
|
||||
Gas: 1_000_000, GasFeeCap: new(uint256.Int), GasTipCap: new(uint256.Int), AuthList: auths,
|
||||
})
|
||||
}
|
||||
|
||||
const authKeyA = "0202020202020202020202020202020202020202020202020202002020202020"
|
||||
|
||||
var delegate8037 = common.HexToAddress("0xde1e8a7e")
|
||||
|
||||
// Intrinsic gas pre-charges the worst-case (account + indicator) per auth.
|
||||
func TestAuthIntrinsicWorstCase(t *testing.T) {
|
||||
cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, false, rules8037, params.CostPerStateByte)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cost.StateGas != authWorstState {
|
||||
t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, authWorstState)
|
||||
}
|
||||
}
|
||||
|
||||
// An invalid authorization refills its entire intrinsic state-gas charge.
|
||||
func TestAuthInvalidRefillFull(t *testing.T) {
|
||||
k, _ := crypto.HexToECDSA(authKeyA)
|
||||
bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{
|
||||
ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id
|
||||
})
|
||||
sdb := mkState(senderAlloc(nil))
|
||||
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{bad}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gp.cumulativeState != 0 {
|
||||
t.Fatalf("state gas = %d, want 0 (fully refilled)", gp.cumulativeState)
|
||||
}
|
||||
}
|
||||
|
||||
// A pre-existing authority refills the account portion (indicator stands).
|
||||
func TestAuthAccountExistsRefill(t *testing.T) {
|
||||
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
|
||||
sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))
|
||||
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gp.cumulativeState != authBaseState {
|
||||
t.Fatalf("state gas = %d, want %d (account refilled)", gp.cumulativeState, authBaseState)
|
||||
}
|
||||
}
|
||||
|
||||
// Setting a delegation on an already-delegated authority refills the indicator
|
||||
// portion (and the account portion, since the authority already exists).
|
||||
func TestAuthSetOnDelegatedRefillBase(t *testing.T) {
|
||||
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
|
||||
pre := types.AddressToDelegation(common.HexToAddress("0xabcd"))
|
||||
sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Code: pre}}))
|
||||
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gp.cumulativeState != 0 {
|
||||
t.Fatalf("state gas = %d, want 0 (account+indicator refilled)", gp.cumulativeState)
|
||||
}
|
||||
}
|
||||
|
||||
// A net-new delegation on a fresh authority keeps the full worst-case charge.
|
||||
func TestAuthSetNetNewNoRefill(t *testing.T) {
|
||||
auth, _ := signAuth(t, authKeyA, delegate8037, 0)
|
||||
sdb := mkState(senderAlloc(nil))
|
||||
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gp.cumulativeState != authWorstState {
|
||||
t.Fatalf("state gas = %d, want %d (no refill)", gp.cumulativeState, authWorstState)
|
||||
}
|
||||
}
|
||||
|
||||
// Clearing a delegation writes no indicator, so the indicator portion refills.
|
||||
func TestAuthClearRefillBase(t *testing.T) {
|
||||
auth, _ := signAuth(t, authKeyA, common.Address{}, 0) // clear (address ZERO)
|
||||
sdb := mkState(senderAlloc(nil))
|
||||
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := uint64(newAccountState); gp.cumulativeState != want {
|
||||
t.Fatalf("state gas = %d, want %d (indicator refilled)", gp.cumulativeState, want)
|
||||
}
|
||||
}
|
||||
|
||||
// 0->a->0 in one tx: the indicator created by an earlier auth and cleared by a
|
||||
// later one writes zero net bytes, so both indicator charges refill.
|
||||
func TestAuthClearSameTxDoubleRefill(t *testing.T) {
|
||||
set, authority := signAuth(t, authKeyA, delegate8037, 0)
|
||||
clr, _ := signAuth(t, authKeyA, common.Address{}, 1)
|
||||
sdb := mkState(senderAlloc(nil))
|
||||
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{set, clr}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = authority
|
||||
if want := uint64(newAccountState); gp.cumulativeState != want {
|
||||
t.Fatalf("state gas = %d, want %d (net-zero delegation)", gp.cumulativeState, want)
|
||||
}
|
||||
}
|
||||
|
||||
// The same authority across two auths is charged for its account only once.
|
||||
func TestAuthDuplicateAuthorityOnce(t *testing.T) {
|
||||
a0, _ := signAuth(t, authKeyA, delegate8037, 0)
|
||||
a1, _ := signAuth(t, authKeyA, delegate8037, 1)
|
||||
sdb := mkState(senderAlloc(nil))
|
||||
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{a0, a1}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gp.cumulativeState != authWorstState {
|
||||
t.Fatalf("state gas = %d, want %d (leaf+indicator once)", gp.cumulativeState, authWorstState)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== System contracts / system calls ===================
|
||||
|
||||
// System call gas limit keeps 30M regular plus a state reservoir for new slots.
|
||||
func TestSystemCallGasLimit(t *testing.T) {
|
||||
limit, budget := systemCallGasBudget(amsterdamCoreEVM(mkState(nil)))
|
||||
if limit != 30_000_000 || budget.RegularGas != 30_000_000 {
|
||||
t.Fatalf("limit/regular = %d/%d, want 30M/30M", limit, budget.RegularGas)
|
||||
}
|
||||
}
|
||||
|
||||
// The extra system budget is placed in the state reservoir (16 new slots).
|
||||
func TestSystemCallExtraInReservoir(t *testing.T) {
|
||||
_, budget := systemCallGasBudget(amsterdamCoreEVM(mkState(nil)))
|
||||
want := uint64(params.SystemMaxSStoresPerCall * params.CostPerStateByte * params.StorageCreationSize)
|
||||
if budget.StateGas != want {
|
||||
t.Fatalf("reservoir = %d, want %d", budget.StateGas, want)
|
||||
}
|
||||
}
|
||||
|
||||
// System calls do not contribute to either block dimension: an empty block
|
||||
// (whose system calls still write state) reports zero gas used.
|
||||
func TestSystemCallNotCountedInBlock(t *testing.T) {
|
||||
env := newBALTestEnv(nil)
|
||||
engine := beacon.New(ethash.NewFaker())
|
||||
_, blocks, _ := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, b *BlockGen) {})
|
||||
if blocks[0].GasUsed() != 0 {
|
||||
t.Fatalf("block gas used = %d, want 0 (system calls excluded)", blocks[0].GasUsed())
|
||||
}
|
||||
}
|
||||
|
|
@ -674,11 +674,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
|
|||
ret []byte
|
||||
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
|
||||
result vm.GasBudget
|
||||
|
||||
// Capture the forwarded regular-gas amount BEFORE ForwardAll consumes
|
||||
// it, so Absorb can back out state-gas spillover from UsedRegularGas
|
||||
// per EIP-8037.
|
||||
forwarded = st.gasRemaining.RegularGas
|
||||
)
|
||||
if contractCreation {
|
||||
// Check whether the init code size has been exceeded.
|
||||
|
|
@ -686,12 +681,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
|
|||
return nil, err
|
||||
}
|
||||
// Execute the transaction's creation.
|
||||
ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value)
|
||||
st.gasRemaining.Absorb(result, forwarded)
|
||||
var creation bool
|
||||
ret, _, result, creation, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value)
|
||||
st.gasRemaining.Absorb(result)
|
||||
|
||||
// If the contract creation failed, refund the account-creation state
|
||||
// gas pre-charged in IntrinsicGas.
|
||||
if rules.IsAmsterdam && vmerr != nil {
|
||||
// If the contract creation failed, or the destination was pre-existing,
|
||||
// refund the account-creation state gas pre-charged in IntrinsicGas.
|
||||
if rules.IsAmsterdam && !creation {
|
||||
st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -711,7 +707,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
|
|||
}
|
||||
// Execute the transaction's call.
|
||||
ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value)
|
||||
st.gasRemaining.Absorb(result, forwarded)
|
||||
st.gasRemaining.Absorb(result)
|
||||
}
|
||||
|
||||
// Settle down the gas usage and refund the ETH back if any remaining
|
||||
|
|
|
|||
|
|
@ -28,17 +28,17 @@ func _() {
|
|||
_ = x[GasChangeWitnessCodeChunk-17]
|
||||
_ = x[GasChangeWitnessContractCollisionCheck-18]
|
||||
_ = x[GasChangeTxDataFloor-19]
|
||||
_ = x[GasChangeAccountCreation-20]
|
||||
_ = x[GasChangeRefundAccountCreation-20]
|
||||
_ = x[GasChangeIgnored-255]
|
||||
}
|
||||
|
||||
const (
|
||||
_GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorAccountCreation"
|
||||
_GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorRefundAccountCreation"
|
||||
_GasChangeReason_name_1 = "Ignored"
|
||||
)
|
||||
|
||||
var (
|
||||
_GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 368}
|
||||
_GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 374}
|
||||
)
|
||||
|
||||
func (i GasChangeReason) String() string {
|
||||
|
|
|
|||
|
|
@ -472,9 +472,9 @@ const (
|
|||
// transaction data. This change will always be a negative change.
|
||||
GasChangeTxDataFloor GasChangeReason = 19
|
||||
|
||||
// GasChangeAccountCreation represents the state gas charging for account
|
||||
// creation inside the call/create frame.
|
||||
GasChangeAccountCreation GasChangeReason = 20
|
||||
// GasChangeRefundAccountCreation represents the cancellation of a
|
||||
// pre-charged account-creation cost when no account is created.
|
||||
GasChangeRefundAccountCreation GasChangeReason = 20
|
||||
|
||||
// GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as
|
||||
// it will be "manually" tracked by a direct emit of the gas change event.
|
||||
|
|
|
|||
|
|
@ -152,10 +152,20 @@ func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.G
|
|||
return true
|
||||
}
|
||||
|
||||
// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state.
|
||||
func (c *Contract) refundGas(child GasBudget, forwarded uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) {
|
||||
// refundState refunds the pre-charged state gas back to state reservoir.
|
||||
func (c *Contract) refundState(s uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) {
|
||||
prior := c.Gas
|
||||
c.Gas.Absorb(child, forwarded)
|
||||
c.Gas.RefundState(s)
|
||||
|
||||
if s != 0 && logger.HasGasHook() && reason != tracing.GasChangeIgnored {
|
||||
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
|
||||
}
|
||||
}
|
||||
|
||||
// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state.
|
||||
func (c *Contract) refundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) {
|
||||
prior := c.Gas
|
||||
c.Gas.Absorb(child)
|
||||
if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
|
||||
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
|
||||
}
|
||||
|
|
|
|||
739
core/vm/eip8037_test.go
Normal file
739
core/vm/eip8037_test.go
Normal file
|
|
@ -0,0 +1,739 @@
|
|||
// Copyright 2026 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <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)
|
||||
}
|
||||
}
|
||||
|
|
@ -587,7 +587,10 @@ func enable7843(jt *JumpTable) {
|
|||
// enable8037 enables the multidimensional-metering as specified in EIP-8037.
|
||||
func enable8037(jt *JumpTable) {
|
||||
jt[CREATE].constantGas = params.CreateGasAmsterdam
|
||||
jt[CREATE].dynamicGas = gasCreateEip8037
|
||||
jt[CREATE2].constantGas = params.CreateGasAmsterdam
|
||||
jt[CREATE2].dynamicGas = gasCreate2Eip8037
|
||||
jt[CALL].dynamicGas = gasCallEIP8037
|
||||
jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037
|
||||
jt[SSTORE].dynamicGas = gasSStore8037
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
|
|||
if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
|
||||
return nil, gas, ErrInsufficientBalance
|
||||
}
|
||||
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
|
||||
snapshot := evm.StateDB.Snapshot()
|
||||
p, isPrecompile := evm.precompile(addr)
|
||||
if !evm.StateDB.Exist(addr) {
|
||||
if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) {
|
||||
|
|
@ -279,7 +279,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
|
|||
wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false)
|
||||
if _, ok := gas.ChargeRegular(wgas); !ok {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
return nil, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
return nil, gas.ExitHalt(), ErrOutOfGas
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,16 +289,6 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
|
|||
}
|
||||
evm.StateDB.CreateAccount(addr)
|
||||
}
|
||||
if evm.chainRules.IsAmsterdam && !value.IsZero() && evm.StateDB.Empty(addr) {
|
||||
prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
|
||||
if !ok {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
return nil, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
}
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
|
||||
}
|
||||
}
|
||||
// Perform the value transfer only in non-syscall mode.
|
||||
// Calling this is required even for zero-value transfers,
|
||||
// to ensure the state clearing mechanism is applied.
|
||||
|
|
@ -324,7 +314,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
|
|||
}
|
||||
|
||||
// Calculate the remaining gas at the end of frame
|
||||
exitGas := gas.Exit(err, reservoir)
|
||||
exitGas := gas.Exit(err)
|
||||
if err != nil {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
|
||||
|
|
@ -360,7 +350,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
|
|||
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
|
||||
return nil, gas, ErrInsufficientBalance
|
||||
}
|
||||
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
|
||||
snapshot := evm.StateDB.Snapshot()
|
||||
|
||||
// It is allowed to call precompiles, even via delegatecall
|
||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||
|
|
@ -375,7 +365,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
|
|||
}
|
||||
|
||||
// Calculate the remaining gas at the end of frame
|
||||
exitGas := gas.Exit(err, reservoir)
|
||||
exitGas := gas.Exit(err)
|
||||
if err != nil {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
|
||||
|
|
@ -406,7 +396,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
|
|||
if evm.depth > int(params.CallCreateDepth) {
|
||||
return nil, gas, ErrDepth
|
||||
}
|
||||
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
|
||||
snapshot := evm.StateDB.Snapshot()
|
||||
|
||||
// It is allowed to call precompiles, even via delegatecall
|
||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||
|
|
@ -419,7 +409,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
|
|||
}
|
||||
|
||||
// Calculate the remaining gas at the end of frame
|
||||
exitGas := gas.Exit(err, reservoir)
|
||||
exitGas := gas.Exit(err)
|
||||
if err != nil {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
|
||||
|
|
@ -453,7 +443,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
|
|||
// after all empty accounts were deleted, so this is not required. However, if we omit this,
|
||||
// then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json.
|
||||
// We could change this, but for now it's left for legacy reasons
|
||||
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
|
||||
snapshot := evm.StateDB.Snapshot()
|
||||
|
||||
// We do an AddBalance of zero here, just in order to trigger a touch.
|
||||
// This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium,
|
||||
|
|
@ -471,7 +461,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
|
|||
}
|
||||
|
||||
// Calculate the remaining gas at the end of frame
|
||||
exitGas := gas.Exit(err, reservoir)
|
||||
exitGas := gas.Exit(err)
|
||||
if err != nil {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
if err != ErrExecutionReverted {
|
||||
|
|
@ -484,7 +474,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
|
|||
}
|
||||
|
||||
// create creates a new contract using code as deployment code.
|
||||
func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, err error) {
|
||||
func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, creation bool, err error) {
|
||||
// Depth check execution. Fail if we're trying to execute above the
|
||||
// limit.
|
||||
var nonce uint64
|
||||
|
|
@ -505,18 +495,17 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
}(gas)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, common.Address{}, gas, err
|
||||
return nil, common.Address{}, gas, false, err
|
||||
}
|
||||
// Increment the caller's nonce after passing all validations
|
||||
evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator)
|
||||
reservoir := gas.StateGas
|
||||
|
||||
// Charge the contract creation init gas in verkle mode
|
||||
if evm.chainRules.IsEIP4762 {
|
||||
statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas)
|
||||
prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas})
|
||||
if !ok {
|
||||
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas
|
||||
}
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
|
||||
|
|
@ -537,13 +526,13 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
if evm.StateDB.GetNonce(address) != 0 ||
|
||||
(contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code
|
||||
isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) {
|
||||
halt := gas.ExitHalt(reservoir)
|
||||
halt := gas.ExitHalt()
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution)
|
||||
}
|
||||
// EIP-8037 collision rule: the state reservoir is fully preserved on
|
||||
// address collision while regular gas is burnt.
|
||||
return nil, common.Address{}, halt, ErrContractAddressCollision
|
||||
return nil, common.Address{}, halt, false, ErrContractAddressCollision
|
||||
}
|
||||
// Create a new account on the state only if the object was not present.
|
||||
// It might be possible the contract code is deployed to a pre-existent
|
||||
|
|
@ -551,18 +540,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
snapshot := evm.StateDB.Snapshot()
|
||||
if !evm.StateDB.Exist(address) {
|
||||
evm.StateDB.CreateAccount(address)
|
||||
|
||||
if evm.chainRules.IsAmsterdam && evm.depth > 0 {
|
||||
// Only charge state gas if we are not doing a create transaction.
|
||||
// Prevents double charging with IntrinsicGas.
|
||||
prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
|
||||
if !ok {
|
||||
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
}
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
|
||||
}
|
||||
}
|
||||
creation = true
|
||||
}
|
||||
// CreateContract means that regardless of whether the account previously existed
|
||||
// in the state trie or not, it _now_ becomes created as a _contract_ account.
|
||||
|
|
@ -577,7 +555,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
if evm.chainRules.IsEIP4762 {
|
||||
consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas)
|
||||
if consumed < wanted {
|
||||
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas
|
||||
}
|
||||
prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
|
|
@ -602,17 +580,17 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
|
||||
exit := contract.Gas.Exit(err, reservoir)
|
||||
exit := contract.Gas.Exit(err)
|
||||
if err != ErrExecutionReverted {
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution)
|
||||
}
|
||||
}
|
||||
return ret, address, exit, err
|
||||
return ret, address, exit, false, err
|
||||
}
|
||||
// Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved).
|
||||
// Both packaged as a success-form GasBudget.
|
||||
return ret, address, contract.Gas.ExitSuccess(), err
|
||||
return ret, address, contract.Gas.ExitSuccess(), creation, err
|
||||
}
|
||||
|
||||
// initNewContract runs a new contract's creation code, performs checks on the
|
||||
|
|
@ -668,7 +646,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
|
|||
}
|
||||
|
||||
// Create creates a new contract using code as deployment code.
|
||||
func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) {
|
||||
func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, creation bool, err error) {
|
||||
contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller))
|
||||
return evm.create(caller, code, gas, value, contractAddr, CREATE)
|
||||
}
|
||||
|
|
@ -677,7 +655,7 @@ func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value
|
|||
//
|
||||
// The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:]
|
||||
// instead of the usual sender-and-nonce-hash as the address where the contract is initialized at.
|
||||
func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) {
|
||||
func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, creation bool, err error) {
|
||||
inithash := crypto.Keccak256Hash(code)
|
||||
contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:])
|
||||
return evm.create(caller, code, gas, endowment, contractAddr, CREATE2)
|
||||
|
|
|
|||
|
|
@ -519,6 +519,117 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
|
|||
return GasCosts{RegularGas: gas}, nil
|
||||
}
|
||||
|
||||
func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
if evm.readOnly {
|
||||
return GasCosts{}, ErrWriteProtection
|
||||
}
|
||||
gas, err := memoryGasCost(mem, memorySize)
|
||||
if err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
size, overflow := stack.back(2).Uint64WithOverflow()
|
||||
if overflow {
|
||||
return GasCosts{}, ErrGasUintOverflow
|
||||
}
|
||||
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
// Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow
|
||||
words := (size + 31) / 32
|
||||
wordGas := params.InitCodeWordGas * words
|
||||
|
||||
// Unconditionally pre-charge the account creation and refunds if the creation
|
||||
// doesn't happen after the create-frame.
|
||||
return GasCosts{
|
||||
RegularGas: gas + wordGas,
|
||||
StateGas: params.AccountCreationSize * evm.Context.CostPerStateByte,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
if evm.readOnly {
|
||||
return GasCosts{}, ErrWriteProtection
|
||||
}
|
||||
gas, err := memoryGasCost(mem, memorySize)
|
||||
if err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
size, overflow := stack.back(2).Uint64WithOverflow()
|
||||
if overflow {
|
||||
return GasCosts{}, ErrGasUintOverflow
|
||||
}
|
||||
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
// Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow
|
||||
words := (size + 31) / 32
|
||||
|
||||
// CREATE2 charges both InitCodeWordGas (EIP-3860) and Keccak256WordGas
|
||||
// (for address hashing).
|
||||
wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * words
|
||||
|
||||
// Unconditionally pre-charge the account creation and refunds if the creation
|
||||
// doesn't happen after the create-frame.
|
||||
return GasCosts{
|
||||
RegularGas: gas + wordGas,
|
||||
StateGas: params.AccountCreationSize * evm.Context.CostPerStateByte,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// regularGasCall8037 is the intrinsic gas calculator for CALL in Amsterdam.
|
||||
// It computes memory expansion + value transfer gas but excludes new account
|
||||
// creation, which is handled as state gas by the wrapper.
|
||||
func regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
|
||||
var (
|
||||
gas uint64
|
||||
transfersValue = !stack.back(2).IsZero()
|
||||
)
|
||||
if evm.readOnly && transfersValue {
|
||||
return 0, ErrWriteProtection
|
||||
}
|
||||
memoryGas, err := memoryGasCost(mem, memorySize)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var transferGas uint64
|
||||
if transfersValue && !evm.chainRules.IsEIP4762 {
|
||||
transferGas = params.CallValueTransferGas
|
||||
}
|
||||
var overflow bool
|
||||
if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
|
||||
return 0, ErrGasUintOverflow
|
||||
}
|
||||
return gas, nil
|
||||
}
|
||||
|
||||
// stateGasCall8037 is the stateful gas calculator for CALL in Amsterdam (EIP-8037).
|
||||
// It only returns the state-dependent gas (account creation as state gas).
|
||||
// Memory gas, transfer gas, and callGas are handled by gasCallStateless and
|
||||
// makeCallVariantGasCall.
|
||||
func stateGasCall8037(evm *EVM, contract *Contract, stack *Stack) (uint64, error) {
|
||||
var (
|
||||
gas uint64
|
||||
transfersValue = !stack.back(2).IsZero()
|
||||
address = common.Address(stack.back(1).Bytes20())
|
||||
)
|
||||
// TODO(rjl, marius), can EIP8037 implicitly means the EIP158 is also activated?
|
||||
// It's technically possible to skip the EIP158 but very unlikely in practice.
|
||||
if evm.chainRules.IsEIP158 {
|
||||
// Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist
|
||||
// in the current state yet still be considered non-existent by EIP-161 if its
|
||||
// nonce, balance, and code are all zero. Such accounts can appear temporarily
|
||||
// during execution (e.g. via SELFDESTRUCT) and are removed at tx end.
|
||||
//
|
||||
// Funding such an account makes it permanent state growth and must be charged.
|
||||
if transfersValue && evm.StateDB.Empty(address) {
|
||||
gas += params.AccountCreationSize * evm.Context.CostPerStateByte
|
||||
}
|
||||
} else if !evm.StateDB.Exist(address) {
|
||||
gas += params.AccountCreationSize * evm.Context.CostPerStateByte
|
||||
}
|
||||
return gas, nil
|
||||
}
|
||||
|
||||
func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
if evm.readOnly {
|
||||
return GasCosts{}, ErrWriteProtection
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@ type GasBudget struct {
|
|||
StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb)
|
||||
UsedRegularGas uint64 // gross regular gas consumed in this frame
|
||||
UsedStateGas int64 // signed net state-gas consumed in this frame
|
||||
|
||||
// Spilled tracks how much of this frame's regular gas (gas_left)
|
||||
// has been borrowed to cover state-gas charges that exceeded the
|
||||
// reservoir.
|
||||
Spilled uint64
|
||||
}
|
||||
|
||||
// NewGasBudget initializes a fresh GasBudget for execution / forwarding,
|
||||
|
|
@ -82,7 +87,7 @@ func (g GasBudget) Used(initial GasBudget) uint64 {
|
|||
|
||||
// String returns a visual representation of the budget.
|
||||
func (g GasBudget) String() string {
|
||||
return fmt.Sprintf("<%v,%v,used=<%v,%v>>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas)
|
||||
return fmt.Sprintf("<%v,%v,used=<%v,%v>,borrowed=%v>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas, g.Spilled)
|
||||
}
|
||||
|
||||
// CanAfford reports whether the running balance can cover the given cost.
|
||||
|
|
@ -117,6 +122,10 @@ func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) {
|
|||
spillover := cost.StateGas - g.StateGas
|
||||
g.StateGas = 0
|
||||
g.RegularGas -= spillover
|
||||
|
||||
// Record the regular gas borrowed to cover the overflowing state gas
|
||||
// so a later inline refund can repay it before topping up the reservoir.
|
||||
g.Spilled += spillover
|
||||
} else {
|
||||
g.StateGas -= cost.StateGas
|
||||
}
|
||||
|
|
@ -146,14 +155,27 @@ func (g *GasBudget) IsZero() bool {
|
|||
}
|
||||
|
||||
// RefundState applies an inline state-gas refund (e.g., SSTORE 0->A->0).
|
||||
// The reservoir is credited and the signed usage counter is decremented
|
||||
// in lockstep, preserving the per-frame invariant:
|
||||
//
|
||||
// StateGas + UsedStateGas == initialStateGas + spillover_so_far
|
||||
// Per EIP-8037, the refund repays the regular gas previously borrowed for
|
||||
// state-gas spillover (tracked by Spilled) before crediting the
|
||||
// reservoir: it is returned to RegularGas up to the outstanding borrowed
|
||||
// amount, and only the remainder tops up StateGas. This keeps the regular and
|
||||
// state pools from drifting into one another.
|
||||
//
|
||||
// which the revert path relies on for the correct gross refund.
|
||||
// The signed usage counter is decremented by the full refund regardless of the
|
||||
// split, preserving the per-frame invariant:
|
||||
//
|
||||
// StateGas + UsedStateGas == initialStateGas + Spilled
|
||||
//
|
||||
// which the revert and halt paths rely on for the correct gross refund.
|
||||
func (g *GasBudget) RefundState(s uint64) {
|
||||
g.StateGas += s
|
||||
// Repay the borrowed regular gas first, capped at the outstanding amount.
|
||||
repay := min(s, g.Spilled)
|
||||
g.RegularGas += repay
|
||||
g.Spilled -= repay
|
||||
|
||||
// Whatever is left tops up the reservoir.
|
||||
g.StateGas += s - repay
|
||||
g.UsedStateGas -= int64(s)
|
||||
}
|
||||
|
||||
|
|
@ -201,53 +223,46 @@ func (g GasBudget) ExitSuccess() GasBudget {
|
|||
return g
|
||||
}
|
||||
|
||||
// ExitRevert produces the leftover for a REVERT exit. Per EIP-8037, all state
|
||||
// gas charged by the reverted frame is refunded to the caller's reservoir:
|
||||
//
|
||||
// leftover.StateGas = StateGas + UsedStateGas
|
||||
//
|
||||
// UsedStateGas is reset since the frame's state changes are discarded.
|
||||
// ExitRevert produces the leftover for a REVERT exit. The frame's state
|
||||
// changes are discarded, so all state gas it charged is refilled to its origin
|
||||
// (EIP-8037): up to Spilled is returned to RegularGas (the regular
|
||||
// gas it borrowed), and the remainder restores the reservoir. Because the
|
||||
// borrowed regular gas is repaid first, the reservoir is made whole back to its
|
||||
// start-of-frame value:
|
||||
func (g GasBudget) ExitRevert() GasBudget {
|
||||
reservoir := int64(g.StateGas) + g.UsedStateGas
|
||||
reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled)
|
||||
if reservoir < 0 {
|
||||
// Reservoir should never be negative. By construction it equals
|
||||
// the initial state-gas allocation plus any spillover to regular
|
||||
// gas.
|
||||
// the initial state-gas allocation.
|
||||
reservoir = 0
|
||||
log.Warn("Negative reservoir at revert", "remaining", g.StateGas, "used", g.UsedStateGas)
|
||||
log.Warn("Negative reservoir at revert", "remaining", g.StateGas, "used", g.UsedStateGas, "borrowed", g.Spilled)
|
||||
}
|
||||
return GasBudget{
|
||||
RegularGas: g.RegularGas,
|
||||
RegularGas: g.RegularGas + g.Spilled,
|
||||
StateGas: uint64(reservoir),
|
||||
UsedRegularGas: g.UsedRegularGas,
|
||||
UsedStateGas: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ExitHalt produces the leftover for an exceptional halt.
|
||||
//
|
||||
// - state_gas_reservoir is reset back to its value at the start of the child frame
|
||||
// - the gas_left initially given to the child is consumed (set to zero)
|
||||
func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
|
||||
reservoir := int64(g.StateGas) + g.UsedStateGas
|
||||
// ExitHalt produces the leftover for an exceptional halt. As with a revert, the
|
||||
// frame's state changes are rolled back and its state gas is refilled to origin
|
||||
// (EIP-8037); the difference is that the frame's gas_left is consumed rather
|
||||
// than returned. The portion refilled to RegularGas is therefore burned along
|
||||
// with the rest of gas_left, leaving only the reservoir portion to survive,
|
||||
// which equals the reservoir's value at the start of the frame.
|
||||
func (g GasBudget) ExitHalt() GasBudget {
|
||||
reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled)
|
||||
if reservoir < 0 {
|
||||
// Reservoir should never be negative. By construction it equals
|
||||
// the initial state-gas allocation plus any spillover to regular
|
||||
// gas.
|
||||
// the initial state-gas allocation.
|
||||
reservoir = 0
|
||||
log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas)
|
||||
}
|
||||
// The portion of state gas charged from regular gas is also burned
|
||||
// together with the regular gas, rather than being returned to the
|
||||
// parent's state-gas reservoir.
|
||||
var spilled uint64
|
||||
if uint64(reservoir) > initStateReservoir {
|
||||
spilled = uint64(reservoir) - initStateReservoir
|
||||
log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas, "borrowed", g.Spilled)
|
||||
}
|
||||
return GasBudget{
|
||||
RegularGas: 0,
|
||||
StateGas: initStateReservoir,
|
||||
UsedRegularGas: g.UsedRegularGas + g.RegularGas + spilled,
|
||||
StateGas: uint64(reservoir),
|
||||
UsedRegularGas: g.UsedRegularGas + g.RegularGas + g.Spilled,
|
||||
UsedStateGas: 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -258,17 +273,14 @@ func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
|
|||
// - err == nil → ExitSuccess
|
||||
// - err == ErrExecutionReverted → ExitRevert
|
||||
// - any other err → ExitHalt
|
||||
//
|
||||
// Soft validation failures (occurring BEFORE evm.Run) should call Preserved
|
||||
// directly instead of going through this dispatcher.
|
||||
func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget {
|
||||
func (g GasBudget) Exit(err error) GasBudget {
|
||||
switch {
|
||||
case err == nil:
|
||||
return g.ExitSuccess()
|
||||
case err == ErrExecutionReverted:
|
||||
return g.ExitRevert()
|
||||
default:
|
||||
return g.ExitHalt(initStateReservoir)
|
||||
return g.ExitHalt()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,18 +288,12 @@ func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget {
|
|||
// budget. Additionally, it does an EIP-8037 spillover correction:
|
||||
// state-gas that spilled into the regular pool inside the child frame is
|
||||
// excluded from the UsedRegularGas.
|
||||
//
|
||||
// spillover = forwarded - child.RegularGas - child.UsedRegularGas
|
||||
//
|
||||
// forwarded is the regular-gas amount that was passed to the child at call
|
||||
// entry (i.e., the regular initial of the child's GasBudget).
|
||||
func (g *GasBudget) Absorb(child GasBudget, forwarded uint64) {
|
||||
spillover := forwarded - child.RegularGas - child.UsedRegularGas
|
||||
|
||||
func (g *GasBudget) Absorb(child GasBudget) {
|
||||
g.UsedRegularGas -= child.RegularGas
|
||||
g.RegularGas += child.RegularGas
|
||||
g.StateGas = child.StateGas
|
||||
g.UsedStateGas += child.UsedStateGas
|
||||
|
||||
g.UsedRegularGas -= spillover
|
||||
g.UsedRegularGas -= child.Spilled
|
||||
g.Spilled += child.Spilled
|
||||
}
|
||||
|
|
|
|||
|
|
@ -646,7 +646,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
stackvalue := size
|
||||
|
||||
child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation)
|
||||
res, addr, result, suberr := evm.Create(scope.Contract.Address(), input, child, &value)
|
||||
res, addr, result, creation, suberr := evm.Create(scope.Contract.Address(), input, child, &value)
|
||||
// Push item on the stack based on the returned error. If the ruleset is
|
||||
// homestead we must check for CodeStoreOutOfGasError (homestead only
|
||||
// rule) and treat as an error, if the ruleset is frontier we must
|
||||
|
|
@ -661,8 +661,12 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
scope.Stack.push(&stackvalue)
|
||||
|
||||
// Refund the leftover gas back to current frame
|
||||
scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
// Refund the state gas of account-creation if creation doesn't happen
|
||||
if evm.GetRules().IsAmsterdam && !creation {
|
||||
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
|
||||
}
|
||||
if suberr == ErrExecutionReverted {
|
||||
evm.returnData = res // set REVERT data to return data buffer
|
||||
return res, nil
|
||||
|
|
@ -685,7 +689,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
// reuse size int for stackvalue
|
||||
stackvalue := size
|
||||
child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2)
|
||||
res, addr, result, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt)
|
||||
res, addr, result, creation, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt)
|
||||
// Push item on the stack based on the returned error.
|
||||
if suberr != nil {
|
||||
stackvalue.Clear()
|
||||
|
|
@ -695,8 +699,12 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
scope.Stack.push(&stackvalue)
|
||||
|
||||
// Refund the leftover gas back to current frame
|
||||
scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
// Refund the state gas of account-creation if creation doesn't happen
|
||||
if evm.GetRules().IsAmsterdam && !creation {
|
||||
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
|
||||
}
|
||||
if suberr == ErrExecutionReverted {
|
||||
evm.returnData = res // set REVERT data to return data buffer
|
||||
return res, nil
|
||||
|
|
@ -740,8 +748,13 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
if err == nil || err == ErrExecutionReverted {
|
||||
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
|
||||
}
|
||||
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
// If the call frame reverts or halts exceptionally, the charged state-gas
|
||||
// is refilled back to the state reservoir in Amsterdam.
|
||||
if evm.chainRules.IsAmsterdam && err != nil && !value.IsZero() && evm.StateDB.Empty(toAddr) {
|
||||
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
|
||||
}
|
||||
evm.returnData = ret
|
||||
return ret, nil
|
||||
}
|
||||
|
|
@ -776,7 +789,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
|
||||
}
|
||||
|
||||
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
evm.returnData = ret
|
||||
return ret, nil
|
||||
|
|
@ -808,7 +821,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
if err == nil || err == ErrExecutionReverted {
|
||||
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
|
||||
}
|
||||
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
evm.returnData = ret
|
||||
return ret, nil
|
||||
|
|
@ -841,7 +854,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
|
||||
}
|
||||
|
||||
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
evm.returnData = ret
|
||||
return ret, nil
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ type (
|
|||
intrinsicGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64
|
||||
// memorySizeFunc returns the required size, and whether the operation overflowed a uint64
|
||||
memorySizeFunc func(*Stack) (size uint64, overflow bool)
|
||||
|
||||
regularGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error)
|
||||
stateGasFunc func(*EVM, *Contract, *Stack) (uint64, error)
|
||||
)
|
||||
|
||||
type operation struct {
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ var (
|
|||
gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic)
|
||||
gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic)
|
||||
gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic)
|
||||
innerGasCallEIP8037 = makeCallVariantGasCallEIP8037(regularGasCall8037, stateGasCall8037)
|
||||
)
|
||||
|
||||
func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
|
|
@ -276,6 +277,14 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
|
|||
return innerGasCallEIP7702(evm, contract, stack, mem, memorySize)
|
||||
}
|
||||
|
||||
func gasCallEIP8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
transfersValue := !stack.back(2).IsZero()
|
||||
if evm.readOnly && transfersValue {
|
||||
return GasCosts{}, ErrWriteProtection
|
||||
}
|
||||
return innerGasCallEIP8037(evm, contract, stack, mem, memorySize)
|
||||
}
|
||||
|
||||
func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
|
||||
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
var (
|
||||
|
|
@ -362,3 +371,89 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
|
|||
return GasCosts{RegularGas: totalCost}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// makeCallVariantGasCallEIP8037 creates a call gas function for Amsterdam (EIP-8037).
|
||||
// It extends the EIP-7702 pattern with state gas handling and GasUsed tracking.
|
||||
// intrinsicFunc computes the regular gas (memory + transfer, no new account creation).
|
||||
// stateGasFunc computes the state gas (new account creation as state gas).
|
||||
func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stateGasFunc) gasFunc {
|
||||
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
var (
|
||||
eip2929Cost uint64
|
||||
eip7702Cost uint64
|
||||
addr = common.Address(stack.back(1).Bytes20())
|
||||
)
|
||||
// EIP-2929 cold access check.
|
||||
if !evm.StateDB.AddressInAccessList(addr) {
|
||||
evm.StateDB.AddAddressToAccessList(addr)
|
||||
eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
|
||||
if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
|
||||
return GasCosts{}, ErrOutOfGas
|
||||
}
|
||||
}
|
||||
|
||||
// Compute regular cost (memory + transfer, no new account creation).
|
||||
regularCost, err := regularFunc(evm, contract, stack, mem, memorySize)
|
||||
if err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
|
||||
// Charge intrinsic cost directly (regular gas). This must happen
|
||||
// BEFORE state gas to prevent reservoir inflation, and also serves
|
||||
// as the OOG guard before stateful operations.
|
||||
if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallOpCode) {
|
||||
return GasCosts{}, ErrOutOfGas
|
||||
}
|
||||
|
||||
// EIP-7702 delegation check.
|
||||
if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
|
||||
if evm.StateDB.AddressInAccessList(target) {
|
||||
eip7702Cost = params.WarmStorageReadCostEIP2929
|
||||
} else {
|
||||
evm.StateDB.AddAddressToAccessList(target)
|
||||
eip7702Cost = params.ColdAccountAccessCostEIP2929
|
||||
}
|
||||
if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
|
||||
return GasCosts{}, ErrOutOfGas
|
||||
}
|
||||
}
|
||||
|
||||
// Compute and charge state gas (new account creation) AFTER regular gas.
|
||||
stateGas, err := stateGasFunc(evm, contract, stack)
|
||||
if err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
if stateGas > 0 {
|
||||
if _, ok := contract.Gas.ChargeState(stateGas); !ok {
|
||||
return GasCosts{}, ErrOutOfGas
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the gas budget for the nested call (63/64 rule).
|
||||
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, 0, stack.back(0))
|
||||
if err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
|
||||
// Temporarily undo direct regular charges for tracer reporting.
|
||||
// The interpreter will charge the returned totalCost.
|
||||
contract.Gas.RegularGas += eip2929Cost + eip7702Cost + regularCost
|
||||
contract.Gas.UsedRegularGas -= eip2929Cost + eip7702Cost + regularCost
|
||||
|
||||
// Aggregate total cost.
|
||||
var (
|
||||
overflow bool
|
||||
totalCost uint64
|
||||
)
|
||||
if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow {
|
||||
return GasCosts{}, ErrGasUintOverflow
|
||||
}
|
||||
if totalCost, overflow = math.SafeAdd(totalCost, regularCost); overflow {
|
||||
return GasCosts{}, ErrGasUintOverflow
|
||||
}
|
||||
if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {
|
||||
return GasCosts{}, ErrGasUintOverflow
|
||||
}
|
||||
return GasCosts{RegularGas: totalCost}, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) {
|
|||
// - reset transient storage(eip 1153)
|
||||
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil)
|
||||
// Call the code with the given configuration.
|
||||
code, address, result, err := vmenv.Create(
|
||||
code, address, result, _, err := vmenv.Create(
|
||||
cfg.Origin,
|
||||
input,
|
||||
vm.NewGasBudget(cfg.GasLimit, 0),
|
||||
|
|
|
|||
Loading…
Reference in a new issue