Skip to content

feat: add block_hash and builder_index to block event - #9854

Draft
markolazic01 wants to merge 11 commits into
ChainSafe:unstablefrom
markolazic01:feat/block-event-new-fields
Draft

markolazic01 wants to merge 11 commits into
ChainSafe:unstablefrom
markolazic01:feat/block-event-new-fields

Conversation

@markolazic01

@markolazic01 markolazic01 commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Adapting block event for Gloas PoC.

Description

Introduces 2 new fields, block_hash and builder_index to the block event.
Related wiring and test adaptation.
Skipping oapi spec test for block event until spec tests are updated with new event fields.

AI Assistance Disclosure

Used Claude to audit the changes.

Comment on lines 532 to 543
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,
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can this be made type safe somehow?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I haven't found a better solution than this so far

Comment on lines +85 to +86
blockHash: stringType,
builderIndex: ssz.BuilderIndex,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: I would like these fields after block and before executionOptimistic

@nflaig nflaig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

just some quick drive-by comments 😄

@markolazic01

Copy link
Copy Markdown
Contributor Author

thank you very much @nflaig 😃

@markolazic01
markolazic01 marked this pull request as ready for review August 19, 2026 17:15
@markolazic01
markolazic01 requested a review from a team as a code owner August 19, 2026 17:15
@markolazic01

Copy link
Copy Markdown
Contributor Author

There was e2e action fail which seems to be fixable with a re-run.
Unit tests pass now, oapi spec block test is skipped until the spec is updated.
Description updated.

@nflaig nflaig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

kinda warrants the question if we should also update block_gossip event

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

makes sense, we should probably do it

@markolazic01

Copy link
Copy Markdown
Contributor Author

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

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.

@markolazic01
markolazic01 marked this pull request as draft August 19, 2026 21:17
nflaig pushed a commit that referenced this pull request Aug 22, 2026
## 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 krisoshea-eth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

nflaig pushed a commit that referenced this pull request Sep 10, 2026
## 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.
@markolazic01

Copy link
Copy Markdown
Contributor Author

merged to resolve a conflict

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants