go-ethereum/core/eip2780_test.go
rjl493456442 1ef0ffb98c
core: implement EIP-2780 and EIP-8037 changes (#35318)
Implement the spec changes of EIP-2780 and EIP-8037.

See the spec diffs in
- https://github.com/ethereum/EIPs/pull/11844
- https://github.com/ethereum/EIPs/pull/11891
- https://github.com/ethereum/EIPs/pull/11906
-
a4801f3bb1

---------

Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
2026-07-14 14:28:26 +02:00

799 lines
31 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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/>.
package core
import (
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"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"
)
// TestEIP2780Intrinsic checks the intrinsic-gas decomposition.
func TestEIP2780Intrinsic(t *testing.T) {
var (
from = common.HexToAddress("0x1111111111111111111111111111111111111111")
to = common.HexToAddress("0x2222222222222222222222222222222222222222")
)
cases := []struct {
name string
to *common.Address
value *uint256.Int
auths []types.SetCodeAuthorization
want uint64
}{
{
name: "self-transfer",
to: &from,
value: uint256.NewInt(1),
want: params.TxBaseCost2780, // 12,000
},
{
name: "self-transfer/zero-value",
to: &from,
value: uint256.NewInt(0),
want: params.TxBaseCost2780, // 12,000
},
{
name: "zero-value call",
to: &to,
value: uint256.NewInt(0),
// TxBaseCost + ColdAccountAccess = 15,000; the recipient touch is
// charged at the cold rate unconditionally at the intrinsic phase.
want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam,
},
{
name: "value transfer to existing EOA",
to: &to,
value: uint256.NewInt(1),
// TxBaseCost + ColdAccountAccess + TxValueCost + TransferLogCost = 21,000
want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam +
params.TxValueCost2780 + params.TransferLogCost2780,
},
{
name: "contract creation, value = 0",
to: nil,
value: uint256.NewInt(0),
// TxBaseCost + CreateAccess = 23,000 regular. The new-account state
// charge depends on whether the deployment target exists and is
// charged at runtime, not intrinsically.
want: params.TxBaseCost2780 + params.CreateAccessAmsterdam,
},
{
name: "contract creation, value > 0",
to: nil,
value: uint256.NewInt(1),
// TxBaseCost + CreateAccess + TransferLogCost = 24,756 regular.
want: params.TxBaseCost2780 + params.CreateAccessAmsterdam + params.TransferLogCost2780,
},
{
name: "value transfer with authorizations",
to: &to,
value: uint256.NewInt(1),
auths: make([]types.SetCodeAuthorization, 3),
// Each authorization adds the state-independent per-auth base
// (cold authority access included).
want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam +
params.TxValueCost2780 + params.TransferLogCost2780 + 3*params.RegularPerAuthBaseCost,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := IntrinsicGas(nil, nil, tc.auths, from, tc.to, tc.value, rules8037)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tc.want {
t.Fatalf("gas mismatch: got %+v, want %+v", got, tc.want)
}
})
}
}
// TestEIP2780Gas checks every "Transaction reference case" in
// the EIP-2780 specification end-to-end, asserting the two-dimensional charge
// (intrinsic + top-level + execution) recorded in the block gas pool.
func TestEIP2780Gas(t *testing.T) {
const (
cold = params.ColdAccountAccessAmsterdam
base = params.TxBaseCost2780
valueCst = params.TxValueCost2780 + params.TransferLogCost2780
)
var (
existingEOA = common.HexToAddress("0xe0a0000000000000000000000000000000000001")
stopContract = common.HexToAddress("0xc0de000000000000000000000000000000000001")
delegated = common.HexToAddress("0xde1e000000000000000000000000000000000001")
emptyTarget = common.HexToAddress("0x7a76000000000000000000000000000000000001") // never allocated
freshEOA = common.HexToAddress("0xbeef000000000000000000000000000000000001") // never allocated
)
// Shared world: a funded EOA, a STOP contract and an account delegated to a
// non-existent (codeless) target. The delegation target is intentionally
// absent so resolving it executes no code.
base7702 := types.GenesisAlloc{
existingEOA: {Balance: big.NewInt(1)},
stopContract: {Code: []byte{0x00}}, // STOP
delegated: {Code: types.AddressToDelegation(emptyTarget)},
}
// valueCreateTx builds a contract-creation transaction carrying value.
valueCreateTx := func(value int64) *types.Transaction {
return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
ChainID: cfg8037.ChainID, Nonce: 0, To: nil, Value: big.NewInt(value),
Gas: 300_000, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0),
})
}
cases := []struct {
name string
tx *types.Transaction
wantRegular, wantState uint64
}{
// case 1: ETH transfer to self.
{"self-transfer", callTx(0, senderAddr, 1, 100_000, nil), base, 0},
// case 2: no-transfer to an existing EOA.
{"zero-value/eoa", callTx(0, existingEOA, 0, 100_000, nil), base + cold, 0},
// case 3: no-transfer to a contract.
{"zero-value/contract", callTx(0, stopContract, 0, 100_000, nil), base + cold, 0},
// case 4: ETH transfer to an existing EOA.
{"value/eoa", callTx(0, existingEOA, 1, 100_000, nil), base + cold + valueCst, 0},
// case 5: ETH transfer to a contract.
{"value/contract", callTx(0, stopContract, 1, 100_000, nil), base + cold + valueCst, 0},
// case 6: no-transfer to a 7702-delegated account.
{"zero-value/delegated", callTx(0, delegated, 0, 100_000, nil), base + 2*cold, 0},
// case 7: ETH transfer to a 7702-delegated account (no new-account charge).
{"value/delegated", callTx(0, delegated, 1, 100_000, nil), base + 2*cold + valueCst, 0},
// case 8: ETH transfer creating a new account.
{"value/new-account", callTx(0, freshEOA, 1, 300_000, nil), base + cold + valueCst, newAccountState},
// case 9: contract-creation transaction, value = 0.
{"create/zero-value", createTx(0, 300_000, nil), base + params.CreateAccessAmsterdam, newAccountState},
// case 10: contract-creation transaction, value > 0.
{"create/value", valueCreateTx(1), base + params.CreateAccessAmsterdam + params.TransferLogCost2780, newAccountState},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
res, gp, err := applyMsg(t, mkState(senderAlloc(base7702)), tc.tx)
if err != nil {
t.Fatalf("consensus error: %v", err)
}
if res.Err != nil {
t.Fatalf("execution failed: %v", res.Err)
}
if gp.cumulativeRegular != tc.wantRegular {
t.Errorf("regular gas = %d, want %d", gp.cumulativeRegular, tc.wantRegular)
}
if gp.cumulativeState != tc.wantState {
t.Errorf("state gas = %d, want %d", gp.cumulativeState, tc.wantState)
}
})
}
}
// callTxAL builds a signed dynamic-fee call carrying an access list.
func callTxAL(nonce uint64, to common.Address, value int64, gas uint64, al types.AccessList) *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), AccessList: al,
})
}
// accessListEntryCost is the total intrinsic cost of one address-only access
// list entry: the EIP-8038 per-address charge plus the EIP-7981 data charge.
const accessListEntryCost = params.TxAccessListAddressGasAmsterdam +
common.AddressLength*params.TxCostFloorPerToken7976*params.TxTokenPerNonZeroByte
// TestEIP2780WarmRecipientStillChargedCold verifies that a recipient warmed by
// the transaction's access list is still charged the recipient at the cold rate.
func TestEIP2780WarmRecipientStillChargedCold(t *testing.T) {
to := common.HexToAddress("0xe0a0000000000000000000000000000000000009")
sdb := mkState(senderAlloc(types.GenesisAlloc{to: {Balance: big.NewInt(1)}}))
al := types.AccessList{{Address: to}}
res, gp, err := applyMsg(t, sdb, callTxAL(0, to, 0, 100_000, al))
if err != nil {
t.Fatal(err)
}
if res.Err != nil {
t.Fatalf("execution failed: %v", res.Err)
}
want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + accessListEntryCost
if gp.cumulativeRegular != want {
t.Errorf("regular gas = %d, want %d (cold recipient, no access-list discount)", gp.cumulativeRegular, want)
}
}
// TestEIP2780DelegatedWarmTarget verifies that resolving the recipient's
// delegation is charged at the warm rate when the target was warmed by the
// access list, rather than the flat cold rate.
func TestEIP2780DelegatedWarmTarget(t *testing.T) {
var (
target = common.HexToAddress("0x7a76000000000000000000000000000000000002") // codeless
delegated = common.HexToAddress("0xde1e000000000000000000000000000000000002")
)
sdb := mkState(senderAlloc(types.GenesisAlloc{
delegated: {Code: types.AddressToDelegation(target)},
}))
al := types.AccessList{{Address: target}}
res, gp, err := applyMsg(t, sdb, callTxAL(0, delegated, 0, 100_000, al))
if err != nil {
t.Fatal(err)
}
if res.Err != nil {
t.Fatalf("execution failed: %v", res.Err)
}
want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + accessListEntryCost + // recipient cold access (intrinsic)
params.WarmAccountAccessAmsterdam // warm delegation-target access (runtime)
if gp.cumulativeRegular != want {
t.Errorf("regular gas = %d, want %d (warm delegation target)", gp.cumulativeRegular, want)
}
}
// TestEIP2780RuntimeOOGRevertsDelegations verifies that running out of gas on
// a runtime authorization charge halts the transaction and reverts all state
// changes, including the already applied EIP-7702 delegations — while the
// sender's nonce increment persists.
//
// The halt burns the regular dimension in full; the state dimension is
// refilled by the revert and the reservoir — if any — is preserved and
// returned to the sender rather than burnt.
func TestEIP2780RuntimeOOGRevertsDelegations(t *testing.T) {
cases := []struct {
name string
gas uint64
numAuths int
wantUsed uint64 // = gas reservoir: all regular burnt, reservoir returned
}{
// No state reservoir (gas below MaxTxGas). Gas covers the intrinsic
// cost (TX_BASE_COST + the cold-inclusive per-authorization base for
// a self-call) but not the runtime authorization charges
// (ACCOUNT_WRITE + account + indicator bytes): everything is burnt.
{"no-reservoir", 30_000, 1, 30_000},
// A 100,000 state reservoir (gas above MaxTxGas). The 100
// authorizations' state charges (~21.9M) overwhelm the reservoir and
// the regular budget they spill into. The reservoir is made whole by
// the halt-refill and returned to the sender.
{"with-reservoir", params.MaxTxGas + 100_000, 100, params.MaxTxGas},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var (
auths = make([]types.SetCodeAuthorization, tc.numAuths)
authorities = make([]common.Address, tc.numAuths)
)
for i := range auths {
key, _ := crypto.GenerateKey()
auth, err := types.SignSetCode(key, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate8037, Nonce: 0,
})
if err != nil {
t.Fatalf("sign auth: %v", err)
}
auths[i], authorities[i] = auth, crypto.PubkeyToAddress(key.PublicKey)
}
sdb := mkState(senderAlloc(nil))
tx := types.MustSignNewTx(senderKey, signer8037,
&types.SetCodeTx{
ChainID: uint256.MustFromBig(cfg8037.ChainID),
Nonce: 0,
To: senderAddr,
Value: new(uint256.Int),
Gas: tc.gas,
GasFeeCap: new(uint256.Int),
GasTipCap: new(uint256.Int),
AuthList: auths,
})
res, gp, err := applyMsg(t, sdb, tx)
if err != nil {
t.Fatalf("transaction should remain valid: %v", err)
}
if res.Err != vm.ErrOutOfGas {
t.Fatalf("expected out of gas, got %v", res.Err)
}
if res.UsedGas != tc.wantUsed {
t.Fatalf("used gas = %d, want %d", res.UsedGas, tc.wantUsed)
}
// The charged state gas was refilled on the halt: the receipt is
// all regular, burnt in full, and only the reservoir survives.
if gp.cumulativeState != 0 {
t.Fatalf("state gas = %d, want 0 (refilled on halt)", gp.cumulativeState)
}
if gp.cumulativeRegular != tc.wantUsed {
t.Fatalf("regular gas = %d, want %d (burnt in full)", gp.cumulativeRegular, tc.wantUsed)
}
for i, authority := range authorities {
if code := sdb.GetCode(authority); len(code) != 0 {
t.Fatalf("delegation %d persisted despite runtime OOG: %x", i, code)
}
if sdb.GetNonce(authority) != 0 {
t.Fatalf("authority %d nonce persisted despite runtime OOG", i)
}
}
if sdb.GetNonce(senderAddr) != 1 {
t.Fatal("sender nonce not consumed")
}
})
}
}
// TestEIP2780SelfTransferDelegated verifies that a self-transfer incurs no
// recipient touch or value charges, while resolving the sender's own
// delegation is still paid for.
func TestEIP2780SelfTransferDelegated(t *testing.T) {
target := common.HexToAddress("0x7a76000000000000000000000000000000000003") // codeless
sdb := mkState(types.GenesisAlloc{
senderAddr: {Balance: big.NewInt(1e18), Code: types.AddressToDelegation(target)},
})
res, gp, err := applyMsg(t, sdb, callTx(0, senderAddr, 1, 100_000, nil))
if err != nil {
t.Fatal(err)
}
if res.Err != nil {
t.Fatalf("execution failed: %v", res.Err)
}
want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam // base + cold delegation target
if gp.cumulativeRegular != want {
t.Errorf("regular gas = %d, want %d (base + delegation resolution)", gp.cumulativeRegular, want)
}
}
// TestEIP2780CreateInsufficientStateGas verifies that a contract-creation
// transaction funded for its intrinsic gas but not the runtime new-account
// state charge is included, halts out of gas and consumes the nonce.
func TestEIP2780CreateInsufficientStateGas(t *testing.T) {
sdb := mkState(senderAlloc(nil))
intrinsic := params.TxBaseCost2780 + params.CreateAccessAmsterdam // 23,000
res, _, err := applyMsg(t, sdb, createTx(0, intrinsic, nil))
if err != nil {
t.Fatalf("transaction should remain valid: %v", err)
}
if res.Err != vm.ErrOutOfGas {
t.Fatalf("expected out of gas, got %v", res.Err)
}
if res.UsedGas != intrinsic {
t.Fatalf("used gas = %d, want %d", res.UsedGas, intrinsic)
}
if sdb.GetNonce(senderAddr) != 1 {
t.Fatal("sender nonce not consumed")
}
}
// TestEIP2780InsufficientGasForCallCharge verifies that a value transfer
// creating a new account, whose gas limit only covers the 21,000 intrinsic base
// and not the additional new-account state gas charged before the call executes,
// halts out of gas. The transaction stays valid (no consensus error) but
// execution fails and the recipient is not created.
func TestEIP2780InsufficientGasForCallCharge(t *testing.T) {
fresh := common.HexToAddress("0xbeef000000000000000000000000000000000003")
sdb := mkState(senderAlloc(nil))
res, _, err := applyMsg(t, sdb, callTx(0, fresh, 1, 21_000, nil))
if err != nil {
t.Fatalf("transaction should remain valid: %v", err)
}
if res.Err != vm.ErrOutOfGas {
t.Fatalf("expected out of gas, got %v", res.Err)
}
if res.UsedGas != 21_000 {
t.Fatalf("expected used gas, got %v", res.UsedGas)
}
if sdb.Exist(fresh) {
t.Fatal("recipient should not be created when the call charge cannot be paid")
}
if sdb.GetNonce(senderAddr) != 1 {
t.Fatal("sender nonce not consumed")
}
}
// TestEIP2780FirstFrameHaltPreservesPreExecution verifies the gas and state
// semantics when the top-most frame — message call or creation — halts
// exceptionally after the pre-execution phase completed:
//
// - state changes applied before the frame was entered persist together
// with their state-gas charge (the EIP-7702 delegations of a call tx);
// - state gas pre-charged for the frame itself is refilled when the halt
// voids it (the account-creation charge of a creation tx);
// - after the refill the regular dimension is burnt in full, while any
// remaining state reservoir is preserved and returned to the sender.
func TestEIP2780FirstFrameHaltPreservesPreExecution(t *testing.T) {
halting := common.HexToAddress("0xbad0000000000000000000000000000000000002")
cases := []struct {
name string
create bool
gas uint64
wantUsed uint64 // = gas preserved reservoir
wantRegular uint64
wantState uint64
}{
// Message call carrying one authorization: the delegation and its
// state charge (account + indicator) survive the halt.
//
// Without a reservoir the charge spills from regular gas and everything is
// burnt;
//
// With a reservoir, the reservoir remainder is preserved.
{"call/no-reservoir", false, 1_000_000, 1_000_000, 1_000_000 - authWorstState, authWorstState},
{"call/with-reservoir", false, params.MaxTxGas + 300_000, params.MaxTxGas + authWorstState, params.MaxTxGas, authWorstState},
// Creation whose init code halts: no durable account is created, so
// the pre-charged account creation is refilled and no state gas
// remains.
//
// Without a reservoir the refill repays spilled regular gas, which the
// halt then burns along with the rest;
//
// With a reservoir, the refill makes the reservoir whole again and it
// is preserved.
{"create/no-reservoir", true, 1_000_000, 1_000_000, 1_000_000, 0},
{"create/with-reservoir", true, params.MaxTxGas + 100_000, params.MaxTxGas, params.MaxTxGas, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sdb := mkState(senderAlloc(types.GenesisAlloc{
halting: {Code: []byte{0xfe}}, // INVALID
}))
var (
tx *types.Transaction
authority common.Address
)
if tc.create {
tx = createTx(0, tc.gas, []byte{0xfe}) // init code: INVALID
} else {
var auth types.SetCodeAuthorization
auth, authority = signAuth(t, authKeyA, delegate8037, 0)
tx = types.MustSignNewTx(senderKey, signer8037,
&types.SetCodeTx{
ChainID: uint256.MustFromBig(cfg8037.ChainID),
Nonce: 0,
To: halting,
Value: new(uint256.Int),
Gas: tc.gas,
GasFeeCap: new(uint256.Int),
GasTipCap: new(uint256.Int),
AuthList: []types.SetCodeAuthorization{auth},
})
}
res, gp, err := applyMsg(t, sdb, tx)
if err != nil {
t.Fatalf("transaction should remain valid: %v", err)
}
if res.Err == nil {
t.Fatal("expected the frame to halt")
}
if res.UsedGas != tc.wantUsed {
t.Fatalf("used gas = %d, want %d", res.UsedGas, tc.wantUsed)
}
if gp.cumulativeRegular != tc.wantRegular {
t.Fatalf("regular gas = %d, want %d (burnt in full)", gp.cumulativeRegular, tc.wantRegular)
}
if gp.cumulativeState != tc.wantState {
t.Fatalf("state gas = %d, want %d", gp.cumulativeState, tc.wantState)
}
if tc.create {
// The halted creation is fully reverted: no durable account.
derived := crypto.CreateAddress(senderAddr, 0)
if code := sdb.GetCode(derived); len(code) != 0 {
t.Fatalf("created code persisted despite halt: %x", code)
}
if sdb.GetNonce(derived) != 0 {
t.Fatal("created account nonce persisted despite halt")
}
} else {
// The delegation applied before the frame was entered persists.
if code := sdb.GetCode(authority); len(code) == 0 {
t.Fatal("delegation should persist through an in-frame halt")
}
}
if sdb.GetNonce(senderAddr) != 1 {
t.Fatal("sender nonce not consumed")
}
})
}
}
// TestEIP2780CreatePreExecutionOOGPreservesReservoir verifies that when a
// creation transaction cannot afford the pre-execution account-creation state
// charge (before the init-code frame is entered), the transaction halts with
// all regular gas burnt while the state reservoir — never touched, since the
// charge is atomic and was not applied — is preserved and returned to the
// sender.
func TestEIP2780CreatePreExecutionOOGPreservesReservoir(t *testing.T) {
// Regular gas left for the pre-execution charge; together with the
// reservoir it must not cover the account-creation cost.
const (
regularLeft = 100_000
reservoir = 50_000
)
// Plain creation intrinsic: TX_BASE_COST + CREATE_ACCESS.
plainIntrinsic, err := IntrinsicGas(nil, nil, nil, senderAddr, nil, new(uint256.Int), rules8037)
if err != nil {
t.Fatal(err)
}
// For the reservoir case the gas limit must exceed MaxTxGas, which leaves
// a huge regular budget by default. A big access list drives the intrinsic
// cost close to MaxTxGas, shrinking the regular budget back down to
// roughly regularLeft. Storage keys work because their intrinsic charge
// exceeds their EIP-7623/7976 floor contribution.
al := types.AccessList{{Address: common.HexToAddress("0xa1")}}
baseIntrinsic, err := IntrinsicGas(nil, al, nil, senderAddr, nil, new(uint256.Int), rules8037)
if err != nil {
t.Fatal(err)
}
perKey := params.TxAccessListStorageKeyGasAmsterdam + uint64(common.HashLength)*params.TxCostFloorPerToken7976*params.TxTokenPerNonZeroByte
// Fill the transaction with accessList, drain the gas and make it
// insufficient for account-creation cost.
al[0].StorageKeys = make([]common.Hash, (params.MaxTxGas-regularLeft-baseIntrinsic)/perKey)
alIntrinsic, err := IntrinsicGas(nil, al, nil, senderAddr, nil, new(uint256.Int), rules8037)
if err != nil {
t.Fatal(err)
}
if left := params.MaxTxGas - alIntrinsic; left+reservoir >= newAccountState {
t.Fatalf("setup: regular %d + reservoir %d must not cover the creation charge %d", left, reservoir, newAccountState)
}
alCreateTx := types.MustSignNewTx(senderKey, signer8037,
&types.DynamicFeeTx{
ChainID: cfg8037.ChainID,
Nonce: 0,
To: nil,
Value: big.NewInt(0),
Gas: params.MaxTxGas + reservoir,
GasFeeCap: big.NewInt(0),
GasTipCap: big.NewInt(0),
AccessList: al,
})
cases := []struct {
name string
tx *types.Transaction
wantUsed uint64 // = gas preserved reservoir
}{
// Gas below MaxTxGas: no reservoir, the whole limit is burnt.
{"no-reservoir", createTx(0, plainIntrinsic+regularLeft, nil), plainIntrinsic + regularLeft},
// Gas above MaxTxGas: the reservoir survives the halt untouched and
// is returned to the sender.
{"with-reservoir", alCreateTx, params.MaxTxGas},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sdb := mkState(senderAlloc(nil))
res, gp, err := applyMsg(t, sdb, tc.tx)
if err != nil {
t.Fatalf("transaction should remain valid: %v", err)
}
if res.Err != vm.ErrOutOfGas {
t.Fatalf("expected out of gas, got %v", res.Err)
}
if res.UsedGas != tc.wantUsed {
t.Fatalf("used gas = %d, want %d", res.UsedGas, tc.wantUsed)
}
if gp.cumulativeRegular != tc.wantUsed {
t.Fatalf("regular gas = %d, want %d (burnt in full)", gp.cumulativeRegular, tc.wantUsed)
}
if gp.cumulativeState != 0 {
t.Fatalf("state gas = %d, want 0 (charge never applied)", gp.cumulativeState)
}
if derived := crypto.CreateAddress(senderAddr, 0); sdb.Exist(derived) {
t.Fatal("target account should not be created when the charge cannot be paid")
}
if sdb.GetNonce(senderAddr) != 1 {
t.Fatal("sender nonce not consumed")
}
})
}
}
// TestEIP2780AuthorityAccountWrite pins the first-write ACCOUNT_WRITE rule for
// authorities: the surcharge applies to the first paid write to the account
// within the transaction, regardless of whether the account exists, and is
// skipped when the write is already paid for: by TX_BASE_COST for the sender,
// by TX_VALUE_COST for the recipient of a value-bearing transaction, or by a
// preceding valid authorization.
func TestEIP2780AuthorityAccountWrite(t *testing.T) {
const (
base = params.TxBaseCost2780
cold = params.ColdAccountAccessAmsterdam
aw = params.AccountWriteAmsterdam
perAuth = params.RegularPerAuthBaseCost
valueCst = params.TxValueCost2780 + params.TransferLogCost2780
)
existingEOA := common.HexToAddress("0xe0a0000000000000000000000000000000000002")
auth0, authority := signAuth(t, authKeyA, delegate8037, 0)
auth1, _ := signAuth(t, authKeyA, delegate8037, 1)
authBadNonce, _ := signAuth(t, authKeyA, delegate8037, 5)
// Self-sponsored authorization: the sender's nonce is bumped before the
// authorization list is processed, hence nonce 1.
senderAuth, err := types.SignSetCode(senderKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate8037, Nonce: 1,
})
if err != nil {
t.Fatal(err)
}
// tx builds a SetCode transaction with an explicit value.
tx := func(to common.Address, value uint64, auths ...types.SetCodeAuthorization) *types.Transaction {
return types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{
ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: 0, To: to,
Value: uint256.NewInt(value), Gas: 1_000_000,
GasFeeCap: new(uint256.Int), GasTipCap: new(uint256.Int), AuthList: auths,
})
}
fundedAuthority := types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}
cases := []struct {
name string
alloc types.GenesisAlloc
tx *types.Transaction
wantRegular, wantState uint64
}{
{
// Materializing a fresh authority pays the first-write surcharge
// alongside the new-account state gas and the indicator bytes.
name: "fresh authority",
tx: tx(existingEOA, 0, auth0),
wantRegular: base + cold + perAuth + aw,
wantState: authWorstState,
},
{
// An existing authority still pays the surcharge: the nonce and
// indicator stores are the first write to the account within the
// transaction.
name: "existing authority",
alloc: fundedAuthority,
tx: tx(existingEOA, 0, auth0),
wantRegular: base + cold + perAuth + aw,
wantState: authBaseState,
},
{
// Self-sponsored: the sender's account write is prepaid by
// TX_BASE_COST, no surcharge.
name: "authority is sender",
tx: tx(existingEOA, 0, senderAuth),
wantRegular: base + cold + perAuth,
wantState: authBaseState,
},
{
// authority == tx.to with zero value: no TX_VALUE_COST was paid,
// so the authorization write is the first paid write and the
// surcharge applies. The recipient becomes delegated, adding a
// cold delegation-target access at runtime.
name: "authority is recipient, zero value",
alloc: fundedAuthority,
tx: tx(authority, 0, auth0),
wantRegular: base + cold + perAuth + aw + cold,
wantState: authBaseState,
},
{
// authority == tx.to with value: TX_VALUE_COST prepaid the
// recipient write, so no surcharge is due.
name: "authority is recipient, value",
alloc: fundedAuthority,
tx: tx(authority, 1, auth0),
wantRegular: base + cold + valueCst + perAuth + cold,
wantState: authBaseState,
},
{
// Fresh authority == tx.to with value: the authorization pays the
// new-account state gas, and the recipient charge then sees an
// existing account, so the leaf is not paid for twice.
name: "authority is fresh recipient, value",
tx: tx(authority, 1, auth0),
wantRegular: base + cold + valueCst + perAuth + cold,
wantState: authWorstState,
},
{
// The same authority twice: only the first valid authorization
// carries the surcharge, the account creation and the indicator.
name: "same authority twice",
tx: tx(existingEOA, 0, auth0, auth1),
wantRegular: base + cold + 2*perAuth + aw,
wantState: authWorstState,
},
{
// An invalid authorization performs no write and does not count
// as the first write; the following valid one pays in full. The
// per-auth intrinsic base is still paid for the invalid tuple.
name: "invalid then valid",
tx: tx(existingEOA, 0, authBadNonce, auth0),
wantRegular: base + cold + 2*perAuth + aw,
wantState: authWorstState,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
alloc := types.GenesisAlloc{existingEOA: {Balance: big.NewInt(1)}}
for addr, acc := range tc.alloc {
alloc[addr] = acc
}
res, gp, err := applyMsg(t, mkState(senderAlloc(alloc)), tc.tx)
if err != nil {
t.Fatalf("consensus error: %v", err)
}
if res.Err != nil {
t.Fatalf("execution failed: %v", res.Err)
}
if gp.cumulativeRegular != tc.wantRegular {
t.Errorf("regular gas = %d, want %d", gp.cumulativeRegular, tc.wantRegular)
}
if gp.cumulativeState != tc.wantState {
t.Errorf("state gas = %d, want %d", gp.cumulativeState, tc.wantState)
}
})
}
}
// TestEIP2780DelegationTargetPrewarmed pins the warm rate for delegation
// targets that are already in accessed_addresses when the recipient is
// loaded.
func TestEIP2780DelegationTargetPrewarmed(t *testing.T) {
const (
base = params.TxBaseCost2780
cold = params.ColdAccountAccessAmsterdam
warm = params.WarmAccountAccessAmsterdam
aw = params.AccountWriteAmsterdam
perAuth = params.RegularPerAuthBaseCost
)
delegatedAcct := common.HexToAddress("0xde1e000000000000000000000000000000000002")
t.Run("target is sender", func(t *testing.T) {
sdb := mkState(senderAlloc(types.GenesisAlloc{
delegatedAcct: {Code: types.AddressToDelegation(senderAddr)},
}))
res, gp, err := applyMsg(t, sdb, callTx(0, delegatedAcct, 0, 100_000, nil))
if err != nil {
t.Fatalf("consensus error: %v", err)
}
if res.Err != nil {
t.Fatalf("execution failed: %v", res.Err)
}
if want := base + cold + warm; gp.cumulativeRegular != want {
t.Errorf("regular gas = %d, want %d (warm delegation target)", gp.cumulativeRegular, want)
}
if gp.cumulativeState != 0 {
t.Errorf("state gas = %d, want 0", gp.cumulativeState)
}
})
t.Run("target warmed by authorization", func(t *testing.T) {
// A clearing authorization from a fresh authority: it creates the
// authority account (nonce bump) and warms it, without installing an
// indicator.
//
// The recipient's pre-existing delegation then resolves to
// the freshly warmed, codeless authority at the warm rate.
authClear, authority := signAuth(t, authKeyA, common.Address{}, 0)
sdb := mkState(senderAlloc(types.GenesisAlloc{
delegatedAcct: {Code: types.AddressToDelegation(authority)},
}))
res, gp, err := applyMsg(t, sdb, setCodeTx(0, delegatedAcct, []types.SetCodeAuthorization{authClear}))
if err != nil {
t.Fatalf("consensus error: %v", err)
}
if res.Err != nil {
t.Fatalf("execution failed: %v", res.Err)
}
if want := base + cold + perAuth + aw + warm; gp.cumulativeRegular != want {
t.Errorf("regular gas = %d, want %d (auth-warmed delegation target)", gp.cumulativeRegular, want)
}
if gp.cumulativeState != newAccountState {
t.Errorf("state gas = %d, want %d (authority account created)", gp.cumulativeState, newAccountState)
}
})
}