// 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 . 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 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 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 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) } }