Repository navigation
feat(beacon): serve the Beacon API eventstream, GET /eth/v1/events - #626
Conversation
🤖 Codex Code ReviewI found 1 correctness issue worth fixing; otherwise the eventstream work looks solid. Findings
Why it matters
What looks good
Validation note
If you want, I can turn this into a ready-to-post GitHub review comment set. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview: PR 626 — Beacon API
|
🤖 Kimi Code ReviewI'll review this PR which adds Beacon API eventstream support ( Overall AssessmentThis is a substantial, well-structured PR that unifies lean and beacon event publishing. The architecture is sound: a persistent Critical Issues1. Potential Division by Zero / Modulo by Zero in
|
…36-638-gloas-live Brings gloas validator duties (produceBlockV4, envelope publication, PTC duties and payload attestations, gloas attestation data and aggregates, VC gloas support) onto the deployment branch, keeping every behavior of #626, #633, #636, #638, #646, #647-#652, #656, #658-#660 and the sync-committee and liveness endpoints. Conflict resolutions keep both sides: the attestation pool stays in Store (tmp) while the payload attestation pool is threaded through P2P and the RPC handles (feature); the aggregate endpoints keep tmp's liveness recording and attesting indices and add the feature's fork-header check and gloas pooling; the VC tests and fake execution client serve both fulu blobs and gloas V6. Semantic fixes: a. POST /eth/v2/beacon/blocks (gloas) calls publish_beacon_block(block, Vec::new()): gloas columns travel with the envelope. RecordingNetwork implements publish_beacon_block(block, sidecars) and both new methods. b. produceBlockV4 appends client versions to the graffiti exactly like produceBlockV3 (graffiti::execution_client_version run alongside the payload build, with_client_versions, Extension<OwnVersion>) and logs it. c. Attestation data, aggregate_attestation keep require_execution_client and require_validated for gloas slots; payload_attestation_data now applies the same two rules (503 without an execution client, or when the voted block's payload is unvalidated). d. Proposer duties v1 and v2 serve gloas epochs from a fulu or gloas state's proposer_lookahead; nothing refuses gloas any more; v2 keeps its dependent root. e. The VC's per-validator ProposerSettings apply to gloas proposals (graffiti in the BlockRequest, fee recipient compared with the bid's); a test pins the graffiti. VC tests updated to the ProposerSettings constructors. f. gloas production reads the attestation pool from Store and calls the stf with the ActiveBalanceCache the perf work added; fulu production and pack_operations are untouched (gloas blocks carry no pooled operations). g. Chain events are emitted by the chain actor only, so nothing on the RPC publish paths needed to move; gloas imports reach it unchanged. h. Cargo.lock unchanged; cargo check --locked passes.
A validator client and most tooling follow a beacon node through /eth/v1/events, and ethlambda beacon served none: the chain actor already had an event bus, but only the lean surface read it, and what it emitted on beacon was lean-shaped (bare integers, slots standing in for epochs) and missed every head move, since a beacon head moves in the head recompute that follows the import cascade, after the import's own snapshot diff. One bus still carries both chains' events. Topic gains every Beacon API name, and each surface accepts its own set: /lean/v0/events keeps its seven exactly, and /eth/v1/events accepts every specification topic (repeated or comma-separated) while emitting head, block, block_gossip, finalized_checkpoint, chain_reorg, attestation and data_column_sidecar, in the specification's payloads with quoted integers. Head and finality events come from a view the actor keeps between calls and diffs at the end of recompute_beacon_head, the one place every beacon head and finality move ends: a view cannot be missed the way a snapshot window was, and several moves between two diffs coalesce into one event. Any head change counts, including one no block caused, as Prysm reports it; heads more than 32 slots behind the wall clock are not reported. Reorgs are detected through the new head's post-state block_roots, and their depth walks the previous head back to the common ancestor. block_gossip now waits for the gossip verdict: NewBlock carries a BlockAnnouncement its sender sets, so a gossip block forwarded on Queue or Ignore(Overloaded) still imports but is not announced. Aggregates this node's own validator client submits reached neither its fork choice nor any event stream, since gossip never delivers a node its own messages. publish_beacon_aggregate now takes the attesting indices the Beacon API's checks resolved and hands the aggregate to the chain actor after gossiping it, as publish_beacon_block already does for blocks. The finalized root is exempt from prune_beacon_optimistic_roots, like the execution-hash cache beside it, so its execution_optimistic flag survives a skipped epoch boundary until the event and the REST handlers read it. Payloads are built only while a client is connected, whatever topics it asked for; per-topic subscriber counts are left for later.
…vents struct The chain actor carried the bus and the beacon view as two fields, with lean's snapshot diffs and the chain-specific payload building spread over its handlers. ChainEvents now holds the EventBus and everything that decides what goes out on it: the subscriber guard, which chain's payload an event takes, lean's snapshot diff around a store call, and beacon's persistent view. The actor keeps one field and only says what happened. The EventBus inside stays the cloneable handle main gives the HTTP server, which only subscribes. ChainEvents is the actor's alone, so nothing outside it can move the beacon view or publish. ChainEventSnapshot and BeaconEventView keep their logic, now private behind it. No behavior change.
…every event in ChainEvents Lean still captured a snapshot before each store call and diffed after it, while beacon diffed a view kept between calls. Lean now diffs the same view, after the same three store calls (tick, block import, the proposer's pre-build catch-up), so both chains follow one rule: whatever moved since the last diff is reported by the next one, once. On lean that reports exactly what the snapshots did, since its head and checkpoints move only inside those calls, but a later path that moves them elsewhere can no longer be silently folded into the next window's baseline. With nobody subscribed, a lean diff now reads only the view's three roots, as a beacon one does. The actor's last direct emits (lean attestations and aggregates, beacon data columns) are ChainEvents methods as well, so the actor never builds a ChainEvent and the bus is only reachable through ChainEvents.
A lean subscriber saw a head move to another branch only as a plain head event, with nothing saying the previous head was abandoned. Lean now accepts and emits chain_reorg: beacon's fields less epoch and execution_optimistic, with lean's bare integers. It means what the beacon one does: the new head does not descend from the previous one, and depth is how many slots the previous head sat above the common ancestor. The lean diff finds that ancestor by walking both heads' parent links, bounded by the finalized checkpoint the previous diff saw. A lean header is cheap to read, and a head that just moved forward meets the old head in a step or two. Like beacon's, the event goes out ahead of the head it moved to, is not gated on recency, and costs nothing with nobody subscribed. The beacon payload is renamed BeaconChainReorg(BeaconChainReorgEvent) so the lean variant takes the unprefixed name, as every other lean variant does.
c258a8e to
af4e0ef
Compare
…#634) Squash of feat/beacon-gloas at 1d667c4 (merge-base 35c074b). Adapted onto #626-#633 and main (#642-#677): - #632/#633's epoch single pass ported to the element accessors; fulu runs through the single pass, with the eth1-bridge gate on electra only. - DB_VERSION 6 (gloas Config 5 + #628's table). - compute_node_weights skips the boost outside the justified subtree (boost_reaches_the_descent), for #627's LiveChain prune; the boosted-finalized-block bound test is dropped and the differential tests skip pruned roots. - #628's set_unrealized_justification slot argument in the tests; get_forkchoice_store keeps the anchor block's slot. - #653's validator-by-id handler on iter_validators/balance.
Squash of feat/beacon-gloas-live at d1ef2fe (merge-base d91c6e5). Adapted onto #626-#638 and main (#642-#677): - 13 tables (#628's BeaconUnrealizedJustifications beside the gloas two). - #626's event bus, test stand-ins and BlockAnnouncement carry the gloas sidecar and the two gloas chain-actor messages. - The Beacon API's optimism readers (syncing, fork, validator-by-id, sync duties, require_validated) go through block_is_optimistic. - #643's liveness recorder ignores envelopes and payload attestations.
Squash of feat/beacon-blobs-and-operations at b0af5bc (merge-base 35c074b). Adapted onto #626-#639: - The BLS fixture tests move to bls_fixtures.rs with #634's cryptography-specs layout; #634's Makefile replaces the gossip tree. - Gloas: operations under a gloas head are IGNOREd and a gloas-shaped attester slashing has no consumer; gloas operation vectors are not run. - Published columns and operations go out under the slot's own digest; column subnets use compute_subnet_for_data_column_sidecar. - Sidecars are the fulu/gloas DataColumnSidecar enum through the RPC.
…ator client (#662) Squash of feat/beacon-gloas-validator-duties at 55adf1b (merge-base d1ef2fe). Adapted onto #626-#659 and main's #653: - Gloas proposer duties come from the lookahead, v1 and #653's v2 alike. - Gloas blocks gossip no sidecars; the payload-attestation data endpoint follows attestation data's 503s; #638's graffiti and client version reach gloas block production.
## 🗒️ Description / Motivation
Merges `beacon-chain-integration` into `main`. This adds an **Ethereum
Beacon Chain** client (phase0 through fulu) to the ethlambda binary,
alongside the Lean consensus client:
- **`ethlambda beacon`**: a beacon node. It checkpoint-syncs, follows
the chain over gossip and req/resp, validates execution payloads through
an execution client's Engine API, and serves the Beacon API, including
the validator endpoints.
- **`ethlambda validator`**: a beacon validator client (attest,
aggregate, propose) that talks to any beacon node over the standard REST
API.
- **`ethlambda node`** (lean) keeps its behavior, apart from the items
under "Behavior changes for lean" below.
Both chains run on the same storage layer, P2P actor, RPC crate and
chain actor. `BeaconState` has a `Lean` variant holding lean's `State`,
and each shared handler dispatches on it once, at the top.
Each piece was reviewed as its own PR into the integration branch
(listed under "Related Issues / PRs"). This PR merges that branch.
## What Changed
| Area | Change |
|---|---|
| `ethlambda-types` (`beacon/`) | Containers for phase0 to fulu, presets
(compile-time `preset-minimal` feature), runtime fork config, and a
`Lean` variant on `ForkName` and `BeaconState` |
| `ethlambda-state-transition` (`beacon/`) | State transition, epoch
processing, fork upgrades, fork choice, BLS (`blst`), KZG (`c-kzg`),
gossip validation rules, DAS custody math |
| `ethlambda-ssz-tree` (**new**) | Persistent Merkle `List`/`Vector` for
the validator registry and balances, so consecutive states share
unchanged subtrees |
| `ethlambda-storage` | One `Store` for both chains. Beacon states are
stored as snapshots plus VCDIFF deltas and written on a background
thread. Adds a committee cache, and `db_version`/`chain`/`preset`
metadata that resume checks |
| `ethlambda-blockchain` | Beacon path in the chain actor
(`BlockChain::spawn_beacon`): import, holding blocks until their parent
or columns arrive, applying gossip aggregates to fork choice, block
production and aggregation |
| `ethlambda-engine` (**new**) | Engine API client with JWT auth:
`newPayload`, `forkchoiceUpdated`, `getPayload`, `getBlobs` |
| `ethlambda-p2p` | Beacon wire: blocks, aggregates, subnet attestations
and data columns are validated before relay;
`beacon_blocks_by_{range,root}/2` served and requested; range sync;
column custody and sampling; attestation subnet backbone; discv5 always
on for `beacon` |
| `ethlambda-rpc` | Beacon API under `/eth/v1` and `/eth/v2`: JSON by
default, SSZ on request. The router is picked by `Store::chain()`, so
`/lean/v0` and the Beacon API are never served together |
| `ethlambda-validator` (**new**) | Validator client: duties,
attestations, aggregation selection, proposals, keymanager API. It has
**no slashing protection** (recorded in `docs/spec_deviations.md`) |
| `bin/ethlambda` | `beacon` and `validator` sub-commands; `--network`
takes a built-in name (`mainnet`, `sepolia`, `hoodi`) or a directory of
published network files; beacon checkpoint sync; `benchmark import`
replays a real block corpus offline; `run_node` is one startup path for
both chains |
| CI / Makefile | One beacon spec-test job per preset, minimal-preset
check, `make test` split into `test-consensus` and `test-node` (one
runner's disk no longer fits both), benchmark-smoke and tooling jobs,
disk-cleanup action |
| Docs | New: `beacon_stf.md`, `beacon_wire.md`, `beacon_engine.md`,
`cli.md`. Updated: `data_storage.md`, `metrics.md`, `rpc.md`,
`checkpoint_sync.md`, `discovery.md`, `benchmarking.md`,
`spec_deviations.md`, README (beacon quickstart with ethrex) |
## Correctness / Behavior Guarantees
- **Separation:** beacon behavior lives in `beacon` modules that lean
code never reads, and those modules never read lean's. The shared crates
dispatch on the chain once per handler.
- **Lean state at a beacon accessor:** this panics, naming the function
it reached (`lean_state_unreachable` / `lean_fork_unreachable`), so the
caller never gets a wrong answer back. `ForkName::Lean` is left out of
`ForkName::ALL`, so neither `parse` nor a fork upgrade can reach it.
- **Spec conformance:** every consensus-specs `v1.6.1` fixture case
passes on both presets: 5705 mainnet and 40009 minimal, including 150
mainnet `fork_choice` cases. Gossip fixtures are pinned to
`v1.7.0-beta.1`.
- **Resume safety:** a data directory written by another chain, preset
or format version is refused at startup, before any state is read.
### ⚠️ Behavior changes for lean (`ethlambda node`)
- **Existing data directories are refused on resume.** A directory
written by `main` has no `db_version` key, so `Store::from_db_state`
returns `DbVersionMismatch { found: 0, expected: 4 }`. There is no
migration: an upgraded node needs a fresh data directory (or checkpoint
sync).
- `blst` and `c-kzg` are now dependencies of the lean binary too, since
the state-transition crate holds both chains' rules. They are not
feature-gated.
- libssz is a git dependency pinned by `rev`: the beacon containers need
`DerefMut`/`IndexMut` on `SszList` (lambdaclass/libssz#33), which no
crates.io release has yet.
- The `release-fast` profile (used for tests) now sets `debug-assertions
= true`.
- `P2P::spawn` now runs before the chain actor is spawned, so gossip
arriving in that short window is dropped.
- `/lean/v0/blocks/finalized` answers JSON when asked for it by name.
SSZ stays the default.
## Tests Added / Run
- **Beacon spec tests:** `make test-beacon` (or `test-beacon-mainnet` /
`test-beacon-minimal`). Each fixture case is a separately named test,
e.g. `cargo test -p ethlambda-state-transition --test beacon_spec_tests
--features beacon-spec-tests -- electra/attester_slashing`. CI runs one
job per preset.
- **`make test`** still needs no consensus-spec download: the beacon
spec target is behind the `beacon-spec-tests` feature, and the BLS/KZG
vector tests report as ignored without it.
- **Minimal preset:** the `Test minimal preset` CI job builds and runs
the `ethlambda-types` unit tests with `preset-minimal` on.
- **Benchmark smoke:** a CI job runs the `benchmark` sub-command end to
end.
- Unit and integration tests in every new crate (`ssz-tree`, `engine`,
`validator`) and in the beacon paths of storage, p2p, rpc and the chain
actor.
## Related Issues / PRs
<details>
<summary>PRs merged into the integration branch (reviewed on
<code>lambdaclass/ethlambda_private</code>)</summary>
**Types and state transition**
- lambdaclass/ethlambda_private#1 Beacon Chain containers under a
`beacon` namespace
- lambdaclass/ethlambda_private#2 Beacon Chain state transition, phase0
through fulu
- lambdaclass/ethlambda_private#37 keep the registry and balances in
persistent Merkle trees
**Follower, sync and storage**
- lambdaclass/ethlambda_private#3 follow mainnet gossip behind an
`ethlambda beacon` sub-command
- lambdaclass/ethlambda_private#6 unify the lean and beacon storage
layer
- lambdaclass/ethlambda_private#7 checkpoint sync for the beacon chain
- lambdaclass/ethlambda_private#8 serve beacon blocks by range and by
root
- lambdaclass/ethlambda_private#9 give the follower a chain, from the
anchor to the tip
- lambdaclass/ethlambda_private#11 fulu data availability sampling,
custody through the availability gate
- lambdaclass/ethlambda_private#13 validate execution payloads over the
Engine API
- lambdaclass/ethlambda_private#15 follow a network described by files
on disk
- lambdaclass/ethlambda_private#26 add sepolia and hoodi as built-in
networks
- lambdaclass/ethlambda_private#48, lambdaclass/ethlambda_private#52,
lambdaclass/ethlambda_private#53, lambdaclass/ethlambda_private#54: held
blocks, their columns and range batches
**P2P**
- lambdaclass/ethlambda_private#18 stop outbound req/resp requests
crossing protocols
- lambdaclass/ethlambda_private#23 rank dial candidates by custody
redundancy
- lambdaclass/ethlambda_private#28 count why peers drop us
- lambdaclass/ethlambda_private#38 validate beacon gossip before
propagating it
- lambdaclass/ethlambda_private#43 run every data column check in p2p
- lambdaclass/ethlambda_private#47 validate beacon aggregates and subnet
attestations before relaying
- lambdaclass/ethlambda_private#19 apply gossip aggregates to fork
choice, backbone two attestation subnets
**API and validator**
- lambdaclass/ethlambda_private#21 serve the Beacon API from `ethlambda
beacon`
- lambdaclass/ethlambda_private#44 report the preset and constants from
`/eth/v1/config/spec`
- lambdaclass/ethlambda_private#16 beacon-chain validator client
- lambdaclass/ethlambda_private#34 serve the validator API
- lambdaclass/ethlambda_private#40 aggregate and propose through
`ethlambda beacon`
- lambdaclass/ethlambda_private#56 pack other nodes' attestations into
produced blocks
**Performance and metrics**
- lambdaclass/ethlambda_private#10, lambdaclass/ethlambda_private#20,
lambdaclass/ethlambda_private#22, lambdaclass/ethlambda_private#24,
lambdaclass/ethlambda_private#33, lambdaclass/ethlambda_private#49:
registry scans, parallel pubkey validation, committee cache, background
state writes, BLS key validation
- lambdaclass/ethlambda_private#17, lambdaclass/ethlambda_private#31:
import timing by phase
- lambdaclass/ethlambda_private#25 benchmark block import by replaying a
real corpus
**CI**
- lambdaclass/ethlambda_private#12, lambdaclass/ethlambda_private#30,
lambdaclass/ethlambda_private#45: split lint/test jobs, benchmark smoke
job, runner selection
</details>
Open PRs based on `beacon-chain-integration`, **not** included here:
lambdaclass#626, lambdaclass#627, lambdaclass#629, lambdaclass#630, lambdaclass#632, lambdaclass#634, lambdaclass#636, lambdaclass#638, lambdaclass#642, lambdaclass#644 to lambdaclass#652,
lambdaclass#656, lambdaclass#658.
## ✅ Verification Checklist
- [ ] Ran `make fmt` (clean)
- [ ] Ran `make lint` (clippy with `-D warnings`, clean)
- [ ] Ran `make test` (`test-consensus` plus `test-node`, at
`release-fast`), all passing
- [ ] Ran `make test-beacon` (both presets), all passing
---------
Co-authored-by: Pablo Deymonnaz <pdeymon@fi.uba.ar>
Co-authored-by: Klaus Lungwitz <klaus.lungwitz@lambdaclass.com>
Co-authored-by: Klaus @ LambdaClass <18153834+klaus993@users.noreply.github.com>
Co-authored-by: Chase Wright <chase.wright@ethereum.org>
Main's eventstream (#626) added only_an_accepted_gossip_block_is_announced, written when every gossip block reached the chain actor whatever its verdict. This branch forwards a block only on Accept or Queue, the verdicts that mean its signature verified, so the Overloaded block in that test is now dropped rather than handed on silently.
🗒️ Description / Motivation
ethlambda beaconserved no Beacon API eventstream. The chain actor already had an event bus, but only/lean/v0/eventsread it, and on beacon it had three problems:slotstanding in forepoch.headevent ever fired for a block import. A beacon head moves inrecompute_beacon_head, which runs after the import cascade. By then the import's own snapshot diff has already run, and the next tick's snapshot starts from the head that already moved.block_gossipfired for blocks forwarded onQueueorIgnore(Overloaded), before gossip validation had passed.This PR adds
GET /eth/v1/eventsfor validator clients and tooling.What Changed
Topics and payloads (
crates/blockchain/src/events.rs)Topicgains every Beacon API name. Each surface accepts its own set throughTopic::parse_accepted:Topic::LEAN: the same seven as before, pluschain_reorg.Topic::BEACON: the 24 names in the specification, plusblob_sidecarfrom its last release.ChainEventvariants carry the specification's payloads with quoted integers:BeaconHead,BeaconBlock,BeaconBlockGossip,BeaconFinalizedCheckpoint,ChainReorg,BeaconAttestation(boxed),DataColumnSidecar.EventBus::has_subscribers(). Payloads that cost something to build are skipped when no client is connected.One publisher struct (
ChainEvents,crates/blockchain/src/events.rs)events: ChainEvents. It holds theEventBusplus everything that decides what goes out on it: the subscriber guard, which chain's payload shape an event takes, and the view both chains' head and checkpoint events diff against (below).ChainEventsmethod (publish_block,publish_block_gossip,publish_lean_attestation, ...), so the actor never builds aChainEventitself.EventBusinside stays the cloneable handle the HTTP server subscribes through.ChainEventsbelongs to the actor alone.Head and finality events (
EventView)ChainEventskeeps a view (head root, justified and finalized checkpoints) between calls. Beacon diffs it at the end ofrecompute_beacon_head, where every beacon head or finality move ends: the tick, the import cascade, and theINVALIDforkchoiceUpdatedpath inside it.blockfor each block as it imports, thenchain_reorg,head,finalized_checkpoint.block_roots. The depth walks the previous head back to the common ancestor.headis gated to within 32 slots of the wall clock.chain_reorgandfinalized_checkpointare not gated.block_gossipwaits for the verdict (crates/net/api,crates/net/p2p)new_blockcarries a newBlockAnnouncement { Announce, Silent }, set by the sender.Announcefor three senders: a beacon gossipAccept, a block published through the Beacon API, and lean gossip (unchanged). Everything else isSilent.BlockSourceand its metric labels are unchanged.Own aggregates reach the chain actor (
crates/net/rpc/src/beacon/pool.rs,gossipsub/handler.rs)post_aggregate_and_proofskeeps the attesting indicesstateful_checksresolves.publish_beacon_aggregatetakes those indices and hands the aggregate to the actor after gossiping it, aspublish_beacon_blockdoes for blocks. Gossip never delivers a node its own messages, so before this these aggregates reached neither fork choice nor any event stream.HTTP (
crates/net/rpc/src/beacon/events.rs,crates/net/rpc/src/events.rs)topicsis accepted repeated (?topics=a&topics=b) or comma-separated. Duplicates collapse.{"code":400,"message":"Invalid topic: …"}. This needs a newApiError::BadRequestDetail(String).X-Accel-Buffering: no.BeaconApiHandles.events.Storage
prune_beacon_optimistic_rootsexempts the finalized root, likeprune_beacon_el_block_hashesbeside it. Without this, the finalized block'sexecution_optimisticwas lost when the epoch boundary slot was skipped.Docs
docs/rpc.md: a newGET /eth/v1/eventssection covering accepted vs emitted topics, payloads, order, gating and coalescing.CLAUDE.md: the HTTP-servers paragraph no longer says the beacon RPC takes no bus.Correctness / Behavior Guarantees
single_attestation: subnet votes never reach the actor.payload_attributes, the light-client topics andblob_sidecar: no producer.head_v2and the other gloas topics: this build stops at fulu.headfires on any head change, including one no block caused (Prysm does the same; Lighthouse sends onlychain_reorg). Optimistic heads are emitted with the flag set (Prysm does the same; Lighthouse suppresses them).on_gossip_beacon_aggregate: covered-bits dedupe, then the one-slot hold. They are also counted in that path's mailbox-wait and outcome metrics./lean/v0/eventsgainschain_reorg(beacon's fields lessepochandexecution_optimistic, bare integers). Its ancestor is found by walking both heads' parent links, bounded by the previous finalized checkpoint. Its other seven topics and payloads are unchanged, and any other beacon-only name (data_column_sidecar, say) is still refused as unknown.Tests Added / Run
apis/eventstream/index.yaml(beacon-APIsa3f0654).as_str/FromStr.headonly;chain_reorgwith the right depth, thenhead;epoch_transitionand the dependent roots;block_gossipfollows the sender's announcement; only an accepted gossip block is announced (p2p); a published aggregate reaches the chain actor with its indices (p2p); the RPC passesstateful_checks' indices on.Commands run:
make fmt,make lintcargo test --profile release-fast --libforethlambda-blockchain,-p2p,-rpc,-storagecargo test -p ethlambda --binscargo test -p ethlambda-rpc --test http_serversNot run: leanSpec spec tests, and a manual
curl -Nagainst a live follower.Related Issues / PRs
beacon-chain-integration@c79fabd5, merged into the branch.✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(test-consensusplustest-node, atrelease-fast) — unit tests only (--lib --bins, plushttp_servers); spec tests left to CI