go-ethereum/docs/partial-state/DEVNET_TESTING.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

5.2 KiB

Partial State Devnet Testing Guide

This document describes how to test partial statefulness with a local devnet using 2 geth instances.

Overview

Partial state nodes:

  • Sync all account data (balances, nonces, code hashes)
  • Only store storage for tracked contracts
  • Process blocks using BAL (Block Access Lists) instead of re-executing transactions

Prerequisites

  • Go 1.22+ installed
  • Two terminal windows
  • Build geth with partial state support:
    go build ./cmd/geth
    

Setup

Terminal 1: Full Node (creates blocks in dev mode)

# Create fresh data directory
rm -rf /tmp/full-node

# Start full node in dev mode
./geth --datadir /tmp/full-node \
    --dev \
    --dev.period 5 \
    --port 30303 \
    --http --http.port 8545 \
    --http.api eth,net,web3,debug,admin \
    --verbosity 3

# Get the enode URL (run in another terminal or use geth attach)
# geth attach /tmp/full-node/geth.ipc --exec admin.nodeInfo.enode

Terminal 2: Partial State Node (receives blocks via P2P)

First, get the enode from the full node:

ENODE=$(geth attach /tmp/full-node/geth.ipc --exec admin.nodeInfo.enode | tr -d '"')
echo "Full node enode: $ENODE"

Then start the partial state node:

# Create fresh data directory
rm -rf /tmp/partial-node

# Start partial state node
./geth --datadir /tmp/partial-node \
    --port 30304 \
    --http --http.port 8546 \
    --http.api eth,net,web3,debug \
    --partial-state \
    --partial-state.contracts 0xContractAddr1,0xContractAddr2 \
    --bootnodes "$ENODE" \
    --networkid 1337 \
    --verbosity 3

Note: Replace 0xContractAddr1,0xContractAddr2 with actual contract addresses you want to track.

Test Scenarios

1. Block Sync Test

Send a transaction on the full node and verify the partial node receives it:

# On full node (Terminal 1 or new terminal)
geth attach /tmp/full-node/geth.ipc

# In geth console, send a transaction
> eth.sendTransaction({from: eth.coinbase, to: "0x1234567890123456789012345678901234567890", value: web3.toWei(1, "ether")})

# Check block number
> eth.blockNumber

Verify on partial node:

curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
    -H "Content-Type: application/json" localhost:8546 | jq

2. Balance Query Test

Both nodes should return the same balance for any account:

# Full node
curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x1234567890123456789012345678901234567890","latest"],"id":1}' \
    -H "Content-Type: application/json" localhost:8545 | jq

# Partial node
curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x1234567890123456789012345678901234567890","latest"],"id":1}' \
    -H "Content-Type: application/json" localhost:8546 | jq

3. Storage Query Test

Deploy a contract and test storage access:

# Query tracked contract storage (should work)
curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["0xTrackedContractAddr","0x0","latest"],"id":1}' \
    -H "Content-Type: application/json" localhost:8546 | jq

# Query untracked contract storage (should fail or return empty)
curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["0xUntrackedContractAddr","0x0","latest"],"id":1}' \
    -H "Content-Type: application/json" localhost:8546 | jq

4. State Root Verification

Verify both nodes have the same state root:

# Get latest block from both nodes
FULL_ROOT=$(curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":1}' \
    -H "Content-Type: application/json" localhost:8545 | jq -r '.result.stateRoot')

PARTIAL_ROOT=$(curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":1}' \
    -H "Content-Type: application/json" localhost:8546 | jq -r '.result.stateRoot')

echo "Full node state root: $FULL_ROOT"
echo "Partial node state root: $PARTIAL_ROOT"

if [ "$FULL_ROOT" = "$PARTIAL_ROOT" ]; then
    echo "State roots match!"
else
    echo "State roots DO NOT match!"
fi

Database Size Comparison

After syncing, compare database sizes:

echo "Full node database size:"
du -sh /tmp/full-node/geth/chaindata

echo "Partial node database size:"
du -sh /tmp/partial-node/geth/chaindata

The partial node should have a significantly smaller database size due to skipped storage.

Cleanup

# Stop both geth instances (Ctrl+C in each terminal)

# Remove test data
rm -rf /tmp/full-node /tmp/partial-node

Troubleshooting

Nodes not connecting

  • Verify bootnodes enode URL is correct
  • Check that network IDs match (dev mode uses 1337)
  • Ensure ports are not blocked

State root mismatch

  • This indicates a bug in BAL processing
  • Check geth logs for errors during block processing
  • Verify the partial node received the BAL with the block

Storage queries failing

  • Verify the contract address is in the tracked contracts list
  • Check that the contract was deployed after the partial node started syncing