Skip to content

feat(sentry): fetch Gloas-aware v2 debug fork choice - #900

Merged
Savid merged 13 commits into
masterfrom
feat/fork-choice-v2
Oct 8, 2026
Merged

Savid merged 13 commits into
masterfrom
feat/fork-choice-v2

Conversation

@Savid

@Savid Savid commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Why

Since Gloas activated on Sepolia, the sentry's fork choice events (which feed forky through xatu-internal-kafka-vector-forky) lose the payload information:

  • The sentry fetches GET /eth/v1/debug/fork_choice. On Gloas, Teku and Lodestar return three v1 nodes per block (pending/empty/full) and Nimbus two, with no payload_status to tell them apart. Production forky on Sepolia currently receives Teku frames with 266 nodes for 89 blocks.
  • ForkChoiceNodeV2.payload_status is read from v1 extra_data["payload_status"], which no client sets, so it is never populated.

What

  • Sentry fetches GET /eth/v2/debug/fork_choice (ethereum/beacon-APIs#615, merged 2026-10-07), one node per (block_root, payload_status), and falls back to v1 when v2 fails. Nodes that cannot serve v2 — no endpoint (Lighthouse 400, Nimbus 404) or a response that doesn't follow the spec, which go-eth2-client rejects with ErrInvalidResponse (Teku, Prysm and Lodestar today) — use v1 for an hour before v2 is retried; any other v2 failure (timeout, 5xx) falls back for that fetch only, and a cancelled fetch returns without falling back. The fallback is fetchForkChoiceWithFallback, unit-tested for each case. Uses the new ForkChoiceV2Provider from feat: add GET /eth/v2/debug/fork_choice and keep fork choice store extra data go-eth2-client#60 via the beacon node's Service(), so no ethpandaops/beacon change is needed.
  • Proto (additive, so no new event names, Kafka topics or Vector routes):
    • ForkChoiceNodeV2: parent_payload_status (same enum as payload_status: EMPTY=0, FULL=1, PENDING=2, matching the spec's PAYLOAD_STATUS_*), payload_attester_count, payload_availability_yes_count, payload_data_availability_yes_count, and the node's justified_checkpoint / finalized_checkpoint (whose epochs also fill justified_epoch / finalized_epoch, as for v1 nodes).
    • ForkChoiceV2.extra_data: the fork choice store's values (unrealized justified/finalized checkpoints, proposer boost root, …), kept from both endpoints.
  • Conversion: NewForkChoiceV2FromGoEth2ClientV2. AsGoEth2ClientV1ForkChoice round-trips the new fields (including the node checkpoints) into node extra_data and the store extra_data onto the v1 type, so existing consumers (forky 1.0.2) still see them. Also fixes a panic in that conversion for nodes without extra data (encoded as JSON null).

Verified

  • Unit tests for the v2 conversion (spec-shaped nodes, null parent payload status, checkpoints), the v1 round trip and the fallback decision; go test and golangci-lint (CI setup) pass.
  • Live, five branch sentries on Sepolia with fork choice every 20s for 150s: each tried v2 once and fell back to v1 (Lighthouse/Nimbus at debug; Teku parent payload status missing, Prysm/Lodestar fork choice data missing as one warning each), and every node kept producing BEACON_API_ETH_V1_DEBUG_FORK_CHOICE_V2 events from v1.

Until clients implement the merged spec, events come from v1, so the payload status forky needs is still missing, as it is today. It arrives automatically, within the retry interval, once a client's v2 response follows the spec.

Notes for rollout

  • Deploy forky first. Production forky (1.0.2) decodes events with strict protojson.Unmarshal, so the new fields (store extra_data, which Prysm and Nimbus send even on v1, and the node fields) would make it drop whole batches. forky release/glamsterdam-sepolia decodes with DiscardUnknown and understands the new fields; roll it out before the server/sentries.

  • Event size: v2 has a node per payload status. Teku's Sepolia event is ~425KB as protojson (under Kafka's default 1MB max.message.bytes), but fork choice trees grow during non-finality, so it's worth checking broker/topic and Vector/ingress body limits for the fork choice topics.

  • payload_status from the v1 extra_data heuristic is kept for any client that starts reporting it there.

  • go-eth2-client#60 decodes v2 strictly to the spec; its description has a table of where each client's current response falls short.

  • go-eth2-client is v0.1.10, which includes feat: add GET /eth/v2/debug/fork_choice and keep fork choice store extra data go-eth2-client#60.

🤖 Generated with Claude Code

Savid and others added 7 commits October 7, 2026 12:43
The sentry now fetches GET /eth/v2/debug/fork_choice
(ethereum/beacon-APIs#615), which returns one node per (block root,
payload status) pair, and falls back to v1 for beacon nodes that do not
serve it (retrying v2 hourly). Since Gloas, the v1 endpoint returns up to
three indistinguishable nodes per block on some clients, and none of
them report payload_status in extra_data, so ForkChoiceNodeV2's
payload_status was never set.

ForkChoiceNodeV2 gains the v2 fields: parent_payload_status and the
Payload Timeliness Committee vote counts. ForkChoiceV2 gains the fork
choice store's extra_data (e.g. the unrealized justified checkpoint),
from both endpoints. All additive, so event names, Kafka topics and
downstream routes are unchanged. Consumers converting to the
go-eth2-client v1 shape still see the new fields in extra_data, and a
node without extra data no longer panics that conversion.

Bumps github.com/ethpandaops/go-eth2-client to the fork choice v2
branch (ethpandaops/go-eth2-client#60).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up decoding unrecognised validities (Lighthouse's
not_yet_revealed for unrevealed Gloas payloads) as unknown instead of
failing the whole fork choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@redpandabot redpandabot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sentry now prefers GET /eth/v2/debug/fork_choice with an hourly-retried v1 fallback, and ForkChoiceV2 (proto + conversions) carries the Gloas parent_payload_status/PTC counts plus the store extra_data and the JSON-null panic fix. I traced the fetch/fallback control flow, every snapshot consumer, the v1↔v2 round trip, the enum mapping against consensus-specs and the pinned go-eth2-client constants, and the generated vtproto; the change is internally consistent and additive, and the dependency bump is clean on OSV.


Reviewed 11 changed file(s) @ ede1de64 — no blocking issues found.
"Debugging is twice as hard as writing the code in the first place." — Brian Kernighan

Savid and others added 5 commits October 8, 2026 13:51
Bump go-eth2-client to ethpandaops/go-eth2-client#60 as aligned with
ethereum/beacon-APIs#615 as merged:

- Nodes carry justified and finalized checkpoints. They are kept as the new
  ForkChoiceNodeV2 justified_checkpoint / finalized_checkpoint fields and,
  as before, as justified_epoch / finalized_epoch, and are round-tripped
  into extra_data for consumers of the v1 shape.
- Weight and the PTC counts are always set.

go-eth2-client now rejects v2 responses that do not follow the spec, which
is every client today. Any v2 failure, not only an unsupported endpoint, now
falls back to v1 for forkChoiceV2RetryInterval, so such nodes keep producing
fork choice data without a warning on every fetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@redpandabot

This comment has been minimized.

Review follow-ups:

- Back off to v1 for an hour only when the node lacks the endpoint or its v2
  response does not follow the spec (go-eth2-client's ErrInvalidResponse). A
  timeout, 5xx or connection error falls back to v1 for that fetch only, and
  a cancelled fetch returns without falling back.
- Move the fallback into fetchForkChoiceWithFallback and test it: v2 served,
  each failure kind, the back-off window, no provider, v1 errors and
  cancellation.
- Decoded v2 values take precedence over a client's extra_data under the
  same keys, for the counts and checkpoints as for payload_status.
- Test the null node extra_data round trip; note that a re-org event's
  before/after snapshots can come from different endpoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Savid added a commit to ethpandaops/forky that referenced this pull request Oct 8, 2026
go-eth2-client v0.1.10 decodes GET /eth/v2/debug/fork_choice strictly to
ethereum/beacon-APIs#615 as merged, so every node carries its checkpoints,
PTC counts and parent payload status.

- ForkChoiceFromV2 maps the spec's node checkpoints, Gwei weight and plain
  counts onto forky's nodes, then runs the shared NormalizeV2.
- Xatu events convert straight to forky's nodes, keeping values a sentry
  did not send unset (rather than zero) for NormalizeV2 to fill in. Node
  checkpoints from ethpandaops/xatu#900 are read, falling back to the epoch
  fields older sentries send.
- v1: Lighthouse's not_yet_revealed now decodes as its own validity; it is
  still shown as optimistic, as is the older unknown + extra_data form.
- The beacon node source backs off v2 for an hour only for nodes without the
  endpoint or with a non-spec response (ErrInvalidResponse); other failures
  fall back to v1 for that fetch.
- Empty extra data is normalised to nil, as forky stores it.
- Tests: a spec-shaped fixture derived from the Teku capture; the pre-spec
  client captures now exercise NormalizeV2 on partial nodes.

Bumps go-eth2-client to v0.1.10 and xatu to ethpandaops/xatu#900.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Savid
Savid merged commit 2be044a into master Oct 8, 2026
7 checks passed
@Savid
Savid deleted the feat/fork-choice-v2 branch October 8, 2026 05:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants