From 0ac419900e8eb7bfc9c532b4542331a962ffb68f Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 13 May 2026 16:05:17 +0800 Subject: [PATCH] core: add bal tests --- consensus/beacon/consensus.go | 2 +- core/bal_test.go | 1319 +++++++++++++++++++++++++++++++++ core/chain_makers.go | 2 +- core/state_processor.go | 25 +- core/vm/evm.go | 9 + 5 files changed, 1346 insertions(+), 11 deletions(-) create mode 100644 core/bal_test.go diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 0dff36a29c..4237418e73 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -356,7 +356,7 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. prev := state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal) // Populate the block-level accessList if Amsterdam is enabled - if bal != nil { + if chain.Config().IsAmsterdam(header.Number, header.Time) { if w.Amount == 0 { // Zero amount withdrawal, account is accessed potential // without state changes. diff --git a/core/bal_test.go b/core/bal_test.go new file mode 100644 index 0000000000..f0b9dc6443 --- /dev/null +++ b/core/bal_test.go @@ -0,0 +1,1319 @@ +// 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) + } +} diff --git a/core/chain_makers.go b/core/chain_makers.go index 8bab68d131..2e856b5161 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -67,7 +67,6 @@ func (b *BlockGen) SetCoinbase(addr common.Address) { } b.header.Coinbase = addr b.gasPool = NewGasPool(b.header.GasLimit) - b.bal = bal.NewConstructionBlockAccessList() } // SetExtra sets the extra data field of the generated block. @@ -359,6 +358,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse genblock := func(i int, parent *types.Block, triedb *triedb.Database, statedb *state.StateDB) (*types.Block, types.Receipts) { b := &BlockGen{i: i, cm: cm, parent: parent, statedb: statedb, engine: engine} b.header = cm.makeHeader(parent, statedb, b.engine) + b.bal = bal.NewConstructionBlockAccessList() // Set the difficulty for clique block. The chain maker doesn't have access // to a chain, so the difficulty will be left unset (nil). Set it here to the diff --git a/core/state_processor.go b/core/state_processor.go index eec5be9ff8..a230a79fac 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -119,7 +119,8 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated } blockAccessList.Merge(bal) - // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) + // Finalize the block, applying any consensus engine specific extras + // (e.g. block rewards). // // TODO(rjl493456442) integrate it into the PostExecution. p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body(), uint32(len(block.Transactions())+1), blockAccessList) @@ -165,17 +166,19 @@ func PostExecution(ctx context.Context, config *params.ChainConfig, number *big. } // Read requests if Prague is enabled. if config.IsPrague(number, time) { + rules := config.Rules(number, true, time) // IsMerge is always true + requests = [][]byte{} // EIP-6110 if err := ParseDepositLogs(&requests, allLogs, config); err != nil { return nil, nil, fmt.Errorf("failed to parse deposit logs: %w", err) } // EIP-7002 - if err := ProcessWithdrawalQueue(&requests, evm, blockAccessIndex, blockAccessList); err != nil { + if err := ProcessWithdrawalQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil { return nil, nil, fmt.Errorf("failed to process withdrawal queue: %w", err) } // EIP-7251 - if err := ProcessConsolidationQueue(&requests, evm, blockAccessIndex, blockAccessList); err != nil { + if err := ProcessConsolidationQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil { return nil, nil, fmt.Errorf("failed to process consolidation queue: %w", err) } } @@ -284,6 +287,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList Data: beaconRoot[:], } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) @@ -312,6 +316,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList * Data: prevHash.Bytes(), } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) @@ -326,17 +331,17 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList * // ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract. // It returns the opaque request data returned by the contract. -func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { - return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex, blockAccessList) +func ProcessWithdrawalQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { + return processRequestsSystemCall(requests, rules, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex, blockAccessList) } // ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract. // It returns the opaque request data returned by the contract. -func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { - return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex, blockAccessList) +func ProcessConsolidationQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { + return processRequestsSystemCall(requests, rules, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex, blockAccessList) } -func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { +func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -352,6 +357,7 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte To: &addr, } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex) evm.StateDB.AddAddressToAccessList(addr) ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) @@ -362,6 +368,8 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte if err != nil { return fmt.Errorf("system call failed to execute: %v", err) } + blockAccessList.Merge(bal) + if len(ret) == 0 { return nil // skip empty output } @@ -370,7 +378,6 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte requestsData[0] = requestType copy(requestsData[1:], ret) *requests = append(*requests, requestsData) - blockAccessList.Merge(bal) return nil } diff --git a/core/vm/evm.go b/core/vm/evm.go index 9fe6faa3a2..3b4e9647b5 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -18,6 +18,7 @@ package vm import ( "errors" + "fmt" "math/big" "sync/atomic" @@ -145,6 +146,9 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon jumpDests: newMapJumpDests(), arena: newArena(), } + if !evm.chainRules.IsAmsterdam { + fmt.Println("DEBUG") + } evm.precompiles = activePrecompiledContracts(evm.chainRules) switch { @@ -709,3 +713,8 @@ func (evm *EVM) GetVMContext() *tracing.VMContext { StateDB: evm.StateDB, } } + +// GetRules returns the chain rules used throughout the EVM execution. +func (evm *EVM) GetRules() params.Rules { + return evm.chainRules +}