This commit is contained in:
rjl493456442 2026-07-17 21:52:54 -07:00 committed by GitHub
commit f7ff0c2ef9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1827 additions and 35 deletions

View file

@ -17,10 +17,12 @@
package core
import (
"errors"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
@ -109,6 +111,44 @@ func TestEIP2780Intrinsic(t *testing.T) {
}
}
// TestEIP2780Boundary distinguishes the state-independent intrinsic validity
// threshold from a valid transaction which later runs out of runtime gas.
func TestEIP2780Boundary(t *testing.T) {
auth, _ := signAuth(t, authKeyA, delegate8037, 0)
to := common.HexToAddress("0xe0a0000000000000000000000000000000000008")
cases := []struct {
name string
tx func(uint64) *types.Transaction
}{
{"call", func(gas uint64) *types.Transaction { return callTx(0, to, 0, gas, nil) }},
{"value", func(gas uint64) *types.Transaction { return callTx(0, to, 1, gas, nil) }},
{"create", func(gas uint64) *types.Transaction { return createTx(0, gas, nil) }},
{"auth", func(gas uint64) *types.Transaction {
return setCodeTxGas(0, to, 0, gas, []types.SetCodeAuthorization{auth})
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
msg, err := TransactionToMessage(tc.tx(1_000_000), signer8037, big.NewInt(0))
if err != nil {
t.Fatal(err)
}
intrinsic, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules8037)
if err != nil {
t.Fatal(err)
}
res, _, err := applyMsg(t, mkState(senderAlloc(nil)), tc.tx(intrinsic-1))
if res != nil || !errors.Is(err, ErrIntrinsicGas) {
t.Fatalf("below intrinsic: result=%v err=%v, want ErrIntrinsicGas", res, err)
}
res, _, err = applyMsg(t, mkState(senderAlloc(nil)), tc.tx(intrinsic))
if err != nil || res == nil {
t.Fatalf("at intrinsic must be included: result=%v err=%v", res, err)
}
})
}
}
// 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.
@ -194,6 +234,32 @@ func callTxAL(nonce uint64, to common.Address, value int64, gas uint64, al types
})
}
func setCodeTxGas(nonce uint64, to common.Address, value, gas uint64, auths []types.SetCodeAuthorization) *types.Transaction {
return setCodeTxGasAL(nonce, to, value, gas, nil, auths)
}
func setCodeTxGasAL(nonce uint64, to common.Address, value, gas uint64, al types.AccessList, auths []types.SetCodeAuthorization) *types.Transaction {
return types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{
ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: nonce, To: to,
Value: uint256.NewInt(value), Gas: gas, AccessList: al,
GasFeeCap: new(uint256.Int), GasTipCap: new(uint256.Int), AuthList: auths,
})
}
func applyMsgCoinbase(t *testing.T, sdb *state.StateDB, tx *types.Transaction, coinbase common.Address) (*ExecutionResult, *GasPool, error) {
t.Helper()
evm := amsterdamCoreEVM(sdb)
evm.Context.Coinbase = coinbase
msg, err := TransactionToMessage(tx, signer8037, evm.Context.BaseFee)
if err != nil {
t.Fatalf("to message: %v", err)
}
gp := NewGasPool(evm.Context.GasLimit)
evm.SetTxContext(NewEVMTxContext(msg))
res, err := newStateTransition(evm, msg, gp).execute()
return res, gp, err
}
// 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 +
@ -332,6 +398,71 @@ func TestEIP2780RuntimeOOGRevertsDelegations(t *testing.T) {
}
}
// TestEIP2780RecipientOOG verifies that an OOG recipient charge rolls back a
// delegation which was successfully installed earlier in the same transaction.
func TestEIP2780RecipientOOG(t *testing.T) {
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
recipient := common.HexToAddress("0xbeef000000000000000000000000000000000004")
intrinsic := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam +
params.TxValueCost2780 + params.TransferLogCost2780 + params.RegularPerAuthBaseCost
// The reservoir case needs a near-cap intrinsic cost. This leaves just
// enough total budget for the authorization but not for the recipient leaf.
const (
regularLeft = 100_000
reservoir = 200_000
)
al := types.AccessList{{Address: common.HexToAddress("0xa1")}}
baseIntrinsic, err := IntrinsicGas(nil, al, []types.SetCodeAuthorization{auth}, senderAddr, &recipient, uint256.NewInt(1), rules8037)
if err != nil {
t.Fatal(err)
}
perKey := params.TxAccessListStorageKeyGasAmsterdam + uint64(common.HashLength)*params.TxCostFloorPerToken7976*params.TxTokenPerNonZeroByte
al[0].StorageKeys = make([]common.Hash, (params.MaxTxGas-regularLeft-baseIntrinsic)/perKey)
alIntrinsic, err := IntrinsicGas(nil, al, []types.SetCodeAuthorization{auth}, senderAddr, &recipient, uint256.NewInt(1), rules8037)
if err != nil {
t.Fatal(err)
}
if available := params.MaxTxGas - alIntrinsic + reservoir; available < params.AccountWriteAmsterdam+authWorstState || available >= params.AccountWriteAmsterdam+authWorstState+newAccountState {
t.Fatalf("setup: available runtime gas %d does not isolate recipient charge", available)
}
cases := []struct {
name string
tx *types.Transaction
want uint64
}{
// This exactly pays the first authorization, leaving no gas for the
// fresh recipient's account-leaf charge.
{"no-reservoir", setCodeTxGas(0, recipient, 1, intrinsic+params.AccountWriteAmsterdam+authWorstState, []types.SetCodeAuthorization{auth}), intrinsic + params.AccountWriteAmsterdam + authWorstState},
// The state reservoir is restored by the halt; only the capped regular
// dimension is burnt.
{"with-reservoir", setCodeTxGasAL(0, recipient, 1, params.MaxTxGas+reservoir, al, []types.SetCodeAuthorization{auth}), 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("execution error = %v, want out of gas", res.Err)
}
if code := sdb.GetCode(authority); len(code) != 0 || sdb.GetNonce(authority) != 0 {
t.Fatalf("authorization persisted after recipient OOG: code=%x nonce=%d", code, sdb.GetNonce(authority))
}
if sdb.Exist(recipient) {
t.Fatal("recipient created despite an unpaid runtime charge")
}
if sdb.GetNonce(senderAddr) != 1 {
t.Fatal("sender nonce not consumed")
}
if res.UsedGas != tc.want || gp.cumulativeState != 0 || gp.cumulativeRegular != tc.want {
t.Fatalf("used/gas = %d/<%d,%d>, want %d/<%d,0>", res.UsedGas, gp.cumulativeRegular, gp.cumulativeState, tc.want, tc.want)
}
})
}
}
// TestEIP2780SelfTransferDelegated verifies that a self-transfer incurs no
// recipient touch or value charges, while resolving the sender's own
// delegation is still paid for.
@ -400,6 +531,242 @@ func TestEIP2780InsufficientGasForCallCharge(t *testing.T) {
}
}
// TestEIP2780RecipientKinds covers EIP-161 and precompile distinctions which
// are invisible to the state-independent intrinsic charge.
func TestEIP2780RecipientKinds(t *testing.T) {
const (
base = params.TxBaseCost2780
cold = params.ColdAccountAccessAmsterdam
valueCst = params.TxValueCost2780 + params.TransferLogCost2780
)
nonceOnly := common.HexToAddress("0xbeef000000000000000000000000000000000005")
precompile := common.BytesToAddress([]byte{4}) // identity; 15 gas for empty input
cases := []struct {
name string
alloc types.GenesisAlloc
tx *types.Transaction
wantRegular, wantState uint64
}{
{
name: "nonce-only",
alloc: types.GenesisAlloc{nonceOnly: {Nonce: 1}},
tx: callTx(0, nonceOnly, 1, 100_000, nil),
wantRegular: base + cold + valueCst,
},
{
name: "precompile/zero",
tx: callTx(0, precompile, 0, 100_000, nil),
wantRegular: base + cold + 15,
},
{
name: "precompile/value",
tx: callTx(0, precompile, 1, 300_000, nil),
wantRegular: base + cold + valueCst + 15,
wantState: newAccountState,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
res, gp, err := applyMsg(t, mkState(senderAlloc(tc.alloc)), tc.tx)
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
if gp.cumulativeRegular != tc.wantRegular || gp.cumulativeState != tc.wantState {
t.Fatalf("gas = <%d,%d>, want <%d,%d>", gp.cumulativeRegular, gp.cumulativeState, tc.wantRegular, tc.wantState)
}
})
}
}
// TestEIP2780RecipientRefill covers the empty-precompile path: the account
// leaf is charged before dispatch, then refilled when the top frame halts.
func TestEIP2780RecipientRefill(t *testing.T) {
// The pairing precompile rejects this malformed input after the recipient's
// account-leaf charge. The excess over MaxTxGas is a state reservoir.
recipient := common.BytesToAddress([]byte{8})
gas := params.MaxTxGas + newAccountState + 50_000
sdb := mkState(senderAlloc(nil))
res, gp, err := applyMsg(t, sdb, callTx(0, recipient, 1, gas, []byte{0}))
if err != nil || res.Err == nil {
t.Fatalf("result=%v err=%v, want exceptional halt", res, err)
}
if gp.cumulativeState != 0 || gp.cumulativeRegular != params.MaxTxGas {
t.Fatalf("gas = <%d,%d>, want <%d,0> after refill", gp.cumulativeRegular, gp.cumulativeState, params.MaxTxGas)
}
if sdb.Exist(recipient) {
t.Fatal("empty recipient persisted after halted dispatch")
}
}
// TestEIP2780Coinbase keeps the intrinsic recipient charge separate from the
// runtime warmth of the coinbase account: calling the coinbase directly is
// still charged at the cold rate. The warm rate for a coinbase delegation
// target is covered by TestEIP2780DelegationWarmth.
func TestEIP2780Coinbase(t *testing.T) {
coinbase := common.HexToAddress("0xc01ba5e000000000000000000000000000000001")
res, gp, err := applyMsgCoinbase(t, mkState(senderAlloc(nil)), callTx(0, coinbase, 0, 100_000, nil), coinbase)
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
if want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam; gp.cumulativeRegular != want {
t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want)
}
}
// TestEIP2780DelegationWarmth adds the special targets which are warm before
// top-level dispatch but are not access-list entries.
func TestEIP2780DelegationWarmth(t *testing.T) {
const (
base = params.TxBaseCost2780
cold = params.ColdAccountAccessAmsterdam
warm = params.WarmAccountAccessAmsterdam
)
recipient := common.HexToAddress("0xde1e000000000000000000000000000000000008")
precompile := common.BytesToAddress([]byte{4})
cases := []struct {
name string
target common.Address
coinbase common.Address
wantRegular uint64
}{
{"precompile", precompile, common.Address{}, base + cold + warm},
{"coinbase", common.HexToAddress("0xc01ba5e000000000000000000000000000000002"), common.HexToAddress("0xc01ba5e000000000000000000000000000000002"), base + cold + warm},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sdb := mkState(senderAlloc(types.GenesisAlloc{
recipient: {Code: types.AddressToDelegation(tc.target)},
}))
res, gp, err := applyMsgCoinbase(t, sdb, callTx(0, recipient, 0, 100_000, nil), tc.coinbase)
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
if gp.cumulativeRegular != tc.wantRegular {
t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, tc.wantRegular)
}
})
}
// A delegation designator pointing to itself eventually faults as bytecode,
// so pin just the pre-frame recipient-target charge rather than the later
// frame result. The recipient was warmed as tx.to before this charge.
sdb := mkState(senderAlloc(types.GenesisAlloc{
recipient: {Code: types.AddressToDelegation(recipient)},
}))
to := recipient
st := newStateTransition(amsterdamCoreEVM(sdb), &Message{To: &to, Value: new(uint256.Int)}, NewGasPool(100_000))
st.gasRemaining = vm.NewGasBudget(1_000, 0)
sdb.AddAddressToAccessList(recipient)
if !st.chargeCallRecipientEIP2780(new(uint256.Int)) || st.gasRemaining.UsedRegularGas != warm {
t.Fatalf("recipient target charge = %d, want warm %d", st.gasRemaining.UsedRegularGas, warm)
}
}
// TestEIP2780InstallDispatch covers an authority installed during the
// pre-frame authorization pass and dispatched to by that same transaction.
func TestEIP2780InstallDispatch(t *testing.T) {
const (
base = params.TxBaseCost2780
cold = params.ColdAccountAccessAmsterdam
perAuth = params.RegularPerAuthBaseCost
valueCst = params.TxValueCost2780 + params.TransferLogCost2780
)
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
senderAuth, err := types.SignSetCode(senderKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate8037, Nonce: 1,
})
if err != nil {
t.Fatal(err)
}
cases := []struct {
name string
alloc types.GenesisAlloc
tx *types.Transaction
account common.Address
wantRegular, wantState uint64
wantNonce uint64
wantBalance *big.Int
}{
{
name: "sender",
tx: setCodeTxGas(0, senderAddr, 0, 1_000_000, []types.SetCodeAuthorization{senderAuth}),
account: senderAddr,
wantRegular: base + perAuth + cold,
wantState: authBaseState,
wantNonce: 2,
},
{
name: "fresh-recipient",
tx: setCodeTxGas(0, authority, 1, 1_000_000, []types.SetCodeAuthorization{auth}),
account: authority,
wantRegular: base + cold + valueCst + perAuth + cold,
wantState: authWorstState,
wantNonce: 1,
wantBalance: big.NewInt(1),
},
{
name: "funded-recipient",
alloc: types.GenesisAlloc{authority: {Balance: big.NewInt(3)}},
tx: setCodeTxGas(0, authority, 1, 1_000_000, []types.SetCodeAuthorization{auth}),
account: authority,
wantRegular: base + cold + valueCst + perAuth + cold,
wantState: authBaseState,
wantNonce: 1,
wantBalance: big.NewInt(4),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sdb := mkState(senderAlloc(tc.alloc))
res, gp, err := applyMsg(t, sdb, tc.tx)
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
if _, delegated := types.ParseDelegation(sdb.GetCode(tc.account)); !delegated || sdb.GetNonce(tc.account) != tc.wantNonce {
t.Fatalf("delegation/nonce = %x/%d, want delegation/%d", sdb.GetCode(tc.account), sdb.GetNonce(tc.account), tc.wantNonce)
}
if tc.wantBalance != nil && sdb.GetBalance(tc.account).Cmp(uint256.MustFromBig(tc.wantBalance)) != 0 {
t.Fatalf("balance = %v, want %v", sdb.GetBalance(tc.account), tc.wantBalance)
}
if gp.cumulativeRegular != tc.wantRegular || gp.cumulativeState != tc.wantState {
t.Fatalf("gas = <%d,%d>, want <%d,%d>", gp.cumulativeRegular, gp.cumulativeState, tc.wantRegular, tc.wantState)
}
})
}
}
// TestEIP2780Floor keeps the EIP-8037 calldata floor in the regular dimension
// when a top-level EIP-2780 account-leaf charge is also present.
func TestEIP2780Floor(t *testing.T) {
recipient := common.HexToAddress("0xbeef000000000000000000000000000000000007")
data := make([]byte, 1_000)
tx := callTx(0, recipient, 1, 300_000, data)
res, gp, err := applyMsg(t, mkState(senderAlloc(nil)), tx)
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
floor, err := FloorDataGas(rules8037, senderAddr, &recipient, uint256.NewInt(1), data, nil)
if err != nil {
t.Fatal(err)
}
intrinsic, err := IntrinsicGas(data, nil, nil, senderAddr, &recipient, uint256.NewInt(1), rules8037)
if err != nil {
t.Fatal(err)
}
stateGas := newAccountState
// This is the v7.2.0 boundary: the floor lifts only the regular
// dimension, while the scalar receipt gas remains the actual intrinsic +
// state charge because it is already above the floor.
if !(intrinsic < floor && floor < intrinsic+stateGas) {
t.Fatalf("expected intrinsic < floor < intrinsic + state: %d < %d < %d", intrinsic, floor, intrinsic+stateGas)
}
if gp.cumulativeRegular != floor || gp.cumulativeState != stateGas {
t.Fatalf("gas = <%d,%d>, want floor/state <%d,%d>", gp.cumulativeRegular, gp.cumulativeState, floor, stateGas)
}
if want := intrinsic + stateGas; res.UsedGas != want {
t.Fatalf("receipt gas = %d, want intrinsic + state = %d", res.UsedGas, want)
}
}
// TestEIP2780FirstFrameHaltPreservesPreExecution verifies the gas and state
// semantics when the top-most frame — message call or creation — halts
// exceptionally after the pre-execution phase completed:

