diff --git a/core/blockchain.go b/core/blockchain.go index 66944db4e0..24a4f94397 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -40,6 +40,7 @@ import ( "github.com/ethereum/go-ethereum/core/history" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/state/partial" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/tracing" @@ -231,6 +232,18 @@ type BlockChainConfig struct { EnableWitnessStats bool // Whether trie access statistics collection is enabled BALExecutionMode bal.BALExecutionMode + + // PartialStateEnabled enables partial statefulness mode where only configured + // contracts have their storage synced and tracked. + PartialStateEnabled bool + + // PartialStateContracts is the list of contracts to track storage for + // when partial state mode is enabled. + PartialStateContracts []common.Address + + // PartialStateBALRetention is the number of blocks to retain BAL history for. + // Default is 256 if not specified. + PartialStateBALRetention uint64 } // DefaultConfig returns the default config. @@ -335,6 +348,7 @@ type BlockChain struct { flushInterval atomic.Int64 // Time interval (processing time) after which to flush a state triedb *triedb.Database // The database handler for maintaining trie nodes. codedb *state.CodeDB // The database handler for maintaining contract codes. + partialState *partial.PartialState // Partial state manager (nil if full node) txIndexer *txIndexer // Transaction indexer, might be nil if not enabled hc *HeaderChain @@ -434,6 +448,19 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, return nil, err } bc.flushInterval.Store(int64(cfg.TrieTimeLimit)) + // Initialize partial state manager if enabled + if cfg.PartialStateEnabled { + balRetention := cfg.PartialStateBALRetention + if balRetention == 0 { + balRetention = 256 // Default retention + } + filter := partial.NewConfiguredFilter(cfg.PartialStateContracts) + bc.partialState = partial.NewPartialState(db, bc.triedb, filter, balRetention) + log.Info("Partial state mode enabled", + "contracts", len(cfg.PartialStateContracts), + "balRetention", balRetention) + } + bc.validator = NewBlockValidator(chainConfig, bc) bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc) bc.processor = NewStateProcessor(bc.hc) diff --git a/core/blockchain_partial.go b/core/blockchain_partial.go new file mode 100644 index 0000000000..b69c3615f2 --- /dev/null +++ b/core/blockchain_partial.go @@ -0,0 +1,182 @@ +// 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 . + +package core + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state/partial" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/log" +) + +// ProcessBlockWithBAL processes a block using BAL instead of execution. +// This is the entry point for partial state block processing. +// +// # Trust Model - Why We Don't Re-Verify Consensus Attestations +// +// Post-Merge (PoS) Architecture Trust Boundary: +// - Consensus Layer (CL): Responsible for block proposal, attestations (2/3+ sync committee +// threshold), finality proofs, proposer signatures, and all consensus rules +// - Execution Layer (EL): Responsible for transaction execution, state computation, receipts +// +// Blocks received via Engine API (engine_newPayloadV5) have ALREADY been attested by the CL +// before being sent to the EL. The EL trusts the CL for consensus validation - this is the +// fundamental trust model of the Merge architecture (see eth/catalyst/api.go). +// +// For partial state nodes: +// - Normal operation: Blocks arrive via Engine API, already consensus-validated by CL +// - We validate: BAL hash matches header commitment, computed state root matches header +// - We trust: CL has verified proposer signatures, attestations, and finality +// +// This is identical to how full nodes operate - they also don't re-verify CL attestations. +// The only difference is we apply BAL diffs instead of re-executing transactions. +// +// Future consideration: If supporting light client sync where blocks come from untrusted +// P2P sources, use beacon light client verification via CommitteeChain.VerifySignedHeader() +// or HeadTracker.ValidateOptimistic() (see beacon/light/). +func (bc *BlockChain) ProcessBlockWithBAL( + block *types.Block, + accessList *bal.BlockAccessList, +) error { + // Sanity check + if bc.partialState == nil { + return errors.New("partial state not enabled") + } + + // Note: No consensus attestation verification here - blocks via Engine API are + // pre-attested by the Consensus Layer. See function documentation above. + + // 1. Validate BAL structure + if err := accessList.Validate(); err != nil { + return fmt.Errorf("invalid BAL structure: %w", err) + } + + // 2. Verify BAL hash matches header commitment + // TODO(EIP-7928): Uncomment when BlockAccessListHash is added to Header + // balHash := accessList.Hash() + // if balHash != block.Header().BlockAccessListHash { + // return fmt.Errorf("BAL hash mismatch: got %x, want %x", + // balHash, block.Header().BlockAccessListHash) + // } + + // 3. Get parent state root + parent := bc.GetBlock(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return errors.New("parent block not found") + } + parentRoot := parent.Root() + + // 4. Apply BAL diffs and compute new state root + newRoot, err := bc.partialState.ApplyBALAndComputeRoot(parentRoot, accessList) + if err != nil { + return fmt.Errorf("failed to apply BAL: %w", err) + } + + // 5. Verify computed root matches header + if newRoot != block.Root() { + return fmt.Errorf("state root mismatch: computed %x, header %x", + newRoot, block.Root()) + } + + // 6. Block is stored via normal chain insertion + // BAL storage for reorgs is handled separately via BALHistory + + log.Debug("Processed block with BAL", + "number", block.NumberU64(), + "hash", block.Hash().Hex(), + "root", newRoot.Hex(), + "accounts", len(accessList.Accesses)) + + return nil +} + +// SupportsPartialState returns true if partial state processing is enabled. +func (bc *BlockChain) SupportsPartialState() bool { + return bc.partialState != nil +} + +// PartialState returns the partial state manager, or nil if not enabled. +func (bc *BlockChain) PartialState() *partial.PartialState { + return bc.partialState +} + +// HandlePartialReorg handles chain reorganization for partial state nodes. +// It reverts state to the common ancestor and then applies BALs from the new chain. +// +// Parameters: +// - commonAncestor: The most recent block that both chains share +// - newBlocks: Ordered list of blocks from the new chain (oldest to newest) +// - getBAL: Function to retrieve BAL for a given block (from BALHistory or Engine API) +func (bc *BlockChain) HandlePartialReorg( + commonAncestor *types.Block, + newBlocks []*types.Block, + getBAL func(blockHash common.Hash, blockNum uint64) (*bal.BlockAccessList, error), +) error { + if bc.partialState == nil { + return errors.New("partial state not enabled") + } + + currentHead := bc.CurrentBlock() + reorgDepth := currentHead.Number.Uint64() - commonAncestor.Number().Uint64() + + // Step 1: Revert state to common ancestor + // Simply set state root to ancestor's root (we have all account trie data) + bc.partialState.SetRoot(commonAncestor.Root()) + + log.Debug("Reverted partial state to ancestor", + "ancestor", commonAncestor.Number(), + "ancestorRoot", commonAncestor.Root().Hex(), + "reorgDepth", reorgDepth) + + // Step 2: Apply new chain's blocks using their BALs + for _, block := range newBlocks { + // Get BAL for this block + accessList, err := getBAL(block.Hash(), block.NumberU64()) + if err != nil { + return fmt.Errorf("failed to get BAL for block %d: %w", block.NumberU64(), err) + } + if accessList == nil { + return fmt.Errorf("block %d missing BAL for reorg", block.NumberU64()) + } + + // Apply BAL to move state forward on new chain + if err := bc.ProcessBlockWithBAL(block, accessList); err != nil { + return fmt.Errorf("failed to apply block %d during reorg: %w", + block.NumberU64(), err) + } + } + + if len(newBlocks) > 0 { + log.Info("Completed partial state reorg", + "ancestor", commonAncestor.Number(), + "newHead", newBlocks[len(newBlocks)-1].NumberU64(), + "reorgDepth", reorgDepth) + } else { + log.Info("Completed partial state reorg (reset to ancestor)", + "ancestor", commonAncestor.Number(), + "reorgDepth", reorgDepth) + } + + return nil +} + +// Note: Deep reorgs beyond block pruning depth require resync from peers. +// This is handled by the downloader, not here. diff --git a/core/blockchain_partial_test.go b/core/blockchain_partial_test.go new file mode 100644 index 0000000000..1bca7babf5 --- /dev/null +++ b/core/blockchain_partial_test.go @@ -0,0 +1,327 @@ +// 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 . + +package core + +import ( + "bytes" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "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/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" +) + +// ============================================================================ +// Task 5: Blockchain Integration Tests for ProcessBlockWithBAL +// ============================================================================ + +// newPartialBlockchain creates a blockchain with partial state enabled. +func newPartialBlockchain(t *testing.T, scheme string, trackedContracts []common.Address) (*BlockChain, *Genesis) { + t.Helper() + + genesis := &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + Alloc: GenesisAlloc{ + common.HexToAddress("0x1234567890123456789012345678901234567890"): { + Balance: big.NewInt(1000000000), + }, + }, + } + + cfg := DefaultConfig().WithStateScheme(scheme) + cfg.PartialStateEnabled = true + cfg.PartialStateContracts = trackedContracts + cfg.PartialStateBALRetention = 256 + + bc, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, ethash.NewFaker(), cfg) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + + return bc, genesis +} + +// TestProcessBlockWithBAL_NotEnabled tests that ProcessBlockWithBAL returns error +// when partial state is not enabled. +func TestProcessBlockWithBAL_NotEnabled(t *testing.T) { + // Create blockchain WITHOUT partial state + genesis := &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + } + cfg := DefaultConfig().WithStateScheme(rawdb.HashScheme) + bc, _ := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, ethash.NewFaker(), cfg) + defer bc.Stop() + + if bc.SupportsPartialState() { + t.Fatal("expected partial state to be disabled") + } + + // Create a dummy block and BAL + block := types.NewBlock(&types.Header{Number: big.NewInt(1)}, nil, nil, nil) + accessList := &bal.BlockAccessList{} + + err := bc.ProcessBlockWithBAL(block, accessList) + if err == nil { + t.Fatal("expected error when partial state not enabled") + } + if err.Error() != "partial state not enabled" { + t.Errorf("unexpected error: %v", err) + } +} + +// TestProcessBlockWithBAL_SupportsPartialState tests the SupportsPartialState helper. +func TestProcessBlockWithBAL_SupportsPartialState(t *testing.T) { + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + bc, _ := newPartialBlockchain(t, rawdb.HashScheme, []common.Address{addr}) + defer bc.Stop() + + if !bc.SupportsPartialState() { + t.Fatal("expected partial state to be enabled") + } + + if bc.PartialState() == nil { + t.Fatal("expected PartialState() to return non-nil") + } +} + +// TestProcessBlockWithBAL_ParentNotFound tests error when parent block is missing. +func TestProcessBlockWithBAL_ParentNotFound(t *testing.T) { + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + bc, _ := newPartialBlockchain(t, rawdb.HashScheme, []common.Address{addr}) + defer bc.Stop() + + // Create a block with non-existent parent + nonExistentParent := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + header := &types.Header{ + Number: big.NewInt(100), + ParentHash: nonExistentParent, + } + block := types.NewBlock(header, nil, nil, nil) + accessList := &bal.BlockAccessList{} + + err := bc.ProcessBlockWithBAL(block, accessList) + if err == nil { + t.Fatal("expected error when parent not found") + } + if err.Error() != "parent block not found" { + t.Errorf("unexpected error: %v", err) + } +} + +// TestProcessBlockWithBAL_InvalidBAL tests error when BAL validation fails. +func TestProcessBlockWithBAL_InvalidBAL(t *testing.T) { + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + bc, _ := newPartialBlockchain(t, rawdb.HashScheme, []common.Address{addr}) + defer bc.Stop() + + // Get genesis block as parent + genesis := bc.GetBlockByNumber(0) + + // Create a block pointing to genesis + header := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesis.Hash(), + Root: genesis.Root(), // Use same root for now + } + block := types.NewBlock(header, nil, nil, nil) + + // Create invalid BAL (nil Accesses slice would be valid, but we need to test validation) + // For now, test with a valid but empty BAL to ensure the flow works + accessList := &bal.BlockAccessList{ + Accesses: []bal.AccountAccess{}, + } + + // This should fail because computed root won't match header root after applying empty BAL + // The actual root computation depends on the parent state + err := bc.ProcessBlockWithBAL(block, accessList) + // We expect either success (if root matches) or state root mismatch error + // Since we used genesis.Root() which is the actual state, empty BAL should preserve it + if err != nil { + t.Logf("ProcessBlockWithBAL error (expected for state root mismatch): %v", err) + } +} + +// TestProcessBlockWithBAL_StateRootMismatch tests error when computed root doesn't match header. +func TestProcessBlockWithBAL_StateRootMismatch(t *testing.T) { + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + bc, _ := newPartialBlockchain(t, rawdb.HashScheme, []common.Address{addr}) + defer bc.Stop() + + // Get genesis block as parent + genesis := bc.GetBlockByNumber(0) + + // Create a block with wrong state root + wrongRoot := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + header := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesis.Hash(), + Root: wrongRoot, // This won't match the computed root + } + block := types.NewBlock(header, nil, nil, nil) + + // Create BAL that changes state + cbal := bal.NewConstructionBlockAccessList() + cbal.BalanceChange(0, addr, uint256.NewInt(5000)) + accessList := constructionToBlockAccessListCore(t, &cbal) + + err := bc.ProcessBlockWithBAL(block, accessList) + if err == nil { + t.Fatal("expected state root mismatch error") + } + // Error should mention state root mismatch + if err.Error()[:16] != "state root mismatch" { + t.Logf("Got error (checking if it's root mismatch): %v", err) + } +} + +// TestProcessBlockWithBAL_Schemes tests both HashScheme and PathScheme. +func TestProcessBlockWithBAL_Schemes(t *testing.T) { + t.Run("HashScheme", func(t *testing.T) { + testProcessBlockWithBALScheme(t, rawdb.HashScheme) + }) + t.Run("PathScheme", func(t *testing.T) { + testProcessBlockWithBALScheme(t, rawdb.PathScheme) + }) +} + +func testProcessBlockWithBALScheme(t *testing.T, scheme string) { + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + bc, _ := newPartialBlockchain(t, scheme, []common.Address{addr}) + defer bc.Stop() + + // Verify blockchain was created with the correct scheme + if !bc.SupportsPartialState() { + t.Fatalf("partial state should be enabled for scheme %s", scheme) + } + + // Test basic functionality + genesis := bc.GetBlockByNumber(0) + if genesis == nil { + t.Fatal("genesis block not found") + } +} + +// ============================================================================ +// Task 6: Integration Tests for HandlePartialReorg +// ============================================================================ + +// TestHandlePartialReorg_NotEnabled tests that HandlePartialReorg returns error +// when partial state is not enabled. +func TestHandlePartialReorg_NotEnabled(t *testing.T) { + genesis := &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + } + cfg := DefaultConfig().WithStateScheme(rawdb.HashScheme) + bc, _ := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, ethash.NewFaker(), cfg) + defer bc.Stop() + + genesisBlock := bc.GetBlockByNumber(0) + newBlocks := []*types.Block{} + getBAL := func(hash common.Hash, num uint64) (*bal.BlockAccessList, error) { + return &bal.BlockAccessList{}, nil + } + + err := bc.HandlePartialReorg(genesisBlock, newBlocks, getBAL) + if err == nil { + t.Fatal("expected error when partial state not enabled") + } + if err.Error() != "partial state not enabled" { + t.Errorf("unexpected error: %v", err) + } +} + +// TestHandlePartialReorg_EmptyNewBlocks tests reorg with empty new blocks list. +func TestHandlePartialReorg_EmptyNewBlocks(t *testing.T) { + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + bc, _ := newPartialBlockchain(t, rawdb.HashScheme, []common.Address{addr}) + defer bc.Stop() + + genesisBlock := bc.GetBlockByNumber(0) + newBlocks := []*types.Block{} + getBAL := func(hash common.Hash, num uint64) (*bal.BlockAccessList, error) { + return &bal.BlockAccessList{}, nil + } + + // Empty reorg should succeed (just sets root to ancestor) + err := bc.HandlePartialReorg(genesisBlock, newBlocks, getBAL) + if err != nil { + t.Fatalf("empty reorg should succeed: %v", err) + } + + // Verify state root is set to genesis root + if bc.PartialState().Root() != genesisBlock.Root() { + t.Errorf("expected root to be genesis root after empty reorg") + } +} + +// TestHandlePartialReorg_MissingBAL tests error when BAL is missing for a block. +func TestHandlePartialReorg_MissingBAL(t *testing.T) { + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + bc, _ := newPartialBlockchain(t, rawdb.HashScheme, []common.Address{addr}) + defer bc.Stop() + + genesisBlock := bc.GetBlockByNumber(0) + + // Create a dummy block + header := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesisBlock.Hash(), + Root: genesisBlock.Root(), + } + block := types.NewBlock(header, nil, nil, nil) + newBlocks := []*types.Block{block} + + // getBAL returns nil for the block + getBAL := func(hash common.Hash, num uint64) (*bal.BlockAccessList, error) { + return nil, nil // Missing BAL + } + + err := bc.HandlePartialReorg(genesisBlock, newBlocks, getBAL) + if err == nil { + t.Fatal("expected error when BAL is missing") + } + // Error should mention missing BAL + if err.Error() != "block 1 missing BAL for reorg" { + t.Errorf("unexpected error: %v", err) + } +} + +// constructionToBlockAccessListCore is a helper to convert ConstructionBlockAccessList +// to BlockAccessList in the core package tests. +func constructionToBlockAccessListCore(t *testing.T, cbal *bal.ConstructionBlockAccessList) *bal.BlockAccessList { + t.Helper() + + var buf bytes.Buffer + if err := cbal.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 +} diff --git a/core/state/partial/state.go b/core/state/partial/state.go index f69ba47601..2c6c9b6fce 100644 --- a/core/state/partial/state.go +++ b/core/state/partial/state.go @@ -17,10 +17,20 @@ package partial import ( + "bytes" + "fmt" + "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/ethdb" + "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" ) // PartialState manages state for partial stateful nodes. @@ -60,21 +70,293 @@ func (s *PartialState) Root() common.Hash { return s.stateRoot } -// ApplyBALAndComputeRoot applies BAL diffs and returns the new state root. -// This is the core function for partial state block processing. -// -// TODO: Implement in Phase 3/4 - this will: -// 1. Open trie at current root -// 2. Apply balance/nonce changes from BAL -// 3. Apply storage changes for tracked contracts -// 4. Commit trie changes using existing pathdb compression -// 5. Return new state root -func (s *PartialState) ApplyBALAndComputeRoot(currentRoot common.Hash, accessList *bal.BlockAccessList) (common.Hash, error) { - // Placeholder - will be implemented in Phase 4 - panic("ApplyBALAndComputeRoot not yet implemented") -} - // History returns the BAL history manager. func (s *PartialState) History() *BALHistory { return s.history } + +// accountState tracks an account being processed with origin info for PathDB StateSet. +type accountState struct { + account *types.StateAccount + origin *types.StateAccount // Original state (for PathDB StateSet) + addr common.Address + existed bool // true if account existed before this block + modified bool // true if any field was changed + storageRoot common.Hash // updated after storage trie commit +} + +// ApplyBALAndComputeRoot applies BAL diffs and returns the new state root. +// This is the core method for partial state block processing. +// +// Commit ordering (critical for correct state root): +// Phase 1: For each account, apply storage changes and commit storage trie +// Phase 2: Update account Root fields with committed storage roots +// Phase 3: Commit account trie to get final state root +func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, accessList *bal.BlockAccessList) (common.Hash, error) { + // Open state trie at parent root + tr, err := trie.NewStateTrie(trie.StateTrieID(parentRoot), s.trieDB) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to open state trie: %w", err) + } + + // Collect all account states with origin tracking + accounts := make([]*accountState, 0, len(accessList.Accesses)) + + // Collect all trie nodes for batched update + allNodes := trienode.NewMergedNodeSet() + + // Phase 1: Process each account's changes from BAL + for _, access := range accessList.Accesses { + addr := common.BytesToAddress(access.Address[:]) + + // Get current account state with origin tracking + data, err := tr.GetAccount(addr) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to get account %s: %w", addr.Hex(), err) + } + + existed := data != nil + var account *types.StateAccount + if existed { + account = data + } else { + // New account - create with defaults + account = &types.StateAccount{ + Balance: new(uint256.Int), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash.Bytes(), + } + } + + // Copy original state for PathDB StateSet + var origin *types.StateAccount + if existed { + origin = &types.StateAccount{ + Nonce: account.Nonce, + Balance: new(uint256.Int).Set(account.Balance), + Root: account.Root, + CodeHash: common.CopyBytes(account.CodeHash), + } + } + + state := &accountState{ + account: account, + origin: origin, + addr: addr, + existed: existed, + modified: false, + storageRoot: account.Root, + } + + // Apply balance changes (use final value from last tx) + if len(access.BalanceChanges) > 0 { + lastChange := access.BalanceChanges[len(access.BalanceChanges)-1] + account.Balance = new(uint256.Int).SetBytes(lastChange.Balance[:]) + state.modified = true + } + + // Apply nonce changes + if len(access.NonceChanges) > 0 { + lastNonce := access.NonceChanges[len(access.NonceChanges)-1] + account.Nonce = lastNonce.Nonce + state.modified = true + } + + // Apply code changes + if len(access.Code) > 0 { + lastCode := access.Code[len(access.Code)-1] + codeHash := crypto.Keccak256Hash(lastCode.Code) + account.CodeHash = codeHash.Bytes() + state.modified = true + + // Only store code bytes for tracked contracts + if s.filter.IsTracked(addr) { + rawdb.WriteCode(s.db, codeHash, lastCode.Code) + } + } + + // Apply storage changes (only for tracked contracts) + // CRITICAL: Commit storage trie HERE, before account trie + if len(access.StorageWrites) > 0 && s.filter.IsTracked(addr) { + newStorageRoot, storageNodes, err := s.applyStorageChanges( + addr, parentRoot, account.Root, &access) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to apply storage for %s: %w", + addr.Hex(), err) + } + state.storageRoot = newStorageRoot + state.modified = true + + // Merge storage nodes + if storageNodes != nil { + if err := allNodes.Merge(storageNodes); err != nil { + return common.Hash{}, err + } + } + } + + accounts = append(accounts, state) + } + + // Phase 2: Update account Root fields and write to account trie + for _, state := range accounts { + // Update storage root (may have changed in Phase 1) + state.account.Root = state.storageRoot + + // Only consider deletion if modified AND now empty (EIP-161) + if state.modified && s.isEmptyAccount(state.account) { + // Only delete if it existed before (don't delete never-existed accounts) + if state.existed { + if err := tr.DeleteAccount(state.addr); err != nil { + return common.Hash{}, fmt.Errorf("failed to delete account %s: %w", + state.addr.Hex(), err) + } + } + // Skip update for accounts that didn't exist and are still empty + continue + } + + if err := tr.UpdateAccount(state.addr, state.account, 0); err != nil { + return common.Hash{}, fmt.Errorf("failed to update account %s: %w", + state.addr.Hex(), err) + } + } + + // Phase 3: Commit account trie + root, accountNodes := tr.Commit(false) + + // Merge account nodes + if accountNodes != nil { + if err := allNodes.Merge(accountNodes); err != nil { + return common.Hash{}, err + } + } + + // Build StateSet for PathDB compatibility + stateSet := s.buildStateSet(accounts, accessList) + + // Write all trie nodes and state to database + if err := s.trieDB.Update(root, parentRoot, 0, allNodes, stateSet); err != nil { + return common.Hash{}, fmt.Errorf("failed to update trie db: %w", err) + } + + s.stateRoot = root + return root, nil +} + +// buildStateSet constructs StateSet for trieDB.Update() (required for PathDB). +// The StateSet tracks account and storage changes along with their original values, +// which PathDB uses for efficient state diff tracking. +func (s *PartialState) buildStateSet(accounts []*accountState, accessList *bal.BlockAccessList) *triedb.StateSet { + stateSet := triedb.NewStateSet() + + for _, state := range accounts { + addrHash := crypto.Keccak256Hash(state.addr.Bytes()) + + // Add account data (slim RLP encoding) + if s.isEmptyAccount(state.account) && state.existed { + stateSet.Accounts[addrHash] = nil // nil = deletion + } else if state.modified { + stateSet.Accounts[addrHash] = types.SlimAccountRLP(*state.account) + } + + // Add account origin (original state before this block) + if state.origin != nil { + stateSet.AccountsOrigin[state.addr] = types.SlimAccountRLP(*state.origin) + } + + // Add storage changes for tracked contracts + if s.filter.IsTracked(state.addr) { + s.addStorageToStateSet(stateSet, state.addr, addrHash, accessList) + } + } + return stateSet +} + +// addStorageToStateSet finds storage writes for the given address and adds them to the StateSet. +func (s *PartialState) addStorageToStateSet(stateSet *triedb.StateSet, addr common.Address, addrHash common.Hash, accessList *bal.BlockAccessList) { + // Find this account's storage writes in BAL + for _, access := range accessList.Accesses { + accessAddr := common.BytesToAddress(access.Address[:]) + if accessAddr != addr { + continue + } + if len(access.StorageWrites) == 0 { + break + } + + storageMap := make(map[common.Hash][]byte) + for _, slotWrite := range access.StorageWrites { + slotHash := crypto.Keccak256Hash(slotWrite.Slot[:]) + if len(slotWrite.Accesses) > 0 { + lastWrite := slotWrite.Accesses[len(slotWrite.Accesses)-1] + value := common.BytesToHash(lastWrite.ValueAfter[:]) + if value == (common.Hash{}) { + storageMap[slotHash] = nil // nil = deletion + } else { + // Prefix-zero-trimmed RLP encoding + blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(value[:])) + storageMap[slotHash] = blob + } + } + } + stateSet.Storages[addrHash] = storageMap + break + } +} + +// isEmptyAccount checks if account is empty per EIP-161. +// An account is empty if it has zero nonce, zero balance, empty storage root, +// and empty code hash. +func (s *PartialState) isEmptyAccount(account *types.StateAccount) bool { + return account.Balance.IsZero() && + account.Nonce == 0 && + account.Root == types.EmptyRootHash && + bytes.Equal(account.CodeHash, types.EmptyCodeHash.Bytes()) +} + +// applyStorageChanges applies storage writes and returns new root + nodes. +// Note: Does NOT write to trieDB - caller batches all writes. +func (s *PartialState) applyStorageChanges( + addr common.Address, + stateRoot common.Hash, + currentStorageRoot common.Hash, + access *bal.AccountAccess, +) (common.Hash, *trienode.NodeSet, error) { + // Open storage trie (use parent state root for ID, not current) + addrHash := crypto.Keccak256Hash(addr.Bytes()) + storageID := trie.StorageTrieID(stateRoot, addrHash, currentStorageRoot) + storageTrie, err := trie.NewStateTrie(storageID, s.trieDB) + if err != nil { + return common.Hash{}, nil, err + } + + // Apply each storage write (use final value) + for _, slotWrite := range access.StorageWrites { + slot := common.BytesToHash(slotWrite.Slot[:]) + + // Get final value (last write wins) + if len(slotWrite.Accesses) == 0 { + continue + } + lastWrite := slotWrite.Accesses[len(slotWrite.Accesses)-1] + value := common.BytesToHash(lastWrite.ValueAfter[:]) + + if value == (common.Hash{}) { + // Delete slot + if err := storageTrie.DeleteStorage(addr, slot.Bytes()); err != nil { + return common.Hash{}, nil, err + } + } else { + // Update slot + if err := storageTrie.UpdateStorage(addr, slot.Bytes(), value.Bytes()); err != nil { + return common.Hash{}, nil, err + } + } + } + + // Commit storage trie (collect nodes, don't write to DB yet) + storageRoot, nodes := storageTrie.Commit(false) + + return storageRoot, nodes, nil +} diff --git a/core/state/partial/state_test.go b/core/state/partial/state_test.go new file mode 100644 index 0000000000..e3b5b08fcd --- /dev/null +++ b/core/state/partial/state_test.go @@ -0,0 +1,1073 @@ +// 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 . + +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" +) + +// constructionToBlockAccessList converts ConstructionBlockAccessList to BlockAccessList +// via RLP encoding/decoding. +func constructionToBlockAccessList(t *testing.T, cbal *bal.ConstructionBlockAccessList) *bal.BlockAccessList { + t.Helper() + + var buf bytes.Buffer + if err := cbal.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 + accessList := &bal.BlockAccessList{ + Accesses: []bal.AccountAccess{}, + } + + newRoot, err := ps.ApplyBALAndComputeRoot(emptyRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.BalanceChange(0, addr, newBalance) + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.NonceChange(addr, 0, 6) + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.StorageWrite(0, addr, slot, value) + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.StorageWrite(0, trackedAddr, slot, value) + cbal.StorageWrite(0, untrackedAddr, slot, value) + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.BalanceChange(0, addr, balance) + cbal.NonceChange(addr, 0, 1) + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(emptyRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.CodeChange(addr, 0, code) + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + 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 := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.StorageWrite(0, addr, slot, common.Hash{}) // Zero value = delete + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.StorageWrite(0, addr, slot, value1) + cbal.StorageWrite(1, addr, slot, value2) + cbal.StorageWrite(2, addr, slot, value3) // Final value + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.BalanceChange(0, addr, uint256.NewInt(0)) // Zero balance + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.BalanceChange(0, addr, uint256.NewInt(0)) // Zero balance on never-existed account + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(emptyRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.CodeChange(trackedAddr, 0, code) + cbal.CodeChange(untrackedAddr, 0, code) + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.BalanceChange(0, addr, newBalance) + cbal.NonceChange(addr, 0, newNonce) + cbal.CodeChange(addr, 0, code) + cbal.StorageWrite(0, addr, slot, value) + + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.BalanceChange(0, addr, uint256.NewInt(1000)) + accessList := constructionToBlockAccessList(t, &cbal) + + _, err := ps.ApplyBALAndComputeRoot(invalidRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.BalanceChange(0, addr, uint256.NewInt(2000)) + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.StorageWrite(0, addr, slot, value) + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + cbal.BalanceChange(0, addr, uint256.NewInt(6000)) + cbal.NonceChange(addr, 0, 11) + accessList := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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 := bal.NewConstructionBlockAccessList() + + // 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 := constructionToBlockAccessList(t, &cbal) + + newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, 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) + } +}