Skip to content

feat: eip-8282 builder execution requests scaffolding#9507

Draft
lodekeeper wants to merge 12 commits into
ChainSafe:unstablefrom
lodekeeper:feat/eip8282-builder-execution-requests
Draft

feat: eip-8282 builder execution requests scaffolding#9507
lodekeeper wants to merge 12 commits into
ChainSafe:unstablefrom
lodekeeper:feat/eip8282-builder-execution-requests

Conversation

@lodekeeper

Copy link
Copy Markdown
Contributor

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

  • Params: MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, prefix bytes BUILDER_DEPOSIT_REQUEST_TYPE = 0x03, BUILDER_EXIT_REQUEST_TYPE = 0x04, plus the DOMAIN_BUILDER_DEPOSIT domain.
  • Types: gloas.BuilderDepositRequest, gloas.BuilderExitRequest, BuilderDepositRequests / BuilderExitRequests list types, and gloas.ExecutionRequests extended with builderDeposits + builderExits.
  • State transition:
    • processBuilderDepositRequest — registers a new builder (PoP-gated) or tops up an existing builder's balance, mirroring the validator deposit contract's top-up semantics.
    • processBuilderExitRequestsource_address-authorized exit (analogous to EIP-7002 0x01-credentialed exits), with active-builder and no-pending-withdrawals preconditions.
    • applyParentExecutionPayload extended to drain builderDeposits / builderExits from the parent envelope.
    • upgrade_to_gloas initializes the builder registry side-state.
  • State view: withParentPayloadApplied and getParentExecutionRequests now take/return gloas.ExecutionRequests (the parent envelope now carries builder requests).
  • Engine: engine_newPayload / engine_getPayload (de)serializers handle the new 0x03 / 0x04-prefixed request lists.

What's deferred (follow-up)

  • Beacon-APIs envelope shape (executionRequests JSON) once the spec PR stabilizes.
  • executionRequestsRoot in the gloas bid still uses electra.ExecutionRequests.hashTreeRoot — needs to switch to the gloas variant once we plumb fork-aware EL response typing through parseExecutionPayload.
  • Spec test wiring (config/builders.yaml, block_processing/builder_deposit_request, block_processing/builder_exit_request).
  • Unit tests for the two new handlers (top-up dedup, PoP rejection, executionAddress mismatch).
  • EL serializer/deserializer wiring through prepareNextSlot and validator-side execution payload accounting.

Test plan

  • pnpm check-types clean across all packages
  • pnpm lint clean (biome)
  • Unit tests for the new handlers
  • Spec-tests once upstream consensus-specs ships test vectors
  • Devnet smoke test (Gloas devnet-0 with builders)

References

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

@gemini-code-assist gemini-code-assist Bot 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.

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.

@lodekeeper

Copy link
Copy Markdown
Contributor Author

Thanks for the pass — appreciated. (Acked on the consumer-version sunset.)

@lodekeeper lodekeeper changed the title feat: EIP-8282 builder execution requests scaffolding feat: eip-8282 builder execution requests scaffolding Jun 12, 2026
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
Comment thread packages/state-transition/src/block/processDepositRequest.ts Outdated
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
Comment thread packages/state-transition/src/block/processDepositRequest.ts Outdated
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++) {

@ensi321 ensi321 Jun 17, 2026

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.

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

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.

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

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.

We probably want deserializeExecutionRequests to be fork aware. Or we will accidentally accept builder deposit/exit request pre-gloas.

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.

Probably also check other places too so 0x03 and 0x04 requests are not in the pre-gloas flow

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.

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

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.

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: serializeExecutionRequests now takes fork and throws if builder requests are present pre-gloas, so they can't be emitted into a newPayloadV4 request. Threaded fork through both callers (engine http + mock).
  • State-transition application — already gloas-only by construction: builder requests are applied via applyParentExecutionPayload / processParentExecutionPayload, which is gated on fork >= ForkSeq.gloas in block/index.ts and 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 executionRequests come through deserializeExecutionRequests (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

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.

Just a reminder to revisit these values after we agree on the final values

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.

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
@ensi321

ensi321 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@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
@lodekeeper

Copy link
Copy Markdown
Contributor Author

Fixed in fa848a0 — good catch 🙏

The mock for isValidBuilderDepositSignature had a stale signature. It was written as (_config, request) => request.signature[0] === 1, but the real function takes positional args: (config, pubkey, withdrawalCredentials, amount, signature). So the mock was receiving pubkey as its 2nd arg, request.signature was undefined, and it threw Cannot read properties of undefined (reading '0') on the two new-builder tests. The top-up test passed only because it returns before reaching the PoP check.

Updated the mock to mirror the real positional signature (signature is the 5th arg). All 3 processBuilderDepositRequest tests pass locally now.

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