feat: add stateless path to publishExecutionPayloadEnvelope (gd5)#9506
Conversation
External builders and stateless validator clients (multi-BN, DVT, failover) submit the envelope wrapped with blobs and KZG proofs per beacon-APIs PR #580. Lodestar previously parsed the body as a bare SignedExecutionPayloadEnvelope, so the wrapped request failed with "JSON is not an array" and the resulting payloads were orphaned because no data column sidecars were gossiped. - Add Gloas.SignedExecutionPayloadEnvelopeContents SSZ container - Extend POST /eth/v1/beacon/execution_payload_envelope to accept either shape (JSON discriminates by `signed_execution_payload_envelope` key; SSZ tries the wrapper first then falls back to the bare envelope) and forward an optional broadcast_validation query - Wire supplied blobs + kzg_proofs through getGloasDataColumnSidecars in the publish handler so data columns are derived and gossiped on the external-builder path 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the route's exposed argument shape so the handler signature remains stable once broadcast_validation enforcement is implemented. Behavior unchanged (still defaults to gossip-level validation). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…al wrapper inputs - publishExecutionPayloadEnvelope handler now checks supplied blob count matches bid.blobKzgCommitments before building data column sidecars - writeReqJson/writeReqSsz throw when only one of blobs/kzgProofs is supplied Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t partial blob inputs in handler - parseReqSsz checks first 4 bytes (offset 12 = Contents, 100 = bare envelope) instead of try/catch deserialize for faster, deterministic dispatch - publishExecutionPayloadEnvelope handler throws on partial blobs/kzgProofs so a client that supplies only one field fails fast instead of silently falling back to the stateful path Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
publishExecutionPayloadEnvelope handler now calls validateCellsAndKzgCommitments against payloadInput.getBlobKzgCommitments() before building data column sidecars. Previously only counts were checked, so malformed proofs could be added to local state and gossiped. Crypto verification now matches the gossip ingest path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the publishExecutionPayloadEnvelope endpoint to support optional blobs, kzgProofs, and broadcastValidation parameters, implementing JSON and SSZ serialization/deserialization (with SSZ offset discrimination) and backend validation of blobs/proofs to construct data column sidecars. The review feedback highlights a critical issue where duplicate publishes for the same block can trigger an unhandled error in payloadInput.addPayloadEnvelope, resulting in a 500 Internal Server Error. It is recommended to check payloadInput.hasPayloadEnvelope() and return early to handle duplicate publishes gracefully.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); | ||
| if (!payloadInput) { | ||
| throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`); | ||
| } |
There was a problem hiding this comment.
If publishExecutionPayloadEnvelope is called multiple times for the same block (which is common in redundant/fallback setups or due to client retries), calling payloadInput.addPayloadEnvelope will throw an unhandled Error: Payload envelope already set for block ... and result in a 500 Internal Server Error.
To prevent this, we should check if the payload envelope is already set using payloadInput.hasPayloadEnvelope() and return early (benign duplicate publish).
| const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); | |
| if (!payloadInput) { | |
| throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`); | |
| } | |
| const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); | |
| if (!payloadInput) { | |
| throw new ApiError(404, "PayloadEnvelopeInput not found for block root " + blockRootHex); | |
| } | |
| if (payloadInput.hasPayloadEnvelope()) { | |
| chain.logger.debug("Execution payload envelope already set, ignoring duplicate publish", { | |
| slot, | |
| blockRoot: blockRootHex, | |
| }); | |
| return; | |
| } |
) Port of #9401 (stateless `publishExecutionPayloadEnvelope`) onto `glamsterdam-devnet-5` instead of `unstable`. Same 7 commits, cherry-picked clean. - Accepts both `SignedExecutionPayloadEnvelopeContents` (envelope + blobs + KZG proofs, stateless) and bare `SignedExecutionPayloadEnvelope` (stateful), discriminated by JSON key / first SSZ offset. - KZG-verifies supplied blobs and proofs against bid `kzg_commitments`; rejects partial wrapper inputs. - Endpoint paths already pluralized on gd5 base per beacon-APIs #613 (`execution_payload_envelopes`, `execution_payload_bids`). Path, request shape, headers, query, and body codec match #580's `envelope_post.yaml`. Known gaps left as-is (pre-existing TODOs referencing #580): - `broadcast_validation` not yet honored (always gossip-level). - No `202` response branch (success always `200`). - [ ] `pnpm check-types` - [ ] `pnpm test:unit` (api + beacon-node envelope publish tests) - [ ] Devnet smoke test of envelope publish (stateless + stateful) Branch creation, cherry-pick, and this PR description were AI-assisted (Claude Code). --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Port of #9401 (stateless
publishExecutionPayloadEnvelope) ontoglamsterdam-devnet-5instead ofunstable. Same 7 commits, cherry-picked clean.SignedExecutionPayloadEnvelopeContents(envelope + blobs + KZG proofs, stateless) and bareSignedExecutionPayloadEnvelope(stateful), discriminated by JSON key / first SSZ offset.kzg_commitments; rejects partial wrapper inputs.execution_payload_envelopes,execution_payload_bids).Spec alignment (beacon-APIs #580)
Path, request shape, headers, query, and body codec match #580's
envelope_post.yaml. Known gaps left as-is (pre-existing TODOs referencing #580):broadcast_validationnot yet honored (always gossip-level).202response branch (success always200).Test plan
pnpm check-typespnpm test:unit(api + beacon-node envelope publish tests)AI disclosure
Branch creation, cherry-pick, and this PR description were AI-assisted (Claude Code).