Skip to content

🤖 [claude] Add Gloas (EIP-7732 ePBS) support - #40

Merged
fschoell merged 11 commits into
mainfrom
feat/gloas
Oct 6, 2026
Merged

fschoell merged 11 commits into
mainfrom
feat/gloas

Conversation

@Johnaverse

@Johnaverse Johnaverse commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The poller fails on every Gloas block with unimplemented spec, so all beacon Firehose readers stop at the Glamsterdam fork. Sepolia forks at epoch 353024 (slot 11,296,768, 2026-10-06 13:53:36 UTC). Our readers there are sepolia-sfdm45c, sepolia-sfdm46c and sepolia-sfdm813c.

Gloas (enshrined proposer-builder separation) moves the execution payload out of the beacon block:

  • The block carries only the builder's signed bid.
  • The builder reveals the payload later in a signed execution payload envelope.
  • The builder may withhold the payload, in which case the chain builds on the empty slot.

Block model

  • New types: Spec.GLOAS = 8, plus a new GloasBody in the Block oneof (field 27).
  • GloasBody fields:
    • the usual block fields;
    • signed_execution_payload_bid;
    • payload_attestations;
    • parent_execution_requests.
  • execution_payload_envelope: holds the revealed payload, which adds block_access_list (EIP-7928) and slot_number (EIP-7843), along with its execution requests.
    • It is set only when the canonical chain built on that payload.
    • When it is unset, no blobs are embedded.
  • ExecutionRequest: gains builder_deposits and builder_exits (fields 4 and 5). This is additive, so existing consumers are unaffected.

When the envelope is included

The next canonical block's bid decides it:

  • If its parent_block_hash equals this block's bid block_hash, the payload is canonical and the envelope is included.
  • Otherwise the envelope is left out, even when the node happens to hold one.

This keeps the output identical across readers, so the merger never sees two versions of the same block. Because of this, from the Gloas fork slot the poller stays one slot behind head. The fork slot comes from /eth/v1/config/spec, so chains without a Gloas fork scheduled behave exactly as before.

Blobs and timestamps

  • Blobs come from /eth/v1/beacon/blobs:
    • Lighthouse v8.3.0-rc.0 rejects Gloas blocks on blob_sidecars, because the commitments moved into the bid.
    • Nimbus returns 410 on blob_sidecars, since beacon-APIs#577 removed that endpoint.
  • The blobs are paired with the bid's commitments. That endpoint returns no KZG proofs or inclusion proofs, so those fields are left empty.
  • Gloas block timestamps are computed as genesis + slot × SECONDS_PER_SLOT, because the payload may be absent.

Dependency

go-eth2-client has no tagged release with Gloas yet. This PR pins its actively maintained gloas branch, v0.29.1-0.20260930050333-77b260ef4472. We should move to a tag once one is published.

Test plan

