You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
🤖 [claude] Add Gloas (EIP-7732 ePBS) support - #40
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.
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.
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
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:
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.
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>
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>
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>
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>
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).
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>
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Block model
Spec.GLOAS = 8, plus a newGloasBodyin theBlockoneof (field 27).GloasBodyfields:signed_execution_payload_bid;payload_attestations;parent_execution_requests.execution_payload_envelope: holds the revealed payload, which addsblock_access_list(EIP-7928) andslot_number(EIP-7843), along with its execution requests.ExecutionRequest: gainsbuilder_depositsandbuilder_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:
parent_block_hashequals this block's bidblock_hash, the payload is canonical and the envelope is included.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
/eth/v1/beacon/blobs:blob_sidecars, because the commitments moved into the bid.410onblob_sidecars, since beacon-APIs#577 removed that endpoint.SECONDS_PER_SLOT, because the payload may be absent.Dependency
go-eth2-clienthas no tagged release with Gloas yet. This PR pins its actively maintainedgloasbranch,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 ./...andgofmt -l .are clean.TestGloasFetchAgainstBeaconNoderan against glamsterdam-devnet-8 (https://beacon.glamsterdam-devnet-8.ethpandaops.io, Nimbus, Gloas since epoch 1536). It is skipped unlessFIREBEACON_GLOAS_BEACON_URLis set.parent_block_hashagainstblock_hashover the last 3000 slots.slot_numberequals the slot;mainbuild and this branch ran over the same 39 Fulu slots against our Sepolia Lighthouse v8.3.0-rc.0 (sepolia-sfdm44). TheFIRE BLOCKlines were byte-identical, with no errors.firebeacon fetch httpbinary ran at devnet-8 head for 150 s and emitted blocks one to two slots behind head.--max-block-fetch-durationand succeeded on retry. A Gloas block costs about five requests, which is fine for a Lighthouse on the LAN.GET /eth/v1/beacon/execution_payload_envelopes/{block_id}and Gloas blobs on/eth/v1/beacon/blobs.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