Skip to content

Repository files navigation

FCR Simulator

Replays historical Ethereum mainnet blocks through each CL client's Fast Confirmation Rule implementation and reports per-slot FCR output for cross-client comparison.

Engines

All engines live under engines/<name>/. Each has a build.sh that produces results/fcr-<name>.

Engine Upstream pin FCR source
lighthouse unstable (6b0e7bbf4) Merged Lighthouse implementation targeting v8.3.0
teku 26.8.0 Release implementation, with simulator adapter patches
nimbus v26.7.0 Release implementation
lodestar v1.46.0 Release implementation
grandine PR #656 (9905f46f) Unreleased; Grandine 2.0.6 does not contain FCR
prysm PR #17122 (5fc7c60d) Unreleased; enabled through Prysm's FCR test harness

Prysm support is pinned to the open PR #17122, not a released client version. The adapter compiles as a test binary so it can reuse Prysm's upstream FCR spectest harness without patching production client code.

Architecture

A Go orchestrator owns everything engine-agnostic — ERA file download, checkpoint state fetch, attestation source plan, output schema, JSONL → CSV merge. Engines are subprocesses that speak HTTP to a localhost beacon-API the orchestrator serves.

┌─────────────────────────────────────────────────────────┐
│  fcr-orchestrator (Go)                                  │
└────────────────────────────────┬────────────────────────┘
                                 │ HTTP (SSZ blocks/states + /fcr-sim/v1/plan)
       ┌──────────┬──────────┬───┴───────┬──────────┬──────────┐
   fcr-lighthouse fcr-teku  fcr-nimbus  fcr-lodestar fcr-grandine fcr-prysm
       (Rust)    (Java)     (Nim)       (TypeScript) (Rust)      (Go)

Per sim slot N the engine: fetches block N, imports it, looks up plan[N].source_block_slot, extracts that block's attestations, injects them using its native equivalent of Lighthouse's AttestationFromBlock::True, runs recompute_head_at_slot(N+1), emits one JSONL record. The orchestrator validates each record, enriches with engine metadata, writes the final CSV / JSONL / manifest.

BLS, execution-layer, and blob/DA checks are bypassed in each engine — historical canonical-chain replay doesn't need them. The orchestrator hard-rejects any engine binary that doesn't declare fake_crypto in its --manifest-json build_flags.

The engine contract is enforced in code: CLI flags + manifest validation in cmd/fcr-orchestrator/main.go, HTTP endpoints in pkg/beaconapi/, output schema in pkg/schema/. Attestation-source planner spec next to its code at pkg/attplan/SPEC.md.

Attestation source modes

The orchestrator owns which block sources attestations for each sim slot:

  • next-non-missed (default): for sim slot N, the first non-missed block in N+1..N+lookahead-cap. --lookahead-cap=4 reproduces today's Lighthouse behavior; --lookahead-cap=32 covers the spec's full inclusion range.
  • strict-source-block-k-minus-1: source is exactly N+1 if it exists, else nothing.
  • greedy-lookahead: consumes every non-missed block in N+1..N+lookahead-cap, bounded to attestations for the FCR evaluation slot.
  • xatu-first-seen-singles: serves per-validator first-seen gossip votes from parquet instead of sourcing attestations from blocks. The orchestrator filters by raw_seen_ms <= --attestation-first-seen-deadline-ms (default 12000) and sends committee-free attesting_indices to the engine for direct fork-choice injection. All 6 engines implement this injection path. Use --attestation-first-seen-base as either a local base path or s3://bucket/prefix; files are expected below network=<network>/source=raw/epoch=<epoch>/data.parquet. --lookahead-cap is inert in this mode.

Build

git clone --recursive https://github.com/ethpandaops/fcr-simulator.git
cd fcr-simulator

# Orchestrator (seconds)
go build -o ./results/fcr-orchestrator ./cmd/fcr-orchestrator

# Engines (each writes results/fcr-<name>)
bash engines/lighthouse/build.sh   # Rust,  ~10-20 min cold
bash engines/teku/build.sh         # Java,  ~5 min cold (needs JDK 25)
bash engines/nimbus/build.sh       # Nim,   ~5-10 min cold
bash engines/lodestar/build.sh     # Node,  ~3-5 min cold (needs Node 24+, Corepack)
bash engines/grandine/build.sh     # Rust,  ~10-15 min cold
bash engines/prysm/build.sh        # Go,    unreleased PR (downloads its pinned Go toolchain)