All tests ran on the build server with Go 1.26.8. The protobuf code was regenerated with protoc 32.1 and protoc-gen-go v1.36.6, the same versions as the existing file.

  • go build ./..., go vet ./... and gofmt -l . are clean.
  • Live Gloas data. TestGloasFetchAgainstBeaconNode ran against glamsterdam-devnet-8 (https://beacon.glamsterdam-devnet-8.ethpandaops.io, Nimbus, Gloas since epoch 1536). It is skipped unless FIREBEACON_GLOAS_BEACON_URL is set.
    • Recent 64 slots: 55 with payload, 9 skipped, 69 blobs.
    • Slots 351200–351249: 38 with payload, 4 without, 8 skipped, 31 blobs.
      • The four slots without a payload (351206, 351207, 351244, 351247) match an independent scan of parent_block_hash against block_hash over the last 3000 slots.
    • Per-block checks:
      • payload hash equals the bid hash;
      • slot_number equals the slot;
      • the block timestamp equals the payload timestamp;
      • the blob count matches the commitments, and each blob is 131072 bytes with its matching commitment;
      • the envelope is present if and only if the next block builds on it.
  • No regression before the fork. The main build and this branch ran over the same 39 Fulu slots against our Sepolia Lighthouse v8.3.0-rc.0 (sepolia-sfdm44). The FIRE BLOCK lines were byte-identical, with no errors.
  • Live mode. The firebeacon fetch http binary ran at devnet-8 head for 150 s and emitted blocks one to two slots behind head.
    • Two fetches of the next block over the internet hit the 5 s --max-block-fetch-duration and succeeded on retry. A Gloas block costs about five requests, which is fine for a Lighthouse on the LAN.
  • Not yet tested against a Lighthouse node past the Gloas fork. The public devnet Lighthouse endpoints are not reachable. Lighthouse v8.3.0-rc.0 source serves GET /eth/v1/beacon/execution_payload_envelopes/{block_id} and Gloas blobs on /eth/v1/beacon/blobs.
  • Before 2026-10-06: build and release a package, then deploy it to the Sepolia CL readers.

Block IDs and payload-status reorgs

Whether block N's payload is canonical is decided by its child, which is itself not final when N is emitted. A reorg of the child can flip that decision while N's beacon root stays the same, which firehose-core could not detect, since it tracks forks by block ID and parent ID.

From the Gloas fork the Firehose block ID therefore encodes the payload status: it is the beacon block root when the payload was included, and a domain-separated hash of the root otherwise. The parent ID is derived from the child's bid, which tells whether the parent's payload was built on. A flipped decision shows up as a parent ID mismatch and the poller refetches N like any other fork. Full-payload blocks, the normal case, keep their root as the ID.


🤖 Generated with Claude Code

https://claude.ai/code/session_01YHvxpuVpZfzefJgwgqjAzn

Gloas blocks no longer carry the execution payload. They carry the
builder's signed bid, and the builder reveals the payload separately in a
signed execution payload envelope. The poller rejected every Gloas block
with "unimplemented spec", so it would stop at the fork (Sepolia
2026-10-06, epoch 353024).

The new GloasBody keeps the block fields plus the bid, payload
attestations and parent execution requests. It embeds the revealed
envelope (payload with block access list and slot number, execution
requests) only when the canonical chain built on it. The next canonical
block's bid decides that: its parent_block_hash either equals this
block's bid block_hash (payload canonical) or not (builder withheld the
payload or the chain built on the empty slot). Deciding from the chain
instead of from whether the node happens to hold an envelope keeps the
output identical across readers. From the Gloas fork slot (read from
/eth/v1/config/spec) the poller therefore stays one slot behind head.

Blobs come from /eth/v1/beacon/blobs, because Lighthouse rejects Gloas
blocks on blob_sidecars and Nimbus has removed that endpoint (410). They
are paired with the bid's commitments; that endpoint has no proofs.
Gloas block timestamps are derived from the slot, since the payload may
be absent.

go-eth2-client has no tagged release with Gloas yet, so this pins its
gloas branch (77b260ef4472). Pre-Gloas output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHvxpuVpZfzefJgwgqjAzn
@Johnaverse
Johnaverse requested a review from fschoell October 1, 2026 07:55
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHvxpuVpZfzefJgwgqjAzn
@Johnaverse

Copy link
Copy Markdown
Contributor Author

Verification against our own -cl endpoints

None of our beacon endpoints is past Gloas yet.

  • Sepolia is the only one with Gloas scheduled: epoch 353024, slot 11,296,768, 2026-10-06 13:53:36 UTC.
  • eth, hoodi, gnosis and chiado (Lighthouse v8.2.3) report GLOAS_FORK_EPOCH = 2^64-1.

So the Gloas decode path has been exercised on glamsterdam-devnet-8 only, as described above. Everything else was confirmed against the endpoints the readers actually use. sepolia-sfdm45c/46c/813c poll the Caddy beacon gateway sepolia-cl.beacon.{kan,riv}-prod-rpc-a.pinax.io, which serves Lighthouse v8.3.0-rc.0.

