From bf4d5d1f567a9ee5431d22b0de7262d66763648b Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 15 Jul 2026 11:36:17 +0800 Subject: [PATCH] core: improve test coverage --- core/eip2780_test.go | 470 +++++++++++++++++++++++++ core/eip7928_test.go | 802 ++++++++++++++++++++++++++++++++++++++++++- core/eip8246_test.go | 196 +++++++++++ 3 files changed, 1466 insertions(+), 2 deletions(-) diff --git a/core/eip2780_test.go b/core/eip2780_test.go index cee5b3f685..503d52e1be 100644 --- a/core/eip2780_test.go +++ b/core/eip2780_test.go @@ -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,121 @@ 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) + } + }) + } +} + +// TestEIP2780AuthOOG pins the atomic authorization charge point: a later +// authorization OOG reverts an earlier successful authorization too. +func TestEIP2780AuthOOG(t *testing.T) { + a0, authority0 := signAuth(t, authKeyA, delegate8037, 0) + key, _ := crypto.GenerateKey() + a1, err := types.SignSetCode(key, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate8037, Nonce: 0, + }) + if err != nil { + t.Fatal(err) + } + authority1 := crypto.PubkeyToAddress(key.PublicKey) + to := common.HexToAddress("0xe0a0000000000000000000000000000000000009") + intrinsic := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + 2*params.RegularPerAuthBaseCost + gas := intrinsic + params.AccountWriteAmsterdam + authWorstState + sdb := mkState(senderAlloc(types.GenesisAlloc{to: {Balance: big.NewInt(1)}})) + res, gp, err := applyMsg(t, sdb, setCodeTxGas(0, to, 0, gas, []types.SetCodeAuthorization{a0, a1})) + if err != nil || res.Err != vm.ErrOutOfGas { + t.Fatalf("result=%v err=%v, want included OOG", res, err) + } + for _, authority := range []common.Address{authority0, authority1} { + if len(sdb.GetCode(authority)) != 0 || sdb.GetNonce(authority) != 0 { + t.Fatalf("authority %x persisted after authorization OOG", authority) + } + } + if gp.cumulativeState != 0 || gp.cumulativeRegular != gas { + t.Fatalf("gas = <%d,%d>, want <%d,0>", gp.cumulativeRegular, gp.cumulativeState, gas) + } +} + +// TestEIP2780AuthRevert distinguishes an EVM REVERT from a pre-frame OOG: +// state paid by a successful authorization precedes the frame and persists. +func TestEIP2780AuthRevert(t *testing.T) { + auth, authority := signAuth(t, authKeyA, delegate8037, 0) + reverter := common.HexToAddress("0xbad0000000000000000000000000000000000003") + sdb := mkState(senderAlloc(types.GenesisAlloc{ + reverter: {Code: []byte{0x60, 0x00, 0x60, 0x00, 0xfd}}, + })) + res, gp, err := applyMsg(t, sdb, setCodeTxGas(0, reverter, 0, 1_000_000, []types.SetCodeAuthorization{auth})) + if err != nil || res.Err != vm.ErrExecutionReverted { + t.Fatalf("result=%v err=%v, want execution revert", res, err) + } + if len(sdb.GetCode(authority)) == 0 || sdb.GetNonce(authority) != 1 { + t.Fatalf("authorization not retained after revert: code=%x nonce=%d", sdb.GetCode(authority), sdb.GetNonce(authority)) + } + if gp.cumulativeState != authWorstState { + t.Fatalf("state gas = %d, want %d", gp.cumulativeState, authWorstState) + } +} + // 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 +581,295 @@ 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") + } +} + +// TestEIP2780ValueEffects checks the observable value-transfer behavior along +// with the decomposition's special treatment of self and creation transfers. +func TestEIP2780ValueEffects(t *testing.T) { + recipient := common.HexToAddress("0xbeef000000000000000000000000000000000006") + sdb := mkState(senderAlloc(nil)) + before := sdb.GetBalance(senderAddr).ToBig() + tx := callTx(0, recipient, 7, 300_000, nil) + res, _, err := applyMsg(t, sdb, tx) + if err != nil || res.Err != nil { + t.Fatalf("result=%v err=%v", res, err) + } + if got := sdb.GetBalance(recipient).ToBig(); got.Cmp(big.NewInt(7)) != 0 { + t.Fatalf("recipient balance = %v, want 7", got) + } + if got := sdb.GetBalance(senderAddr).ToBig(); got.Cmp(new(big.Int).Sub(before, big.NewInt(7))) != 0 { + t.Fatalf("sender balance = %v, want value transfer", got) + } + logs := sdb.GetLogs(common.Hash{}, 0, common.Hash{}, 0) + if len(logs) != 1 || logs[0].Topics[0] != params.EthTransferLogEvent { + t.Fatalf("logs = %+v, want one EIP-7708 transfer", logs) + } + + // A self transfer is not a value move and emits no transfer log. + sdb = mkState(senderAlloc(nil)) + before = sdb.GetBalance(senderAddr).ToBig() + tx = callTx(0, senderAddr, 7, 100_000, nil) + res, _, err = applyMsg(t, sdb, tx) + if err != nil || res.Err != nil { + t.Fatalf("self result=%v err=%v", res, err) + } + logs = sdb.GetLogs(common.Hash{}, 0, common.Hash{}, 0) + if sdb.GetBalance(senderAddr).Cmp(uint256.MustFromBig(before)) != 0 || len(logs) != 0 { + t.Fatalf("self transfer changed balance or emitted logs: balance=%v logs=%+v", sdb.GetBalance(senderAddr), logs) + } +} + +// TestEIP2780Coinbase keeps the intrinsic recipient charge separate from the +// runtime warmth of the coinbase account. +func TestEIP2780Coinbase(t *testing.T) { + const ( + base = params.TxBaseCost2780 + cold = params.ColdAccountAccessAmsterdam + warm = params.WarmAccountAccessAmsterdam + ) + coinbase := common.HexToAddress("0xc01ba5e000000000000000000000000000000001") + t.Run("recipient", func(t *testing.T) { + 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 gp.cumulativeRegular != base+cold { + t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, base+cold) + } + }) + t.Run("target", func(t *testing.T) { + delegated := common.HexToAddress("0xde1e000000000000000000000000000000000007") + res, gp, err := applyMsgCoinbase(t, mkState(senderAlloc(types.GenesisAlloc{ + delegated: {Code: types.AddressToDelegation(coinbase)}, + })), callTx(0, delegated, 0, 100_000, nil), coinbase) + if err != nil || res.Err != nil { + t.Fatalf("result=%v err=%v", res, err) + } + if gp.cumulativeRegular != base+cold+warm { + t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, base+cold+warm) + } + }) +} + +// 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: diff --git a/core/eip7928_test.go b/core/eip7928_test.go index 64c2714b16..11f0a5e09f 100644 --- a/core/eip7928_test.go +++ b/core/eip7928_test.go @@ -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. @@ -248,6 +294,33 @@ func TestBALCoinbaseTipCapturesBalance(t *testing.T) { } } +// 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, receipts := env.run(t, func(g *BlockGen) { + g.SetCoinbase(coinbase) + 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)) + }) + + 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) + } + } +} + // TestBALSystemAddressExcluded: SYSTEM_ADDRESS (0xff…fe) is not in the BAL // for a regular block. func TestBALSystemAddressExcluded(t *testing.T) { @@ -582,7 +655,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,6 +692,30 @@ func TestBALSenderRecordedOnRevert(t *testing.T) { } } +// 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}, + }) + + 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 ============================== // TestBALStorageWriteRecorded: SSTORE places the slot in storage_changes and @@ -890,6 +987,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 +1195,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 +1289,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 +1360,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 +1650,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 +1811,443 @@ 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)) +} + +// TestBALDelegatedDelegateCallStorage checks the other storage context: +// DELEGATECALL to a delegated authority still executes the resolved code +// against the original caller's storage. +func TestBALDelegatedDelegateCallStorage(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 + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xf4 /* DELEGATECALL */, 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)) + }) + + callerAccess := assertPresent(t, b, caller) + if !hasSlotIn(callerAccess.StorageReads, slot) { + t.Fatalf("delegated DELEGATECALL read must belong to caller\n%s", b.PrettyPrint()) + } + assertEmpty(t, assertPresent(t, b, authority)) + assertEmpty(t, assertPresent(t, b, implementation)) +} + +// TestBALDelegatedStaticCallStorage covers STATICCALL's context. It resolves +// the authority's implementation code, but SLOAD remains attached to the static +// call target's storage. +func TestBALDelegatedStaticCallStorage(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 + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xfa /* STATICCALL */, 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)) + }) + + authorityAccess := assertPresent(t, b, authority) + if !hasSlotIn(authorityAccess.StorageReads, slot) { + t.Fatalf("delegated STATICCALL read must belong to authority\n%s", b.PrettyPrint()) + } + assertEmpty(t, assertPresent(t, b, implementation)) +} + +// TestBALDelegatedCallCodeStorage covers CALLCODE's storage context. As with +// DELEGATECALL, the implementation address supplies code but is not the owner +// of storage reads. +func TestBALDelegatedCallCodeStorage(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 + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xf2 /* CALLCODE */, 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)) + }) + + callerAccess := assertPresent(t, b, caller) + if !hasSlotIn(callerAccess.StorageReads, slot) { + t.Fatalf("delegated CALLCODE read must belong to caller\n%s", b.PrettyPrint()) + } + assertEmpty(t, assertPresent(t, b, authority)) + 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) +} diff --git a/core/eip8246_test.go b/core/eip8246_test.go index 18049d2279..bc2918467b 100644 --- a/core/eip8246_test.go +++ b/core/eip8246_test.go @@ -100,3 +100,199 @@ func TestEIP8246SelfdestructNoBurn(t *testing.T) { t.Errorf("created account code size = %d, want 0 (code must be cleared)", got) } } + +// TestEIP8246SelfdestructClearsStorage verifies the other half of the +// balance-only-account rule: a same-transaction selfdestruct preserves balance +// but clears storage during transaction finalization. +func TestEIP8246SelfdestructClearsStorage(t *testing.T) { + var ( + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + sender = crypto.PubkeyToAddress(key.PublicKey) + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + value = big.NewInt(1_000_000) + slot = common.BigToHash(big.NewInt(0x05)) + stored = common.BigToHash(big.NewInt(0x2a)) + created = crypto.CreateAddress(sender, 0) + ) + config.AmsterdamTime = new(uint64) + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{sender: {Balance: newGwei(1_000_000_000)}}, + } + // SSTORE(5, 0x2a); ADDRESS; SELFDESTRUCT. + initcode := []byte{0x60, 0x2a, 0x60, 0x05, 0x55, 0x30, 0xff} + db, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(_ int, b *BlockGen) { + b.AddTx(types.MustSignNewTx(key, signer, &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 0, + Gas: 1_000_000, + GasFeeCap: newGwei(5), + GasTipCap: newGwei(5), + Value: value, + Data: initcode, + })) + }) + 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(created).ToBig(); got.Cmp(value) != 0 { + t.Errorf("created balance = %v, want %v", got, value) + } + if got := state.GetNonce(created); got != 0 { + t.Errorf("created nonce = %d, want 0", got) + } + if got := state.GetCodeSize(created); got != 0 { + t.Errorf("created code size = %d, want 0", got) + } + if got := state.GetState(created, slot); got != (common.Hash{}) { + t.Errorf("created storage slot %x = %x, want 0 (stored %x before selfdestruct)", slot, got, stored) + } +} + +// 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) + } +}