go-ethereum/docs/partial-state/PHASE2_PLAN.md
CPerezz c3c4dfd838
core, eth: fix post-sync block processing and BAL type compatibility
Fix the post-sync deadlock where blocks validated via BAL in newPayload
were never written to the database, causing ForkchoiceUpdated to fail
finding them and triggering infinite sync cycles.

Changes:
- Export WriteBlockWithoutState and call it after ProcessBlockWithBAL
  in newPayload, so FCU can find blocks via GetBlockByHash
- Guard SetCanonical against recoverAncestors for partial state nodes
  (they can't re-execute blocks, only apply BAL diffs)
- Auto-disable log indexing when partial state is enabled (no receipts)
- Fix BAL type field accesses to match upstream bal-devnet-2 types
  (StorageChanges, CodeChanges, BalanceChanges, Validate signature)
- Update newPayload signature (BAL now comes from ExecutableData params)
- Add partial sync scripts and documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 12:04:09 +02:00

31 KiB

Phase 2: Snap Sync Modifications for Partial Statefulness

Pre-Execution Tasks

Before implementing Phase 2, complete these preparatory tasks:

Task 0.1: Commit Phase 1 Changes

Commit all existing Phase 1 work (configuration, filters, BAL infrastructure):

git add cmd/geth/chaincmd.go cmd/geth/main.go cmd/utils/flags.go \
        core/rawdb/schema.go core/rawdb/accessors_bal.go \
        eth/ethconfig/config.go eth/ethconfig/gen_config.go \
        core/state/partial/
git commit -m "eth: add partial statefulness foundation (Phase 1)

Implements EIP-7928 BAL-based partial statefulness infrastructure:

- Add PartialStateConfig to eth/ethconfig with CLI flags
- Add ContractFilter interface in core/state/partial/
- Add BAL history database accessors in core/rawdb/
- Add PartialState and BALHistory managers

This enables nodes to track only configured contracts' storage
while maintaining full account trie integrity."

Task 0.2: Save Plan Documentation

Create a reference document in the repo (not to be committed):

mkdir -p docs/partial-state
# Copy plan content to docs/partial-state/PHASE2_PLAN.md

Executive Summary

This plan modifies go-ethereum's snap sync to support partial statefulness: downloading ALL accounts but only storage/bytecode for configured contracts. This enables nodes to operate with ~30-40GB instead of ~1TB+ while maintaining full account trie integrity.


Snap Sync Protocol Overview

Based on comprehensive analysis of 10 different aspects of the snap sync implementation:

Current Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Syncer.Sync()                            │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ PHASE 1: Snap Download                                    │   │
│  │  1. assignAccountTasks() → Download account ranges        │   │
│  │  2. processAccountResponse() → Analyze each account:      │   │
│  │     • CodeHash != Empty → Add to codeTasks                │   │
│  │     • Root != Empty → Add to stateTasks                   │   │
│  │  3. assignBytecodeTasks() → Download bytecodes            │   │
│  │  4. assignStorageTasks() → Download storage slots         │   │
│  └──────────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ PHASE 2: Healing                                          │   │
│  │  • Fill gaps in trie structure                            │   │
│  │  • Download missing intermediate nodes                    │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

Key Decision Points for Filtering

Location Function Decision
sync.go:1908-1928 processAccountResponse() Checks CodeHash != EmptyCodeHash → adds to codeTasks
sync.go:1930-1969 processAccountResponse() Checks Root != EmptyRootHash → adds to stateTasks
sync.go:1117-1215 assignBytecodeTasks() Iterates codeTasks map
sync.go:1220-1373 assignStorageTasks() Iterates stateTasks map

Key Data Structures

type accountTask struct {
    needCode  []bool                         // Which accounts need bytecode
    needState []bool                         // Which accounts need storage
    needHeal  []bool                         // Which accounts need healing
    codeTasks map[common.Hash]struct{}       // Pending bytecode hashes
    stateTasks map[common.Hash]common.Hash   // Account hash → storage root
    stateCompleted map[common.Hash]struct{}  // Completed storage syncs
}

Design: Minimal-Invasion Approach

Instead of creating a separate PartialSyncer, we'll add filter checks at decision points within the existing Syncer. This is less invasive and easier to maintain.

Changes Overview

┌─────────────────────────────────────────────────────────────┐
│ eth/protocols/snap/sync.go                                   │
│  • Add filter field to Syncer struct                         │
│  • Modify processAccountResponse() to check filter           │
│  • Add skip markers for intentionally skipped storage        │
│  • Modify healing to skip intentionally-skipped accounts     │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ eth/protocols/snap/sync_partial.go (NEW)                     │
│  • PartialSyncConfig struct                                  │
│  • Skip marker database functions                            │
│  • Helper functions for filter integration                   │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ eth/downloader/downloader.go                                 │
│  • Pass PartialStateConfig to snap.Syncer                    │
└─────────────────────────────────────────────────────────────┘

Detailed Implementation Plan

Task 2.1: Add Filter to Syncer Struct

File: eth/protocols/snap/sync.go

Add filter field to Syncer:

type Syncer struct {
    // ... existing fields ...

    // Partial state filter (nil = sync everything)
    filter partial.ContractFilter
}

Modify NewSyncer():

func NewSyncer(db ethdb.KeyValueStore, scheme string, filter partial.ContractFilter) *Syncer {
    return &Syncer{
        db:     db,
        scheme: scheme,
        filter: filter,  // May be nil for full sync
        // ... rest unchanged
    }
}

Estimated changes: ~10 lines


Task 2.2: Create sync_partial.go Helper File

File: eth/protocols/snap/sync_partial.go (NEW)

package snap

import (
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/rawdb"
    "github.com/ethereum/go-ethereum/core/state/partial"
    "github.com/ethereum/go-ethereum/ethdb"
)

// Database key prefix for tracking intentionally skipped storage
var skippedStoragePrefix = []byte("SnapSkipped")

// skippedStorageKey returns the database key for a skipped storage marker
func skippedStorageKey(accountHash common.Hash) []byte {
    return append(skippedStoragePrefix, accountHash.Bytes()...)
}

// markStorageSkipped records that storage was intentionally skipped for an account
func markStorageSkipped(db ethdb.KeyValueWriter, accountHash common.Hash, storageRoot common.Hash) {
    db.Put(skippedStorageKey(accountHash), storageRoot.Bytes())
}

// isStorageSkipped checks if storage was intentionally skipped for an account
func isStorageSkipped(db ethdb.KeyValueReader, accountHash common.Hash) bool {
    has, _ := db.Has(skippedStorageKey(accountHash))
    return has
}

// deleteStorageSkipped removes the skip marker (used during cleanup)
func deleteStorageSkipped(db ethdb.KeyValueWriter, accountHash common.Hash) {
    db.Delete(skippedStorageKey(accountHash))
}

// shouldSyncStorage returns true if storage should be synced for this address
func (s *Syncer) shouldSyncStorage(addr common.Address) bool {
    if s.filter == nil {
        return true  // No filter = sync everything
    }
    return s.filter.ShouldSyncStorage(addr)
}

// shouldSyncCode returns true if bytecode should be synced for this address
func (s *Syncer) shouldSyncCode(addr common.Address) bool {
    if s.filter == nil {
        return true  // No filter = sync everything
    }
    return s.filter.ShouldSyncCode(addr)
}

Estimated changes: ~50 lines


Task 2.3: Modify processAccountResponse() for Filtering

File: eth/protocols/snap/sync.go

Current code (lines 1908-1969):

// Check if the account is a contract with an unknown code
if !bytes.Equal(account.CodeHash, types.EmptyCodeHash.Bytes()) {
    if !rawdb.HasCodeWithPrefix(s.db, common.BytesToHash(account.CodeHash)) {
        res.task.codeTasks[common.BytesToHash(account.CodeHash)] = struct{}{}
        res.task.needCode[i] = true
        res.task.pend++
    }
}
// Check if the account is a contract with an unknown storage trie
if account.Root != types.EmptyRootHash {
    // ... adds to stateTasks
}

Modified code:

// Derive address from account hash for filter check
// Note: We have the hash, need to track address mapping
addr := s.hashToAddress(res.hashes[i])  // New helper needed

// Check if the account is a contract with an unknown code
if !bytes.Equal(account.CodeHash, types.EmptyCodeHash.Bytes()) {
    if !rawdb.HasCodeWithPrefix(s.db, common.BytesToHash(account.CodeHash)) {
        // NEW: Check filter before adding to codeTasks
        if s.shouldSyncCode(addr) {
            res.task.codeTasks[common.BytesToHash(account.CodeHash)] = struct{}{}
            res.task.needCode[i] = true
            res.task.pend++
        }
        // If filtered out, bytecode just won't be fetched
    }
}

// Check if the account is a contract with an unknown storage trie
if account.Root != types.EmptyRootHash {
    // NEW: Check filter before adding to stateTasks
    if s.shouldSyncStorage(addr) {
        // ... existing logic to add to stateTasks
    } else {
        // Mark as intentionally skipped for healing phase
        markStorageSkipped(s.db, res.hashes[i], account.Root)
        res.task.stateCompleted[res.hashes[i]] = struct{}{}
        // Don't increment pend - we're not waiting for this storage
    }
}

Challenge: We have account hashes but need addresses for filter checks.

Solution: The filter operates on addresses, but snap sync uses hashes. Two options:

  1. Store hash→address mapping during sync (memory overhead)
  2. Modify filter to work with hashes (requires pre-computing hashes of configured addresses)

Recommended: Option 2 - Pre-compute hashes in filter:

type ConfiguredFilter struct {
    contracts     map[common.Address]struct{}
    contractHashes map[common.Hash]struct{}  // Pre-computed: keccak256(address)
}

func (f *ConfiguredFilter) ShouldSyncStorageByHash(hash common.Hash) bool {
    _, ok := f.contractHashes[hash]
    return ok
}

Estimated changes: ~40 lines in sync.go, ~20 lines in filter.go


Task 2.4: Modify Healing to Skip Storage for Non-Tracked Contracts

Important Clarification: We NEVER skip accounts - ALL accounts are always synced (this is the core value proposition). We only skip storage and bytecode for contracts not in the configured filter.

File: eth/protocols/snap/sync.go

In onHealState() callback (lines 3071-3092), add check for storage leaves only:

func (s *Syncer) onHealState(paths [][]byte, value []byte) error {
    if len(paths) == 1 {
        // Account trie leaf - ALWAYS process (never skip accounts)
        var account types.StateAccount
        if err := rlp.DecodeBytes(value, &account); err != nil {
            return nil
        }
        blob := types.SlimAccountRLP(account)
        rawdb.WriteAccountSnapshot(s.stateWriter, common.BytesToHash(paths[0]), blob)
        s.accountHealed += 1
        // ... rest unchanged
    }
    if len(paths) == 2 {
        // Storage trie leaf
        accountHash := common.BytesToHash(paths[0])

        // NEW: Skip STORAGE healing for non-tracked contracts
        // (accounts themselves are always synced/healed)
        if isStorageSkipped(s.db, accountHash) {
            return nil  // Don't heal storage we intentionally skipped
        }

        // ... existing storage handling
        rawdb.WriteStorageSnapshot(s.stateWriter, accountHash, ...)
    }
    return nil
}

Also modify healing task creation to avoid requesting storage trie nodes for non-tracked contracts.

Key principle: Account healing always proceeds. Only storage trie node requests are filtered.

Estimated changes: ~30 lines


Task 2.5: Update Downloader to Pass Filter

File: eth/downloader/downloader.go

Modify New() to accept and pass filter:

func New(stateDb ethdb.Database, mode ethconfig.SyncMode, ...,
         partialConfig *ethconfig.PartialStateConfig) *Downloader {

    var filter partial.ContractFilter
    if partialConfig != nil && partialConfig.Enabled {
        filter = partial.NewConfiguredFilter(partialConfig.Contracts)
    }

    dl := &Downloader{
        // ... existing fields
        SnapSyncer: snap.NewSyncer(stateDb, chain.TrieDB().Scheme(), filter),
    }
    // ...
}

File: eth/handler.go

Pass config through handler:

h.downloader = downloader.New(config.Database, config.Sync, h.eventMux,
                               h.chain, h.removePeer, h.enableSyncedFeatures,
                               &config.Eth.PartialState)

Estimated changes: ~20 lines


Task 2.6: Add Hash-Based Filter Methods

File: core/state/partial/filter.go

Extend ConfiguredFilter:

type ConfiguredFilter struct {
    contracts      map[common.Address]struct{}
    contractHashes map[common.Hash]struct{}  // NEW: Pre-computed hashes
}

func NewConfiguredFilter(addresses []common.Address) *ConfiguredFilter {
    m := make(map[common.Address]struct{}, len(addresses))
    h := make(map[common.Hash]struct{}, len(addresses))
    for _, addr := range addresses {
        m[addr] = struct{}{}
        h[crypto.Keccak256Hash(addr.Bytes())] = struct{}{}  // Pre-compute hash
    }
    return &ConfiguredFilter{contracts: m, contractHashes: h}
}

// NEW: Hash-based filter for snap sync (which works with hashes, not addresses)
func (f *ConfiguredFilter) ShouldSyncStorageByHash(hash common.Hash) bool {
    _, ok := f.contractHashes[hash]
    return ok
}

func (f *ConfiguredFilter) ShouldSyncCodeByHash(hash common.Hash) bool {
    _, ok := f.contractHashes[hash]
    return ok
}

Update ContractFilter interface:

type ContractFilter interface {
    ShouldSyncStorage(address common.Address) bool
    ShouldSyncCode(address common.Address) bool
    IsTracked(address common.Address) bool

    // Hash-based methods for snap sync
    ShouldSyncStorageByHash(hash common.Hash) bool
    ShouldSyncCodeByHash(hash common.Hash) bool
}

Estimated changes: ~30 lines


Task 2.7: Persist Skip Markers for Resumption

File: eth/protocols/snap/sync.go

In saveSyncStatus(), ensure skip markers are preserved (they're already in DB, just verify):

func (s *Syncer) saveSyncStatus() {
    // ... existing serialization

    // Skip markers are already in DB (written during processAccountResponse)
    // They persist across restarts automatically
}

In loadSyncStatus(), log skipped storage count for visibility:

func (s *Syncer) loadSyncStatus() {
    // ... existing deserialization

    if s.filter != nil {
        log.Info("Partial state sync active",
            "trackedContracts", len(s.filter.Contracts()))
    }
}

Estimated changes: ~10 lines


Task 2.8: Add Metrics for Partial Sync

File: eth/protocols/snap/sync.go

Add counters:

var (
    storageSkippedGauge = metrics.NewRegisteredGauge("snap/sync/storage/skipped", nil)
    bytecodeSkippedGauge = metrics.NewRegisteredGauge("snap/sync/bytecode/skipped", nil)
)

Increment in processAccountResponse:

if !s.shouldSyncStorage(addr) {
    storageSkippedGauge.Inc(1)
    // ...
}

Estimated changes: ~15 lines


Task 2.9: Unit Tests

File: eth/protocols/snap/sync_partial_test.go (NEW)

package snap

import (
    "testing"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/state/partial"
)

func TestPartialSyncFilterStorage(t *testing.T) {
    // Create filter with specific contracts
    tracked := []common.Address{
        common.HexToAddress("0x1234..."),
    }
    filter := partial.NewConfiguredFilter(tracked)

    // Verify tracked contracts pass filter
    if !filter.ShouldSyncStorage(tracked[0]) {
        t.Error("Tracked contract should pass filter")
    }

    // Verify untracked contracts are filtered
    untracked := common.HexToAddress("0xABCD...")
    if filter.ShouldSyncStorage(untracked) {
        t.Error("Untracked contract should be filtered")
    }

    // Verify hash-based filter works
    trackedHash := crypto.Keccak256Hash(tracked[0].Bytes())
    if !filter.ShouldSyncStorageByHash(trackedHash) {
        t.Error("Tracked contract hash should pass filter")
    }
}

func TestSkipMarkerPersistence(t *testing.T) {
    db := rawdb.NewMemoryDatabase()
    accountHash := common.HexToHash("0x1234...")
    storageRoot := common.HexToHash("0xABCD...")

    // Mark as skipped
    markStorageSkipped(db, accountHash, storageRoot)

    // Verify marker persists
    if !isStorageSkipped(db, accountHash) {
        t.Error("Skip marker should persist")
    }

    // Delete and verify
    deleteStorageSkipped(db, accountHash)
    if isStorageSkipped(db, accountHash) {
        t.Error("Skip marker should be deleted")
    }
}

Estimated changes: ~100 lines


Task 2.10: Integration Test

File: eth/protocols/snap/sync_partial_integration_test.go (NEW)

Create end-to-end test that:

  1. Sets up a mock state with multiple contracts
  2. Configures partial sync with subset of contracts
  3. Runs sync
  4. Verifies:
    • All accounts synced
    • Only configured contracts have storage
    • Skip markers present for non-configured contracts
    • Healing doesn't try to heal skipped storage

Estimated changes: ~200 lines


Local Testing Strategy

1. Unit Test Execution

cd eth/protocols/snap
go test -v -run TestPartialSync
go test -v -run TestSkipMarker

2. Build Verification

go build ./...
go build ./cmd/geth

3. Simulated Network Test

Create a test script that:

# Terminal 1: Start full node (serves as peer)
./geth --datadir /tmp/full-node --syncmode snap --port 30303

# Terminal 2: Start partial node
./geth --datadir /tmp/partial-node --syncmode snap --port 30304 \
    --partial-state \
    --partial-state.contracts 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \
    --bootnodes "enode://..."

4. Verification Checks

After sync completes:

# Check database size (should be significantly smaller)
du -sh /tmp/partial-node/geth/chaindata

# Query RPC to verify:
# - Account balance works for any address
curl -X POST -H "Content-Type: application/json" \
    --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x...", "latest"],"id":1}' \
    http://localhost:8545

# - Storage works for tracked contracts
curl -X POST -H "Content-Type: application/json" \
    --data '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0x0", "latest"],"id":1}' \
    http://localhost:8545

# - Storage fails for untracked contracts (once RPC phase implemented)

5. Devnet Testing

For full integration testing:

  1. Use a local devnet with known state
  2. Configure partial sync with specific test contracts
  3. Verify sync completion and state correctness
  4. Test reorg handling with BAL history

Files to Modify Summary

File Changes Lines
eth/protocols/snap/sync.go Add filter field, modify processAccountResponse, healing ~80
eth/protocols/snap/sync_partial.go NEW: Skip markers, helpers ~50
core/state/partial/filter.go Add hash-based filter methods ~30
eth/downloader/downloader.go Pass filter to Syncer ~15
eth/handler.go Pass config through ~5
eth/protocols/snap/sync_partial_test.go NEW: Unit tests ~100
eth/protocols/snap/sync_partial_integration_test.go NEW: Integration tests ~200

Total estimated changes: ~480 lines


Task Summary

Task ID Description Dependencies Effort
2.1 Add filter to Syncer struct None S
2.2 Create sync_partial.go helpers 2.1 S
2.3 Modify processAccountResponse 2.1, 2.2, 2.6 M
2.4 Modify healing to skip filtered 2.2 S
2.5 Update Downloader to pass filter 2.1 S
2.6 Add hash-based filter methods None S
2.7 Persist skip markers 2.2 S
2.8 Add metrics 2.3 S
2.9 Unit tests 2.1-2.8 M
2.10 Integration test 2.9 L

Effort: S = Small (few hours), M = Medium (1-2 days), L = Large (3-5 days)


Critical Design Decisions

  1. Minimal Invasion: Modify existing Syncer rather than creating parallel implementation
  2. Hash-Based Filtering: Pre-compute address hashes for efficient snap sync filtering
  3. Skip Markers in DB: Persist skip decisions for healing phase and resumption
  4. Healing Awareness: Healing phase must respect skip markers to avoid errors

Verification Checklist

After implementation:

  • ALL accounts synced (complete account trie - this is critical)
  • Account trie fully intact - eth_getProof works for ANY account
  • Only configured contracts have storage synced
  • Only configured contracts have bytecode synced
  • Skip markers present for non-tracked contract storage
  • Healing completes successfully (doesn't try to heal skipped storage)
  • Sync can resume after restart (skip markers persist)
  • Database size significantly reduced (~30-40GB vs ~1TB+)
  • eth_getBalance works for ANY account (not just tracked)
  • eth_getTransactionCount works for ANY account
  • eth_getStorageAt works for tracked contracts
  • Metrics show skipped storage/bytecode counts

Critical Invariant: The account trie must be 100% complete. Only storage tries for non-tracked contracts are skipped.

After implemenmtation:

★ Insight ───────────────────────────────────── Phase 2 Implementation Complete: The key architectural change is that snap sync now supports partial statefulness through a filter mechanism. The filter is passed through the entire stack:

  1. ethconfig.PartialStateConfig → CLI configuration
  2. partial.ConfiguredFilter → Pre-computes keccak256 hashes for efficient lookup
  3. snap.Syncer.filter → Makes filtering decisions during sync
  4. markStorageSkipped() / isStorageSkipped() → Persists skip decisions for healing phase

This allows a node to sync ~30-40GB (all accounts) instead of ~1TB+ (all accounts + all storage). ─────────────────────────────────────────────────

⏺ Bash(git diff --stat HEAD 2>&1 | tail -20) ⎿   core/state/partial/filter.go | 46 +++++++++++++++++++++++++-- eth/backend.go | 10 ++++++ eth/downloader/downloader.go | 5 +-- … +5 lines (ctrl+o to expand)

⏺ Bash(git status --short 2>&1) ⎿   M core/state/partial/filter.go M eth/backend.go M eth/downloader/downloader.go … +9 lines (ctrl+o to expand)

⏺ Here's a summary of the Phase 2 Snap Sync Modifications that have been implemented:

Phase 2 Implementation Summary

Tasks Completed ┌──────┬───────────────────────────────────────────────┬───────────────────────────────────┐ │ Task │ Description │ Status │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.1 │ Add filter to Syncer struct │ ✓ │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.2 │ Create sync_partial.go helpers │ ✓ │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.3 │ Modify processAccountResponse() for filtering │ ✓ │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.4 │ Modify healing to skip storage │ ✓ │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.5 │ Update Downloader to pass filter │ ✓ │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.6 │ Add hash-based filter methods │ ✓ │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.7 │ Persist skip markers │ ✓ │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.8 │ Add metrics │ ✓ │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.9 │ Unit tests │ ✓ │ ├──────┼───────────────────────────────────────────────┼───────────────────────────────────┤ │ 2.10 │ Integration test │ Deferred (requires network setup) │ └──────┴───────────────────────────────────────────────┴───────────────────────────────────┘ Files Modified (7 files, +112 lines)

  • core/state/partial/filter.go - Added hash-based filter methods
  • eth/backend.go - Create and pass partial filter
  • eth/downloader/downloader.go - Accept filter parameter
  • eth/handler.go - Add PartialFilter to handlerConfig
  • eth/protocols/snap/sync.go - Filter storage/bytecode in processAccountResponse and healing

Files Created (4 files)

  • eth/protocols/snap/sync_partial.go - Skip marker DB functions and filter helpers
  • eth/protocols/snap/sync_partial_test.go - Unit tests for partial sync
  • core/state/partial/filter_test.go - Unit tests for filter
  • docs/partial-state/PHASE2_PLAN.md - Plan documentation

Key Design Decisions

  1. Minimal invasion: Modified existing Syncer rather than creating parallel implementation
  2. Hash-based filtering: Pre-compute keccak256 hashes for O(1) lookup during sync
  3. Skip markers: Persist decisions in DB so healing phase knows what was intentionally skipped
  4. All accounts synced: Only storage/bytecode are filtered, account trie is always complete

Testing

All 8 new tests pass:

  • TestPartialSyncFilterStorage
  • TestAllowAllFilter
  • TestSkipMarkerPersistence
  • TestSyncerFilterMethods
  • TestConfiguredFilterContracts
  • TestConfiguredFilterBasic
  • TestConfiguredFilterHashConsistency
  • TestAllowAllFilterInterface