feat: add block_hash and builder_index to block event - #9854
markolazic01 wants to merge 11 commits into
Conversation
| const gloasFields = isGloasBeaconBlock(block.message) | ||
| ? { | ||
| blockHash: toRootHex(block.message.body.signedExecutionPayloadBid.message.blockHash), | ||
| builderIndex: block.message.body.signedExecutionPayloadBid.message.builderIndex, | ||
| } | ||
| : {}; | ||
| this.emitter.emit(routes.events.EventType.block, { | ||
| block: blockRootHex, | ||
| slot: blockSlot, | ||
| executionOptimistic: blockSummary != null && isOptimisticBlock(blockSummary), | ||
| ...gloasFields, | ||
| }); |
There was a problem hiding this comment.
can this be made type safe somehow?
There was a problem hiding this comment.
ah it probably is fine either way, I don't think there is a good way to enforce the event itself to require blockhash and builderindex after gloas
There was a problem hiding this comment.
I haven't found a better solution than this so far
| blockHash: stringType, | ||
| builderIndex: ssz.BuilderIndex, |
There was a problem hiding this comment.
nit: I would like these fields after block and before executionOptimistic
nflaig
left a comment
There was a problem hiding this comment.
just some quick drive-by comments 😄
|
thank you very much @nflaig 😃 |
|
There was e2e action fail which seems to be fixable with a re-run. |
nflaig
left a comment
There was a problem hiding this comment.
thanks @markolazic01 for looking into this, I am a bit skeptical after seeing the implementation if this is really a good direction, besides the fact that it's kinda implicit that clients should add these fields on the spec side, it seems also kinda error prone on the implementation side, and seeing that it looks a bit hacky even on our code I am not so certain it's a good direction for the spec
| fromJson: (json) => | ||
| (config.getForkSeq((json as {slot: Slot}).slot) >= ForkSeq.gloas ? blockGloas : blockBase).fromJson(json), | ||
| }, | ||
| [EventType.blockGossip]: new ContainerType( |
There was a problem hiding this comment.
kinda warrants the question if we should also update block_gossip event
There was a problem hiding this comment.
makes sense, we should probably do it
I agree, I am happy to reimplement this if you have another approach in mind, at least we discovered that this isn't the best way to do it. We can discuss a new solution on discord. |
## Motivation Split out from the block / builder event PRs (#9854, #9875, #9876) as a standalone change, as suggested by @markolazic01. The `eventstream` handler in `getEventsApi` forwards every emitter event through a single `onEvent({type: topic, message: data})` call, where `topic` is the full `EventType` union and `data` (and therefore `message`) is `any`. That object literal only type-checks via TypeScript's discriminated-union distribution path (`typeRelatedToDiscriminatedType`), which bails out once the number of source discriminant combinations exceeds 25. `EventType` currently has exactly **25** members, so it compiles today. Adding a **26th** event tips it over the cap and fails with: ``` TS2345: Argument of type '{ type: EventType; message: any; }' is not assignable to parameter of type 'BeaconEvent'. Types of property 'type' are incompatible. Type 'EventType' is not assignable to type 'EventType.<lastMember>'. ``` which is why each new-event PR currently has to add this cast. Landing it once here unblocks those PRs without each carrying the change. ## Description Assert `routes.events.BeaconEvent` at the `onEvent` call. The cast only makes explicit the type erasure that already exists at this `chain.emitter` boundary — `message` is `any`, so the topic/message pairing was never verified by the compiler regardless. The only cast-free alternative would be to construct the event per-topic instead of funneling every topic through one union-typed `onEvent({type, message})` call, which is a larger refactor not worth it here. **No runtime or current-compile behavior change** on `unstable` (25 `EventType` members). --- 🤖 Generated with AI assistance --------- Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
krisoshea-eth
left a comment
There was a problem hiding this comment.
One codec issue to account for if we keep this event variant.
| let gloasFields: undefined | {blockHash: string; builderIndex: BuilderIndex}; | ||
| if (isGloasBeaconBlock(block.message)) { | ||
| const builderIndex = block.message.body.signedExecutionPayloadBid.message.builderIndex; | ||
| if (builderIndex !== BUILDER_INDEX_SELF_BUILD) { |
There was a problem hiding this comment.
While comparing this PoC, I found that self-built Gloas blocks omit both new fields here, but the Gloas serializer requires them and throws. If we keep this variant, could we emit the block hash and SELF_BUILD index too, with a self-build round-trip test?
There was a problem hiding this comment.
ah, nice catch, thanks
if we choose this version the check should get fixed at minimum, think the easiest fix here really is to emit self built as well, but initially we decided not to emit on such scenario so we should discuss it first
## Motivation The Lodestar Builder needs a source beacon node API path for learning whether its bid was selected without joining libp2p. This PR implements the standard `block` SSE topic plus `getBlockV2` as a bounded compatibility path. Beacon APIs #630 and merged Lodestar #9832 also forward the signed winning block directly to the selected external Builder. The main purpose of that flow is to let the Builder help disseminate the block, with timely win notification as an additional benefit. Maintainer input is requested on whether this observer should remain as a compatibility fallback alongside direct delivery. ## Changes - Subscribe only to the standard `block` SSE topic. - Deduplicate roots before asynchronous work and retain a FIFO window of 256 roots. - Skip locally pre-Gloas slots and retrieve each new post-Gloas block by root with `getBlockV2`. - Use the Builder abort signal for both the SSE stream and block requests. - Retry 404, server, timeout, and non-input transport failures for up to six attempts with five 200 ms delays. Other 4xx responses, cancellation, decoding failures, input fetch errors, and structural failures are terminal. - Treat response version metadata as fork authority, require a Gloas-compatible body, and verify the returned slot against the event. - Preserve the exact signed bid, including later-fork fields, exact-width values, and `BUILDER_INDEX_SELF_BUILD`. - Dispatch observations concurrently through isolated `runOnBlock` callbacks. The existing API-client request timeout is unchanged. This PR bounds attempts and explicit retry delays, not total wall-clock or slot-relative time. Selection and reveal code will own deadline policy. The observer starts after the existing genesis, configuration, readiness, Gloas, and Builder identity gates. It joins the clock and `BuilderStatusTracker` as a long-lived Builder background service and shares their abort controller. This PR does not add p2p, `block_gossip`, canonical-chain filtering, local-bid matching, reveal behavior, metrics, reconnect, replay, restart recovery, multi-BN failover, or a new API endpoint. The observer intentionally evaluates blocks before SELECT-01 registers its first production consumer so this compatibility path remains active and evidenced. Event-time `executionOptimistic` comes from the triggering event, while the response metadata is authoritative for the fork. Terminally failed roots remain consumed until FIFO eviction; REL-01 owns controlled reconciliation. Aggregate retrieval concurrency, observer-specific metrics, and block-root recomputation before financial decisions are tracked in SEC-01, QA-01, and SELECT-01 respectively. ## API behavior and compatibility The standard `block` event contains the slot, beacon block root, and execution optimism. `getBlockV2` supplies the signed fork-correct block and `Eth-Consensus-Version` metadata. Imported non-head blocks remain valid observations, so `head` and `head_v2` are not substitutes. Lodestar emits `block` after state transition and fork-choice import. Root lookup checks fork choice for presence, then serves the block from the seen-block input cache or database. This ordering provides no expected Lodestar event-before-block window, but the Beacon API does not require equivalent ordering across clients, so bounded 404 retry remains a cross-client precaution. The implementation audit is recorded in merged [Builder docs PR #13](krisoshea-eth/lodestar-eip-7732-builder-docs#13). The provisional direct-Engine planning reconciliation is recorded in merged [Builder docs PR #18](krisoshea-eth/lodestar-eip-7732-builder-docs#18), the reproducible real-BN and shutdown evidence in merged [Builder docs PR #19](krisoshea-eth/lodestar-eip-7732-builder-docs#19), and the recent upstream PR audit in merged [Builder docs PR #20](krisoshea-eth/lodestar-eip-7732-builder-docs#20). Implementation evidence was posted to [beacon-APIs #599](ethereum/beacon-APIs#599 (comment)). Marco's open Lodestar PoCs [#9854](#9854), [#9875](#9875), [#9876](#9876), and [#9896](#9896) explore optional event improvements separately. The API-02 diff is limited to five Builder files. The latest review fixes build on Nico's updated branch at `09ea035863a5712eb417949f71e31c9d0f97f0fb`; no additional `unstable` merge was made for these fixes. Historical validation base: `f22c5ce63e`. Historical specification baseline: consensus-specs `v1.7.0-alpha.14`. Project issue: [krisoshea-eth#12](krisoshea-eth#12). ## Testing Validated locally on 2026-09-10 at `d74cf21de82d8ef4a8ad4b65627d2df131b42a16`, with Node 24.13.0 and pnpm 11.0.0: - 58 targeted tests across the observer, Builder lifecycle, identity, readiness and payload store. - Ordinary Builder package type-check. - Changed-file Biome. - Builder package build and module import check. - `git diff --check`. Dependencies were installed from this branch's lockfile and the Builder dependency packages were rebuilt in an isolated checkout. The new startup-log and warning regressions failed before the source changes and passed afterward. Coverage includes SSE wiring, cancellation, Gloas and Heze output, signed-bid preservation, slot consistency, duplicate suppression, bounded retry, decoding failures, FIFO eviction, self-builds, stream failures and callback isolation. These are local results, not a fresh CI or real-BN/EL run. The earlier real-BN and shutdown evidence remains in Builder docs PR #19. ## AI assistance disclosure - [x] External Contributors: I have read the contributor guidelines and disclosed my usage of AI below. > AI assistance was used for codebase research, implementation, testing, and review. I reviewed and revised the submitted code and PR text, made manual edits and technical decisions, and take responsibility for the final result.
|
merged to resolve a conflict |
Motivation
Adapting block event for Gloas PoC.
Description
Introduces 2 new fields,
block_hashandbuilder_indexto the block event.Related wiring and test adaptation.
Skipping oapi spec test for
blockevent until spec tests are updated with new event fields.AI Assistance Disclosure
Used Claude to audit the changes.