413
core/eip7708_test.go Normal file
View file

@ -0,0 +1,413 @@
// 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/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/state"
"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"
)
// EIP-7708 transfer-log tests. The older TestEthTransferLogs keeps its
// historical end-to-end Solidity fixture; these tests use small bytecode
// programs to pin the normative edge cases independently.
func transferLogs7708(sdb *state.StateDB) []*types.Log {
return sdb.GetLogs(common.Hash{}, 0, common.Hash{}, 0)
}
func assertTransfer7708(t *testing.T, log *types.Log, from, to common.Address, value uint64) {
t.Helper()
want := types.EthTransferLog(from, to, uint256.NewInt(value))
if log.Address != want.Address || len(log.Topics) != 3 || log.Topics[0] != want.Topics[0] ||
log.Topics[1] != want.Topics[1] || log.Topics[2] != want.Topics[2] || string(log.Data) != string(want.Data) {
t.Fatalf("transfer log = %+v, want %+v", log, want)
}
}
func callCode7708(target common.Address, value byte, suffix []byte) []byte {
return callOpcode7708(target, value, 0xf1, suffix)
}
func callOpcode7708(target common.Address, value, opcode byte, suffix []byte) []byte {
// Optional LOG0, then CALL(gas=0xffff, to=target, value=value, empty input
// and output), discard the success flag, and run suffix.
code := []byte{0x60, 0x00, 0x60, 0x00, 0xa0}
code = append(code,
0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, // output/input sizes and offsets
0x60, value, 0x73)
code = append(code, target.Bytes()...)
code = append(code, 0x61, 0xff, 0xff, opcode, 0x50)
return append(code, suffix...)
}
func delegateCode7708(target common.Address) []byte {
// DELEGATECALL(gas=0xffff, to=target, empty input and output), followed by
// STOP. Value is inherited from the outer call's executing context.
code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
code = append(code, target.Bytes()...)
return append(code, 0x61, 0xff, 0xff, 0xf4, 0x50, 0x00)
}
func createTx7708(value uint64, initcode []byte) *types.Transaction {
return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
ChainID: cfg8037.ChainID, Nonce: 0, Value: new(big.Int).SetUint64(value),
Gas: 500_000, GasFeeCap: new(big.Int), GasTipCap: new(big.Int), Data: initcode,
})
}
// TestEIP7708Transactions covers ordinary transaction value transfers across
// the principal transaction encodings, including a delegated recipient.
func TestEIP7708Transactions(t *testing.T) {
recipient := common.HexToAddress("0x7708000000000000000000000000000000000001")
auth, _ := signAuth(t, authKeyA, delegate8037, 0)
cases := []struct {
name string
tx func() *types.Transaction
}{
{
"legacy",
func() *types.Transaction {
return types.MustSignNewTx(senderKey, signer8037, &types.LegacyTx{
Nonce: 0, To: &recipient, Value: big.NewInt(1), Gas: 100_000, GasPrice: new(big.Int),
})
},
},
{
"access-list",
func() *types.Transaction {
return types.MustSignNewTx(senderKey, signer8037, &types.AccessListTx{
ChainID: cfg8037.ChainID, Nonce: 0, To: &recipient, Value: big.NewInt(1), Gas: 100_000, GasPrice: new(big.Int),
})
},
},
{"dynamic", func() *types.Transaction { return callTx(0, recipient, 1, 100_000, nil) }},
{"set-code", func() *types.Transaction {
return setCodeTxGas(0, recipient, 1, 1_000_000, []types.SetCodeAuthorization{auth})
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sdb := mkState(senderAlloc(types.GenesisAlloc{recipient: {Balance: big.NewInt(1)}}))
res, _, err := applyMsg(t, sdb, tc.tx())
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
logs := transferLogs7708(sdb)
if len(logs) != 1 {
t.Fatalf("logs = %+v, want one", logs)
}
assertTransfer7708(t, logs[0], senderAddr, recipient, 1)
})
}
delegated := common.HexToAddress("0x7708000000000000000000000000000000000002")
sdb := mkState(senderAlloc(types.GenesisAlloc{
delegated: {Code: types.AddressToDelegation(delegate8037)},
delegate8037: {Code: []byte{0x00}},
}))
res, _, err := applyMsg(t, sdb, callTx(0, delegated, 1, 100_000, nil))
if err != nil || res.Err != nil {
t.Fatalf("delegated result=%v err=%v", res, err)
}
logs := transferLogs7708(sdb)
if len(logs) != 1 {
t.Fatalf("delegated logs = %+v, want one", logs)
}
// The transfer is to the delegation account, never to its implementation.
assertTransfer7708(t, logs[0], senderAddr, delegated, 1)
}
// TestEIP7708Special covers recipients that are frequently warmed or handled
// specially by the EVM, and checks that fee recipients do not produce logs.
func TestEIP7708Special(t *testing.T) {
precompile := common.BytesToAddress([]byte{4})
system := params.SystemAddress
coinbase := common.HexToAddress("0x7708000000000000000000000000000000000003")
cases := []struct {
name string
to common.Address
coinbase bool
}{
{"precompile", precompile, false},
{"system", system, false},
{"coinbase", coinbase, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sdb := mkState(senderAlloc(nil))
tx := callTx(0, tc.to, 1, 300_000, nil)
var (
res *ExecutionResult
err error
)
if tc.coinbase {
res, _, err = applyMsgCoinbase(t, sdb, tx, coinbase)
} else {
res, _, err = applyMsg(t, sdb, tx)
}
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
logs := transferLogs7708(sdb)
if len(logs) != 1 {
t.Fatalf("logs = %+v, want one transfer only", logs)
}
assertTransfer7708(t, logs[0], senderAddr, tc.to, 1)
})
}
}
// TestEIP7708Calls checks ordering, attribution, and rollback for value CALLs.
func TestEIP7708Calls(t *testing.T) {
caller := common.HexToAddress("0x7708000000000000000000000000000000000010")
callee := common.HexToAddress("0x7708000000000000000000000000000000000011")
reverter := common.HexToAddress("0x7708000000000000000000000000000000000012")
t.Run("order", func(t *testing.T) {
sdb := mkState(senderAlloc(types.GenesisAlloc{
caller: {Code: callCode7708(callee, 3, []byte{0x00})},
callee: {Code: []byte{0x00}},
}))
res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
logs := transferLogs7708(sdb)
if len(logs) != 3 {
t.Fatalf("logs = %+v, want tx transfer, LOG0, inner transfer", logs)
}
assertTransfer7708(t, logs[0], senderAddr, caller, 10)
// Ordinary log emitted by contract
if logs[1].Address != caller || len(logs[1].Topics) != 0 || len(logs[1].Data) != 0 {
t.Fatalf("ordinary log = %+v, want caller LOG0", logs[1])
}
assertTransfer7708(t, logs[2], caller, callee, 3)
})
t.Run("inner-revert", func(t *testing.T) {
sdb := mkState(senderAlloc(types.GenesisAlloc{
caller: {Code: callCode7708(reverter, 3, []byte{0x00})},
reverter: {Code: []byte{0x60, 0x00, 0x60, 0x00, 0xfd}},
}))
res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
logs := transferLogs7708(sdb)
if len(logs) != 2 { // top-level transfer and the caller's LOG0
t.Fatalf("logs = %+v, want no rolled-back inner transfer", logs)
}
assertTransfer7708(t, logs[0], senderAddr, caller, 10)
})
t.Run("outer-revert", func(t *testing.T) {
sdb := mkState(senderAlloc(types.GenesisAlloc{
caller: {Code: callCode7708(callee, 3, []byte{0x60, 0x00, 0x60, 0x00, 0xfd})},
callee: {Code: []byte{0x00}},
}))
res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
if err != nil || res.Err != vm.ErrExecutionReverted {
t.Fatalf("result=%v err=%v, want execution revert", res, err)
}
if logs := transferLogs7708(sdb); len(logs) != 0 {
t.Fatalf("reverted transaction retained logs: %+v", logs)
}
})
t.Run("callcode", func(t *testing.T) {
sdb := mkState(senderAlloc(types.GenesisAlloc{
caller: {Code: callOpcode7708(callee, 3, 0xf2, []byte{0x00})},
callee: {Code: []byte{0x00}},
}))
res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
logs := transferLogs7708(sdb)
if len(logs) != 2 {
t.Fatalf("logs = %+v, want only top-level transfer and LOG0", logs)
}
assertTransfer7708(t, logs[0], senderAddr, caller, 10)
})
t.Run("delegatecall", func(t *testing.T) {
implementation := common.HexToAddress("0x7708000000000000000000000000000000000013")
sdb := mkState(senderAlloc(types.GenesisAlloc{
caller: {Code: delegateCode7708(implementation)},
implementation: {Code: callCode7708(callee, 3, []byte{0x00})},
callee: {Code: []byte{0x00}},
}))
res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
logs := transferLogs7708(sdb)
if len(logs) != 3 {
t.Fatalf("logs = %+v, want top-level transfer, delegated LOG0, inner transfer", logs)
}
assertTransfer7708(t, logs[0], senderAddr, caller, 10)
if logs[1].Address != caller {
t.Fatalf("delegated LOG0 address = %s, want executing context %s", logs[1].Address, caller)
}
assertTransfer7708(t, logs[2], caller, callee, 3)
})
}
// TestEIP7708Create checks successful CREATE/CREATE2 endowments and that
// failing initcode rolls the transfer log back with the failed creation.
func TestEIP7708Create(t *testing.T) {
t.Run("transaction", func(t *testing.T) {
sdb := mkState(senderAlloc(nil))
res, _, err := applyMsg(t, sdb, createTx7708(5, []byte{0x00}))
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
logs := transferLogs7708(sdb)
if len(logs) != 1 {
t.Fatalf("logs = %+v, want one", logs)
}
assertTransfer7708(t, logs[0], senderAddr, crypto.CreateAddress(senderAddr, 0), 5)
})
t.Run("revert", func(t *testing.T) {
sdb := mkState(senderAlloc(nil))
res, _, err := applyMsg(t, sdb, createTx7708(5, []byte{0x60, 0x00, 0x60, 0x00, 0xfd}))
if err != nil || res.Err != vm.ErrExecutionReverted {
t.Fatalf("result=%v err=%v, want execution revert", res, err)
}
if logs := transferLogs7708(sdb); len(logs) != 0 {
t.Fatalf("failed create retained logs: %+v", logs)
}
})
t.Run("create2", func(t *testing.T) {
sdb := mkState(senderAlloc(nil))
evm := amsterdamCoreEVM(sdb)
_, created, _, err := evm.Create2(senderAddr, []byte{0x00}, vm.NewGasBudget(500_000, 0), uint256.NewInt(5), new(uint256.Int))
if err != nil {
t.Fatal(err)
}
logs := transferLogs7708(sdb)
if len(logs) != 1 {
t.Fatalf("logs = %+v, want one", logs)
}
assertTransfer7708(t, logs[0], senderAddr, created, 5)
})
}
// TestEIP7708Selfdestruct verifies the EIP-8246-compatible SELFDESTRUCT
// cases: different beneficiaries transfer and log, self beneficiaries do not.
func TestEIP7708Selfdestruct(t *testing.T) {
contract := common.HexToAddress("0x7708000000000000000000000000000000000020")
beneficiary := common.HexToAddress("0x7708000000000000000000000000000000000021")
code := append([]byte{0x73}, beneficiary.Bytes()...)
code = append(code, 0xff)
sdb := mkState(senderAlloc(types.GenesisAlloc{contract: {Code: code}}))
res, _, err := applyMsg(t, sdb, callTx(0, contract, 7, 300_000, nil))
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
logs := transferLogs7708(sdb)
if len(logs) != 2 {
t.Fatalf("logs = %+v, want transaction and selfdestruct transfers", logs)
}
assertTransfer7708(t, logs[0], senderAddr, contract, 7)
assertTransfer7708(t, logs[1], contract, beneficiary, 7)
selfCode := append([]byte{0x73}, contract.Bytes()...)
selfCode = append(selfCode, 0xff)
sdb = mkState(senderAlloc(types.GenesisAlloc{contract: {Code: selfCode}}))
res, _, err = applyMsg(t, sdb, callTx(0, contract, 7, 300_000, nil))
if err != nil || res.Err != nil {
t.Fatalf("self result=%v err=%v", res, err)
}
logs = transferLogs7708(sdb)
if len(logs) != 1 {
t.Fatalf("selfdestruct-to-self logs = %+v, want top-level transfer only", logs)
}
assertTransfer7708(t, logs[0], senderAddr, contract, 7)
}
// TestEIP7708Negative and TestEIP7708Transition pin the no-log cases and the
// activation guard independently of transaction construction.
func TestEIP7708Negative(t *testing.T) {
recipient := common.HexToAddress("0x7708000000000000000000000000000000000030")
for _, tc := range []struct {
name string
to common.Address
value int64
}{
{"zero", recipient, 0},
{"self", senderAddr, 1},
} {
t.Run(tc.name, func(t *testing.T) {
sdb := mkState(senderAlloc(nil))
res, _, err := applyMsg(t, sdb, callTx(0, tc.to, tc.value, 100_000, nil))
if err != nil || res.Err != nil {
t.Fatalf("result=%v err=%v", res, err)
}
if logs := transferLogs7708(sdb); len(logs) != 0 {
t.Fatalf("unexpected logs: %+v", logs)
}
})
}
}
func TestEIP7708Transition(t *testing.T) {
from := common.HexToAddress("0x7708000000000000000000000000000000000040")
to := common.HexToAddress("0x7708000000000000000000000000000000000041")
sdb := mkState(types.GenesisAlloc{from: {Balance: big.NewInt(2)}})
Transfer(sdb, from, to, uint256.NewInt(1), &params.Rules{})
if logs := transferLogs7708(sdb); len(logs) != 0 {
t.Fatalf("pre-Amsterdam logs = %+v, want none", logs)
}
Transfer(sdb, from, to, uint256.NewInt(1), &rules8037)
logs := transferLogs7708(sdb)
if len(logs) != 1 {
t.Fatalf("Amsterdam logs = %+v, want one", logs)
}
assertTransfer7708(t, logs[0], from, to, 1)
}
// TestEIP7708Fees ensures a priority-fee credit to coinbase remains outside
// EIP-7708: the transaction value transfer is the receipt's only log.
func TestEIP7708Fees(t *testing.T) {
env := newBALTestEnv(nil)
coinbase := common.HexToAddress("0x7708000000000000000000000000000000000050")
recipient := common.HexToAddress("0x7708000000000000000000000000000000000051")
engine := beacon.New(ethash.NewFaker())
_, _, receipts := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, g *BlockGen) {
g.SetCoinbase(coinbase)
g.AddTx(env.tx(0, &recipient, big.NewInt(1), txGasNewAccount, 1, nil))
})
logs := receipts[0][0].Logs
if len(logs) != 1 {
t.Fatalf("logs = %+v, want transaction transfer only", logs)
}
assertTransfer7708(t, logs[0], env.from, recipient, 1)
}

