Skip to content

iankressin/validation

Repository files navigation

RPC Scorecard

Enter a block number or a transaction hash. The app validates that block against every public RPC provider and the SQD Portal, by re-deriving the cryptographic commitments the block header already carries (transactionsRoot, receiptsRoot, logsBloom, parentHash).

This turns the central claim of SQD's blog post on RPC validation into something you can operate interactively: type a block number, watch 10 public Ethereum RPCs race SQD to reproduce the same MPT roots and bloom filter, see which ones succeed and which ones don't — down to the byte.

How it works

      block # or 0x tx hash
              │
              ▼
     ┌──────────────────────┐
     │  /api/validate       │  (one-shot; Node runtime)
     └────────┬─────────────┘
              │
   ┌──────────┼──────────────┐
   ▼          ▼              ▼
 SQD        RPC 1 … RPC N
 Portal     fetch block + receipts + raw signed txs
 finalized  (eth_getBlockByNumber, eth_getBlockReceipts,
 stream      eth_getRawTransactionByHash — with @ethereumjs/tx
 (single-    reserialization fallback)
  block)
   │          │
   ▼          ▼
 validate/ — pure primitives (MPT, bloom, indices, diff)
   │
   ▼
 JSON report → dashboard

For every block, each column in the report answers seven checks:

# Check Meaning
1 Transactions trie MPT root rebuilt over the RPC's raw signed txs equals header.transactionsRoot
2 Receipts trie MPT root rebuilt over eth_getBlockReceipts equals header.receiptsRoot
3 Logs bloom 2048-bit bloom recomputed from the RPC's logs byte-equals header.logsBloom (geth bloom9.go port)
4 Log-index continuity Flattened log indices form 0..N with no gaps and no duplicates
5 Parent-hash The RPC's parentHash for this block equals SQD's parentHash for this block
6 Tx set = SQD RPC's tx-hash set equals SQD's validated set
7 Log set = SQD RPC's (txHash, logIndex) set equals SQD's validated set

Checks 1–4 catch an RPC returning bytes that don't reproduce the root in its own header. Checks 5–7 catch RPCs whose view diverges from SQD's independently-validated stream. The SQD row itself runs checks 1, 3, 4 — a baseline green proving SQD's view re-derives the header commitments.

UI

  • Input form — block number (decimal or hex) or 0x-prefixed tx hash. For a tx hash, all RPCs race to resolve it to a block first.
  • Header card — the block's cryptographic commitments, plus a quick pass / fail / errored tally.
  • Verdict table — one row per source (SQD + each RPC), one column per check. Click any cell for a forensic detail view.
  • Forensic detail (inline, opens under the table):
    • 64-nibble root-diff strip with per-nibble red highlight for divergent nibbles;
    • 2048-bit bloom heatmap (green = both set, red = RPC under-reported, yellow = RPC over-reported);
    • log-index timeline with gaps and duplicates called out.
  • Permalink — any report is also available as JSON at /api/validate?chain=X&block=N or ?tx=0x… or ?latest=1.

Running

Requirements: Node ≥ 20, pnpm ≥ 8.

pnpm install
pnpm test        # 12 unit tests — validators round-trip the geth-filled
                 # SimpleTx block test vector from ethereum/tests, plus
                 # canonical constants for empty trie and all-zero bloom
pnpm dev         # http://localhost:3000

pnpm test cross-checks the transactions-trie code against a block from ethereum/tests's SimpleTx vector — a block whose transactionsTrie was filled by geth. If it passes, the MPT code is byte-compatible with a real Ethereum client.

API

  • GET /api/providers?chain=<id> — list supported chains and the RPC roster.
  • GET /api/validate?chain=<id>&block=<n> — validate a specific block.
  • GET /api/validate?chain=<id>&tx=0x<hash> — resolve the tx to a block, then validate.
  • GET /api/validate?chain=<id>&latest=1 — use the Portal's latest finalized block.

Supported chains: ethereum, base, arbitrum, optimism, polygon.

Architecture notes

  • On-demand, not streaming. The SQD Portal client is invoked per request with fromBlock = toBlock = N, which terminates the stream as soon as the requested block ships. No background process, no long-lived connections, no rolling-window state to worry about.
  • Typed-tx safety. Raw-tx-by-hash is preferred; when unavailable, txs are re-serialized via @ethereumjs/tx (LegacyTransaction, AccessListEIP2930Transaction, FeeMarketEIP1559Transaction, BlobEIP4844Transaction). Blob-txs go into the trie without the network sidecar — the trie leaf is the no-sidecar envelope.
  • eth_getBlockReceipts coverage varies. Providers that don't expose it (e.g. Cloudflare) are marked N/A for the receipts-trie and bloom checks, not scored as failures.
  • 429 ≠ fraud. HTTP 429 responses bubble up as their own category and do not count as a validation failure.
  • Reorg / finality lag. SQD serves finalized data; RPCs serve head. For very recent blocks the RPC may not yet have finalized what you're asking about. The Portal call waits up to 20s; on timeout you'll get a clear "may not be finalized yet" error.

Files

  • src/lib/validate/* — pure primitives (trie-tx, trie-receipts, bloom, log-indices, parent-chain, crosscheck-sqd), plus the orchestrator validate-one-block.ts.
  • src/lib/rpc/fetch-block.ts — per-provider fan-out with p-limit and distinct 429 path.
  • src/lib/rpc/raw-tx.ts — typed-tx reserialization via @ethereumjs/tx.
  • src/lib/sqd/portal-one-shot.ts — single-block fetch from @subsquid/portal-client.
  • src/lib/chains.ts — static chain + provider catalog.
  • src/app/api/validate/route.ts — request handler (block / tx / latest).
  • src/app/page.tsx — input form, verdict table, inline forensic detail.

Deploy

Self-hosted Node (Railway, Render, VPS, Fly). Because requests fetch from the Portal every time, there's no singleton constraint; you can horizontally scale without coordination. Not compatible with Edge runtimes — uses Node http/https.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors