mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-08 07:58:40 +00:00
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>
353 lines
12 KiB
Bash
Executable file
353 lines
12 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# verify_partial_sync.sh - Verify partial state sync correctness.
|
|
#
|
|
# Runs JSON-RPC checks against a running geth node to verify:
|
|
# 1. All accounts are accessible (full account trie synced)
|
|
# 2. Tracked contract storage and code are present
|
|
# 3. Untracked contract storage and code are correctly rejected
|
|
#
|
|
# Usage:
|
|
# ./verify_partial_sync.sh # RPC checks (geth must be running)
|
|
# ./verify_partial_sync.sh --db-only # Database inspection (geth must be stopped)
|
|
# ./verify_partial_sync.sh --all # Both (stops geth for DB checks)
|
|
#
|
|
set -euo pipefail
|
|
|
|
RPC_URL="${RPC_URL:-http://localhost:8545}"
|
|
DATADIR="${DATADIR:-$HOME/.ethereum-partial-test}"
|
|
GETH="${GETH:-$(dirname "${BASH_SOURCE[0]}")/../../build/bin/geth}"
|
|
|
|
# Tracked contracts (WETH, DAI)
|
|
WETH="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
|
DAI="0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
|
|
|
# Untracked contracts (USDC, Uniswap V2 Router)
|
|
USDC="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
UNISWAP_ROUTER="0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
|
|
|
|
# ERC20 totalSupply() selector
|
|
TOTAL_SUPPLY="0x18160ddd"
|
|
|
|
# Counters
|
|
PASS=0
|
|
FAIL=0
|
|
TOTAL=0
|
|
|
|
# ─── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
check_deps() {
|
|
for cmd in curl jq; do
|
|
if ! command -v "$cmd" &>/dev/null; then
|
|
echo "ERROR: '$cmd' is required but not installed."
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
rpc_call() {
|
|
local method="$1"
|
|
local params="$2"
|
|
curl -s -X POST "$RPC_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"params\":$params,\"id\":1}"
|
|
}
|
|
|
|
# Check that result field is non-zero hex
|
|
check_nonzero() {
|
|
local label="$1"
|
|
local method="$2"
|
|
local params="$3"
|
|
|
|
TOTAL=$((TOTAL + 1))
|
|
local response
|
|
response=$(rpc_call "$method" "$params")
|
|
|
|
local error
|
|
error=$(echo "$response" | jq -r '.error // empty')
|
|
if [ -n "$error" ]; then
|
|
echo " [FAIL] $label"
|
|
echo " Error: $(echo "$response" | jq -r '.error.message')"
|
|
FAIL=$((FAIL + 1))
|
|
return
|
|
fi
|
|
|
|
local result
|
|
result=$(echo "$response" | jq -r '.result')
|
|
|
|
if [ "$result" = "0x0" ] || [ "$result" = "0x" ] || [ "$result" = "null" ] || [ -z "$result" ]; then
|
|
echo " [FAIL] $label (got: $result)"
|
|
FAIL=$((FAIL + 1))
|
|
else
|
|
# Truncate long results for display
|
|
local display="$result"
|
|
if [ ${#display} -gt 20 ]; then
|
|
display="${display:0:20}..."
|
|
fi
|
|
echo " [PASS] $label ($display)"
|
|
PASS=$((PASS + 1))
|
|
fi
|
|
}
|
|
|
|
# Check that result is non-empty bytecode (not "0x")
|
|
check_code() {
|
|
local label="$1"
|
|
local addr="$2"
|
|
|
|
TOTAL=$((TOTAL + 1))
|
|
local response
|
|
response=$(rpc_call "eth_getCode" "[\"$addr\",\"latest\"]")
|
|
|
|
local error
|
|
error=$(echo "$response" | jq -r '.error // empty')
|
|
if [ -n "$error" ]; then
|
|
echo " [FAIL] $label"
|
|
echo " Error: $(echo "$response" | jq -r '.error.message')"
|
|
FAIL=$((FAIL + 1))
|
|
return
|
|
fi
|
|
|
|
local result
|
|
result=$(echo "$response" | jq -r '.result')
|
|
local len=$(( (${#result} - 2) / 2 )) # bytes = (hex_len - "0x" prefix) / 2
|
|
|
|
if [ "$result" = "0x" ] || [ "$len" -le 0 ]; then
|
|
echo " [FAIL] $label (empty code)"
|
|
FAIL=$((FAIL + 1))
|
|
else
|
|
echo " [PASS] $label ($len bytes)"
|
|
PASS=$((PASS + 1))
|
|
fi
|
|
}
|
|
|
|
# Check that RPC returns a specific error code
|
|
check_error() {
|
|
local label="$1"
|
|
local method="$2"
|
|
local params="$3"
|
|
local expected_code="$4"
|
|
|
|
TOTAL=$((TOTAL + 1))
|
|
local response
|
|
response=$(rpc_call "$method" "$params")
|
|
|
|
local error_code
|
|
error_code=$(echo "$response" | jq -r '.error.code // empty')
|
|
|
|
if [ "$error_code" = "$expected_code" ]; then
|
|
local msg
|
|
msg=$(echo "$response" | jq -r '.error.message')
|
|
echo " [PASS] $label (error $error_code: $msg)"
|
|
PASS=$((PASS + 1))
|
|
elif [ -n "$error_code" ]; then
|
|
echo " [FAIL] $label (expected error $expected_code, got $error_code)"
|
|
FAIL=$((FAIL + 1))
|
|
else
|
|
local result
|
|
result=$(echo "$response" | jq -r '.result')
|
|
echo " [FAIL] $label (expected error $expected_code, but got result: ${result:0:20}...)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
# Check that eth_call returns an error (any error)
|
|
check_call_error() {
|
|
local label="$1"
|
|
local to="$2"
|
|
local data="$3"
|
|
|
|
TOTAL=$((TOTAL + 1))
|
|
local response
|
|
response=$(rpc_call "eth_call" "[{\"to\":\"$to\",\"data\":\"$data\"},\"latest\"]")
|
|
|
|
local error
|
|
error=$(echo "$response" | jq -r '.error // empty')
|
|
|
|
if [ -n "$error" ]; then
|
|
local msg
|
|
msg=$(echo "$response" | jq -r '.error.message')
|
|
echo " [PASS] $label (error: ${msg:0:50})"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
local result
|
|
result=$(echo "$response" | jq -r '.result')
|
|
echo " [FAIL] $label (expected error, got result: ${result:0:20}...)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
# ─── RPC Verification ────────────────────────────────────────────────
|
|
|
|
run_rpc_checks() {
|
|
echo "=== Partial State Sync Verification ==="
|
|
echo ""
|
|
echo "RPC endpoint: $RPC_URL"
|
|
echo ""
|
|
|
|
# A. Sync Status
|
|
echo "Sync Status:"
|
|
|
|
TOTAL=$((TOTAL + 1))
|
|
local syncing
|
|
syncing=$(rpc_call "eth_syncing" "[]" | jq -r '.result')
|
|
if [ "$syncing" = "false" ]; then
|
|
echo " [PASS] eth_syncing returns false"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo " [WARN] eth_syncing returns: $syncing (sync may still be in progress)"
|
|
echo " Some checks may fail until sync completes."
|
|
PASS=$((PASS + 1)) # Not a failure, just a warning
|
|
fi
|
|
|
|
TOTAL=$((TOTAL + 1))
|
|
local block_hex
|
|
block_hex=$(rpc_call "eth_blockNumber" "[]" | jq -r '.result')
|
|
if [ -n "$block_hex" ] && [ "$block_hex" != "null" ]; then
|
|
local block_dec
|
|
block_dec=$(printf "%d" "$block_hex" 2>/dev/null || echo "?")
|
|
echo " [PASS] Block number: $block_dec ($block_hex)"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo " [FAIL] Could not get block number"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
echo ""
|
|
|
|
# B. Account Data (all accounts - full trie synced)
|
|
echo "Account Data (all accounts - full trie synced):"
|
|
check_nonzero "USDC contract balance" "eth_getBalance" "[\"$USDC\",\"latest\"]"
|
|
check_nonzero "WETH contract balance" "eth_getBalance" "[\"$WETH\",\"latest\"]"
|
|
check_nonzero "Uniswap Router balance" "eth_getBalance" "[\"$UNISWAP_ROUTER\",\"latest\"]"
|
|
check_nonzero "USDC nonce" "eth_getTransactionCount" "[\"$USDC\",\"latest\"]"
|
|
echo ""
|
|
|
|
# C. Tracked Contracts (WETH, DAI)
|
|
echo "Tracked Contracts (WETH, DAI):"
|
|
check_code "WETH code" "$WETH"
|
|
check_code "DAI code" "$DAI"
|
|
check_nonzero "WETH storage slot 0x0" "eth_getStorageAt" "[\"$WETH\",\"0x0\",\"latest\"]"
|
|
check_nonzero "DAI storage slot 0x0" "eth_getStorageAt" "[\"$DAI\",\"0x0\",\"latest\"]"
|
|
check_nonzero "eth_call WETH.totalSupply()" "eth_call" "[{\"to\":\"$WETH\",\"data\":\"$TOTAL_SUPPLY\"},\"latest\"]"
|
|
check_nonzero "eth_call DAI.totalSupply()" "eth_call" "[{\"to\":\"$DAI\",\"data\":\"$TOTAL_SUPPLY\"},\"latest\"]"
|
|
echo ""
|
|
|
|
# D. Untracked Contracts (USDC, Uniswap V2 Router)
|
|
echo "Untracked Contracts (USDC, Uniswap V2 Router):"
|
|
check_error "USDC eth_getStorageAt" "eth_getStorageAt" "[\"$USDC\",\"0x0\",\"latest\"]" "-32001"
|
|
check_error "Router eth_getStorageAt" "eth_getStorageAt" "[\"$UNISWAP_ROUTER\",\"0x0\",\"latest\"]" "-32001"
|
|
check_error "USDC eth_getCode" "eth_getCode" "[\"$USDC\",\"latest\"]" "-32002"
|
|
check_error "Router eth_getCode" "eth_getCode" "[\"$UNISWAP_ROUTER\",\"latest\"]" "-32002"
|
|
check_call_error "eth_call USDC.totalSupply()" "$USDC" "$TOTAL_SUPPLY"
|
|
echo ""
|
|
|
|
# Summary
|
|
echo "========================================="
|
|
if [ $FAIL -eq 0 ]; then
|
|
echo " Results: $PASS/$TOTAL passed"
|
|
else
|
|
echo " Results: $PASS/$TOTAL passed, $FAIL FAILED"
|
|
fi
|
|
echo "========================================="
|
|
}
|
|
|
|
# ─── Database Verification ───────────────────────────────────────────
|
|
|
|
run_db_checks() {
|
|
echo ""
|
|
echo "=== Database-Level Verification ==="
|
|
echo ""
|
|
echo "Data directory: $DATADIR"
|
|
echo ""
|
|
|
|
# Check geth binary exists
|
|
if [ ! -x "$GETH" ]; then
|
|
echo "ERROR: geth binary not found at $GETH"
|
|
echo "Set GETH env var or build first: go build -o build/bin/geth ./cmd/geth"
|
|
exit 1
|
|
fi
|
|
|
|
# Check datadir exists
|
|
if [ ! -d "$DATADIR" ]; then
|
|
echo "ERROR: Data directory not found: $DATADIR"
|
|
exit 1
|
|
fi
|
|
|
|
# Check geth is not running (LevelDB requires exclusive access)
|
|
if pgrep -f "geth.*partial-test" > /dev/null 2>&1; then
|
|
echo "WARNING: geth appears to be running. Stop it first for database inspection."
|
|
echo " kill \$(pgrep -f 'geth.*partial-test')"
|
|
echo ""
|
|
fi
|
|
|
|
echo "Running: geth db inspect"
|
|
echo "(this may take a while for large databases)"
|
|
echo ""
|
|
|
|
"$GETH" db inspect --datadir "$DATADIR" 2>&1 | tee /tmp/partial-sync-inspect.txt
|
|
|
|
echo ""
|
|
echo "Inspection output saved to: /tmp/partial-sync-inspect.txt"
|
|
echo ""
|
|
echo "What to check in the output above:"
|
|
echo " - 'Account snapshot' : Should be large (~45 GiB) - full account trie"
|
|
echo " - 'Storage snapshot' : Should be TINY (< 1 GiB) - only WETH + DAI"
|
|
echo " - 'Contract codes' : Should be very small - only 2 contracts"
|
|
echo " - 'Bodies' : Should be tiny (< 10 MiB) - chain retention=1024"
|
|
echo " - 'Receipts' : Should be tiny (< 10 MiB) - chain retention=1024"
|
|
echo " - 'Headers' : ~9 GiB (full chain, non-prunable)"
|
|
echo " - Compare total DB size to a full node (~640+ GiB)"
|
|
echo " - Expected total: ~59 GiB (headers + partial state)"
|
|
echo ""
|
|
|
|
# Try dumptrie for tracked contract (WETH)
|
|
echo "Verifying tracked contract storage (WETH)..."
|
|
echo "Running: geth db dumptrie (limited to 5 entries)"
|
|
echo ""
|
|
|
|
# Compute WETH account hash (keccak256 of address bytes)
|
|
local weth_hash
|
|
weth_hash=$(python3 -c "
|
|
from hashlib import sha3_256
|
|
addr = bytes.fromhex('C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')
|
|
print('0x' + sha3_256(addr).hexdigest())
|
|
" 2>/dev/null || echo "")
|
|
|
|
if [ -n "$weth_hash" ]; then
|
|
echo "WETH account hash: $weth_hash"
|
|
# Note: dumptrie requires state-root and storage-root which need the account data.
|
|
# For now, just note the hash for manual inspection.
|
|
echo "(Use 'geth db dumptrie <state-root> $weth_hash <storage-root> \"\" 5' for manual inspection)"
|
|
else
|
|
echo "Python3 not available for hash computation. Skipping dumptrie."
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
# ─── Main ────────────────────────────────────────────────────────────
|
|
|
|
check_deps
|
|
|
|
MODE="${1:-rpc}"
|
|
|
|
case "$MODE" in
|
|
--db-only)
|
|
run_db_checks
|
|
;;
|
|
--all)
|
|
run_rpc_checks
|
|
echo ""
|
|
echo "Stopping geth for database inspection..."
|
|
kill "$(pgrep -f 'geth.*partial-test')" 2>/dev/null || true
|
|
sleep 3
|
|
run_db_checks
|
|
;;
|
|
*)
|
|
run_rpc_checks
|
|
echo ""
|
|
echo "For database-level verification, run:"
|
|
echo " $0 --db-only (after stopping geth)"
|
|
echo " $0 --all (stops geth automatically)"
|
|
;;
|
|
esac
|
|
|
|
exit $FAIL
|