View file

@ -152,6 +152,52 @@ func assertEmpty(t *testing.T, aa *bal.AccountAccess) {
}
}
type balStorageChangeExpectation struct {
index uint32
value common.Hash
}
func assertStorageChanges(t *testing.T, aa *bal.AccountAccess, key common.Hash, want []balStorageChangeExpectation) {
t.Helper()
wantKey := new(uint256.Int).SetBytes(key[:])
for _, slot := range aa.StorageChanges {
if slot.Slot.Cmp(wantKey) != 0 {
continue
}
if len(slot.SlotChanges) != len(want) {
t.Fatalf("slot %x changes: have %+v, want %d entries", key, slot.SlotChanges, len(want))
}
for i, expected := range want {
if slot.SlotChanges[i].BlockAccessIndex != expected.index {
t.Fatalf("slot %x change %d index: have %d, want %d", key, i, slot.SlotChanges[i].BlockAccessIndex, expected.index)
}
wantValue := new(uint256.Int).SetBytes(expected.value[:])
if slot.SlotChanges[i].PostValue.Cmp(wantValue) != 0 {
t.Fatalf("slot %x change %d value: have %s, want %s", key, i, slot.SlotChanges[i].PostValue, wantValue)
}
}
return
}
t.Fatalf("slot %x missing from storage_changes", key)
}
func assertStorageChangeAt(t *testing.T, aa *bal.AccountAccess, key common.Hash, index uint32) {
t.Helper()
wantKey := new(uint256.Int).SetBytes(key[:])
for _, slot := range aa.StorageChanges {
if slot.Slot.Cmp(wantKey) != 0 {
continue
}
for _, change := range slot.SlotChanges {
if change.BlockAccessIndex == index {
return
}
}
t.Fatalf("slot %x has no change at index %d: %+v", key, index, slot.SlotChanges)
}
t.Fatalf("slot %x missing from storage_changes", key)
}
// txGasNewAccount covers the base tx cost plus the EIP-8037 account-creation
// state-gas charge (STATE_BYTES_PER_NEW_ACCOUNT × CPSB ≈ 183,600) that is
// incurred when value is transferred to a non-existent account under Amsterdam.
@ -230,21 +276,30 @@ func TestBALEmptyBlockExcludesCoinbase(t *testing.T) {
assertAbsent(t, b, coinbase)
}
// TestBALCoinbaseTipCapturesBalance: positive priority fee credits coinbase
// and the balance change appears in the BAL.
func TestBALCoinbaseTipCapturesBalance(t *testing.T) {
coinbase := common.Address{0xc0}
to := common.HexToAddress("0xabba")
// TestBALCoinbasePerTxBalance checks that fee-recipient balances are recorded
// after each transaction, rather than only once at the end of the block.
func TestBALCoinbasePerTxBalance(t *testing.T) {
coinbase := common.HexToAddress("0xc01babe")
to := common.HexToAddress("0xc0ffee")
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
b, receipts := env.run(t, func(g *BlockGen) {
g.SetCoinbase(coinbase)
g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 2 /* gwei tip */, nil))
g.AddTx(env.tx(0, &to, big.NewInt(0), 100_000, 1, nil))
g.AddTx(env.tx(1, &to, big.NewInt(0), 100_000, 1, nil))
})
cb := assertPresent(t, b, coinbase)
if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].PostBalance.Sign() == 0 {
t.Fatalf("coinbase missing positive balance change: %+v", cb.BalanceChanges)
feeRecipient := assertPresent(t, b, coinbase)
if len(feeRecipient.BalanceChanges) != 2 {
t.Fatalf("fee recipient must have one balance per transaction: %+v", feeRecipient.BalanceChanges)
}
first := new(big.Int).Mul(new(big.Int).SetUint64(receipts[0].GasUsed), newGwei(1))
second := new(big.Int).Add(first, new(big.Int).Mul(new(big.Int).SetUint64(receipts[1].GasUsed), newGwei(1)))
for i, want := range []*big.Int{first, second} {
change := feeRecipient.BalanceChanges[i]
if change.BlockAccessIndex != uint32(i+1) || change.PostBalance.ToBig().Cmp(want) != 0 {
t.Fatalf("fee recipient change %d: have %+v, want index %d balance %s", i, change, i+1, want)
}
}
}
@ -582,7 +637,7 @@ func TestBALCallCodeToDelegatedTargetBalanceFail(t *testing.T) {
assertEmpty(t, assertPresent(t, b, impl))
}
// ============================== Revert behaviour ==============================
// ============================== Reverts and exceptional halts ==============================
// TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still
// records the touched contract in the BAL with an empty change set.
@ -619,30 +674,32 @@ func TestBALSenderRecordedOnRevert(t *testing.T) {
}
}
// ============================== Storage inclusion ==============================
// TestBALStorageWriteRecorded: SSTORE places the slot in storage_changes and
// keeps it out of storage_reads.
func TestBALStorageWriteRecorded(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x01))
// PUSH1 0x42 PUSH1 0x01 SSTORE STOP
code := []byte{0x60, 0x42, 0x60, 0x01, 0x55, 0x00}
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
// TestBALDelegationTargetOOG exercises the exceptional-halt
// boundary in EIP-7928. The top-level recipient is loaded to discover its
// delegation, but the runtime budget is insufficient for the cold target
// access, so the implementation must not enter the BAL.
func TestBALDelegationTargetOOG(t *testing.T) {
authority := common.HexToAddress("0xa7702")
implementation := common.HexToAddress("0x1a11")
env := newBALTestEnv(types.GenesisAlloc{
authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
implementation: {Code: []byte{0x00}, Balance: common.Big0},
})
aa := assertPresent(t, b, contract)
if !hasStorageWrite(b, contract, slot) {
t.Fatalf("expected slot 0x01 in storage_changes\n%s", b.PrettyPrint())
}
if hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("slot 0x01 must NOT appear in storage_reads")
b, receipts := env.run(t, func(g *BlockGen) {
// This transaction has exactly enough gas for its intrinsic charges, but
// less than the 3,000 cold-account runtime charge needed to load target.
g.AddTx(env.tx(0, &authority, big.NewInt(0), 15_000, 0, nil))
})
if receipts[0].Status != types.ReceiptStatusFailed {
t.Fatalf("expected runtime out-of-gas receipt, have status %d", receipts[0].Status)
}
assertEmpty(t, assertPresent(t, b, authority))
assertAbsent(t, b, implementation)
}
// ============================== Storage inclusion ==============================
// TestBALStorageSloadOnly: SLOAD without a write puts the slot in storage_reads.
func TestBALStorageSloadOnly(t *testing.T) {
contract := common.HexToAddress("0xc1")
@ -890,6 +947,38 @@ func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) {
}
}
// TestBALInEVMCreateOOGDestination distinguishes a CREATE precheck abort from
// an account-creation runtime OOG. The latter calls StateDB.Empty on the
// destination to determine whether the creation charge is due, so the
// destination has been accessed and must appear in the BAL even though the
// failed charge halts the transaction before evm.create runs.
func TestBALInEVMCreateOOGDestination(t *testing.T) {
factory := common.HexToAddress("0xfac4")
// PUSH1 0 (length) PUSH1 0 (offset) PUSH1 0 (value) CREATE POP STOP.
// The factory has enough regular gas for CREATE's opcode cost but not enough
// combined gas to pay Amsterdam's 183,600 account-creation state charge.
code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0xf0, 0x50, 0x00}
env := newBALTestEnv(types.GenesisAlloc{
factory: {Code: code, Balance: common.Big0, Nonce: 1},
})
b, receipts := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &factory, big.NewInt(0), 30_000, 0, nil))
})
if receipts[0].Status != types.ReceiptStatusFailed {
t.Fatalf("expected account-creation runtime OOG, have status %d", receipts[0].Status)
}
wouldBeDest := crypto.CreateAddress(factory, 1)
assertEmpty(t, assertPresent(t, b, wouldBeDest))
// evm.create is never entered, so its creator-nonce bump does not occur.
aa := assertPresent(t, b, factory)
if len(aa.NonceChanges) != 0 {
t.Fatalf("factory nonce must not be bumped before account-creation charge succeeds: %+v", aa.NonceChanges)
}
}
// TestBALInEVMCreateDeploysContract: a CREATE issued by an existing contract
// (not a top-level CREATE tx) records the deployed address in the BAL.
func TestBALInEVMCreateDeploysContract(t *testing.T) {
@ -1066,7 +1155,60 @@ func TestBALSelfDestructToSelfPrefundedUnchanged(t *testing.T) {
assertEmpty(t, aa)
}
// ============================== Mid-tx balance round-trip ==============================
// TestBALSelfDestructStorageRead checks the in-transaction
// SELFDESTRUCT rule: storage changed by an account that is subsequently
// deleted is retained as a read footprint, not as a storage change. The
// deleted account also must not carry nonce or code changes.
func TestBALSelfDestructStorageRead(t *testing.T) {
beneficiary := common.HexToAddress("0xbeefbeef")
slot := common.BigToHash(big.NewInt(0x05))
env := newBALTestEnv(nil)
// SSTORE(5, 0x42); SELFDESTRUCT(beneficiary). This executes in init code,
// so the created account is deleted under EIP-6780.
init := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x73}
init = append(init, beneficiary.Bytes()...)
init = append(init, 0xff)
b, receipts := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, nil, big.NewInt(100), 1_000_000, 0, init))
})
deleted := assertPresent(t, b, receipts[0].ContractAddress)
if !hasSlotIn(deleted.StorageReads, slot) {
t.Fatalf("deleted account storage slot %x must be in storage_reads\n%s", slot, b.PrettyPrint())
}
if hasStorageWrite(b, deleted.Address, slot) {
t.Fatalf("deleted account storage slot %x must not remain in storage_changes\n%s", slot, b.PrettyPrint())
}
if len(deleted.NonceChanges) != 0 || len(deleted.CodeChanges) != 0 {
t.Fatalf("deleted account must not record nonce or code: %+v", deleted)
}
}
// TestBALSelfDestructDelegatedBeneficiary checks that SELFDESTRUCT treats a
// delegated authority as its beneficiary account, without loading the
// implementation as executable code.
func TestBALSelfDestructDelegatedBeneficiary(t *testing.T) {
victim := common.HexToAddress("0x5e1f")
authority := common.HexToAddress("0xa7702")
implementation := common.HexToAddress("0x1a11")
victimCode := append([]byte{0x73}, authority.Bytes()...)
victimCode = append(victimCode, 0xff) // SELFDESTRUCT
env := newBALTestEnv(types.GenesisAlloc{
victim: {Code: victimCode, Balance: common.Big0},
authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
implementation: {Code: []byte{0x00}, Balance: common.Big0},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &victim, big.NewInt(0), 1_000_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, authority))
assertAbsent(t, b, implementation)
}
// ============================== Balance accounting ==============================
// TestBALMidTxBalanceRoundTrip: when an address's balance changes during a
// transaction but returns to its pre-transaction value, the address is still
@ -1107,6 +1249,38 @@ func TestBALMidTxBalanceRoundTrip(t *testing.T) {
}
}
// TestBALGasRefundSenderBalance ensures a gas-refunding SSTORE still records
// the sender's post-transaction balance, after the refund has been applied
// to the transaction's final gas charge.
func TestBALGasRefundSenderBalance(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x05))
env := newBALTestEnv(types.GenesisAlloc{
contract: {
Code: []byte{0x5f, 0x60, 0x05, 0x55, 0x00}, // SSTORE(5, 0); STOP
Balance: common.Big0,
Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(1))},
},
})
_, blocks, receipts := GenerateChainWithGenesis(env.gspec, beacon.New(ethash.NewFaker()), 1, func(_ int, g *BlockGen) {
g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
b := blocks[0].AccessList()
if b == nil {
t.Fatal("expected non-nil block access list")
}
sender := assertPresent(t, b, env.from)
if len(sender.BalanceChanges) != 1 || sender.BalanceChanges[0].BlockAccessIndex != 1 {
t.Fatalf("sender needs one post-tx balance at index 1: %+v", sender.BalanceChanges)
}
gasCost := new(big.Int).Mul(new(big.Int).SetUint64(receipts[0][0].GasUsed), blocks[0].BaseFee())
want := new(big.Int).Sub(newGwei(1_000_000_000), gasCost)
if sender.BalanceChanges[0].PostBalance.ToBig().Cmp(want) != 0 {
t.Fatalf("sender post-refund balance: have %s, want %s", sender.BalanceChanges[0].PostBalance, want)
}
}
// ============================== System contracts (pre/post-execution) ==============================
// TestBALSystemContractsPresent: per EIP-7928, "System contract addresses
@ -1146,6 +1320,107 @@ func TestBALSystemContractsPresent(t *testing.T) {
}
}
// TestBALPreExecutionStorage checks the precise pre-execution entries required
// by EIP-7928: both EIP-2935 and EIP-4788 changes belong to index zero, before
// any transaction in the block.
func TestBALPreExecutionStorage(t *testing.T) {
beaconRoot := common.HexToHash("0xbeac")
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.SetParentBeaconRoot(beaconRoot)
})
// The generated block is number 1 and timestamp 10. EIP-2935 stores the
// parent hash at (block.number - 1) % 8191, i.e. slot zero here.
history := assertPresent(t, b, params.HistoryStorageAddress)
assertStorageChanges(t, history, common.Hash{}, []balStorageChangeExpectation{{
index: 0,
value: env.gspec.ToBlock().Hash(),
}})
// EIP-4788 stores timestamp at timestamp % 8191 and the parent beacon root
// at that slot plus 8191.
const timestamp = 10
beacon := assertPresent(t, b, params.BeaconRootsAddress)
assertStorageChanges(t, beacon, common.BigToHash(big.NewInt(timestamp)), []balStorageChangeExpectation{{
index: 0,
value: common.BigToHash(big.NewInt(timestamp)),
}})
assertStorageChanges(t, beacon, common.BigToHash(big.NewInt(timestamp+8191)), []balStorageChangeExpectation{{
index: 0,
value: beaconRoot,
}})
}
// TestBALPostExecutionQueueReads covers the EIP-7002 and EIP-7251 rule that
// a post-execution system call accesses queue metadata in slots 0..3 at index
// n+1, while the queued payload slots are read-only.
func TestBALPostExecutionQueueReads(t *testing.T) {
withdrawalData := common.FromHex("b917cfdc0d25b72d55cf94db328e1629b7f4fde2c30cdacf873b664416f76a0c7f7cc50c9f72a3cb84be88144cde91250000000000000d80")
consolidationData := common.FromHex("b917cfdc0d25b72d55cf94db328e1629b7f4fde2c30cdacf873b664416f76a0c7f7cc50c9f72a3cb84be88144cde9125b9812f7d0b1f2f969b52bbb2d316b0c2fa7c9dba85c428c5e6c27766bcc4b0c6e874702ff1eb1c7024b08524a9771601")
for _, tc := range []struct {
name string
addr common.Address
data []byte
}{
{"withdrawal queue (EIP-7002)", params.WithdrawalQueueAddress, withdrawalData},
{"consolidation queue (EIP-7251)", params.ConsolidationQueueAddress, consolidationData},
} {
t.Run(tc.name, func(t *testing.T) {
env := newBALTestEnv(nil)
// A request transaction writes several new storage slots under
// Amsterdam's state-gas schedule. Raise the test chain's gas limit so
// all 17 requests fit in the first block.
env.gspec.GasLimit = 200_000_000
_, blocks, _ := GenerateChainWithGenesis(env.gspec, beacon.New(ethash.NewFaker()), 2, func(i int, g *BlockGen) {
if i == 1 {
// Make the post-execution system call occur after one ordinary
// transaction, proving it uses n + 1 rather than a fixed index.
to := common.HexToAddress("0xf00")
g.AddTx(env.tx(17, &to, big.NewInt(0), 100_000, 0, nil))
return
}
// The system call processes at most 16 requests per block. Leave one
// payload for block 2 so its storage access is genuinely a read.
for nonce := uint64(0); nonce < 17; nonce++ {
g.AddTx(env.tx(nonce, &tc.addr, newGwei(1), 5_000_000, 0, tc.data))
}
})
b := blocks[1].AccessList()
if b == nil {
t.Fatal("expected non-nil block access list")
}
aa := assertPresent(t, b, tc.addr)
// Block 2 contains one user transaction, so its post-execution call is
// index 2 (= n + 1).
for slot := int64(0); slot < 4; slot++ {
key := common.BigToHash(big.NewInt(slot))
if hasSlotIn(aa.StorageReads, key) {
continue // A metadata slot whose value is unchanged is read-only.
}
assertStorageChangeAt(t, aa, key, 2)
}
foundPayloadRead := false
for _, slot := range aa.StorageReads {
if slot.Uint64() >= 4 {
foundPayloadRead = true
}
}
if !foundPayloadRead {
t.Fatalf("post-execution queue call must leave payload slots in storage_reads\n%s", b.PrettyPrint())
}
for _, change := range aa.StorageChanges {
if change.Slot.Uint64() >= 4 {
t.Fatalf("post-execution queue call must not write payload slot %s\n%s", change.Slot, b.PrettyPrint())
}
}
})
}
}
// ============================== Withdrawals ==============================
// TestBALWithdrawalZeroAmountIncluded: a withdrawal with amount 0 still puts
@ -1335,6 +1610,49 @@ func TestBALAuthFailedAfterLoadEmptyChangeSet(t *testing.T) {
}
}
// TestBALAuthOOGRecipientExcluded covers the exceptional halt boundary where
// an EIP-7702 authorization exhausts runtime gas before the transaction's
// recipient is loaded. The authority was already loaded by authorization
// validation, but tx.to must not enter the BAL.
func TestBALAuthOOGRecipientExcluded(t *testing.T) {
authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
authority := crypto.PubkeyToAddress(authKey.PublicKey)
recipient := common.HexToAddress("0xdec1a1")
implementation := common.HexToAddress("0x1a11")
env := newBALTestEnv(nil)
auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: implementation,
Nonce: 0,
})
if err != nil {
t.Fatalf("sign auth: %v", err)
}
b, receipts := env.run(t, func(g *BlockGen) {
tx, err := types.SignTx(types.NewTx(&types.SetCodeTx{
ChainID: uint256.MustFromBig(env.cfg.ChainID),
Nonce: 0,
To: recipient,
Value: new(uint256.Int),
Gas: 30_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int),
AuthList: []types.SetCodeAuthorization{auth},
}), env.signer, env.key)
if err != nil {
t.Fatalf("sign SetCodeTx: %v", err)
}
g.AddTx(tx)
})
if receipts[0].Status != types.ReceiptStatusFailed {
t.Fatalf("expected authorization runtime OOG, have status %d", receipts[0].Status)
}
assertEmpty(t, assertPresent(t, b, authority))
assertAbsent(t, b, recipient)
assertAbsent(t, b, implementation)
}
// TestBALMultipleAuthsOnlyLoadedIncluded: a SetCode tx with a mix of valid and
// pre-load-failed auths lists only the loaded authorities in the BAL.
func TestBALMultipleAuthsOnlyLoadedIncluded(t *testing.T) {
@ -1453,3 +1771,409 @@ func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) {
t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges)
}
}
// TestBALReapplyDelegation covers an authorization whose final code equals its
// pre-transaction code. The authority nonce changes, but the unchanged
// delegation must not create a code change or implementation read.
func TestBALReapplyDelegation(t *testing.T) {
authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
authority := crypto.PubkeyToAddress(authKey.PublicKey)
implementation := common.HexToAddress("0x1a11")
env := newBALTestEnv(types.GenesisAlloc{
authority: {Nonce: 1, Code: types.AddressToDelegation(implementation), Balance: common.Big0},
implementation: {Code: []byte{0x00}, Balance: common.Big0},
})
auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: implementation,
Nonce: 1,
})
if err != nil {
t.Fatalf("sign auth: %v", err)
}
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth}))
})
aa := assertPresent(t, b, authority)
if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].PostNonce != 2 {
t.Fatalf("reapplied delegation nonce: %+v", aa.NonceChanges)
}
if len(aa.CodeChanges) != 0 {
t.Fatalf("reapplying unchanged delegation must not record code: %+v", aa.CodeChanges)
}
assertAbsent(t, b, implementation)
}
// TestBALSetCodeTxDelegatedRecipient verifies that authorizations are applied
// before the top-level call. A newly delegated tx.to must immediately load and
// execute its implementation.
func TestBALSetCodeTxDelegatedRecipient(t *testing.T) {
authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
authority := crypto.PubkeyToAddress(authKey.PublicKey)
implementation := common.HexToAddress("0x1a11")
slot := common.BigToHash(big.NewInt(0x07))
implementationCode := []byte{0x60, 0x07, 0x54, 0x50, 0x00} // SLOAD(7); POP; STOP.
env := newBALTestEnv(types.GenesisAlloc{
implementation: {Code: implementationCode, Balance: common.Big0},
})
auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: implementation,
Nonce: 0,
})
if err != nil {
t.Fatalf("sign auth: %v", err)
}
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.newSetCodeTx(t, 0, authority, []types.SetCodeAuthorization{auth}))
})
authorityAccess := assertPresent(t, b, authority)
if len(authorityAccess.NonceChanges) != 1 || authorityAccess.NonceChanges[0].PostNonce != 1 {
t.Fatalf("authority nonce after installation: %+v", authorityAccess.NonceChanges)
}
if len(authorityAccess.CodeChanges) != 1 || !bytes.Equal(authorityAccess.CodeChanges[0].NewCode, types.AddressToDelegation(implementation)) {
t.Fatalf("authority delegation code after installation: %+v", authorityAccess.CodeChanges)
}
if !hasSlotIn(authorityAccess.StorageReads, slot) {
t.Fatalf("same-transaction delegated execution must read authority storage\n%s", b.PrettyPrint())
}
assertEmpty(t, assertPresent(t, b, implementation))
}
// ============================== Read-list regression cases ==============================
// TestBALStorageWriteThenRead covers the opposite ordering of
// TestBALStorageReadThenWriteOnlyInWrites. Once an index changes a slot, all
// subsequent reads of that slot must be hidden by storage_changes.
func TestBALStorageWriteThenRead(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x05))
// SSTORE(0x05, 0x42); SLOAD(0x05); POP; STOP.
code := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x60, 0x05, 0x54, 0x50, 0x00}
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, contract)
assertStorageChanges(t, aa, slot, []balStorageChangeExpectation{{index: 1, value: common.BigToHash(big.NewInt(0x42))}})
if hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("written slot %x must not remain in storage_reads\n%s", slot, b.PrettyPrint())
}
}
// TestBALStorageNoOpAfterWrite checks that the no-op decision
// is relative to the state before the current block-access index. Tx2 writes
// the value that Tx1 already committed, so it must not replace Tx1's change
// with a read entry.
func TestBALStorageNoOpAfterWrite(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x05))
// SSTORE(0x05, 0x42); STOP.
code := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x00}
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
g.AddTx(env.tx(1, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, contract)
assertStorageChanges(t, aa, slot, []balStorageChangeExpectation{{index: 1, value: common.BigToHash(big.NewInt(0x42))}})
if hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("later no-op must not add slot %x to storage_reads\n%s", slot, b.PrettyPrint())
}
}
// TestBALStorageChangesAcrossTxs verifies that a slot changed at two
// distinct transaction indices retains both post-index values, including a
// later reset to its pre-block value.
func TestBALStorageChangesAcrossTxs(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x05))
// CALLDATALOAD(0); SSTORE(0x05, value); STOP.
code := []byte{0x60, 0x00, 0x35, 0x60, 0x05, 0x55, 0x00}
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, common.LeftPadBytes([]byte{0x42}, 32)))
g.AddTx(env.tx(1, &contract, big.NewInt(0), 1_000_000, 0, make([]byte, 32)))
})
aa := assertPresent(t, b, contract)
assertStorageChanges(t, aa, slot, []balStorageChangeExpectation{
{index: 1, value: common.BigToHash(big.NewInt(0x42))},
{index: 2, value: common.Hash{}},
})
if hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("slot with committed changes must not be in storage_reads\n%s", b.PrettyPrint())
}
}
// TestBALRevertedSStoreRead verifies that SSTORE performs an implicit read once
// its access boundary is crossed. The later REVERT discards the write but not
// that read footprint.
func TestBALRevertedSStoreRead(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x05))
// SSTORE(0x05, 0x42); REVERT(0, 0).
code := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x60, 0x00, 0x60, 0x00, 0xfd}
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, contract)
if !hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("reverted SSTORE must leave slot %x in storage_reads\n%s", slot, b.PrettyPrint())
}
if hasStorageWrite(b, contract, slot) {
t.Fatalf("reverted SSTORE must not leave a storage change\n%s", b.PrettyPrint())
}
}
// TestBALParentRevertSStoreRead checks the journal boundary between call frames:
// a child returns successfully after SSTORE, then its parent reverts the whole
// call tree. The child's slot is still a BAL read.
func TestBALParentRevertSStoreRead(t *testing.T) {
child := common.HexToAddress("0xc1")
parent := common.HexToAddress("0xc2")
slot := common.BigToHash(big.NewInt(0x05))
childCode := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x00}
parentCode := makeStubCaller(0xf1 /* CALL */, child)
parentCode = append(parentCode[:len(parentCode)-1], 0x60, 0x00, 0x60, 0x00, 0xfd)
env := newBALTestEnv(types.GenesisAlloc{
child: {Code: childCode, Balance: common.Big0},
parent: {Code: parentCode, Balance: common.Big0},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &parent, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, child)
if !hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("parent-reverted child write must leave slot %x in storage_reads\n%s", slot, b.PrettyPrint())
}
if hasStorageWrite(b, child, slot) {
t.Fatalf("parent-reverted child write must not leave a storage change\n%s", b.PrettyPrint())
}
}
// TestBALStorageReadsSorted exercises the final encoding of a read-only set.
// Repeated reads must produce one key, and keys are ordered lexicographically
// regardless of execution order.
func TestBALStorageReadsSorted(t *testing.T) {
contract := common.HexToAddress("0xc1")
// SLOAD(9); SLOAD(1); SLOAD(9); STOP.
code := []byte{
0x60, 0x09, 0x54, 0x50,
0x60, 0x01, 0x54, 0x50,
0x60, 0x09, 0x54, 0x50,
0x00,
}
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, contract)
if len(aa.StorageReads) != 2 || aa.StorageReads[0].Uint64() != 1 || aa.StorageReads[1].Uint64() != 9 {
t.Fatalf("storage_reads must be deduplicated and sorted: %+v", aa.StorageReads)
}
}
// TestBALAccessListSlotExcluded ensures an EIP-2930 storage-key warming entry
// changes gas only. It must not create a storage_reads entry unless the EVM
// actually executes an access to that slot.
func TestBALAccessListSlotExcluded(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x07))
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: []byte{0x00}, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
tx := types.MustSignNewTx(env.key, env.signer, &types.DynamicFeeTx{
ChainID: env.cfg.ChainID,
Nonce: 0,
To: &contract,
Value: new(big.Int),
Gas: 1_000_000,
GasFeeCap: newGwei(10),
GasTipCap: new(big.Int),
AccessList: types.AccessList{{
Address: contract,
StorageKeys: []common.Hash{slot},
}},
})
g.AddTx(tx)
})
aa := assertPresent(t, b, contract)
if hasSlotIn(aa.StorageReads, slot) || hasStorageWrite(b, contract, slot) {
t.Fatalf("untouched access-list slot %x must be absent from BAL\n%s", slot, b.PrettyPrint())
}
}
// ============================== EIP-7702 execution-time delegation ==============================
func makeDelegationProbe(op byte, target common.Address) []byte {
if op == 0x3c { // EXTCODECOPY needs length, code offset and memory offset.
code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
code = append(code, target.Bytes()...)
return append(code, op, 0x00)
}
code := append([]byte{0x73}, target.Bytes()...)
return append(code, op, 0x50, 0x00) // opcode, POP, STOP
}
// TestBALDelegationInspection ensures the code-reading opcodes observe an
// authority's 7702 indicator as its own code. They access the authority
// but MUST NOT load the implementation address.
func TestBALDelegationInspection(t *testing.T) {
authority := common.HexToAddress("0xa7702")
implementation := common.HexToAddress("0x1a11")
for _, tc := range []struct {
name string
op byte
}{
{"balance", 0x31},
{"extcodesize", 0x3b},
{"extcodecopy", 0x3c},
{"extcodehash", 0x3f},
} {
t.Run(tc.name, func(t *testing.T) {
caller := common.HexToAddress("0xca11")
env := newBALTestEnv(types.GenesisAlloc{
caller: {Code: makeDelegationProbe(tc.op, authority), Balance: common.Big0},
authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
implementation: {Code: []byte{0x00}, Balance: common.Big0},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, authority))
assertAbsent(t, b, implementation)
})
}
}
// TestBALDelegatedCallStorage checks CALL's storage context. The implementation
// code is fetched from implementation, but its SLOAD executes in the delegated
// authority's storage, not implementation's.
func TestBALDelegatedCallStorage(t *testing.T) {
authority := common.HexToAddress("0xa7702")
implementation := common.HexToAddress("0x1a11")
slot := common.BigToHash(big.NewInt(0x07))
implCode := []byte{0x60, 0x07, 0x54, 0x50, 0x00} // SLOAD(7), POP, STOP
env := newBALTestEnv(types.GenesisAlloc{
authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
implementation: {Code: implCode, Balance: common.Big0},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &authority, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, authority)
if !hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("delegated CALL read must belong to authority\n%s", b.PrettyPrint())
}
assertEmpty(t, assertPresent(t, b, implementation))
}
// TestBALDelegatedCallFamilyStorage checks the storage context of the CALL
// family against a delegated authority: the resolved implementation supplies
// code only, while the storage read belongs to the executing context — the
// authority for CALL/STATICCALL, the caller for DELEGATECALL/CALLCODE.
func TestBALDelegatedCallFamilyStorage(t *testing.T) {
caller := common.HexToAddress("0xca11")
authority := common.HexToAddress("0xa7702")
implementation := common.HexToAddress("0x1a11")
slot := common.BigToHash(big.NewInt(0x07))
implCode := []byte{0x60, 0x07, 0x54, 0x50, 0x00} // SLOAD(7), POP, STOP
cases := []struct {
name string
op byte
readOwner common.Address
}{
{"call", 0xf1, authority},
{"callcode", 0xf2, caller},
{"delegatecall", 0xf4, caller},
{"staticcall", 0xfa, authority},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{
caller: {Code: makeStubCaller(tc.op, authority), Balance: common.Big0},
authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
implementation: {Code: implCode, Balance: common.Big0},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
owner := assertPresent(t, b, tc.readOwner)
if !hasSlotIn(owner.StorageReads, slot) {
t.Fatalf("%s read must belong to %x\n%s", tc.name, tc.readOwner, b.PrettyPrint())
}
for _, other := range []common.Address{caller, authority} {
if other != tc.readOwner {
assertEmpty(t, assertPresent(t, b, other))
}
}
assertEmpty(t, assertPresent(t, b, implementation))
})
}
}
// TestBALDelegationOneHop verifies that resolving A -> B does not
// recursively resolve B -> C. A and B are state accesses; C is not.
func TestBALDelegationOneHop(t *testing.T) {
caller := common.HexToAddress("0xca11")
authority := common.HexToAddress("0xa7702")
delegated := common.HexToAddress("0xb7702")
secondHop := common.HexToAddress("0xc7702")
env := newBALTestEnv(types.GenesisAlloc{
caller: {Code: makeStubCaller(0xf1 /* CALL */, authority), Balance: common.Big0},
authority: {Code: types.AddressToDelegation(delegated), Balance: common.Big0},
delegated: {Code: types.AddressToDelegation(secondHop), Balance: common.Big0},
secondHop: {Code: []byte{0x00}, Balance: common.Big0},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
assertPresent(t, b, authority)
assertPresent(t, b, delegated)
assertAbsent(t, b, secondHop)
}
// TestBALDelegatedSender distinguishes transaction origination from execution.
// A sender may carry a valid delegation indicator, but its implementation is
// not accessed unless a call actually targets sender.
func TestBALDelegatedSender(t *testing.T) {
to := common.HexToAddress("0xb0b")
implementation := common.HexToAddress("0x1a11")
env := newBALTestEnv(types.GenesisAlloc{
implementation: {Code: []byte{0x00}, Balance: common.Big0},
})
sender := env.gspec.Alloc[env.from]
sender.Code = types.AddressToDelegation(implementation)
env.gspec.Alloc[env.from] = sender
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil))
})
assertPresent(t, b, env.from)
assertAbsent(t, b, implementation)
}