The orchestrator auto-runs engines/<engine>/build.sh if the corresponding binary is missing.

Run

./results/fcr-orchestrator \
  --engine lighthouse \
  --network mainnet \
  --beacon-node-url http://your-beacon-node:5052 \
  --start-epoch 435000 --end-epoch 435100 \
  --warmup-epochs 10 --parallel 2 \
  --output results/results.csv --output-format both \
  --attestation-source-mode next-non-missed --lookahead-cap 4 \
  --cache-dir ~/.cache/fcr-simulator

xatu-first-seen-singles fixture run:

./results/fcr-orchestrator \
  --engine lighthouse \
  --network mainnet \
  --beacon-node-url "$BEACON_NODE_URL" \
  --start-epoch 349000 --end-epoch 349015 \
  --warmup-epochs 0 --parallel 1 \
  --attestation-source-mode xatu-first-seen-singles \
  --attestation-first-seen-base deploy/attestation-backfill/fixtures \
  --attestation-first-seen-deadline-ms 12000 \
  --output deploy/firstseen-test/results.csv --output-format both \
  --cache-dir deploy/firstseen-test/cache

--engine-binary defaults to ./results/fcr-<engine>; override via --engine-binary flag or FCR_ENGINE_BINARY env if you want a different path.

For long resumable runs, run.sh chunks the range and merges on completion:

./run.sh --start-epoch 435000 --end-epoch 440000 --chunk-size 1000 \
         --beacon-node-url http://your-beacon-node:5052 --parallel 4

Beacon node: must serve /eth/v2/debug/beacon/states/{slot} and /eth/v1/beacon/headers/{slot} for the warmup slots. Lighthouse needs --reconstruct-historic-states; Teku needs --data-storage-mode=ARCHIVE. Recent ranges (within ~256 epochs of head) often work without archive mode.

Per-worker RAM: ~2–3 GB for the Rust engines; Lodestar settles around 4 GB after the bounded-cache fix. --parallel should be at most num_cpus / 4.

Cross-client comparison

scripts/cross-client/ contains:

  • pick-epochs.py — deterministic 10-epoch mainnet sample (seed 20260514, range [435000, 445000)).
  • manifest-check.sh — fast contract validation across every results/fcr-<engine> binary.
  • run.sh <engine> — run that engine over the sample, write per-epoch CSVs under results/cross-client/<engine>/.
  • _run-1hr.sh, _run-12hr.sh — parallel 5-engine runs over a fixed window, write results/<window>/<engine>/run.csv.
  • diff.py — per-slot cross-engine diff, emits per-slot.csv + divergences.csv + a per-column disagreement summary.

Output

CSV starts with # fcr-simulator-csv-schema-version:4 followed by the header row. JSONL is one record per line. Schema is the SlotResult struct in pkg/schema/schema.go. Key columns: slot, epoch, has_block, block_root, head_root, confirmed_root, confirmed_slot, confirmed_non_canonical, confirmation_delay_slots, fast_confirmed, strict_one_slot_confirmed, source_block_slot, num_attestations_injected, engine_name, engine_version, engine_commit.

A sidecar <output>.manifest.json captures engine manifest, run config, ERA file hashes, and output hashes for reproducibility.

Datasets

Cross-client FCR replays are published as GitHub releases. Latest: v0.2.0 (12 months lighthouse + 4-5 months each of lodestar, grandine, nimbus, teku; schema v4). The release page has the by-month scoreboard and METADATA.json has per-CSV image SHAs and the full 24-column schema.

An older lighthouse-only run is mirrored at data.ethpandaops.io/fcr-simulator/lighthouse/mainnet/epochs-<start>-<end>.csv as five 640k-slot chunks covering epochs 349000 to 449000. Note this uses greedy-lookahead (block-sourced attestations with lookahead_cap=8), not xatu-first-seen-singles, so it's not directly comparable to v0.2.0.

References

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages