// 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 snap import ( "bytes" "math/big" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "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/rlp" "github.com/holiman/uint256" ) // buildTestBAL constructs a BlockAccessList from a ConstructionBlockAccessList // by RLP round-tripping (construction types use unexported encoding types). func buildTestBAL(t *testing.T, cb *bal.ConstructionBlockAccessList) *bal.BlockAccessList { t.Helper() var buf bytes.Buffer if err := cb.EncodeRLP(&buf); err != nil { t.Fatalf("failed to encode BAL: %v", err) } var b bal.BlockAccessList if err := rlp.DecodeBytes(buf.Bytes(), &b); err != nil { t.Fatalf("failed to decode BAL: %v", err) } return &b } // TestAccessListVerification checks that verifyAccessList accepts valid BALs // and rejects tampered ones. func TestAccessListVerification(t *testing.T) { t.Parallel() cb := bal.NewConstructionBlockAccessList() addr := common.HexToAddress("0x01") cb.BalanceChange(0, addr, uint256.NewInt(100)) b := buildTestBAL(t, &cb) correctHash := b.Hash() // Valid: hash matches header header := &types.Header{ Number: big.NewInt(1), BlockAccessListHash: &correctHash, } if err := verifyAccessList(b, header); err != nil { t.Fatalf("valid access list rejected: %v", err) } // Invalid: wrong hash in header wrongHash := common.HexToHash("0xdead") badHeader := &types.Header{ Number: big.NewInt(1), BlockAccessListHash: &wrongHash, } if err := verifyAccessList(b, badHeader); err == nil { t.Fatal("tampered access list accepted") } // Invalid: no hash in header noHashHeader := &types.Header{ Number: big.NewInt(1), } if err := verifyAccessList(b, noHashHeader); err == nil { t.Fatal("header without access list hash accepted") } } // TestAccessListApplication verifies that applyAccessList correctly updates // flat state (balance, nonce, code, storage) and leaves storageRoot stale. func TestAccessListApplication(t *testing.T) { t.Parallel() db := rawdb.NewMemoryDatabase() syncer := NewSyncer(db, rawdb.HashScheme) addr := common.HexToAddress("0x01") accountHash := crypto.Keccak256Hash(addr[:]) // Write an existing account to flat state original := types.StateAccount{ Nonce: 5, Balance: uint256.NewInt(1000), Root: common.HexToHash("0xbeef"), // intentionally non-empty CodeHash: types.EmptyCodeHash[:], } rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original)) // Write an existing storage slot. The BAL uses raw slot keys, but the // snapshot layer stores slots under keccak256(slot). rawSlot := common.HexToHash("0xaa") slotHash := crypto.Keccak256Hash(rawSlot[:]) rawdb.WriteStorageSnapshot(db, accountHash, slotHash, common.HexToHash("0x01").Bytes()) // Build a BAL that changes balance, nonce, code, and storage cb := bal.NewConstructionBlockAccessList() cb.BalanceChange(0, addr, uint256.NewInt(2000)) cb.NonceChange(addr, 0, 6) cb.CodeChange(addr, 0, []byte{0x60, 0x00}) // PUSH1 0x00 cb.StorageWrite(0, addr, rawSlot, common.HexToHash("0x02")) b := buildTestBAL(t, &cb) if err := syncer.applyAccessList(b); err != nil { t.Fatalf("applyAccessList failed: %v", err) } // Verify account fields updated data := rawdb.ReadAccountSnapshot(db, accountHash) if len(data) == 0 { t.Fatal("account snapshot missing after apply") } updated, err := types.FullAccount(data) if err != nil { t.Fatalf("failed to decode updated account: %v", err) } if updated.Balance.Cmp(uint256.NewInt(2000)) != 0 { t.Errorf("balance wrong: got %v, want 2000", updated.Balance) } if updated.Nonce != 6 { t.Errorf("nonce wrong: got %d, want 6", updated.Nonce) } wantCodeHash := crypto.Keccak256([]byte{0x60, 0x00}) if !bytes.Equal(updated.CodeHash, wantCodeHash) { t.Errorf("code hash wrong: got %x, want %x", updated.CodeHash, wantCodeHash) } // Verify code was written if code := rawdb.ReadCode(db, common.BytesToHash(wantCodeHash)); !bytes.Equal(code, []byte{0x60, 0x00}) { t.Errorf("code wrong: got %x, want 6000", code) } // Verify storage updated storageVal := rawdb.ReadStorageSnapshot(db, accountHash, slotHash) if !bytes.Equal(storageVal, common.HexToHash("0x02").Bytes()) { t.Errorf("storage wrong: got %x, want %x", storageVal, common.HexToHash("0x02").Bytes()) } // Verify storageRoot left stale (unchanged from original) if updated.Root != original.Root { t.Errorf("storageRoot should be stale: got %v, want %v", updated.Root, original.Root) } } // TestAccessListApplicationMultiTx verifies that when an account has multiple // changes at different transaction indices, only the highest index (post-block // state) is applied. func TestAccessListApplicationMultiTx(t *testing.T) { t.Parallel() db := rawdb.NewMemoryDatabase() syncer := NewSyncer(db, rawdb.HashScheme) addr := common.HexToAddress("0x02") accountHash := crypto.Keccak256Hash(addr[:]) // Write initial account original := types.StateAccount{ Nonce: 0, Balance: uint256.NewInt(100), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash[:], } rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original)) // Build BAL with multiple balance/nonce changes at different tx indices cb := bal.NewConstructionBlockAccessList() cb.BalanceChange(0, addr, uint256.NewInt(200)) // tx 0 cb.BalanceChange(3, addr, uint256.NewInt(500)) // tx 3 cb.BalanceChange(7, addr, uint256.NewInt(9999)) // tx 7 (final) cb.NonceChange(addr, 0, 1) // tx 0 cb.NonceChange(addr, 3, 2) // tx 3 cb.NonceChange(addr, 7, 3) // tx 7 (final) b := buildTestBAL(t, &cb) if err := syncer.applyAccessList(b); err != nil { t.Fatalf("applyAccessList failed: %v", err) } data := rawdb.ReadAccountSnapshot(db, accountHash) updated, err := types.FullAccount(data) if err != nil { t.Fatalf("failed to decode updated account: %v", err) } // Only the highest tx index values should be applied if updated.Balance.Cmp(uint256.NewInt(9999)) != 0 { t.Errorf("balance wrong: got %v, want 9999", updated.Balance) } if updated.Nonce != 3 { t.Errorf("nonce wrong: got %d, want 3", updated.Nonce) } } // TestAccessListApplicationNewAccount verifies that applyAccessList creates // new accounts that don't exist in the DB yet. func TestAccessListApplicationNewAccount(t *testing.T) { t.Parallel() db := rawdb.NewMemoryDatabase() syncer := NewSyncer(db, rawdb.HashScheme) addr := common.HexToAddress("0x03") accountHash := crypto.Keccak256Hash(addr[:]) // Verify account doesn't exist if data := rawdb.ReadAccountSnapshot(db, accountHash); len(data) > 0 { t.Fatal("account should not exist yet") } // Build BAL for a new account. BAL uses raw slot keys. cb := bal.NewConstructionBlockAccessList() cb.BalanceChange(0, addr, uint256.NewInt(42)) cb.NonceChange(addr, 0, 1) rawSlot := common.HexToHash("0xbb") cb.StorageWrite(0, addr, rawSlot, common.HexToHash("0xff")) b := buildTestBAL(t, &cb) if err := syncer.applyAccessList(b); err != nil { t.Fatalf("applyAccessList failed: %v", err) } // Verify account was created data := rawdb.ReadAccountSnapshot(db, accountHash) if len(data) == 0 { t.Fatal("account should exist after apply") } account, err := types.FullAccount(data) if err != nil { t.Fatalf("failed to decode new account: %v", err) } if account.Balance.Cmp(uint256.NewInt(42)) != 0 { t.Errorf("balance wrong: got %v, want 42", account.Balance) } if account.Nonce != 1 { t.Errorf("nonce wrong: got %d, want 1", account.Nonce) } if account.Root != types.EmptyRootHash { t.Errorf("root should be empty for new account: got %v", account.Root) } // Verify storage was written under keccak256(rawSlot) slotHash := crypto.Keccak256Hash(rawSlot[:]) storageVal := rawdb.ReadStorageSnapshot(db, accountHash, slotHash) if !bytes.Equal(storageVal, common.HexToHash("0xff").Bytes()) { t.Errorf("storage wrong: got %x, want %x", storageVal, common.HexToHash("0xff").Bytes()) } } // TestAccessListApplicationSameTxCreateDestroy tests the edge case where an // account is created and self-destructed in the same transaction during the // pivot gap. Per EIP-7928, such accounts appear in the BAL with a balance // change to zero but no nonce or code changes. Since the account didn't exist // at the old pivot and doesn't exist at the new pivot (destroyed), // applyAccessList should not leave a zero-balance account in the snapshot. // Per EIP-161, empty accounts (zero balance, zero nonce, no code) must not exist // in state. func TestAccessListApplicationSameTxCreateDestroy(t *testing.T) { t.Parallel() db := rawdb.NewMemoryDatabase() syncer := NewSyncer(db, rawdb.HashScheme) addr := common.HexToAddress("0x04") accountHash := crypto.Keccak256Hash(addr[:]) // Verify account doesn't exist before apply if data := rawdb.ReadAccountSnapshot(db, accountHash); len(data) > 0 { t.Fatal("account should not exist yet") } // Build a BAL mimicking same-tx create+destroy: the account appears // with a balance change to zero and nothing else. cb := bal.NewConstructionBlockAccessList() cb.BalanceChange(0, addr, uint256.NewInt(0)) b := buildTestBAL(t, &cb) if err := syncer.applyAccessList(b); err != nil { t.Fatalf("applyAccessList failed: %v", err) } // Check if applyAccessList created an account. data := rawdb.ReadAccountSnapshot(db, accountHash) if len(data) > 0 { // Account was created account, err := types.FullAccount(data) if err != nil { t.Fatalf("failed to decode account: %v", err) } t.Errorf("account created for same-tx create+destroy: "+ "balance=%v, nonce=%d, codeHash=%x, root=%v", account.Balance, account.Nonce, account.CodeHash, account.Root) } }