View file

@ -192,6 +192,9 @@ func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool, floor uint6
if res.UsedGas > res.MaxUsedGas {
t.Fatalf("post-refund gas %d exceeds peak %d", res.UsedGas, res.MaxUsedGas)
}
if gp.cumulativeRegular > res.MaxUsedGas {
t.Fatalf("regular %d exceeds peak %d", gp.cumulativeRegular, res.MaxUsedGas)
}
if gp.cumulativeState > res.MaxUsedGas {
t.Fatalf("state %d exceeds peak %d", gp.cumulativeState, res.MaxUsedGas)
}

View file

@ -31,7 +31,8 @@ import (
// TestEIP8246SelfdestructNoBurn verifies that, once EIP-8246 is active
// (Amsterdam), a contract that is created and self-destructs to itself within
// the same transaction keeps its balance instead of burning it: the account
// survives as a balance-only account (no code, zero nonce, balance preserved).
// survives as a balance-only account (no code, zero nonce, balance preserved)
// whose storage is cleared at transaction finalization.
//
// https://eips.ethereum.org/EIPS/eip-8246
func TestEIP8246SelfdestructNoBurn(t *testing.T) {
@ -42,9 +43,11 @@ func TestEIP8246SelfdestructNoBurn(t *testing.T) {
signer = types.LatestSigner(&config)
engine = beacon.New(ethash.NewFaker())
value = big.NewInt(1_000_000)
// Init code: ADDRESS (0x30) ; SELFDESTRUCT (0xff). The created contract
// self-destructs to itself during its own creation transaction.
initcode = common.FromHex("30ff")
slot = common.BigToHash(big.NewInt(0x05))
// Init code: SSTORE(5, 0x2a); ADDRESS (0x30); SELFDESTRUCT (0xff). The
// created contract stores a value and self-destructs to itself during
// its own creation transaction.
initcode = []byte{0x60, 0x2a, 0x60, 0x05, 0x55, 0x30, 0xff}
)
// TODO: drop this hacky Amsterdam config initialization once the final
// Amsterdam config is available (mirrors TestEthTransferLogs).
@ -99,4 +102,147 @@ func TestEIP8246SelfdestructNoBurn(t *testing.T) {
if got := state.GetCodeSize(created); got != 0 {
t.Errorf("created account code size = %d, want 0 (code must be cleared)", got)
}
if got := state.GetState(created, slot); got != (common.Hash{}) {
t.Errorf("created storage slot %x = %x, want 0 (storage must be cleared)", slot, got)
}
}
// TestEIP8246SelfdestructRefunded verifies that ETH sent back to a
// same-transaction selfdestructed account is retained at finalization instead
// of being burned. The factory funds the account twice after SELFDESTRUCT.
func TestEIP8246SelfdestructRefunded(t *testing.T) {
var (
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
sender = crypto.PubkeyToAddress(key.PublicKey)
factory = common.HexToAddress("0xfac8246")
beneficiary = common.HexToAddress("0xbeef")
config = *params.MergedTestChainConfig
signer = types.LatestSigner(&config)
engine = beacon.New(ethash.NewFaker())
)
config.AmsterdamTime = new(uint64)
// The child initcode selfdestructs to another account. The factory then
// sends it 7 and 8 wei after it is marked for selfdestruction.
childInit := append([]byte{0x73}, beneficiary.Bytes()...)
childInit = append(childInit, 0xff)
var word [32]byte
copy(word[32-len(childInit):], childInit)
factoryCode := append([]byte{0x7f}, word[:]...)
factoryCode = append(factoryCode, 0x5f, 0x52) // PUSH0; MSTORE
// CREATE(value=0, offset=10, length=22), then store the returned address.
factoryCode = append(factoryCode, 0x60, byte(len(childInit)), 0x60, byte(32-len(childInit)), 0x5f, 0xf0, 0x5f, 0x52)
for _, amount := range []byte{7, 8} {
// CALL(child, value=amount) with empty input/output.
factoryCode = append(factoryCode, 0x5f, 0x5f, 0x5f, 0x5f, 0x60, amount, 0x5f, 0x51, 0x5a, 0xf1, 0x50)
}
factoryCode = append(factoryCode, 0x00)
gspec := &Genesis{
Config: &config,
Alloc: types.GenesisAlloc{
sender: {Balance: newGwei(1_000_000_000)},
factory: {Nonce: 1, Code: factoryCode, Balance: big.NewInt(15)},
},
}
child := crypto.CreateAddress(factory, 1)
db, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(_ int, b *BlockGen) {
b.AddTx(types.MustSignNewTx(key, signer, &types.DynamicFeeTx{
ChainID: gspec.Config.ChainID,
Nonce: 0,
To: &factory,
Gas: 1_000_000,
GasFeeCap: newGwei(5),
GasTipCap: newGwei(5),
}))
})
chain, err := NewBlockChain(db, gspec, engine, nil)
if err != nil {
t.Fatalf("failed to create chain: %v", err)
}
defer chain.Stop()
state, err := chain.StateAt(blocks[0].Header())
if err != nil {
t.Fatalf("failed to obtain block state: %v", err)
}
if got := state.GetBalance(child).Uint64(); got != 15 {
t.Errorf("refunded child balance = %d, want 15", got)
}
if got := state.GetNonce(child); got != 0 {
t.Errorf("refunded child nonce = %d, want 0", got)
}
if got := state.GetCodeSize(child); got != 0 {
t.Errorf("refunded child code size = %d, want 0", got)
}
}
// TestEIP8246Create2RecreatesBalanceOnly verifies that an EIP-8246
// balance-only account does not block recreating the same CREATE2 address in a
// later transaction. The second creation contributes another wei to the
// preserved balance, proving that it executed rather than collided.
func TestEIP8246Create2RecreatesBalanceOnly(t *testing.T) {
var (
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
sender = crypto.PubkeyToAddress(key.PublicKey)
factory = common.HexToAddress("0xfac8246")
config = *params.MergedTestChainConfig
signer = types.LatestSigner(&config)
engine = beacon.New(ethash.NewFaker())
init = []byte{0x30, 0xff} // ADDRESS; SELFDESTRUCT
)
config.AmsterdamTime = new(uint64)
var word [32]byte
copy(word[32-len(init):], init)
factoryCode := append([]byte{0x7f}, word[:]...)
factoryCode = append(factoryCode,
0x5f, 0x52, // PUSH0; MSTORE
0x60, 0x01, // salt
0x60, byte(len(init)),
0x60, byte(32-len(init)),
0x34, // CALLVALUE
0xf5, // CREATE2
0x50, // POP
0x00,
)
gspec := &Genesis{
Config: &config,
Alloc: types.GenesisAlloc{
sender: {Balance: newGwei(1_000_000_000)},
factory: {Nonce: 1, Code: factoryCode, Balance: common.Big0},
},
}
var salt [32]byte
salt[31] = 1
child := crypto.CreateAddress2(factory, salt, crypto.Keccak256(init))
db, blocks, _ := GenerateChainWithGenesis(gspec, engine, 2, func(i int, b *BlockGen) {
value := big.NewInt(5)
if i == 1 {
value.SetInt64(1)
}
b.AddTx(types.MustSignNewTx(key, signer, &types.DynamicFeeTx{
ChainID: gspec.Config.ChainID,
Nonce: uint64(i),
To: &factory,
Gas: 1_000_000,
GasFeeCap: newGwei(5),
GasTipCap: newGwei(5),
Value: value,
}))
})
chain, err := NewBlockChain(db, gspec, engine, nil)
if err != nil {
t.Fatalf("failed to create chain: %v", err)
}
defer chain.Stop()
state, err := chain.StateAt(blocks[1].Header())
if err != nil {
t.Fatalf("failed to obtain block state: %v", err)
}
if got := state.GetBalance(child).Uint64(); got != 6 {
t.Errorf("CREATE2 child balance = %d, want 6", got)
}
if got := state.GetNonce(child); got != 0 {
t.Errorf("CREATE2 child nonce = %d, want 0", got)
}
if got := state.GetCodeSize(child); got != 0 {
t.Errorf("CREATE2 child code size = %d, want 0", got)
}
}

View file

@ -210,6 +210,145 @@ func TestEIP8038AccountAccess(t *testing.T) {
})
}
// callFamily8038 builds a zero-input/output call-family operation that forwards
// all remaining regular gas and discards its success flag. CALL and CALLCODE
// take a value argument; DELEGATECALL and STATICCALL do not.
func callFamily8038(to common.Address, op OpCode, value byte) []byte {
code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00}
if op == CALL || op == CALLCODE {
code = append(code, 0x60, value)
}
code = append(code, 0x73)
code = append(code, to.Bytes()...)
return append(code, 0x5a, byte(op), 0x50, 0x00) // GAS; <op>; POP; STOP
}
// TestEIP8038Calls pins the re-priced account access and value-transfer costs
// for every member of the CALL family. The opcode's constant cost is the warm
// access component, so a cold target adds only COLD_ACCOUNT_ACCESS-WARM.
func TestEIP8038Calls(t *testing.T) {
const (
push1 = uint64(3)
push20 = uint64(3)
gasOp = uint64(2)
pop = uint64(2)
)
cold := params.ColdAccountAccessAmsterdam - params.WarmAccountAccessAmsterdam
callBase := 5*push1 + push20 + gasOp + pop + params.WarmAccountAccessAmsterdam
plainBase := 4*push1 + push20 + gasOp + pop + params.WarmAccountAccessAmsterdam
target := common.BytesToAddress([]byte("call-target"))
cases := []struct {
name string
op OpCode
value byte
fundSelf bool
wantReg uint64
wantState int64
}{
{"call/cold", CALL, 0, false, callBase + cold, 0},
// A callee that immediately returns gives the 2,300 stipend back, so
// the net regular cost is ACCOUNT_WRITE.
{"call/value", CALL, 1, true, callBase + cold + params.AccountWriteAmsterdam, stateGasNewAccount},
{"callcode/value", CALLCODE, 1, true, callBase + cold + params.AccountWriteAmsterdam, 0},
{"delegatecall/cold", DELEGATECALL, 0, false, plainBase + cold, 0},
{"staticcall/cold", STATICCALL, 0, false, plainBase + cold, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var setup func(*state.StateDB, common.Address)
if tc.fundSelf {
setup = fund(common.BytesToAddress([]byte("self")), 1)
}
res, _, err := run8038(t, callFamily8038(target, tc.op, tc.value), hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if res.UsedRegularGas != tc.wantReg {
t.Fatalf("regular gas = %d, want %d", res.UsedRegularGas, tc.wantReg)
}
if res.UsedStateGas != tc.wantState {
t.Fatalf("state gas = %d, want %d", res.UsedStateGas, tc.wantState)
}
})
}
// The first CALL makes its target warm. A second CALL pays no dynamic
// access surcharge, leaving only the CALL base cost (which includes warm).
first := callFamily8038(target, CALL, 0)
code := append(first[:len(first)-1], callFamily8038(target, CALL, 0)...)
res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if want := 2*callBase + cold; res.UsedRegularGas != want {
t.Fatalf("cold+warm CALL = %d, want %d", res.UsedRegularGas, want)
}
// Calling an EIP-7702 authority accesses both the authority and its
// delegation target. The authority uses CALL's warm-included pricing; the
// separately resolved cold target pays a full COLD_ACCOUNT_ACCESS charge.
authority := common.BytesToAddress([]byte("delegated-authority"))
implementation := common.BytesToAddress([]byte("delegated-target"))
setup := func(db *state.StateDB, _ common.Address) {
db.CreateAccount(authority)
db.SetCode(authority, types.AddressToDelegation(implementation), tracing.CodeChangeUnspecified)
db.CreateAccount(implementation)
db.SetCode(implementation, []byte{0x00}, tracing.CodeChangeUnspecified)
}
res, _, err = run8038(t, callFamily8038(authority, CALL, 0), hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if want := callBase + cold + params.ColdAccountAccessAmsterdam; res.UsedRegularGas != want {
t.Fatalf("delegated CALL = %d, want %d (authority + target)", res.UsedRegularGas, want)
}
// A value CALL receives the 2,300 stipend even when it asks to forward no
// regular gas. If the child burns that stipend, the full CALL_VALUE
// (ACCOUNT_WRITE + stipend) remains charged to the caller.
stipendTarget := common.BytesToAddress([]byte("stipend-target"))
base := callFamily8038(stipendTarget, CALL, 1)
code = append(base[:len(base)-4], 0x60, 0x00, byte(CALL), 0x50, 0x00) // PUSH1 0; CALL; POP; STOP
setup = func(db *state.StateDB, self common.Address) {
db.AddBalance(self, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
db.CreateAccount(stipendTarget)
db.SetCode(stipendTarget, []byte{0xfe}, tracing.CodeChangeUnspecified)
}
res, _, err = run8038(t, code, hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if want := 5*push1 + push20 + push1 + pop + params.WarmAccountAccessAmsterdam + cold + params.CallValueTransferAmsterdam; res.UsedRegularGas != want {
t.Fatalf("value CALL with burnt stipend = %d, want %d", res.UsedRegularGas, want)
}
}
// TestEIP8038Create checks that CREATE and CREATE2 always pay CREATE_ACCESS
// in regular gas. With otherwise identical initcode, CREATE2 additionally has
// one salt push and the address-hash word charge.
func TestEIP8038Create(t *testing.T) {
create, _, err := run8038(t, deployCode(deploy0Init, false, 0), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
create2, _, err := run8038(t, deployCode(deploy0Init, true, 0), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
// Outer setup is PUSH32 + PUSH1 + MSTORE (including one-word expansion),
// then three CREATE operands. The child initcode is two PUSH1s and RETURN.
const outer = uint64(3 + 3 + 3 + 3 + 3*3)
const init = uint64(2 * 3)
want := outer + params.CreateAccessAmsterdam + params.InitCodeWordGas + init
if create.UsedRegularGas != want {
t.Fatalf("CREATE regular gas = %d, want %d", create.UsedRegularGas, want)
}
if want := create.UsedRegularGas + 3 + params.Keccak256WordGas; create2.UsedRegularGas != want {
t.Fatalf("CREATE2 regular gas = %d, want %d", create2.UsedRegularGas, want)
}
}
// TestEIP8038SelfdestructAccountWrite checks that SELFDESTRUCT sending a positive
// balance to an empty account is charged the cold access, an additional
// ACCOUNT_WRITE (regular) and GAS_NEW_ACCOUNT (state).