erigontech / erigontech/erigon

Caplin GLOAS fork choice: maintain per-node weights via delta propagation instead of a per-attestation vote index

Open
#21,704 4 comments 0 reactions 1 assignee Claimed by @domiwei View on GitHub
Caplin Glamsterdam performance
Dominant language
Go
Stars
3.6k
Forks
1.5k
Avg merge
1d 16h
Merged PRs (30d)
455

Description

## Background

#21694 wires GLOAS head selection (`getHeadGloas`) onto the incremental `indexedWeightStore`: votes are maintained in a `directVotes` index per attestation (`IndexVote`/`RemoveVote`), and weights are computed lazily by a subtree DFS per `GetWeight`. This is correct in steady state (differential test vs the full-scan store) and far faster than the previous O(V) full-scan — but it diverges from the canonical fork-choice design and keeps a per-attestation maintenance cost on the hot path.

#21698 removed that per-attestation index maintenance **pre-GLOAS** (where its result was never read). Under GLOAS it returns, because the index is maintained on every GLOAS attestation and `getHeadGloas` now reads it. #21694 trims it (allocation-free `RemoveVote`, no per-vote `getCheckpointState`), but `RemoveVote` is still an O(E) scan per attestation under the fork-choice lock.

## What Prysm and Lighthouse do (verified on `glamsterdam-devnet-5`)

Both use the proto-array / doubly-linked-tree model:

- **Per attestation: O(1), no weight work** — just record the validator's vote (current/next root + payload-present). Prysm `ProcessAttestation` updates `f.votes[index]`; Lighthouse `process_attestation` sets `vote.next_root` / `next_payload_present`.
- **Per head: delta propagation over a maintained per-node weight tree.** Lighthouse `apply_score_changes` computes per-validator deltas from vote *changes* and applies them to `node.weight`, propagated up — O(nodes + changed votes). Prysm `applyWeightChangesConsensusNode` / `...PayloadNode` recompute `weight = balance + Σ children` bottom-up.
- **`(root, payload_status)` handled per node.** Lighthouse: each `ProtoNode` carries `empty_delta` / `full_delta` and `attestation_score(payload_status)`. Prysm: a two-tier tree of consensus `Node` → `EMPTY`/`FULL` `PayloadNode`.

Net: votes stored per validator (cheap), per-node cumulative weights materialized incrementally; head = follow best-descendant; the debug dump is free.

## Proposal

Adopt the delta model for Caplin GLOAS fork choice:

1. Stop maintaining the `directVotes` index per attestation; keep votes in `latestMessages` (already O(1)/attestation).
2. Maintain a per-node cumulative weight (with Empty/Full payload-status variants), updated by deltas computed from `latestMessages` changes at head time and propagated over the filtered tree — O(nodes + changed votes).
3. `getHeadGloas` follows best-descendant over the maintained weights.

### Benefits

- Eliminates the per-attestation maintenance cost entirely (no `IndexVote`/`RemoveVote` on the hot path) under GLOAS as well as pre-GLOAS — finishing what #21698 started.
- Replaces the per-`GetWeight` subtree DFS with O(nodes) delta propagation.
- Materializes per-node weights → fixes the GLOAS `f.weights`/`ForkNodes` gap (`/eth/v1/debug/fork_choice` is currently empty under GLOAS because `getHeadGloas` doesn't populate `f.weights`).
- Matches the canonical design both major clients ship.
- Removes the interim transition seed (`seedFromLatestMessages` + the `seeded` flag) added in #21694: deriving weights from the continuously-maintained `latestMessages` leaves no cold index to seed.

### Cost / notes

- Larger rework: Caplin currently uses `latestMessages` + on-demand scoring, not a proto-array-style node tree; this adds a maintained weight tree.
- Not urgent: GLOAS isn't live and the #21694 index is a correct interim. Validate on a GLOAS devnet (head agreement vs a reference client + per-attestation cost under high-validator-count load).

## References

- Follows up #21694; addresses the perf concern raised in #21698.
- Current Caplin code: `cl/phase1/forkchoice/weight_store_indexed.go`, `get_head.go` (`getHeadGloas`), `on_attestation.go` (`setLatestMessage`).
- Prysm `glamsterdam-devnet-5`: `beacon-chain/forkchoice/doubly-linked-tree/{gloas.go,forkchoice.go,node.go}`.
- Lighthouse `glamsterdam-devnet-5`: `consensus/proto_array/src/{proto_array.rs,proto_array_fork_choice.rs}`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.