FCR source comparison deep dive
Question
The previous investigation traced a 1.15 pp Fast Confirmation Rule disagreement between two offline FCR simulators to a non-spec support_discount term. The two simulators also read different inputs: one uses attestations from canonical block bodies, the other uses sentry gossip. Could the input difference itself be moving the FCR verdict, or is support_discount doing all the work?
Background
Two offline FCR simulators replay mainnet slot-by-slot and decide whether the Fast Confirmation Rule would have fired:
- Lighthouse-style: reads attestations from canonical block bodies (
canonical_beacon_elaborated_attestation), capped atMAX_ATTESTATIONS_ELECTRA = 8per block. - Teku-style: reads
beacon_aggregate_and_proofgossip from 5 Xatu sentries (libp2p_gossipsub_aggregate_and_proof), decodingaggregation_bitsagainstcanonical_beacon_committee.
They disagree on 1.15% of slots. The previous investigation isolated the cause to support_discount. This page tests whether the input source is also implicated, by comparing the actual voter sets the two simulators see slot-by-slot.
800 slots (400 disagreement, 400 random) for the per-slot overlap; 1,995 slots for the timing analysis.
Investigation
When comparing the two voter sets per slot
| Bucket | Slots | Share |
|---|---|---|
| 1.000 (exact) | 337 | 42.1% |
| 0.9999 to 1.0 | 260 | 32.5% |
| 0.999 to 0.9999 | 141 | 17.6% |
| 0.998 to 0.999 | 28 | 3.5% |
| 0.995 to 0.998 | 23 | 2.9% |
| 0.990 to 0.995 | 8 | 1.0% |
| 0.980 to 0.990 | 2 | 0.3% |
< 0.980 | 1 | 0.1% |
Mean Jaccard 0.99968, median 0.99997, p10 still 0.9996.
| Metric | Mean | Median | p75 | p90 | Max |
|---|---|---|---|---|---|
| block voters | 30,196 | 30,615 | 30,881 | 30,936 | 31,026 |
| gossip voters (5-sentry union) | 30,187 | 30,604 | 30,873 | 30,931 | 31,024 |
block_only | 9.7 | 1 | 3 | 12 | 642 |
gossip_only | 0 | 0 | 0 | 0 | 0 |
gossip_only is zero on every one of the 800 slots; gossip is a strict subset of block-included. Block has ~10 extra voters per slot (0.03% of committee). The two inputs see effectively the same population, with a small structural surplus on the block side.
That ~10 already rules out the input source as a 1.15pp driver. Even if every missed voter happened to flip the FCR verdict (they don't), the input could account for at most ~0.03pp.
When explaining where those ~10 come from
The Teku-style simulator only subscribes to the beacon_aggregate_and_proof topic. Two protocol facts push voters into the block but not into that topic:
- Aggregators select at t=8s. Every aggregator picks its aggregate at t=8s into the slot and broadcasts it on
beacon_aggregate_and_proof. Nothing structurally aggregates after that, so any single attestation that arrives later has no path into the agg-and-proof topic. - Block proposers see late attestations anyway. The proposer for slot N+1 finalizes block contents around the start of N+1 (~t=12s of slot N), giving them ~4s past the aggregator cutoff. They also pull from both gossip topics (aggregates AND
beacon_attestation_<subnet>) plus their peer attestation mempool. Late single-attestations make it in.
Adding the single-attestation topic to the simulator's input proves the mechanism. Re-running on 1,995 slots:
| Source | Mean | Median | p90 | p99 | Mean gap vs block |
|---|---|---|---|---|---|
| Block-included | 30,640 | 30,796 | 30,951 | 31,005 | (baseline) |
| Subnet single-attestation | 30,580 | 30,766 | 30,948 | 31,004 | 59.2 |
| Aggregate-and-proof | 30,632 | 30,790 | 30,949 | 30,999 | 8.1 |
Block-minus-agg drops from 8.1 to 3.1 voters/slot once the subnet topic is included (the raw 59.2 block-minus-subnet number is dragged up by 165 outage slots; on the 1,830 clean slots it's 3.1).
A vanilla beacon node only listens to 2 of 64 single-attestation subnets, so no single sentry can decode the full subnet topic on its own. The 5-sentry pool collectively covers more, which is why this analysis can read the subnet topic at all.
To confirm the timing story: of the 16,049 agg-miss validators that the subnet did observe, per-validator min propagation_slot_start_diff lines up like this:
| Percentile | agg-hits (n=61.0M) | agg-misses (n=16,049) |
|---|---|---|
| p25 | 3,113 ms | 8,270 ms |
| p50 | 4,313 ms | 8,732 ms |
| p75 | 4,971 ms | 9,507 ms |
| p90 | 5,513 ms | 11,755 ms |
| p95 | 5,923 ms | 14,756 ms |
| p99 | 6,683 ms | 28,911 ms |
| mean | 4,113 ms | 10,072 ms |
| Threshold | % of agg-hits past | % of agg-misses past |
|---|---|---|
| > 4,000 ms | 59.17% | 100.00% |
| > 6,000 ms | 4.37% | 97.87% |
| > 8,000 ms | 0.05% | 93.63% |
| > 12,000 ms | 0.02% | 8.87% |
| > 16,000 ms | 0.01% | 3.83% |
93.6% of agg-misses arrived on a subnet after the 8s aggregator cutoff. Only 1,022 (6.4%) were on time but still missed; another 189 never appeared on any subnet sentry yet landed in a canonical block (almost certainly via someone's peer mempool).
When splitting by MEV builder
MEV-relay builders subscribe to all 64 subnets to maximize attestation inclusion, vs vanilla builders on 2. Does that change the late-arrival pattern? Joining canonical execution_payload_block_hash to mev_relay_proposer_payload_delivered.block_hash classifies 1,824 slots as MEV-built across 8 relays, 167 locally-built, 4 unclassifiable.
| Metric | MEV-built (n=1,824) | Locally-built (n=167) |
|---|---|---|
| mean block_minus_agg | 7.77 | 6.84 |
| median block_minus_agg | 1 | 0 |
| agg-miss validators with timing | 13,991 | 1,126 |
| agg-misses > 8s | 99.30% | 100.00% |
| agg-misses > 12s | 9.72% | 5.68% |
| agg-miss p90 first-seen | 11,873 ms | 10,179 ms |
| agg-miss p99 first-seen | 29,882 ms | 23,979 ms |
Post-8s share is ~100% in both groups; late arrival is structural, not a builder choice. MEV-built blocks do pull more in the upper tail (p90 11.9s vs 10.2s, p99 29.9s vs 24.0s), consistent with wider subnet coverage and bigger peer mempools, but the 0.93 voters/slot mean difference sits inside per-slot noise of ~±10. The 167 local slots are too few to make the upper-tail divergence load-bearing.
Takeaways
- The input source is not the FCR culprit. The two voter sets agree 99.97% per-slot; the ~10-voter block surplus could move FCR by at most ~0.03pp. The 1.15pp simulator disagreement is
support_discount, not the data source. Switching the Teku-style simulator to read block-included would not close it. - The ~10/slot block surplus is structural to reading only
aggregate_and_proof. That topic has an effective 8s aggregator cutoff; 93.6% of missed voters arrived on the single-attestation subnet topic after 8s. Block proposers grab them via both topics + peer mempool + the ~4s before the next block is built. - Adding the single-attestation subnet topic closes most of the gap (3.1 vs 8.4 voters/slot on clean slots), at the cost of much higher gossip volume. Worth it if you want a gossip-only view that tracks block-inclusion closely.