go-ethereum/core/state/partial/state_test.go
CPerezz c958bbc73b
core/state/partial: adopt new CodeChanges type for bal-devnet-3
Upstream bal-devnet-3 replaced the CodeChange struct with raw []byte
in ConstructionAccountAccesses.CodeChanges (map[uint16][]byte). Update
the test builder accordingly so the package compiles against the
new API.
2026-04-18 16:03:10 +02:00

1126 lines
38 KiB
Go

// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package partial
import (
"bytes"
"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/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb"
"github.com/holiman/uint256"
)
// testBALBuilder is a test helper for constructing BlockAccessLists.
// It wraps ConstructionBlockAccessList and provides convenience methods
// matching the test patterns (BalanceChange, NonceChange, StorageWrite, CodeChange).
type testBALBuilder struct {
accesses bal.ConstructionBlockAccessList
}
func newTestBALBuilder() *testBALBuilder {
return &testBALBuilder{
accesses: make(bal.ConstructionBlockAccessList),
}
}
func (b *testBALBuilder) ensureAccount(addr common.Address) *bal.ConstructionAccountAccesses {
if _, ok := b.accesses[addr]; !ok {
b.accesses[addr] = &bal.ConstructionAccountAccesses{}
}
return b.accesses[addr]
}
func (b *testBALBuilder) BalanceChange(txIdx uint16, addr common.Address, balance *uint256.Int) {
acc := b.ensureAccount(addr)
if acc.BalanceChanges == nil {
acc.BalanceChanges = make(map[uint16]*uint256.Int)
}
acc.BalanceChanges[txIdx] = balance
}
func (b *testBALBuilder) NonceChange(addr common.Address, txIdx uint16, nonce uint64) {
acc := b.ensureAccount(addr)
if acc.NonceChanges == nil {
acc.NonceChanges = make(map[uint16]uint64)
}
acc.NonceChanges[txIdx] = nonce
}
func (b *testBALBuilder) StorageWrite(txIdx uint16, addr common.Address, slot, value common.Hash) {
acc := b.ensureAccount(addr)
if acc.StorageWrites == nil {
acc.StorageWrites = make(map[common.Hash]map[uint16]common.Hash)
}
if _, ok := acc.StorageWrites[slot]; !ok {
acc.StorageWrites[slot] = make(map[uint16]common.Hash)
}
acc.StorageWrites[slot][txIdx] = value
}
func (b *testBALBuilder) CodeChange(addr common.Address, txIdx uint16, code []byte) {
acc := b.ensureAccount(addr)
if acc.CodeChanges == nil {
acc.CodeChanges = make(map[uint16][]byte)
}
acc.CodeChanges[txIdx] = code
}
// Build converts the construction BAL to the encoding format via RLP round-trip.
func (b *testBALBuilder) Build(t *testing.T) *bal.BlockAccessList {
t.Helper()
var buf bytes.Buffer
if err := b.accesses.EncodeRLP(&buf); err != nil {
t.Fatalf("failed to encode BAL: %v", err)
}
var result bal.BlockAccessList
if err := result.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil {
t.Fatalf("failed to decode BAL: %v", err)
}
return &result
}
// setupTestPartialState creates a test partial state with the given tracked contracts.
func setupTestPartialState(t *testing.T, trackedContracts []common.Address) (*PartialState, *triedb.Database, common.Hash) {
t.Helper()
db := rawdb.NewMemoryDatabase()
trieDB := triedb.NewDatabase(db, triedb.HashDefaults)
filter := NewConfiguredFilter(trackedContracts)
ps := NewPartialState(db, trieDB, filter, 256)
// Create empty state trie
stateTrie, err := trie.NewStateTrie(trie.StateTrieID(types.EmptyRootHash), trieDB)
if err != nil {
t.Fatalf("failed to create state trie: %v", err)
}
emptyRoot, _ := stateTrie.Commit(false)
return ps, trieDB, emptyRoot
}
// setupTestStateWithAccount creates a state trie with a single account.
func setupTestStateWithAccount(t *testing.T, trieDB *triedb.Database, addr common.Address, account *types.StateAccount) common.Hash {
t.Helper()
stateTrie, err := trie.NewStateTrie(trie.StateTrieID(types.EmptyRootHash), trieDB)
if err != nil {
t.Fatalf("failed to create state trie: %v", err)
}
if err := stateTrie.UpdateAccount(addr, account, 0); err != nil {
t.Fatalf("failed to update account: %v", err)
}
root, nodeSet := stateTrie.Commit(false)
if nodeSet != nil {
merged := trienode.NewWithNodeSet(nodeSet)
if err := trieDB.Update(root, types.EmptyRootHash, 0, merged, nil); err != nil {
t.Fatalf("failed to update trieDB: %v", err)
}
if err := trieDB.Commit(root, false); err != nil {
t.Fatalf("failed to commit trieDB: %v", err)
}
}
return root
}
func TestApplyBALAndComputeRoot_EmptyBAL(t *testing.T) {
ps, _, emptyRoot := setupTestPartialState(t, nil)
// Apply empty BAL
emptyBAL := bal.BlockAccessList{}
accessList := &emptyBAL
newRoot, _, err := ps.ApplyBALAndComputeRoot(emptyRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply empty BAL: %v", err)
}
// Empty BAL should result in same root
if newRoot != emptyRoot {
t.Errorf("expected empty root %x, got %x", emptyRoot, newRoot)
}
}
func TestApplyBALAndComputeRoot_BalanceChange(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, _ := setupTestPartialState(t, []common.Address{addr})
// Create initial account
initialBalance := uint256.NewInt(1000)
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: initialBalance,
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Create BAL with balance change using ConstructionBlockAccessList
newBalance := uint256.NewInt(2000)
cbal := newTestBALBuilder()
cbal.BalanceChange(0, addr, newBalance)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify new root is different
if newRoot == parentRoot {
t.Error("expected different root after balance change")
}
// Verify the account balance was updated
newTrie, err := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
if err != nil {
t.Fatalf("failed to open new trie: %v", err)
}
updatedAccount, err := newTrie.GetAccount(addr)
if err != nil {
t.Fatalf("failed to get updated account: %v", err)
}
if updatedAccount.Balance.Cmp(newBalance) != 0 {
t.Errorf("expected balance %v, got %v", newBalance, updatedAccount.Balance)
}
}
func TestApplyBALAndComputeRoot_NonceChange(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, _ := setupTestPartialState(t, []common.Address{addr})
// Create initial account
initialAccount := &types.StateAccount{
Nonce: 5,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Create BAL with nonce change
cbal := newTestBALBuilder()
cbal.NonceChange(addr, 0, 6)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify the account nonce was updated
newTrie, err := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
if err != nil {
t.Fatalf("failed to open new trie: %v", err)
}
updatedAccount, err := newTrie.GetAccount(addr)
if err != nil {
t.Fatalf("failed to get updated account: %v", err)
}
if updatedAccount.Nonce != 6 {
t.Errorf("expected nonce 6, got %d", updatedAccount.Nonce)
}
}
func TestApplyBALAndComputeRoot_StorageChange(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, _ := setupTestPartialState(t, []common.Address{addr})
// Create initial account (tracked contract)
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(0),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Create BAL with storage change
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
value := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000042")
cbal := newTestBALBuilder()
cbal.StorageWrite(0, addr, slot, value)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify new root is different (storage changed)
if newRoot == parentRoot {
t.Error("expected different root after storage change")
}
// Verify the account storage root changed
newTrie, err := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
if err != nil {
t.Fatalf("failed to open new trie: %v", err)
}
updatedAccount, err := newTrie.GetAccount(addr)
if err != nil {
t.Fatalf("failed to get updated account: %v", err)
}
if updatedAccount.Root == types.EmptyRootHash {
t.Error("expected non-empty storage root after storage change")
}
}
func TestApplyBALAndComputeRoot_UntrackedContractStorageIgnored(t *testing.T) {
trackedAddr := common.HexToAddress("0x1111111111111111111111111111111111111111")
untrackedAddr := common.HexToAddress("0x2222222222222222222222222222222222222222")
// Only track one contract
ps, trieDB, _ := setupTestPartialState(t, []common.Address{trackedAddr})
// Create initial accounts
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
// Add both accounts to state
stateTrie, _ := trie.NewStateTrie(trie.StateTrieID(types.EmptyRootHash), trieDB)
stateTrie.UpdateAccount(trackedAddr, initialAccount, 0)
stateTrie.UpdateAccount(untrackedAddr, initialAccount, 0)
parentRoot, nodeSet := stateTrie.Commit(false)
if nodeSet != nil {
merged := trienode.NewWithNodeSet(nodeSet)
trieDB.Update(parentRoot, types.EmptyRootHash, 0, merged, nil)
trieDB.Commit(parentRoot, false)
}
// Create BAL with storage changes for both contracts
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
value := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000042")
cbal := newTestBALBuilder()
cbal.StorageWrite(0, trackedAddr, slot, value)
cbal.StorageWrite(0, untrackedAddr, slot, value)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify tracked contract has storage
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
trackedAccount, _ := newTrie.GetAccount(trackedAddr)
if trackedAccount.Root == types.EmptyRootHash {
t.Error("tracked contract should have storage root")
}
// Verify untracked contract has NO storage (storage was ignored)
untrackedAccount, _ := newTrie.GetAccount(untrackedAddr)
if untrackedAccount.Root != types.EmptyRootHash {
t.Error("untracked contract should have empty storage root")
}
}
func TestApplyBALAndComputeRoot_NewAccount(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, emptyRoot := setupTestPartialState(t, []common.Address{addr})
// Create BAL that creates a new account
balance := uint256.NewInt(1000)
cbal := newTestBALBuilder()
cbal.BalanceChange(0, addr, balance)
cbal.NonceChange(addr, 0, 1)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(emptyRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify new account was created
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, err := newTrie.GetAccount(addr)
if err != nil {
t.Fatalf("failed to get new account: %v", err)
}
if account == nil {
t.Fatal("expected account to exist")
}
if account.Balance.Cmp(balance) != 0 {
t.Errorf("expected balance %v, got %v", balance, account.Balance)
}
if account.Nonce != 1 {
t.Errorf("expected nonce 1, got %d", account.Nonce)
}
}
func TestApplyBALAndComputeRoot_CodeChange(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
db := rawdb.NewMemoryDatabase()
trieDB := triedb.NewDatabase(db, triedb.HashDefaults)
filter := NewConfiguredFilter([]common.Address{addr})
ps := NewPartialState(db, trieDB, filter, 256)
// Create initial account
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(0),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Create BAL with code deployment
code := []byte{0x60, 0x60, 0x60, 0x40, 0x52} // Some bytecode
codeHash := crypto.Keccak256Hash(code)
cbal := newTestBALBuilder()
cbal.CodeChange(addr, 0, code)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify code hash was updated
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, _ := newTrie.GetAccount(addr)
if common.BytesToHash(account.CodeHash) != codeHash {
t.Errorf("expected code hash %x, got %x", codeHash, account.CodeHash)
}
// Verify code was stored (tracked contract)
storedCode := rawdb.ReadCode(db, codeHash)
if storedCode == nil {
t.Error("expected code to be stored for tracked contract")
}
}
func TestApplyBALAndComputeRoot_MultipleTransactions(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, _ := setupTestPartialState(t, []common.Address{addr})
// Create initial account
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Create BAL with multiple balance/nonce changes (only last should apply)
balance1 := uint256.NewInt(500)
balance2 := uint256.NewInt(2000)
balance3 := uint256.NewInt(1500) // Final balance
cbal := newTestBALBuilder()
cbal.BalanceChange(0, addr, balance1)
cbal.BalanceChange(1, addr, balance2)
cbal.BalanceChange(2, addr, balance3) // Final
cbal.NonceChange(addr, 0, 1)
cbal.NonceChange(addr, 1, 2)
cbal.NonceChange(addr, 2, 3) // Final
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify only final values are applied
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, _ := newTrie.GetAccount(addr)
if account.Balance.Cmp(balance3) != 0 {
t.Errorf("expected final balance %v, got %v", balance3, account.Balance)
}
if account.Nonce != 3 {
t.Errorf("expected final nonce 3, got %d", account.Nonce)
}
}
// ============================================================================
// Task 1: Edge Case Tests for ApplyBALAndComputeRoot
// ============================================================================
// TestApplyBALAndComputeRoot_StorageDeletion tests deleting a storage slot by writing zero value.
func TestApplyBALAndComputeRoot_StorageDeletion(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
db := rawdb.NewMemoryDatabase()
trieDB := triedb.NewDatabase(db, triedb.HashDefaults)
filter := NewConfiguredFilter([]common.Address{addr})
ps := NewPartialState(db, trieDB, filter, 256)
// Create initial account with storage
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
initialValue := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000042")
// First, create account and add storage
stateTrie, _ := trie.NewStateTrie(trie.StateTrieID(types.EmptyRootHash), trieDB)
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
// Create storage trie with initial value
addrHash := crypto.Keccak256Hash(addr.Bytes())
storageTrie, _ := trie.NewStateTrie(trie.StorageTrieID(types.EmptyRootHash, addrHash, types.EmptyRootHash), trieDB)
storageTrie.UpdateStorage(addr, slot.Bytes(), initialValue.Bytes())
storageRoot, storageNodes := storageTrie.Commit(false)
initialAccount.Root = storageRoot
stateTrie.UpdateAccount(addr, initialAccount, 0)
parentRoot, accountNodes := stateTrie.Commit(false)
// Commit storage and account nodes
allNodes := trienode.NewMergedNodeSet()
if storageNodes != nil {
allNodes.Merge(storageNodes)
}
if accountNodes != nil {
allNodes.Merge(accountNodes)
}
trieDB.Update(parentRoot, types.EmptyRootHash, 0, allNodes, nil)
trieDB.Commit(parentRoot, false)
// Create BAL that deletes the storage slot (write zero value)
cbal := newTestBALBuilder()
cbal.StorageWrite(0, addr, slot, common.Hash{}) // Zero value = delete
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify storage was deleted (root should be empty)
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, _ := newTrie.GetAccount(addr)
if account.Root != types.EmptyRootHash {
t.Errorf("expected empty storage root after deletion, got %x", account.Root)
}
}
// TestApplyBALAndComputeRoot_MultipleStorageWritesSameSlot tests last-write-wins for storage.
func TestApplyBALAndComputeRoot_MultipleStorageWritesSameSlot(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, _ := setupTestPartialState(t, []common.Address{addr})
// Create initial account
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Create BAL with multiple writes to same slot
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
value1 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
value2 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000002")
value3 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000003") // Final
cbal := newTestBALBuilder()
cbal.StorageWrite(0, addr, slot, value1)
cbal.StorageWrite(1, addr, slot, value2)
cbal.StorageWrite(2, addr, slot, value3) // Final value
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify only final value is stored
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, _ := newTrie.GetAccount(addr)
// Open storage trie and check value
addrHash := crypto.Keccak256Hash(addr.Bytes())
storageTrie, err := trie.NewStateTrie(trie.StorageTrieID(newRoot, addrHash, account.Root), trieDB)
if err != nil {
t.Fatalf("failed to open storage trie: %v", err)
}
storedValue, err := storageTrie.GetStorage(addr, slot.Bytes())
if err != nil {
t.Fatalf("failed to get storage: %v", err)
}
if common.BytesToHash(storedValue) != value3 {
t.Errorf("expected final value %x, got %x", value3, storedValue)
}
}
// TestApplyBALAndComputeRoot_AccountDeletion_EIP161 tests EIP-161 account deletion.
// An account should be deleted if: existed before, modified, and now empty.
func TestApplyBALAndComputeRoot_AccountDeletion_EIP161(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, _ := setupTestPartialState(t, []common.Address{addr})
// Create initial account with balance
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Create BAL that empties the account
cbal := newTestBALBuilder()
cbal.BalanceChange(0, addr, uint256.NewInt(0)) // Zero balance
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify account was deleted (EIP-161: empty account should be removed)
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, err := newTrie.GetAccount(addr)
if err != nil {
t.Fatalf("failed to get account: %v", err)
}
if account != nil {
t.Errorf("expected account to be deleted (EIP-161), but it still exists with balance %v", account.Balance)
}
}
// TestApplyBALAndComputeRoot_NeverExistedEmptyAccount tests that empty accounts that never existed
// are not added to the trie.
func TestApplyBALAndComputeRoot_NeverExistedEmptyAccount(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, emptyRoot := setupTestPartialState(t, []common.Address{addr})
// Create BAL that "touches" an account but leaves it empty
// This simulates an account that receives 0 balance and sends 0 balance
cbal := newTestBALBuilder()
cbal.BalanceChange(0, addr, uint256.NewInt(0)) // Zero balance on never-existed account
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(emptyRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Root should be the same as empty root (no account added)
if newRoot != emptyRoot {
t.Errorf("expected empty root (no account added), got different root")
}
// Verify account does not exist
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, _ := newTrie.GetAccount(addr)
if account != nil {
t.Errorf("expected no account (never existed + empty), but found one")
}
}
// TestApplyBALAndComputeRoot_CodeChangeUntracked tests that code hash is updated for untracked
// contracts but the code bytes are NOT stored.
func TestApplyBALAndComputeRoot_CodeChangeUntracked(t *testing.T) {
trackedAddr := common.HexToAddress("0x1111111111111111111111111111111111111111")
untrackedAddr := common.HexToAddress("0x2222222222222222222222222222222222222222")
db := rawdb.NewMemoryDatabase()
trieDB := triedb.NewDatabase(db, triedb.HashDefaults)
// Only track one contract
filter := NewConfiguredFilter([]common.Address{trackedAddr})
ps := NewPartialState(db, trieDB, filter, 256)
// Create initial accounts
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
stateTrie, _ := trie.NewStateTrie(trie.StateTrieID(types.EmptyRootHash), trieDB)
stateTrie.UpdateAccount(trackedAddr, initialAccount, 0)
stateTrie.UpdateAccount(untrackedAddr, initialAccount, 0)
parentRoot, nodeSet := stateTrie.Commit(false)
if nodeSet != nil {
merged := trienode.NewWithNodeSet(nodeSet)
trieDB.Update(parentRoot, types.EmptyRootHash, 0, merged, nil)
trieDB.Commit(parentRoot, false)
}
// Create BAL with code changes for both
code := []byte{0x60, 0x60, 0x60, 0x40, 0x52}
codeHash := crypto.Keccak256Hash(code)
cbal := newTestBALBuilder()
cbal.CodeChange(trackedAddr, 0, code)
cbal.CodeChange(untrackedAddr, 0, code)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify both accounts have updated code hash
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
trackedAccount, _ := newTrie.GetAccount(trackedAddr)
if common.BytesToHash(trackedAccount.CodeHash) != codeHash {
t.Errorf("tracked contract should have updated code hash")
}
untrackedAccount, _ := newTrie.GetAccount(untrackedAddr)
if common.BytesToHash(untrackedAccount.CodeHash) != codeHash {
t.Errorf("untracked contract should have updated code hash")
}
// Verify code is stored for tracked contract
storedCode := rawdb.ReadCode(db, codeHash)
if storedCode == nil {
t.Error("code should be stored for tracked contract")
}
// Note: We can't directly test that code is NOT stored for untracked because
// both contracts use the same code hash, and it's stored once for the tracked one.
// The key invariant is that the code hash is correct for both.
}
// TestApplyBALAndComputeRoot_MixedChanges tests applying multiple types of changes to one account.
func TestApplyBALAndComputeRoot_MixedChanges(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
db := rawdb.NewMemoryDatabase()
trieDB := triedb.NewDatabase(db, triedb.HashDefaults)
filter := NewConfiguredFilter([]common.Address{addr})
ps := NewPartialState(db, trieDB, filter, 256)
// Create initial account
initialAccount := &types.StateAccount{
Nonce: 5,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Create BAL with balance, nonce, code, and storage changes
newBalance := uint256.NewInt(2000)
newNonce := uint64(10)
code := []byte{0x60, 0x60, 0x60, 0x40, 0x52}
codeHash := crypto.Keccak256Hash(code)
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
value := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000042")
cbal := newTestBALBuilder()
cbal.BalanceChange(0, addr, newBalance)
cbal.NonceChange(addr, 0, newNonce)
cbal.CodeChange(addr, 0, code)
cbal.StorageWrite(0, addr, slot, value)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify all changes applied
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, _ := newTrie.GetAccount(addr)
if account.Balance.Cmp(newBalance) != 0 {
t.Errorf("expected balance %v, got %v", newBalance, account.Balance)
}
if account.Nonce != newNonce {
t.Errorf("expected nonce %d, got %d", newNonce, account.Nonce)
}
if common.BytesToHash(account.CodeHash) != codeHash {
t.Errorf("expected code hash %x, got %x", codeHash, account.CodeHash)
}
if account.Root == types.EmptyRootHash {
t.Error("expected non-empty storage root")
}
// Verify storage value
addrHash := crypto.Keccak256Hash(addr.Bytes())
storageTrie, _ := trie.NewStateTrie(trie.StorageTrieID(newRoot, addrHash, account.Root), trieDB)
storedValue, _ := storageTrie.GetStorage(addr, slot.Bytes())
if common.BytesToHash(storedValue) != value {
t.Errorf("expected storage value %x, got %x", value, storedValue)
}
}
// ============================================================================
// Task 3: Error Path Tests
// ============================================================================
// TestApplyBALAndComputeRoot_ErrorInvalidParentRoot tests error handling for invalid parent root.
func TestApplyBALAndComputeRoot_ErrorInvalidParentRoot(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, _, _ := setupTestPartialState(t, []common.Address{addr})
// Use a non-existent root
invalidRoot := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
cbal := newTestBALBuilder()
cbal.BalanceChange(0, addr, uint256.NewInt(1000))
accessList := cbal.Build(t)
_, _, err := ps.ApplyBALAndComputeRoot(invalidRoot, common.Hash{}, accessList)
if err == nil {
t.Fatal("expected error for invalid parent root, got nil")
}
// Error should mention trie opening failure
if !bytes.Contains([]byte(err.Error()), []byte("failed to open state trie")) {
t.Errorf("expected 'failed to open state trie' error, got: %v", err)
}
}
// ============================================================================
// Task 4: isEmptyAccount Tests
// ============================================================================
// TestIsEmptyAccount tests the EIP-161 empty account detection logic.
func TestIsEmptyAccount(t *testing.T) {
ps, _, _ := setupTestPartialState(t, nil)
tests := []struct {
name string
account *types.StateAccount
expected bool
}{
{
name: "completely empty account",
account: &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(0),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
},
expected: true,
},
{
name: "non-zero balance",
account: &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
},
expected: false,
},
{
name: "non-zero nonce",
account: &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(0),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
},
expected: false,
},
{
name: "non-empty storage root",
account: &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(0),
Root: common.HexToHash("0x1234567890123456789012345678901234567890123456789012345678901234"),
CodeHash: types.EmptyCodeHash.Bytes(),
},
expected: false,
},
{
name: "non-empty code hash",
account: &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(0),
Root: types.EmptyRootHash,
CodeHash: common.HexToHash("0x1234567890123456789012345678901234567890123456789012345678901234").Bytes(),
},
expected: false,
},
{
name: "large balance (uint256)",
account: &types.StateAccount{
Nonce: 0,
Balance: uint256.MustFromHex("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ps.isEmptyAccount(tt.account)
if result != tt.expected {
t.Errorf("isEmptyAccount() = %v, expected %v", result, tt.expected)
}
})
}
}
// ============================================================================
// Task 2: buildStateSet Tests (indirect verification)
// ============================================================================
// TestBuildStateSet_AccountModification verifies that modified accounts are correctly
// tracked in the StateSet by checking the resulting state.
func TestBuildStateSet_AccountModification(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, _ := setupTestPartialState(t, []common.Address{addr})
// Create initial account
initialAccount := &types.StateAccount{
Nonce: 5,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Apply balance change
cbal := newTestBALBuilder()
cbal.BalanceChange(0, addr, uint256.NewInt(2000))
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify the state was correctly updated (indirectly tests buildStateSet)
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, _ := newTrie.GetAccount(addr)
// The nonce should be preserved (not modified)
if account.Nonce != 5 {
t.Errorf("nonce should be preserved: expected 5, got %d", account.Nonce)
}
// Balance should be updated
if account.Balance.Cmp(uint256.NewInt(2000)) != 0 {
t.Errorf("balance should be updated: expected 2000, got %v", account.Balance)
}
}
// TestBuildStateSet_StorageRLPEncoding verifies that storage values are correctly
// RLP-encoded in the StateSet.
func TestBuildStateSet_StorageRLPEncoding(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
db := rawdb.NewMemoryDatabase()
trieDB := triedb.NewDatabase(db, triedb.HashDefaults)
filter := NewConfiguredFilter([]common.Address{addr})
ps := NewPartialState(db, trieDB, filter, 256)
// Create initial account
initialAccount := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Write storage value
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
value := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000042")
cbal := newTestBALBuilder()
cbal.StorageWrite(0, addr, slot, value)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify storage is readable
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, _ := newTrie.GetAccount(addr)
addrHash := crypto.Keccak256Hash(addr.Bytes())
storageTrie, err := trie.NewStateTrie(trie.StorageTrieID(newRoot, addrHash, account.Root), trieDB)
if err != nil {
t.Fatalf("failed to open storage trie: %v", err)
}
storedValue, err := storageTrie.GetStorage(addr, slot.Bytes())
if err != nil {
t.Fatalf("failed to get storage: %v", err)
}
if common.BytesToHash(storedValue) != value {
t.Errorf("storage value mismatch: expected %x, got %x", value, storedValue)
}
}
// TestBuildStateSet_OriginTracking verifies that account origins are tracked correctly
// for PathDB compatibility.
func TestBuildStateSet_OriginTracking(t *testing.T) {
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
ps, trieDB, _ := setupTestPartialState(t, []common.Address{addr})
// Create initial account with specific values
initialAccount := &types.StateAccount{
Nonce: 10,
Balance: uint256.NewInt(5000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
parentRoot := setupTestStateWithAccount(t, trieDB, addr, initialAccount)
// Modify the account
cbal := newTestBALBuilder()
cbal.BalanceChange(0, addr, uint256.NewInt(6000))
cbal.NonceChange(addr, 0, 11)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify the new state is correct (origin tracking happens internally)
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
account, _ := newTrie.GetAccount(addr)
if account.Nonce != 11 {
t.Errorf("expected nonce 11, got %d", account.Nonce)
}
if account.Balance.Cmp(uint256.NewInt(6000)) != 0 {
t.Errorf("expected balance 6000, got %v", account.Balance)
}
// The fact that this works with PathDB verifies origin tracking is correct
// (PathDB requires origins for diff computation)
}
// TestApplyBALAndComputeRoot_MultipleAccountTypes tests processing multiple accounts with
// different modification patterns in one block.
func TestApplyBALAndComputeRoot_MultipleAccountTypes(t *testing.T) {
addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111") // Balance only
addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222") // Storage only
addr3 := common.HexToAddress("0x3333333333333333333333333333333333333333") // New account
db := rawdb.NewMemoryDatabase()
trieDB := triedb.NewDatabase(db, triedb.HashDefaults)
filter := NewConfiguredFilter([]common.Address{addr1, addr2, addr3})
ps := NewPartialState(db, trieDB, filter, 256)
// Create initial accounts for addr1 and addr2
initialAccount1 := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
initialAccount2 := &types.StateAccount{
Nonce: 5,
Balance: uint256.NewInt(500),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
stateTrie, _ := trie.NewStateTrie(trie.StateTrieID(types.EmptyRootHash), trieDB)
stateTrie.UpdateAccount(addr1, initialAccount1, 0)
stateTrie.UpdateAccount(addr2, initialAccount2, 0)
parentRoot, nodeSet := stateTrie.Commit(false)
if nodeSet != nil {
merged := trienode.NewWithNodeSet(nodeSet)
trieDB.Update(parentRoot, types.EmptyRootHash, 0, merged, nil)
trieDB.Commit(parentRoot, false)
}
// Create BAL with different changes for each account
cbal := newTestBALBuilder()
// addr1: balance change
cbal.BalanceChange(0, addr1, uint256.NewInt(2000))
// addr2: storage write
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
value := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000042")
cbal.StorageWrite(0, addr2, slot, value)
// addr3: new account
cbal.BalanceChange(0, addr3, uint256.NewInt(3000))
cbal.NonceChange(addr3, 0, 1)
accessList := cbal.Build(t)
newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil {
t.Fatalf("failed to apply BAL: %v", err)
}
// Verify all accounts
newTrie, _ := trie.NewStateTrie(trie.StateTrieID(newRoot), trieDB)
// addr1: balance changed
acc1, _ := newTrie.GetAccount(addr1)
if acc1.Balance.Cmp(uint256.NewInt(2000)) != 0 {
t.Errorf("addr1: expected balance 2000, got %v", acc1.Balance)
}
// addr2: storage changed
acc2, _ := newTrie.GetAccount(addr2)
if acc2.Root == types.EmptyRootHash {
t.Error("addr2: expected non-empty storage root")
}
// addr3: new account created
acc3, _ := newTrie.GetAccount(addr3)
if acc3 == nil {
t.Fatal("addr3: expected account to exist")
}
if acc3.Balance.Cmp(uint256.NewInt(3000)) != 0 {
t.Errorf("addr3: expected balance 3000, got %v", acc3.Balance)
}
if acc3.Nonce != 1 {
t.Errorf("addr3: expected nonce 1, got %d", acc3.Nonce)
}
}