feat: eip-8282 builder execution requests scaffolding#9507
Conversation
Initial scaffolding for the Gloas-era Builder Execution Requests (EIP-8282 / consensus-specs PR ChainSafe#5359): * Add MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD and MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD presets plus the two new request-type prefix bytes (0x03, 0x04). * Add BuilderDepositRequest / BuilderExitRequest SSZ containers and extend gloas ExecutionRequests with builderDeposits / builderExits. * Implement processBuilderDepositRequest and processBuilderExitRequest per the spec (PoP-gated register vs. balance top-up; isActiveBuilder + executionAddress-authorized exit). * Wire builder requests into apply_parent_execution_payload, the upgrade_to_gloas init, and process_deposit_request fallthrough to the builder registry. * Lift withParentPayloadApplied and getParentExecutionRequests from electra.ExecutionRequests to gloas.ExecutionRequests now that the parent envelope carries builder requests. * Extend engine_newPayload / engine_getPayload (de)serializer with the new 0x03 / 0x04 prefixed request lists. Refs: ethereum/consensus-specs#5359, ethereum/EIPs#11760 Draft PR — types compile and lint is clean; further wiring (envelope construction, fork-choice, spec/unit tests) and EIP-7251-style top-up semantics still to come. 🤖 Generated with AI assistance
There was a problem hiding this comment.
Code Review
This pull request implements EIP-8282 (Builder deposits and exits) for the Gloas hard fork. It introduces builder deposit and exit requests, updating execution requests serialization/deserialization, state transition processing, and SSZ types to accommodate these new request types. Builder-credentialed deposits on the validator deposit contract are made inert post-fork, and voluntary exits are reverted to being validator-only, as builder exits are now handled via the EIP-7685 request bus. There are no review comments, and I have no feedback to provide.
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.
|
Thanks for the pass — appreciated. (Acked on the consumer-version sunset.) |
The gloas bid and envelope carry an `ExecutionRequests` extended with `builder_deposits` (0x03) and `builder_exits` (0x04) per ethereum/consensus-specs#5359. Using `ssz.electra.ExecutionRequests.hashTreeRoot` in the gloas-only code paths produced a root over the electra-shaped container — wrong even when both builder lists are empty, because the gloas container has two additional zero-hash leaves. Switch the four gloas-only call sites to `ssz.gloas.ExecutionRequests`: * `produceBlockBody.ts` self-build bid construction * `verifyExecutionPayloadEnvelope.ts` bid↔envelope cross-check * `executionPayloadEnvelope.ts` gossip validation * `upgradeStateToGloas.ts` genesis bid default-value root Refs: ethereum/consensus-specs#5359, ethereum/EIPs#11760 🤖 Generated with AI assistance
processBuilderDepositRequest (3 cases): * register on first appearance when PoP verifies * drop silently when PoP fails * top-up an existing builder without re-verifying signature and without rebinding withdrawal credentials (matches validator deposit contract semantics) processBuilderExitRequest (5 cases): * drop for unknown pubkey * drop for inactive builder (`deposit_epoch >= finalized_epoch`) * drop when `source_address` ≠ builder execution address * drop when builder already has pending withdrawal queued * initiate exit on the happy path (sets withdrawableEpoch = currentEpoch + MIN_BUILDER_WITHDRAWABILITY_DELAY) JSON shape (4 cases): * BuilderDepositRequest snake_case round-trip * BuilderExitRequest snake_case round-trip * ExecutionRequests carries builder_deposits / builder_exits alongside the existing deposits/withdrawals/consolidations keys * ExecutionPayloadEnvelope JSON round-trip with builder requests populated Refs: ethereum/consensus-specs#5359, ethereum/EIPs#11760 🤖 Generated with AI assistance
Per jtraglia (discussion_r3406820840) and consensus-specs#5359, `process_deposit_request` is intentionally unchanged in Gloas. The new builder onboarding flow goes through a separate `process_builder_deposit_request` keyed on the new `BuilderDeposit` request type, NOT through this function. `BUILDER_WITHDRAWAL_PREFIX = 0x03` is a temporary onboarding-only constant — spec note "It will be deprecated after the upgrade and a future validator withdrawal prefix may reuse this value." Pre-fork pending deposits with the 0x03 prefix are converted to builders at the fork via `onboard_builders_from_pending_deposits`, but a regular DepositRequest arriving post-fork with the 0x03 prefix still creates a regular validator. The previous branch silently dropped any 0x03 deposit that reached `processDepositRequest`, which would block legitimate validator creation once the prefix is repurposed for a future withdrawal credential variant. Remove the special case and leave a comment documenting the spec intent. 🤖 Generated with AI assistance
Per discussion_r3407093020. 🤖 Generated with AI assistance
…positRequest Adds `applyDepositForBuilder` to processDepositRequest.ts matching the spec's `apply_deposit_for_builder` function signature. Refactors `isValidBuilderDepositSignature` to accept individual params instead of a BuilderDepositRequest struct, enabling the helper to call it without constructing an SSZ view. `processBuilderDepositRequest` now delegates to the new helper. Fixes check-specrefs CI failure on PR ChainSafe#9507. 🤖 Generated with AI assistance
…uests' into feat/eip8282-builder-execution-requests # Conflicts: # packages/state-transition/src/block/processDepositRequest.ts
Co-locate the apply_deposit_for_builder helper with its only caller, processBuilderDepositRequest, per review feedback. No behavior change: the function and its util/gloas imports move verbatim; processDepositRequest drops the now-unused gloas registry imports. 🤖 Generated with AI assistance
Function moved to processBuilderDepositRequest.ts in this PR. 🤖 Generated with AI assistance Co-Authored-By: lodekeeper <lodekeeper@users.noreply.github.com>
| const depositEpoch = computeEpochAtSlot(slot); | ||
|
|
||
| let builderIndex = state.builders.length; | ||
| for (let i = 0; i < state.builders.length; i++) { |
There was a problem hiding this comment.
I think there can be some optimization on this. We don't want to loop over state.builders every time we onboard a new builder. Especially at the beginning of gloas where most of the deposits are for new builders. Could be another PR
There was a problem hiding this comment.
Agreed — addBuilderToRegistry scans all of state.builders for a reusable slot on every onboard, so during the initial gloas onboarding wave (mostly new builders, few/no reclaimable slots) it degrades to O(N) per deposit / O(N²) overall. Keeping it as-is in this PR and tracking the optimization as a follow-up.
Likely direction: maintain a free-list/cursor of reclaimable builder indices (a slot becomes reusable when balance == 0 && withdrawableEpoch <= currentEpoch) so onboarding is amortized O(1) instead of a linear scan. Out of scope for this scaffolding PR.
| result.consolidations = deserializeConsolidationRequests(requests); | ||
| break; | ||
| } | ||
| case BUILDER_DEPOSIT_REQUEST_TYPE: { |
There was a problem hiding this comment.
We probably want deserializeExecutionRequests to be fork aware. Or we will accidentally accept builder deposit/exit request pre-gloas.
There was a problem hiding this comment.
Probably also check other places too so 0x03 and 0x04 requests are not in the pre-gloas flow
There was a problem hiding this comment.
Good catch — fixed in 7f4b84e. deserializeExecutionRequests now takes fork (threaded from parseExecutionPayload, which already had it in scope) and throws on BUILDER_DEPOSIT_REQUEST_TYPE / BUILDER_EXIT_REQUEST_TYPE when ForkSeq[fork] < ForkSeq.gloas, so a pre-gloas EL response carrying a builder request is rejected instead of silently deserialized into builderDeposits/builderExits.
The serialize side didn't need the same gate — it's type-driven: builderDeposits/builderExits only exist on the gloas ExecutionRequests container, and the length !== 0 checks skip them otherwise. Added a unit test covering both directions (round-trips post-gloas, throws pre-gloas).
There was a problem hiding this comment.
Audited the other paths so 0x03/0x04 can't enter the pre-gloas flow:
- EL serialize (CL→EL
newPayload) — hardened in 116c4e0, symmetric with the deserialize gate:serializeExecutionRequestsnow takesforkand throws if builder requests are present pre-gloas, so they can't be emitted into anewPayloadV4request. Threadedforkthrough both callers (engine http + mock). - State-transition application — already gloas-only by construction: builder requests are applied via
applyParentExecutionPayload/processParentExecutionPayload, which is gated onfork >= ForkSeq.gloasinblock/index.tsand only reads gloas-only block fields (block.body.parentExecutionRequests/signedExecutionPayloadBid). A pre-gloas block can't carry them — the SSZ type has no builder fields. - Block production — the EL response's
executionRequestscome throughdeserializeExecutionRequests(now fork-gated), and pre-gloas block bodies have no builder fields by type.
So the deserialize gate + the new serialize gate cover EL transport both ways, and the state path is structurally gloas-only. Added unit tests for the pre-gloas-reject / post-gloas-accept cases on both serialize and deserialize.
| // GLOAS | ||
| PTC_SIZE: 512, | ||
| MAX_PAYLOAD_ATTESTATIONS: 4, | ||
| MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 16, // 2**4 |
There was a problem hiding this comment.
Just a reminder to revisit these values after we agree on the final values
There was a problem hiding this comment.
Ack — these are placeholders tracking the current spec draft (MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, BUILDER_REGISTRY_LIMIT, BUILDER_PENDING_WITHDRAWALS_LIMIT, MAX_BUILDERS_PER_WITHDRAWALS_SWEEP). Will revisit and update them once the EIP-8282 / consensus-specs constants are finalized, before this leaves draft.
Pre-gloas, deserializeExecutionRequests accepted BuilderDeposit/BuilderExit request types (0x03/0x04) from the EL because isExecutionRequestType recognizes them globally. Thread fork through from parseExecutionPayload and reject builder request types before gloas. The serialize side is already type-gated (builder fields only exist on the gloas ExecutionRequests container), so no change there. Adds a unit test asserting builder requests round-trip post-gloas and throw pre-gloas. Addresses ensi321 review comment on ChainSafe#9507. 🤖 Generated with AI assistance
|
@lodekeeper my guy your mock code in processBuilderDepositRequest.test.ts caused it to fail. Take a look at it |
The mock was written for an older (config, request) shape and read request.signature[0], but the real isValidBuilderDepositSignature takes positional args (config, pubkey, withdrawalCredentials, amount, signature). The mock received pubkey as its 2nd arg, so request.signature was undefined and threw "Cannot read properties of undefined (reading '0')" for the two new-builder tests (top-up test passed since it returns before the PoP check). Match the real positional signature. 🤖 Generated with AI assistance
|
Fixed in fa848a0 — good catch 🙏 The mock for Updated the mock to mirror the real positional signature ( |
Symmetric with the deserialize-side fork gate: serializeExecutionRequests now takes fork and throws if builder requests (0x03/0x04) are present in a pre-gloas ExecutionRequests, so they can never be emitted into a newPayloadV4 request. Thread fork through from the two callers (engine http newPayload and the mock engine), both of which already have it in scope. Adds serialize-side unit tests (serializes builder requests post-gloas, throws pre-gloas). Addresses ensi321's follow-up to check other places so 0x03/0x04 stay out of the pre-gloas flow. The state-transition application path is already gloas-only by construction (processParentExecutionPayload is gated on fork >= gloas and operates on gloas-only block fields). 🤖 Generated with AI assistance
Summary
Initial scaffolding for EIP-8282 (Builder Execution Requests), introduced in consensus-specs#5359 / ethereum/EIPs#11760. Opens as draft — the goal is to land the type plumbing and the two new request handlers so follow-up work (envelope plumbing, fork-choice, full test coverage) can build on a stable base.
What's in
MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD,MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, prefix bytesBUILDER_DEPOSIT_REQUEST_TYPE = 0x03,BUILDER_EXIT_REQUEST_TYPE = 0x04, plus theDOMAIN_BUILDER_DEPOSITdomain.gloas.BuilderDepositRequest,gloas.BuilderExitRequest,BuilderDepositRequests/BuilderExitRequestslist types, andgloas.ExecutionRequestsextended withbuilderDeposits+builderExits.processBuilderDepositRequest— registers a new builder (PoP-gated) or tops up an existing builder's balance, mirroring the validator deposit contract's top-up semantics.processBuilderExitRequest—source_address-authorized exit (analogous to EIP-7002 0x01-credentialed exits), with active-builder and no-pending-withdrawals preconditions.applyParentExecutionPayloadextended to drainbuilderDeposits/builderExitsfrom the parent envelope.upgrade_to_gloasinitializes the builder registry side-state.withParentPayloadAppliedandgetParentExecutionRequestsnow take/returngloas.ExecutionRequests(the parent envelope now carries builder requests).engine_newPayload/engine_getPayload(de)serializers handle the new0x03/0x04-prefixed request lists.What's deferred (follow-up)
executionRequestsRootin the gloas bid still useselectra.ExecutionRequests.hashTreeRoot— needs to switch to the gloas variant once we plumb fork-aware EL response typing throughparseExecutionPayload.config/builders.yaml,block_processing/builder_deposit_request,block_processing/builder_exit_request).prepareNextSlotand validator-side execution payload accounting.Test plan
pnpm check-typesclean across all packagespnpm lintclean (biome)consensus-specsships test vectorsReferences