On our Lighthouse v8.3.0-rc.0 (directly and through both gateways), using the code in this PR:

  • The spec gives a Gloas fork slot of 11296768 and a 12 s block time.
  • GET /eth/v1/beacon/execution_payload_envelopes/{id} reaches Lighthouse through the gateway. Before the fork it returns Lighthouse's own 404 NOT_FOUND: execution payload envelope for block root ….
  • fetchBlobs (/eth/v1/beacon/blobs) decodes blobs byte-identical to /blob_sidecars: 12 slots and 100–102 blobs per endpoint.

Lighthouse v8.3.0-rc.0 source, field by field: the JSON field names match the go-eth2-client gloas types this PR decodes. Checked types:

  • BeaconBlockBodyGloas: signed_execution_payload_bid, payload_attestations, parent_execution_requests.
  • ExecutionPayloadBid.
  • ExecutionPayloadEnvelope and its signed wrapper.
  • PayloadAttestation, PayloadAttestationData.
  • ExecutionRequestsGloas, including builder_deposits and builder_exits.
  • ExecutionPayloadGloas, including block_access_list and slot_number.

No regression before the fork. I ran the main build and this branch side by side through the kan gateway, starting 40 slots back and running for 120 s each. The FIRE BLOCK lines were byte-identical:

network common blocks differing
eth 51 0
hoodi 43 0
gnosis (5 s slots, 16 slots/epoch) 64 0
chiado 62 0
sepolia 51 0

The one error in the new run on eth is the existing failed to fetch block header … 404 race. The gateway load-balances, so the header request can land on a backend one block behind, and the retry succeeds. The main build hit the same class of failure twice in its run.

Review notes

  • Pruned blobs. With pruned data columns, Lighthouse answers /blobs with 400 Insufficient data columns, not 404. --ignore-missing-blobs, which only matches 404, will not skip it. The existing Fulu blob_sidecars path goes through the same Lighthouse function and has the same gap. This only matters for backfills, not live readers.
  • Requests per block. A Gloas block costs one extra signed-block fetch (the next block) plus the envelope. Behind the load-balanced gateway, a backend that has not yet seen the next block or the envelope produces an error and a retry, never a wrong decision:
    • a missing next block hits "no block after slot";
    • a missing envelope gives a 404 on a payload the chain has accepted.
  • spkg. The package is bumped to v0.8.0 for the new messages. Publishing it is a separate step.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YHvxpuVpZfzefJgwgqjAzn

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Fork detection uses the wrong runtime type, and skipped child slots can permanently stall live polling.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Adds Gloas/ePBS support so Firehose readers can continue past the Glamsterdam fork.

Changes:

  • Adds Gloas protobuf models and generated bindings.
  • Fetches canonical payload envelopes and blobs using child-block bids.
  • Updates dependencies, tests, documentation, and package version.
File Description
README.md Documents Gloas support.
blockfetcher/​block.go Converts Gloas blocks.
blockfetcher/​gloas.go Maps Gloas data to protobuf.
blockfetcher/​gloas_integration_test.go Adds live and readiness tests.
blockfetcher/​http.go Fetches Gloas payloads and blobs.
go.mod Updates Gloas-capable dependencies.
go.sum Updates dependency checksums.
pb/​sf/​beacon/​type/​v1/​type.pb.go Regenerates Go protobuf bindings.
proto/​sf/​beacon/​type/​v1/​type.proto Defines Gloas data models.
substreams/​substreams.yaml Bumps package version.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread blockfetcher/http.go
Comment thread blockfetcher/http.go
fschoell and others added 2 commits October 1, 2026 10:57
Encode the execution payload status in the Firehose block ID of Gloas
blocks (root if the payload was included, a hash derived from the root
otherwise) and derive the parent ID from the child's bid, so that a
reorg of the payload status after a block was emitted is detected by
firehose-core like any other fork.

Wait for the head to advance when no block exists after the requested
slot instead of failing, as the failure was retried against the same
stale head forever.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- treat a blob response with fewer blobs than commitments like pruned
  blobs, honouring --ignore-missing-blobs
- take the block time from SECONDS_PER_SLOT instead of deriving it from
  the head execution payload timestamp
- cache the child block looked up by the payload check so the next slot
  does not fetch it again, validating it against the current header
