go-ethereum/core/bal_test.go
rjl493456442 1bdc4a60d9
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run
core, consensus, internal, eth, miner: construct block accessList (#34957)
This PR finally lands EIP-7928, collecting the block accessList during
the block execution and verifying against the block header.

---------

Co-authored-by: jwasinger <j-wasinger@hotmail.com>
Co-authored-by: Marius van der Wijden <m.vanderwijden@live.de>
2026-05-19 21:51:53 +08:00

1319 lines
47 KiB
Go

// 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 (
"bytes"
"crypto/ecdsa"
"maps"
"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/types"
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
// EIP-7928 BAL inclusion tests.
//
// Each test exercises a single rule from the spec and asserts both presence
// and absence in the resulting block access list.
// balChainConfig returns a MergedTestChainConfig clone with Amsterdam active from genesis.
func balChainConfig() *params.ChainConfig {
cfg := *params.MergedTestChainConfig
cfg.AmsterdamTime = new(uint64)
blob := *cfg.BlobScheduleConfig
blob.Amsterdam = blob.Osaka
cfg.BlobScheduleConfig = &blob
return &cfg
}
// balTestEnv bundles common identities used across the tests.
type balTestEnv struct {
cfg *params.ChainConfig
signer types.Signer
key *ecdsa.PrivateKey
from common.Address
gspec *Genesis
}
// newBALTestEnv builds an Amsterdam chain config, funds a sender and pre-deploys
// the EIP-7928 system contracts. Extra accounts can be merged into Alloc.
func newBALTestEnv(extra types.GenesisAlloc) *balTestEnv {
cfg := balChainConfig()
key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
from := crypto.PubkeyToAddress(key.PublicKey)
alloc := types.GenesisAlloc{
from: {Balance: newGwei(1_000_000_000)},
params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode, Balance: common.Big0},
params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0},
params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0},
params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0},
}
maps.Copy(alloc, extra)
return &balTestEnv{
cfg: cfg,
signer: types.LatestSigner(cfg),
key: key,
from: from,
gspec: &Genesis{Config: cfg, Alloc: alloc},
}
}
// run generates exactly one Amsterdam block and returns its BAL.
func (e *balTestEnv) run(t *testing.T, gen func(*BlockGen)) (*bal.BlockAccessList, types.Receipts) {
t.Helper()
engine := beacon.New(ethash.NewFaker())
_, blocks, receipts := GenerateChainWithGenesis(e.gspec, engine, 1, func(_ int, b *BlockGen) {
gen(b)
})
if blocks[0].AccessList() == nil {
t.Fatal("expected non-nil block access list")
}
return blocks[0].AccessList(), receipts[0]
}
// --- assertion helpers ---
func findAccount(b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess {
for i := range *b {
if (*b)[i].Address == addr {
return &(*b)[i]
}
}
return nil
}
func hasSlotIn(slots []*uint256.Int, key common.Hash) bool {
want := new(uint256.Int).SetBytes(key[:])
for _, s := range slots {
if s.Cmp(want) == 0 {
return true
}
}
return false
}
func hasStorageWrite(b *bal.BlockAccessList, addr common.Address, key common.Hash) bool {
aa := findAccount(b, addr)
if aa == nil {
return false
}
want := new(uint256.Int).SetBytes(key[:])
for _, w := range aa.StorageWrites {
if w.Slot.Cmp(want) == 0 {
return true
}
}
return false
}
func assertPresent(t *testing.T, b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess {
t.Helper()
aa := findAccount(b, addr)
if aa == nil {
t.Fatalf("address %x missing from BAL\n%s", addr, b.PrettyPrint())
}
return aa
}
func assertAbsent(t *testing.T, b *bal.BlockAccessList, addr common.Address) {
t.Helper()
if findAccount(b, addr) != nil {
t.Fatalf("address %x must NOT be in BAL\n%s", addr, b.PrettyPrint())
}
}
func assertEmpty(t *testing.T, aa *bal.AccountAccess) {
t.Helper()
if len(aa.StorageWrites) != 0 || len(aa.StorageReads) != 0 ||
len(aa.BalanceChanges) != 0 || len(aa.NonceChanges) != 0 || len(aa.CodeChanges) != 0 {
t.Fatalf("expected empty change set for %x, got %+v", aa.Address, aa)
}
}
// --- tx builders ---
func (e *balTestEnv) tx(nonce uint64, to *common.Address, value *big.Int, gas uint64, tipGwei int64, data []byte) *types.Transaction {
return types.MustSignNewTx(e.key, e.signer, &types.DynamicFeeTx{
ChainID: e.cfg.ChainID,
Nonce: nonce,
To: to,
Value: value,
Gas: gas,
GasFeeCap: newGwei(10),
GasTipCap: newGwei(tipGwei),
Data: data,
})
}
// ============================== Account inclusion ==============================
// TestBALTxSenderAndRecipient: a value transfer records balance+nonce for sender
// and a balance entry for the recipient.
func TestBALTxSenderAndRecipient(t *testing.T) {
to := common.HexToAddress("0xc0ffee")
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &to, big.NewInt(1000), params.TxGas, 0, nil))
})
sender := assertPresent(t, b, env.from)
if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 {
t.Fatalf("sender nonce not bumped: %+v", sender.NonceChanges)
}
if len(sender.BalanceChanges) == 0 {
t.Fatalf("sender missing balance change")
}
recipient := assertPresent(t, b, to)
if len(recipient.BalanceChanges) != 1 || recipient.BalanceChanges[0].Balance.Uint64() != 1000 {
t.Fatalf("recipient balance: %+v", recipient.BalanceChanges)
}
}
// TestBALZeroValueRecipient: a tx with value 0 still lists the recipient,
// but without a balance entry.
func TestBALZeroValueRecipient(t *testing.T) {
to := common.HexToAddress("0x0123456789abcdef")
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil))
})
r := assertPresent(t, b, to)
if len(r.BalanceChanges) != 0 {
t.Fatalf("zero-value recipient should have no balance entry: %+v", r.BalanceChanges)
}
}
// TestBALEmptyBlockExcludesCoinbase: an empty block (no txs, no withdrawals)
// never touches the coinbase, so it must NOT appear in the BAL — the zero
// block reward alone does not trigger inclusion.
func TestBALEmptyBlockExcludesCoinbase(t *testing.T) {
coinbase := common.Address{0xc0}
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
// SetCoinbase initialises b.bal but does not record any access.
g.SetCoinbase(coinbase)
})
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")
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.SetCoinbase(coinbase)
g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 2 /* gwei tip */, nil))
})
cb := assertPresent(t, b, coinbase)
if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].Balance.Sign() == 0 {
t.Fatalf("coinbase missing positive balance change: %+v", cb.BalanceChanges)
}
}
// TestBALSystemAddressExcluded: SYSTEM_ADDRESS (0xff…fe) is not in the BAL
// for a regular block.
func TestBALSystemAddressExcluded(t *testing.T) {
to := common.HexToAddress("0xabba")
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil))
})
assertAbsent(t, b, params.SystemAddress)
}
// TestBALSystemAddressIncludedWhenTouched: SYSTEM_ADDRESS becomes a regular
// account in the BAL once it experiences state access (here: receives value).
func TestBALSystemAddressIncludedWhenTouched(t *testing.T) {
sys := params.SystemAddress
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &sys, big.NewInt(1000), params.TxGas, 0, nil))
})
aa := assertPresent(t, b, sys)
if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 1000 {
t.Fatalf("system-address balance change missing: %+v", aa.BalanceChanges)
}
}
// TestBALPrecompileInvokedFromContractIncluded: a precompile that is invoked
// indirectly — via STATICCALL from a regular contract — must still appear in
// the BAL with no balance entry.
func TestBALPrecompileInvokedFromContractIncluded(t *testing.T) {
identity := common.BytesToAddress([]byte{0x04})
caller := common.HexToAddress("0xca11")
// PUSH1 0 (retSize) PUSH1 0 (retOff) PUSH1 0 (argsSize) PUSH1 0 (argsOff)
// PUSH20 0x04 GAS STATICCALL POP STOP
code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
code = append(code, identity.Bytes()...)
code = append(code, 0x5a, 0xfa, 0x50, 0x00)
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
})
aa := assertPresent(t, b, identity)
if len(aa.BalanceChanges) != 0 {
t.Fatalf("precompile invoked via STATICCALL must not record balance: %+v", aa.BalanceChanges)
}
}
// TestBALPrecompileCalledNoValueIncluded: a tx targeting the identity precompile
// with zero value lists the precompile but records no balance entry.
func TestBALPrecompileCalledNoValueIncluded(t *testing.T) {
identity := common.BytesToAddress([]byte{0x04})
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &identity, big.NewInt(0), 50_000, 0, []byte{0xde, 0xad}))
})
aa := assertPresent(t, b, identity)
if len(aa.BalanceChanges) != 0 {
t.Fatalf("precompile must not record balance change: %+v", aa.BalanceChanges)
}
}
// TestBALPrecompileValueTransferRecordsBalance: a precompile receives ETH only
// in the form of a value transfer — the balance entry is then recorded.
func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) {
identity := common.BytesToAddress([]byte{0x04})
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &identity, big.NewInt(5), 50_000, 0, nil))
})
aa := assertPresent(t, b, identity)
if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 5 {
t.Fatalf("precompile balance change wrong: %+v", aa.BalanceChanges)
}
}
// TestBALBalanceProbeOnNonExistent: BALANCE against a never-allocated address
// still adds it to the BAL with an empty change set.
func TestBALBalanceProbeOnNonExistent(t *testing.T) {
probe := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
caller := common.HexToAddress("0xc1")
code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe
code = append(code, 0x31, 0x50, 0x00) // BALANCE, POP, STOP
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, probe))
}
// TestBALExtCodeSizeProbeOnNonExistent: EXTCODESIZE against a never-allocated
// address adds it to the BAL with an empty change set.
func TestBALExtCodeSizeProbeOnNonExistent(t *testing.T) {
probe := common.HexToAddress("0xcafecafecafecafecafecafecafecafecafecafe")
caller := common.HexToAddress("0xc1")
code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe
code = append(code, 0x3b, 0x50, 0x00) // EXTCODESIZE, POP, STOP
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, probe))
}
// TestBALExtCodeHashProbeOnNonExistent: EXTCODEHASH against a never-allocated
// address adds it to the BAL with an empty change set.
func TestBALExtCodeHashProbeOnNonExistent(t *testing.T) {
probe := common.HexToAddress("0xfacefacefacefacefacefacefacefacefacefacE")
caller := common.HexToAddress("0xc1")
code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe
code = append(code, 0x3f, 0x50, 0x00) // EXTCODEHASH, POP, STOP
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, probe))
}
// TestBALExtCodeCopyProbeOnNonExistent: EXTCODECOPY against a never-allocated
// address adds it to the BAL with an empty change set.
func TestBALExtCodeCopyProbeOnNonExistent(t *testing.T) {
probe := common.HexToAddress("0xfeedfeedfeedfeedfeedfeedfeedfeedfeedfeed")
caller := common.HexToAddress("0xc1")
// PUSH1 0 (length) PUSH1 0 (codeOffset) PUSH1 0 (destOffset)
// PUSH20 probe EXTCODECOPY STOP
code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
code = append(code, probe.Bytes()...)
code = append(code, 0x3c, 0x00) // EXTCODECOPY, STOP
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, probe))
}
// TestBALAccessListNotAutoPromoted: an EIP-2930 access-list entry that is
// never actually touched must NOT appear in the BAL.
func TestBALAccessListNotAutoPromoted(t *testing.T) {
to := common.HexToAddress("0xabba")
dormant := common.HexToAddress("0xd0d0")
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
tx := types.MustSignNewTx(env.key, env.signer, &types.DynamicFeeTx{
ChainID: env.cfg.ChainID,
Nonce: 0,
To: &to,
Value: big.NewInt(0),
Gas: params.TxGas + 4000,
GasFeeCap: newGwei(10),
GasTipCap: newGwei(0),
AccessList: types.AccessList{{Address: dormant, StorageKeys: nil}},
})
g.AddTx(tx)
})
assertAbsent(t, b, dormant)
}
// ============================== CALL family ==============================
// makeStubCaller emits a single CALL-family op against `target` then STOPs,
// with zero call data and discarded return data.
//
// op = 0xf1 (CALL) / 0xf2 (CALLCODE):
// stack = retSize, retOff, argsSize, argsOff, value, addr, gas
// op = 0xf4 (DELEGATECALL) / 0xfa (STATICCALL):
// stack = retSize, retOff, argsSize, argsOff, addr, gas
func makeStubCaller(op byte, target common.Address) []byte {
// retSize, retOff, argsSize, argsOff = 0
prelude := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00}
if op == 0xf1 || op == 0xf2 { // CALL/CALLCODE need an extra value=0
prelude = append(prelude, 0x60, 0x00)
}
prelude = append(prelude, 0x73) // PUSH20
prelude = append(prelude, target.Bytes()...)
prelude = append(prelude, 0x5a) // GAS
prelude = append(prelude, op)
prelude = append(prelude, 0x50, 0x00) // POP, STOP
return prelude
}
// TestBALCallTargetWithEmptyChangeSet: a zero-value CALL to an existing
// contract that has no state changes lists the target with empty entries.
func TestBALCallTargetWithEmptyChangeSet(t *testing.T) {
target := common.HexToAddress("0xbabe")
env := newBALTestEnv(types.GenesisAlloc{
target: {Code: []byte{0x00}, Balance: common.Big0}, // STOP
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &target, big.NewInt(0), 100_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, target))
}
// TestBALCallCodeTargetIncluded: CALLCODE puts the target in the BAL with an
// empty change set (CALLCODE executes target's code in the caller's storage
// context, so the target itself records no state changes).
func TestBALCallCodeTargetIncluded(t *testing.T) {
target := common.HexToAddress("0xdeed")
caller := common.HexToAddress("0xca11")
env := newBALTestEnv(types.GenesisAlloc{
caller: {Code: makeStubCaller(0xf2 /* CALLCODE */, target), Balance: common.Big0},
target: {Code: []byte{0x00}, Balance: common.Big0},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
})
assertPresent(t, b, caller)
assertEmpty(t, assertPresent(t, b, target))
}
// TestBALDelegateCallTargetIncluded: DELEGATECALL puts both caller and target
// in the BAL even when neither produces state changes.
func TestBALDelegateCallTargetIncluded(t *testing.T) {
target := common.HexToAddress("0xdeed")
caller := common.HexToAddress("0xca11")
env := newBALTestEnv(types.GenesisAlloc{
caller: {Code: makeStubCaller(0xf4 /* DELEGATECALL */, target), Balance: common.Big0},
target: {Code: []byte{0x00}, Balance: common.Big0},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
})
assertPresent(t, b, caller)
assertEmpty(t, assertPresent(t, b, target))
}
// TestBALStaticCallTargetIncluded: STATICCALL puts the target in the BAL with
// no balance entry recorded.
func TestBALStaticCallTargetIncluded(t *testing.T) {
target := common.HexToAddress("0xdeed")
caller := common.HexToAddress("0xca11")
env := newBALTestEnv(types.GenesisAlloc{
caller: {Code: makeStubCaller(0xfa /* STATICCALL */, target), Balance: common.Big0},
target: {Code: []byte{0x00}, Balance: common.Big0},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
})
assertPresent(t, b, caller)
assertEmpty(t, assertPresent(t, b, target))
}
// ============================== Revert behaviour ==============================
// TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still
// records the touched contract in the BAL with an empty change set.
func TestBALRevertedTxStillIncluded(t *testing.T) {
reverter := common.HexToAddress("0xbeef")
// PUSH1 0 PUSH1 0 REVERT
revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, reverter))
}
// TestBALSenderRecordedOnRevert: even when the top-level call reverts, the
// sender's final nonce and balance MUST be recorded.
func TestBALSenderRecordedOnRevert(t *testing.T) {
reverter := common.HexToAddress("0xbeef")
revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil))
})
sender := assertPresent(t, b, env.from)
if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 {
t.Fatalf("sender nonce must be bumped even on revert: %+v", sender.NonceChanges)
}
if len(sender.BalanceChanges) == 0 {
t.Fatalf("sender balance change (gas paid) must be present on revert")
}
}
// ============================== 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), 100_000, 0, nil))
})
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")
}
}
// TestBALStorageSloadOnly: SLOAD without a write puts the slot in storage_reads.
func TestBALStorageSloadOnly(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x07))
// PUSH1 0x07 SLOAD POP STOP
code := []byte{0x60, 0x07, 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), 100_000, 0, nil))
})
aa := assertPresent(t, b, contract)
if !hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("expected slot in storage_reads\n%s", b.PrettyPrint())
}
if hasStorageWrite(b, contract, slot) {
t.Fatalf("slot must NOT appear in storage_changes")
}
}
// TestBALStorageReadThenWriteOnlyInWrites: SLOAD followed by SSTORE on the
// same slot drops the slot from storage_reads (write-wins invariant).
func TestBALStorageReadThenWriteOnlyInWrites(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x05))
// PUSH1 5 SLOAD POP PUSH1 0x42 PUSH1 5 SSTORE STOP
code := []byte{
0x60, 0x05, 0x54, 0x50,
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), 100_000, 0, nil))
})
aa := assertPresent(t, b, contract)
if !hasStorageWrite(b, contract, slot) {
t.Fatalf("slot must be in storage_changes\n%s", b.PrettyPrint())
}
if hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("slot must NOT appear in storage_reads (write-wins)\n%s", b.PrettyPrint())
}
}
// TestBALNoOpSSTOREDemotesToRead: an SSTORE whose value equals the committed
// value lands the slot in storage_reads only.
func TestBALNoOpSSTOREDemotesToRead(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x09))
// SSTORE(0x09, 0x42) — slot pre-state is 0x42, so the write is a no-op.
code := []byte{0x60, 0x42, 0x60, 0x09, 0x55, 0x00}
env := newBALTestEnv(types.GenesisAlloc{
contract: {
Code: code,
Balance: common.Big0,
Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))},
},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
})
aa := assertPresent(t, b, contract)
if !hasSlotIn(aa.StorageReads, slot) {
t.Fatalf("no-op SSTORE should leave slot in storage_reads\n%s", b.PrettyPrint())
}
if hasStorageWrite(b, contract, slot) {
t.Fatalf("no-op SSTORE must NOT register a write")
}
}
// TestBALStorageWriteZeroIsAWrite: writing 0 to a non-zero slot is still a
// state change and lands in storage_changes.
func TestBALStorageWriteZeroIsAWrite(t *testing.T) {
contract := common.HexToAddress("0xc1")
slot := common.BigToHash(big.NewInt(0x03))
// PUSH1 0 PUSH1 3 SSTORE STOP
code := []byte{0x60, 0x00, 0x60, 0x03, 0x55, 0x00}
env := newBALTestEnv(types.GenesisAlloc{
contract: {
Code: code,
Balance: common.Big0,
Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))},
},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
})
aa := assertPresent(t, b, contract)
if !hasStorageWrite(b, contract, slot) {
t.Fatalf("SSTORE to zero must record a write\n%s", b.PrettyPrint())
}
for _, w := range aa.StorageWrites {
if w.Slot.Uint64() == 0x03 {
if len(w.Accesses) != 1 || !w.Accesses[0].ValueAfter.IsZero() {
t.Fatalf("expected post-value 0 for slot 0x03, got %+v", w.Accesses)
}
}
}
}
// ============================== CREATE / contract deployment ==============================
// TestBALCreateDeploysCode: a successful contract-creation tx records the new
// address with nonce 0→1, a balance entry (value transferred), and a code entry.
func TestBALCreateDeploysCode(t *testing.T) {
env := newBALTestEnv(nil)
// Init: deploy runtime [0x00] (single STOP byte).
// PUSH1 0 PUSH1 0 MSTORE8 PUSH1 1 PUSH1 0 RETURN
init := []byte{0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3}
b, receipts := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, nil, big.NewInt(7), 200_000, 0, init))
})
created := receipts[0].ContractAddress
aa := assertPresent(t, b, created)
if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 {
t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges)
}
if len(aa.CodeChanges) != 1 || !bytes.Equal(aa.CodeChanges[0].Code, []byte{0x00}) {
t.Fatalf("expected code [0x00], got %+v", aa.CodeChanges)
}
if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 7 {
t.Fatalf("expected balance 7, got %+v", aa.BalanceChanges)
}
}
// TestBALCreateEmptyRuntimeNoCodeEntry: when init code returns 0 bytes the
// new address is still listed with nonce 0→1 but no code entry.
func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) {
env := newBALTestEnv(nil)
// Init: PUSH1 0 PUSH1 0 RETURN → returns 0 bytes
init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
b, receipts := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
})
created := receipts[0].ContractAddress
aa := assertPresent(t, b, created)
if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 {
t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges)
}
if len(aa.CodeChanges) != 0 {
t.Fatalf("empty runtime must NOT record a code entry, got %+v", aa.CodeChanges)
}
}
// TestBALCreateInitRevertEmptyChangeSet: when init code reverts, the would-be
// contract address is in the BAL with an empty change set.
func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) {
env := newBALTestEnv(nil)
// PUSH1 0 PUSH1 0 REVERT
init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
b, receipts := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
})
created := receipts[0].ContractAddress
assertEmpty(t, assertPresent(t, b, created))
}
// TestBALCreateInitOOGEmptyChangeSet: init code that runs out of gas leaves
// the deployed address in the BAL with an empty change set.
func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) {
env := newBALTestEnv(nil)
// Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG.
init := []byte{0x5b, 0x60, 0x00, 0x56}
b, receipts := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, nil, big.NewInt(0), 60_000, 0, init))
})
created := receipts[0].ContractAddress
assertEmpty(t, assertPresent(t, b, created))
}
// TestBALCreateAddressCollisionStillIncluded: when CREATE targets an address
// that already holds a contract, the deployment fails but the address was
// probed during execution and MUST appear in the BAL with an empty change set.
func TestBALCreateAddressCollisionStillIncluded(t *testing.T) {
env := newBALTestEnv(nil)
// For a top-level CREATE tx the deployed address is CreateAddress(sender, 0).
// Pre-allocate a contract at that address to provoke ErrContractAddressCollision.
collide := crypto.CreateAddress(env.from, 0)
env.gspec.Alloc[collide] = types.Account{
Nonce: 1,
Code: []byte{0x00},
Balance: common.Big0,
}
// Init code doesn't matter — execution never starts.
init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
})
aa := assertPresent(t, b, collide)
// The address must be present but the pre-existing nonce/code MUST NOT
// be overwritten by the failed creation.
if len(aa.NonceChanges) != 0 {
t.Fatalf("collision must not bump nonce: %+v", aa.NonceChanges)
}
if len(aa.CodeChanges) != 0 {
t.Fatalf("collision must not write code: %+v", aa.CodeChanges)
}
}
// TestBALInEVMCreatePreAccessAbortDestinationExcluded: if a CREATE frame
// aborts BEFORE the destination is read from state (here: the caller has 0
// balance and CREATE requests value > 0, tripping evm.create's CanTransfer
// check before GetCodeHash), the would-be address MUST NOT appear in the
// BAL — only "if target account is accessed" qualifies for inclusion.
func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) {
factory := common.HexToAddress("0xfac4")
// PUSH1 0 (length) PUSH1 0 (offset) PUSH1 1 (value) CREATE POP STOP
code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0xf0, 0x50, 0x00}
env := newBALTestEnv(types.GenesisAlloc{
factory: {Code: code, Balance: common.Big0, Nonce: 1}, // factory has no balance
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &factory, big.NewInt(0), 200_000, 0, nil))
})
// The address that WOULD have been deployed had the create succeeded.
wouldBeDest := crypto.CreateAddress(factory, 1)
assertAbsent(t, b, wouldBeDest)
// The factory itself is in BAL (it ran), but its nonce MUST NOT have been
// bumped because evm.create returned before the SetNonce call.
aa := assertPresent(t, b, factory)
if len(aa.NonceChanges) != 0 {
t.Fatalf("factory nonce must not be bumped on pre-access abort: %+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) {
factory := common.HexToAddress("0xfac4")
// Factory code:
// Write 5-byte init code (0x60 0x00 0x60 0x00 0xf3) into memory starting at offset 0.
// Then CREATE(value=0, offset=0, length=5).
//
// Layout: store the init code as a single 32-byte word at offset 0 via MSTORE
// with leftmost 27 bytes garbage, then call CREATE with offset = 27, length = 5.
initBlob := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
var word [32]byte
copy(word[32-len(initBlob):], initBlob)
code := []byte{0x7f} // PUSH32
code = append(code, word[:]...)
code = append(code, 0x60, 0x00, 0x52) // PUSH1 0, MSTORE
// CREATE expects [value, offset, length] with value on bottom of stack.
code = append(code,
0x60, 0x05, // PUSH1 5 (length)
0x60, 0x1b, // PUSH1 27 (offset)
0x60, 0x00, // PUSH1 0 (value)
0xf0, // CREATE
0x00, // STOP (discard result)
)
env := newBALTestEnv(types.GenesisAlloc{factory: {Code: code, Balance: common.Big0, Nonce: 1}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &factory, big.NewInt(0), 300_000, 0, nil))
})
// Deployed address depends on the factory's nonce at the moment of CREATE,
// which is the factory's genesis nonce (1).
deployed := crypto.CreateAddress(factory, 1)
aa := assertPresent(t, b, deployed)
if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 {
t.Fatalf("deployed contract nonce: %+v", aa.NonceChanges)
}
}
// ============================== SELFDESTRUCT ==============================
// TestBALSelfDestructBeneficiaryWithZeroBalance: SELFDESTRUCT to a fresh
// beneficiary when the destructing account has 0 balance — both addresses are
// listed with empty change sets (no balance entry).
func TestBALSelfDestructBeneficiaryWithZeroBalance(t *testing.T) {
beneficiary := common.HexToAddress("0xbeefbeef")
env := newBALTestEnv(nil)
// Init code performs SELFDESTRUCT to beneficiary inside the constructor,
// so EIP-6780's same-tx requirement is satisfied. The destructing account
// starts with balance 0 because the creation tx sends 0 value.
// PUSH20 <ben> SELFDESTRUCT
init := append([]byte{0x73}, beneficiary.Bytes()...)
init = append(init, 0xff)
b, receipts := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
})
created := receipts[0].ContractAddress
ben := assertPresent(t, b, beneficiary)
if len(ben.BalanceChanges) != 0 {
t.Fatalf("zero-value SELFDESTRUCT must not credit beneficiary: %+v", ben.BalanceChanges)
}
cc := assertPresent(t, b, created)
if len(cc.BalanceChanges) != 0 {
t.Fatalf("destructing contract must not record a balance entry: %+v", cc.BalanceChanges)
}
}
// TestBALSelfDestructBeneficiaryWithValueTransfer: SELFDESTRUCT from a freshly
// created contract that received positive value — beneficiary records the
// credit; destructing account's balance entry is omitted because its
// pre-transaction balance was 0.
func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) {
beneficiary := common.HexToAddress("0xbeefbeef")
env := newBALTestEnv(nil)
// Init code: PUSH20 <ben> SELFDESTRUCT
init := append([]byte{0x73}, beneficiary.Bytes()...)
init = append(init, 0xff)
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, nil, big.NewInt(100), 200_000, 0, init))
})
ben := assertPresent(t, b, beneficiary)
if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 100 {
t.Fatalf("beneficiary balance must be credited with 100: %+v", ben.BalanceChanges)
}
}
// TestBALSelfDestructPreExistingContract: SELFDESTRUCT on a pre-existing
// contract with positive balance records balance→0 for the contract and the
// corresponding credit on the beneficiary. EIP-6780 means the contract is
// only credited and not deleted, but its balance moves regardless.
func TestBALSelfDestructPreExistingContract(t *testing.T) {
suicidal := common.HexToAddress("0x5e1f")
beneficiary := common.HexToAddress("0xbeefbeef")
// PUSH20 <ben> SELFDESTRUCT
code := append([]byte{0x73}, beneficiary.Bytes()...)
code = append(code, 0xff)
env := newBALTestEnv(types.GenesisAlloc{
suicidal: {Code: code, Balance: big.NewInt(50)},
})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 200_000, 0, nil))
})
aa := assertPresent(t, b, suicidal)
if len(aa.BalanceChanges) != 1 || !aa.BalanceChanges[0].Balance.IsZero() {
t.Fatalf("suicidal contract balance should drop to 0: %+v", aa.BalanceChanges)
}
ben := assertPresent(t, b, beneficiary)
if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 50 {
t.Fatalf("beneficiary should receive 50: %+v", ben.BalanceChanges)
}
}
// ============================== Mid-tx balance round-trip ==============================
// TestBALMidTxBalanceRoundTrip: when an address's balance changes during a
// transaction but returns to its pre-transaction value, the address is still
// listed in the BAL but MUST NOT have a balance entry.
func TestBALMidTxBalanceRoundTrip(t *testing.T) {
bouncer := common.HexToAddress("0xb0unce")
// On receiving value, the bouncer immediately CALLs CALLER with CALLVALUE
// and zero data. Net effect: bouncer.balance returns to its pre-tx value.
//
// PUSH1 0 (retSize)
// PUSH1 0 (retOff)
// PUSH1 0 (argsSize)
// PUSH1 0 (argsOff)
// CALLVALUE
// CALLER
// GAS
// CALL
// POP
// STOP
code := []byte{
0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00,
0x34, // CALLVALUE
0x33, // CALLER
0x5a, // GAS
0xf1, // CALL
0x50, // POP
0x00, // STOP
}
env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 200_000, 0, nil))
})
aa := assertPresent(t, b, bouncer)
if len(aa.BalanceChanges) != 0 {
t.Fatalf("mid-tx round-trip must not record a balance entry: %+v", aa.BalanceChanges)
}
}
// ============================== System contracts (pre/post-execution) ==============================
// TestBALSystemContractsPresent: per EIP-7928, "System contract addresses
// accessed during pre/post-execution" MUST be included in the BAL. That
// means all four of the post-merge system contracts touched by every
// Amsterdam block:
//
// - EIP-4788 beacon roots (pre-execution, when ParentBeaconRoot is set)
// - EIP-2935 history storage (pre-execution)
// - EIP-7002 withdrawal queue (post-execution)
// - EIP-7251 consolidation queue (post-execution)
func TestBALSystemContractsPresent(t *testing.T) {
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
// SetCoinbase initialises b.bal; SetParentBeaconRoot triggers EIP-4788.
g.SetCoinbase(common.Address{0xc0})
g.SetParentBeaconRoot(common.Hash{0xbe, 0xac})
})
for _, sys := range []struct {
name string
addr common.Address
}{
{"BeaconRoots (4788)", params.BeaconRootsAddress},
{"HistoryStorage (2935)", params.HistoryStorageAddress},
{"WithdrawalQueue (7002)", params.WithdrawalQueueAddress},
{"ConsolidationQueue (7251)", params.ConsolidationQueueAddress},
} {
if findAccount(b, sys.addr) == nil {
t.Errorf("%s (%x) MUST appear in BAL but is missing\n%s", sys.name, sys.addr, b.PrettyPrint())
}
}
}
// ============================== Withdrawals ==============================
// TestBALWithdrawalZeroAmountIncluded: a withdrawal with amount 0 still puts
// the recipient in the BAL (with no balance entry).
func TestBALWithdrawalZeroAmountIncluded(t *testing.T) {
recipient := common.HexToAddress("0xdada")
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.SetCoinbase(common.Address{0xc0})
g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 0})
})
r := assertPresent(t, b, recipient)
if len(r.BalanceChanges) != 0 {
t.Fatalf("zero-amount withdrawal must not record balance: %+v", r.BalanceChanges)
}
}
// TestBALWithdrawalNonZeroAmountRecordsBalance: a positive-amount withdrawal
// records a balance change for the recipient.
func TestBALWithdrawalNonZeroAmountRecordsBalance(t *testing.T) {
recipient := common.HexToAddress("0xdada")
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
g.SetCoinbase(common.Address{0xc0})
g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 7})
})
r := assertPresent(t, b, recipient)
if len(r.BalanceChanges) != 1 || r.BalanceChanges[0].Balance.Sign() == 0 {
t.Fatalf("withdrawal balance change missing: %+v", r.BalanceChanges)
}
}
// ============================== EIP-7702 authority ==============================
// TestBALAuthorityIncludedOnSetCodeTx: the authority of an EIP-7702 set-code
// transaction is added to the BAL once its delegation is loaded, recording
// both the nonce bump and the delegation-pointer code entry.
func TestBALAuthorityIncludedOnSetCodeTx(t *testing.T) {
env := newBALTestEnv(nil)
authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
authority := crypto.PubkeyToAddress(authKey.PublicKey)
delegate := common.HexToAddress("0xdeadbeef")
auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: delegate,
Nonce: 0,
})
if err != nil {
t.Fatalf("sign auth: %v", err)
}
b, _ := env.run(t, func(g *BlockGen) {
tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{
ChainID: uint256.MustFromBig(env.cfg.ChainID),
Nonce: 0,
To: env.from,
Value: new(uint256.Int),
Gas: 200_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int),
AuthList: []types.SetCodeAuthorization{auth},
})
g.AddTx(tx)
})
aa := assertPresent(t, b, authority)
if len(aa.NonceChanges) == 0 {
t.Fatalf("authority nonce should be bumped by delegation: %+v", aa.NonceChanges)
}
if len(aa.CodeChanges) == 0 {
t.Fatalf("authority code (delegation pointer) should be recorded: %+v", aa.CodeChanges)
}
}
// TestBALDelegationTargetNotIncludedOnAuthOnly: the EIP-7702 delegation target
// MUST NOT appear in the BAL when only the authorization is installed and the
// target is never loaded as an execution target.
func TestBALDelegationTargetNotIncludedOnAuthOnly(t *testing.T) {
env := newBALTestEnv(nil)
authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
delegate := common.HexToAddress("0xdeadbeef") // never accessed
auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: delegate,
Nonce: 0,
})
if err != nil {
t.Fatalf("sign auth: %v", err)
}
b, _ := env.run(t, func(g *BlockGen) {
tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{
ChainID: uint256.MustFromBig(env.cfg.ChainID),
Nonce: 0,
To: env.from, // tx.to is an EOA with no code: delegate is never called
Value: new(uint256.Int),
Gas: 200_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int),
AuthList: []types.SetCodeAuthorization{auth},
})
g.AddTx(tx)
})
assertAbsent(t, b, delegate)
}
// newSetCodeTx is a small constructor used by the multi-auth tests below.
func (e *balTestEnv) newSetCodeTx(t *testing.T, nonce uint64, to common.Address, auths []types.SetCodeAuthorization) *types.Transaction {
t.Helper()
tx, err := types.SignTx(types.NewTx(&types.SetCodeTx{
ChainID: uint256.MustFromBig(e.cfg.ChainID),
Nonce: nonce,
To: to,
Value: new(uint256.Int),
Gas: 400_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int),
AuthList: auths,
}), e.signer, e.key)
if err != nil {
t.Fatalf("sign SetCodeTx: %v", err)
}
return tx
}
// TestBALAuthFailedBeforeLoadExcluded: an EIP-7702 auth whose ChainID check
// fails returns before the authority is loaded, so the authority address
// MUST NOT appear in the BAL.
func TestBALAuthFailedBeforeLoadExcluded(t *testing.T) {
env := newBALTestEnv(nil)
authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
authority := crypto.PubkeyToAddress(authKey.PublicKey)
auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.NewInt(999), // wrong chain → fails ChainID check (pre-load)
Address: common.HexToAddress("0xdeadbeef"),
Nonce: 0,
})
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}))
})
assertAbsent(t, b, authority)
}
// TestBALAuthFailedAfterLoadEmptyChangeSet: an EIP-7702 auth that fails the
// nonce check happens AFTER the authority's code is loaded (and the address
// added to accessed_addresses), so the authority MUST appear in the BAL —
// but with no nonce or code change.
func TestBALAuthFailedAfterLoadEmptyChangeSet(t *testing.T) {
env := newBALTestEnv(nil)
authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
authority := crypto.PubkeyToAddress(authKey.PublicKey)
// The authority's actual nonce is 0; supplying auth.Nonce=99 makes
// validation fail only after the code has been loaded.
auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: common.HexToAddress("0xdeadbeef"),
Nonce: 99,
})
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) != 0 {
t.Fatalf("failed auth must not bump nonce: %+v", aa.NonceChanges)
}
if len(aa.CodeChanges) != 0 {
t.Fatalf("failed auth must not record a code change: %+v", aa.CodeChanges)
}
}
// 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) {
env := newBALTestEnv(nil)
goodKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
badKey, _ := crypto.HexToECDSA("0303030303030303030303030303030303030303030303030303003030303030")
good := crypto.PubkeyToAddress(goodKey.PublicKey)
bad := crypto.PubkeyToAddress(badKey.PublicKey)
delegate := common.HexToAddress("0xdeadbeef")
goodAuth, err := types.SignSetCode(goodKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: delegate,
Nonce: 0,
})
if err != nil {
t.Fatalf("sign good auth: %v", err)
}
badAuth, err := types.SignSetCode(badKey, types.SetCodeAuthorization{
ChainID: *uint256.NewInt(999), // fails before load
Address: delegate,
Nonce: 0,
})
if err != nil {
t.Fatalf("sign bad auth: %v", err)
}
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{goodAuth, badAuth}))
})
assertPresent(t, b, good) // loaded → in BAL
assertAbsent(t, b, bad) // never loaded → not in BAL
}
// TestBALAuthCodeRoundTripNoCodeEntry: two auths on the same authority that
// (1) install a delegation and (2) clear it again. Final code equals pre-tx
// code (empty), so the BAL records only the cumulative nonce bump and NO
// code change.
func TestBALAuthCodeRoundTripNoCodeEntry(t *testing.T) {
env := newBALTestEnv(nil)
authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
authority := crypto.PubkeyToAddress(authKey.PublicKey)
delegateA := common.HexToAddress("0xa11ce")
auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: delegateA, // empty → A
Nonce: 0,
})
if err != nil {
t.Fatalf("sign auth1: %v", err)
}
auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: common.Address{}, // delegation to zero clears the code (A → empty)
Nonce: 1,
})
if err != nil {
t.Fatalf("sign auth2: %v", err)
}
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2}))
})
aa := assertPresent(t, b, authority)
if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 {
t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges)
}
if len(aa.CodeChanges) != 0 {
t.Fatalf("code round-trip (empty→A→empty) must NOT record a code change: %+v", aa.CodeChanges)
}
}
// TestBALAuthCodeOverwrittenFinalRecorded: two auths on the same authority
// switching delegation A → B record exactly one code change carrying the
// final delegation pointer (B), not the intermediate value.
func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) {
env := newBALTestEnv(nil)
authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
authority := crypto.PubkeyToAddress(authKey.PublicKey)
delegateA := common.HexToAddress("0xa11ce")
delegateB := common.HexToAddress("0xb0b0b0")
auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: delegateA,
Nonce: 0,
})
if err != nil {
t.Fatalf("sign auth1: %v", err)
}
auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(env.cfg.ChainID),
Address: delegateB,
Nonce: 1,
})
if err != nil {
t.Fatalf("sign auth2: %v", err)
}
b, _ := env.run(t, func(g *BlockGen) {
g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2}))
})
aa := assertPresent(t, b, authority)
if len(aa.CodeChanges) != 1 {
t.Fatalf("expected exactly 1 code change (final), got %+v", aa.CodeChanges)
}
want := types.AddressToDelegation(delegateB)
if !bytes.Equal(aa.CodeChanges[0].Code, want) {
t.Fatalf("final code mismatch: want %x, got %x", want, aa.CodeChanges[0].Code)
}
if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 {
t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges)
}
}