mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
Merge 407839baec into dddbaa4bf3
This commit is contained in:
commit
f7ff0c2ef9
6 changed files with 1827 additions and 35 deletions
|
|
@ -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
413
core/eip7708_test.go
Normal 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), ¶ms.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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in a new issue