- reuse the Electra attestation converters for the Gloas types
- share the missing-blobs error handling between both fetch paths
- fix the integration test guards for a fork at genesis and for a node
  without a scheduled Gloas fork

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Signed block and header requests can straddle a reorganization and produce an internally inconsistent Firehose block.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
Resolved since last review (1)

Comment thread blockfetcher/http.go Outdated
Comment thread README.md
Request the block header first and the signed block by its root instead
of by slot, so that a reorg between the two requests cannot pair the
header of one block with the body of another. The cached child block is
accepted only if its root matches the header.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

The shared signed-block cache is unsafe under concurrent batched fetching.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (3)

Comment thread blockfetcher/http.go Outdated
fschoell and others added 2 commits October 1, 2026 11:23
Fetch runs concurrently for several slots with the batched poller, so
keep the cached child blocks in a sync.Map keyed by slot instead of a
single shared pointer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@fschoell
fschoell requested a balanced review from Copilot October 1, 2026 09:36

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Concurrent late cache writes can retain large signed blocks indefinitely in a long-running poller.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)
Files not reviewed (1)
  • .idea/go.imports.xml: Generated file

Comment thread blockfetcher/http.go Outdated
fschoell and others added 2 commits October 1, 2026 11:42
The CI builds, vets and tests with the race detector, and runs
golangci-lint. The lint config enables correctness, style and
performance linters on top of the standard set and fixes the findings
on the current code: redundant slicing, constant fmt.Errorf messages,
literal HTTP status codes, a single-case switch and import grouping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The cache saved one request per Gloas slot in live mode, but with the
batched poller fetching slots concurrently its entries could be stored
after their consumer had already passed, accumulating indefinitely.
Removing it is simpler than bounding it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Blob retrieval can cross forks during reorgs, and the new bid cache grows without bounds.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)
Files not reviewed (1)
  • .idea/go.imports.xml: Generated file
Previously missed (1)

In code that hasn't changed since last review

Medium severity Bound Gloas bid hash cache growth

blockfetcher/​http.go:53

This new cache stores one bid hash for every Gloas block and never evicts entries, so a continuously running poller grows memory linearly with chain history. Bound the cache to the active/reorg window (or evict entries once their child has been processed, relying on the existing parent-fetch fallback for misses).

Comment thread blockfetcher/http.go Outdated
Like the signed block, request the pre-Gloas blob sidecars by the
header's root instead of by slot, so that a reorg between the requests
cannot pair one block's body with another block's sidecars.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

The new cache grows without bounds, and the core Gloas fetch path lacks deterministic CI coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Files not reviewed (1)
  • .idea/go.imports.xml: Generated file
Previously missed (2)

In code that hasn't changed since last review

Medium severity Add deterministic CI coverage for the skipped fetch path

blockfetcher/​gloas_integration_test.go:28

This is the only end-to-end coverage of the new fetch path, but it is always skipped in CI because the workflow does not provide FIREBEACON_GLOAS_BEACON_URL. Consequently, canonical and non-canonical payload decisions, skipped-child handling, envelope validation, and blob/error behavior can regress without a test failure. Add deterministic provider-backed unit tests for those branches while retaining this live smoke test.

Medium severity Unbounded Gloas cache causes monotonically increasing memory use

blockfetcher/​http.go:316

This cache grows by one root/hash entry for every Gloas block and is never pruned. An always-on reader will therefore retain its full post-fork history even though deriving the next parent ID only needs recent/unfinalized entries and already has a fetch-on-miss path. Bound or prune the cache (for example after finalization) to avoid monotonically increasing memory use.

@fschoell fschoell 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.

fixed some issues, ready for testing

@Johnaverse
Johnaverse marked this pull request as ready for review October 6, 2026 01:22
@Johnaverse

Copy link
Copy Markdown
Contributor Author

@fschoell is this PR ready to be merged ?

@fschoell

fschoell commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

yes

@fschoell
fschoell merged commit 802cbb4 into main Oct 6, 2026
3 checks passed
@fschoell
fschoell deleted the feat/gloas branch October 6, 2026 09:08
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.

4 participants