From 3566c77d0f6cc38a6d786bcdddb419dc7adc4d6a Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 8 Jul 2026 16:37:36 -0700 Subject: [PATCH 01/28] fix one typo and some inprecise links --- apis/builder/execution_payload_bid.yaml | 6 +++--- specs/gloas/builder.md | 4 ++-- specs/gloas/validator.md | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index 6aa12cfd..d2f3144c 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -9,13 +9,13 @@ post: - The slot for which the block should be proposed. - The hash of the execution layer block the proposer will build on. - The root of the beacon block the proposer will build on. - - The index of the proposer. + - The public key of the proposer. - Optionally, a `SignedRequestAuthV1` in the request body that authenticates the request. The body MAY be encoded as JSON or SSZ. The proposer's `max_execution_payment` is communicated exclusively via the - `submitBuilderPreferences` endpoint. If no `BuilderPreferencesV1` have been - submitted for the proposer, the builder MUST treat `max_execution_payment` as `0` + `submitBuilderPreferences` endpoint. If no `BuilderPreferencesV1` have been + submitted for the proposer, the builder MUST treat `max_execution_payment` as `0` and MUST NOT include an execution layer payment in the bid. The `SignedRequestAuthV1` body is optional. If it is present but malformed diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 21a5caff..26587ee1 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -208,8 +208,8 @@ documented in the [Gloas consensus specs][gloas-builder-specs]. [get-execution-payload-bid-api]: ./../../apis/builder/execution_payload_bid.yaml [gloas-builder-specs]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/builder.md [gloas-consensus-specs]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas -[proposer-preferences]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md -[proposer-preferences-topic]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md +[proposer-preferences]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#new-proposerpreferences +[proposer-preferences-topic]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#new-proposer_preferences [signed-execution-payload-bid]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid [signed-execution-payload-envelope]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadenvelope [signed-request-auth]: ./validator.md#signedrequestauthv1 diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index db29e601..fa98f654 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -244,15 +244,15 @@ block on top of a beacon `state` must take the following actions: [`SignedExecutionPayloadEnvelope`][signed-execution-payload-envelope] and broadcasts it to the PTC committee. -[can-builder-cover-bid]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#can_builder_cover_bid +[can-builder-cover-bid]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#new-can_builder_cover_bid [get-execution-payload-bid-api]: ./../../apis/builder/execution_payload_bid.yaml [gloas-consensus-specs]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas [gloas-validator-specs]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/validator.md#block-proposal -[is-active-builder]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#is_active_builder -[proposer-preferences]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md -[proposer-preferences-topic]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md +[is-active-builder]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#new-is_active_builder +[proposer-preferences]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#new-proposerpreferences +[proposer-preferences-topic]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#new-proposer_preferences [signed-execution-payload-bid]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid [signed-execution-payload-envelope]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadenvelope [submit-builder-preferences-api]: ./../../apis/builder/builder_preferences.yaml -[submit-signed-beacon-block]: ./../../apis/builder/beacon_block.yaml -[verify-execution-payload-bid-signature]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#verify_execution_payload_bid_signature +[submit-signed-beacon-block]: ./../../apis/builder/beacon_blocks.yaml +[verify-execution-payload-bid-signature]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#new-verify_execution_payload_bid_signature From d23c70861a8240e6d21daef8d36f9fe7aeaadee4 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 13 Jul 2026 15:38:36 -0700 Subject: [PATCH 02/28] clarify RequestAuthV1.slot as the proposal slot on both channels The slot in a signed request auth is the slot the validator will propose in, not the slot at which the request is signed or sent. Anchor the bid channel check to the slot path parameter, define the preferences channel semantics (the proposal slot the preferences apply to), and reject preferences for past slots so a replayed request cannot roll preferences back to a stale value. --- apis/builder/builder_preferences.yaml | 6 ++++-- apis/builder/execution_payload_bid.yaml | 8 ++++---- specs/gloas/builder.md | 14 +++++++++----- specs/gloas/validator.md | 10 ++++++---- types/gloas/request_auth.yaml | 4 ++-- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/apis/builder/builder_preferences.yaml b/apis/builder/builder_preferences.yaml index 2d603bb0..5c1a07af 100644 --- a/apis/builder/builder_preferences.yaml +++ b/apis/builder/builder_preferences.yaml @@ -12,7 +12,9 @@ post: The builder MUST verify the BLS signature in `auth` against `validator_pubkey`, and check that `auth.message.data` - matches its own URL. If the signature check fails, the builder MUST + matches its own URL. `auth.message.slot` is the proposal slot the + preferences apply to; the builder MUST reject preferences whose slot has + already passed. If the signature check fails, the builder MUST return a 401 response. If the URL or slot check fails, the builder MUST return a 400 response. @@ -62,7 +64,7 @@ post: SlotMismatch: value: code: 400 - message: "Invalid SignedRequestAuthV1: auth.message.slot does not match the requested slot" + message: "Invalid SignedRequestAuthV1: auth.message.slot has already passed" WrongBuilder: value: code: 400 diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index d2f3144c..5b14986b 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -93,9 +93,9 @@ post: Optional `SignedRequestAuthV1` authenticating the request. If provided, the builder MAY verify the BLS signature against the validator pubkey resolved from the `proposer_pubkey` path parameter, and check that - `data` matches its own URL and that `slot` matches the - requested slot. If absent, the builder MAY still serve a bid subject - to its own policy. + `data` matches its own URL and that `auth.message.slot` matches the + `slot` path parameter (the proposal slot). If absent, the builder MAY + still serve a bid subject to its own policy. required: false content: application/json: @@ -143,7 +143,7 @@ post: SlotMismatch: value: code: 400 - message: "Invalid SignedRequestAuthV1: auth.message.slot does not match the requested slot" + message: "Invalid SignedRequestAuthV1: auth.message.slot does not match the proposal slot in the request path" WrongBuilder: value: code: 400 diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 26587ee1..e71fa9fd 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -102,10 +102,13 @@ containing: - `preferences`: A `BuilderPreferencesV1` with: - `max_execution_payment`: The maximum execution layer payment the proposer will accept from this builder (in Gwei). -- `auth`: A `SignedRequestAuthV1` authenticating the request. The builder MUST - check that `auth.message.data` matches its own URL and MUST verify the - BLS signature against the `validator_pubkey` path parameter. If either check - fails, the builder MUST return a 400 response. +- `auth`: A `SignedRequestAuthV1` authenticating the request. + `auth.message.slot` is the proposal slot the preferences apply to. The + builder MUST check that `auth.message.data` matches its own URL and MUST + verify the BLS signature against the `validator_pubkey` path parameter. If + either check fails, the builder MUST return a 400 response. The builder MUST + reject preferences whose `auth.message.slot` has already passed, so that a + replayed request cannot roll preferences back to a stale value. The builder SHOULD store the preferences for each proposer and apply the `max_execution_payment` constraint when constructing bids. If no preferences @@ -142,7 +145,8 @@ treat `max_execution_payment` as `0` or can choose to not serve the bid. If the request body is present, builders MAY verify the `SignedRequestAuthV1` signature against the `proposer_pubkey` path parameter, and check that -`data` matches their own URL and that `slot` matches the requested slot. +`data` matches their own URL and that `auth.message.slot` matches the proposal `slot` +path parameter (see [Constructing the `RequestAuthV1`][signed-request-auth]). If verification fails, the builder MAY return a 401 response. ```python diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index fa98f654..a6050cbc 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -99,9 +99,10 @@ The validator then constructs a `BuilderPreferencesRequestV1` with the `BuilderPreferencesV1` as `preferences` and a `SignedRequestAuthV1` as `auth`. The `SignedRequestAuthV1` is constructed as described in [Constructing the `RequestAuthV1`](#constructing-the-requestauthv1); its -`auth.message.data` identifies the intended builder. The builder MUST -verify the `auth` signature against the `validator_pubkey` path parameter and -MUST reject the request with a 400 response if `auth.message.data` does +`auth.message.data` identifies the intended builder and its +`auth.message.slot` is the proposal slot the preferences apply to. The builder +MUST verify the `auth` signature against the `validator_pubkey` path parameter +and MUST reject the request with a 400 response if `auth.message.data` does not match its own URL. If no preferences have been submitted, the builder MUST treat the proposer's @@ -138,7 +139,8 @@ If the validator chooses to authenticate its request, it constructs a `RequestAuthV1` with the following fields: - `data`: MUST be set to the URL of the builder the request is intended for. -- `slot`: The slot for which the request is being sent. +- `slot`: The proposal slot this request is authorized for, not the slot at + which the request is signed or sent. The proposer's public key is already carried as a path parameter in the relevant API request, so it does not need to be carried inside `RequestAuthV1`. diff --git a/types/gloas/request_auth.yaml b/types/gloas/request_auth.yaml index 8255ffb2..016624e3 100644 --- a/types/gloas/request_auth.yaml +++ b/types/gloas/request_auth.yaml @@ -11,7 +11,7 @@ Gloas: pattern: "^0x[a-fA-F0-9]{0,8192}$" slot: $ref: "../../beacon-apis/types/primitive.yaml#/Uint64" - description: "The slot for which the bid is being requested." + description: "The proposal slot this request is authorized for." SignedRequestAuthV1: type: object required: [message, signature] @@ -19,4 +19,4 @@ Gloas: message: $ref: "#/Gloas/RequestAuthV1" signature: - $ref: "../../beacon-apis/types/primitive.yaml#/Signature" \ No newline at end of file + $ref: "../../beacon-apis/types/primitive.yaml#/Signature" From ea472f5aad1068cb865d87e18b99274e42a01f3d Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 13 Jul 2026 16:18:09 -0700 Subject: [PATCH 03/28] define the canonical byte form of the builder URL in RequestAuthV1.data data identifies the builder, not an API resource or a transport detail, so its canonical form is just the scheme and host: lowercased, with no port, path, query, fragment, or userinfo. The validator applies the rules before signing and the builder applies them to its own URL before the byte comparison, so both sides derive the same bytes. --- apis/builder/builder_preferences.yaml | 5 +++- apis/builder/execution_payload_bid.yaml | 5 +++- specs/gloas/builder.md | 28 ++++++++++-------- specs/gloas/validator.md | 38 +++++++++++++++++++++---- types/gloas/request_auth.yaml | 6 +++- 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/apis/builder/builder_preferences.yaml b/apis/builder/builder_preferences.yaml index 5c1a07af..7b62dcc2 100644 --- a/apis/builder/builder_preferences.yaml +++ b/apis/builder/builder_preferences.yaml @@ -12,7 +12,10 @@ post: The builder MUST verify the BLS signature in `auth` against `validator_pubkey`, and check that `auth.message.data` - matches its own URL. `auth.message.slot` is the proposal slot the + matches the canonical form of its own URL (the lowercased scheme and + host only; no port, path, query, fragment, or userinfo), with both + sides applying the same canonicalization before comparison. + `auth.message.slot` is the proposal slot the preferences apply to; the builder MUST reject preferences whose slot has already passed. If the signature check fails, the builder MUST return a 401 response. If the URL or slot check fails, the builder MUST diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index 5b14986b..05c4ad64 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -93,7 +93,10 @@ post: Optional `SignedRequestAuthV1` authenticating the request. If provided, the builder MAY verify the BLS signature against the validator pubkey resolved from the `proposer_pubkey` path parameter, and check that - `data` matches its own URL and that `auth.message.slot` matches the + `data` matches the canonical form of its own URL (the lowercased + scheme and host only; no port, path, query, fragment, or userinfo) + and that + `auth.message.slot` matches the `slot` path parameter (the proposal slot). If absent, the builder MAY still serve a bid subject to its own policy. required: false diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index e71fa9fd..6c4a8987 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -94,8 +94,8 @@ Validators MAY communicate their per-builder preferences ahead of the bid request by calling the [`submitBuilderPreferences`][submit-builder-preferences-api] API in the epoch prior to the epoch in which they will be proposing, as determined from -`state.proposer_lookahead`. The builder receives a `BuilderPreferencesRequestV1` object -containing: +`state.proposer_lookahead`. The builder receives a `BuilderPreferencesRequestV1` +object containing: - `validator_pubkey`: The BLS public key of the validator submitting these preferences, passed as a path parameter. @@ -103,12 +103,13 @@ containing: - `max_execution_payment`: The maximum execution layer payment the proposer will accept from this builder (in Gwei). - `auth`: A `SignedRequestAuthV1` authenticating the request. - `auth.message.slot` is the proposal slot the preferences apply to. The - builder MUST check that `auth.message.data` matches its own URL and MUST - verify the BLS signature against the `validator_pubkey` path parameter. If - either check fails, the builder MUST return a 400 response. The builder MUST - reject preferences whose `auth.message.slot` has already passed, so that a - replayed request cannot roll preferences back to a stale value. + `auth.message.slot` is the proposal slot the preferences apply to. The builder + MUST check that `auth.message.data` matches the canonical form of its own URL + ([URL canonicalization][url-canonicalization]) and MUST verify the BLS + signature against the `validator_pubkey` path parameter. If either check + fails, the builder MUST return a 400 response. The builder MUST reject + preferences whose `auth.message.slot` has already passed, so that a replayed + request cannot roll preferences back to a stale value. The builder SHOULD store the preferences for each proposer and apply the `max_execution_payment` constraint when constructing bids. If no preferences @@ -144,10 +145,12 @@ The proposer's `max_execution_payment` is communicated exclusively via the treat `max_execution_payment` as `0` or can choose to not serve the bid. If the request body is present, builders MAY verify the `SignedRequestAuthV1` -signature against the `proposer_pubkey` path parameter, and check that -`data` matches their own URL and that `auth.message.slot` matches the proposal `slot` -path parameter (see [Constructing the `RequestAuthV1`][signed-request-auth]). -If verification fails, the builder MAY return a 401 response. +signature against the `proposer_pubkey` path parameter, and check that `data` +matches the canonical form of their own URL +([URL canonicalization][url-canonicalization]) and that `auth.message.slot` +matches the proposal `slot` path parameter (see +[Constructing the `RequestAuthV1`][signed-request-auth]). If verification fails, +the builder MAY return a 401 response. ```python def verify_request_auth_signature( @@ -218,3 +221,4 @@ documented in the [Gloas consensus specs][gloas-builder-specs]. [signed-execution-payload-envelope]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadenvelope [signed-request-auth]: ./validator.md#signedrequestauthv1 [submit-builder-preferences-api]: ./../../apis/builder/builder_preferences.yaml +[url-canonicalization]: ./validator.md#url-canonicalization diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index a6050cbc..d5bbd611 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -14,6 +14,7 @@ - [`max_execution_payment`](#max_execution_payment) - [Bid Request](#bid-request) - [Constructing the `RequestAuthV1`](#constructing-the-requestauthv1) + - [URL canonicalization](#url-canonicalization) - [Proposer Preferences](#proposer-preferences) - [Validating a `SignedExecutionPayloadBid`](#validating-a-signedexecutionpayloadbid) - [Block proposal](#block-proposal) @@ -99,11 +100,12 @@ The validator then constructs a `BuilderPreferencesRequestV1` with the `BuilderPreferencesV1` as `preferences` and a `SignedRequestAuthV1` as `auth`. The `SignedRequestAuthV1` is constructed as described in [Constructing the `RequestAuthV1`](#constructing-the-requestauthv1); its -`auth.message.data` identifies the intended builder and its -`auth.message.slot` is the proposal slot the preferences apply to. The builder -MUST verify the `auth` signature against the `validator_pubkey` path parameter -and MUST reject the request with a 400 response if `auth.message.data` does -not match its own URL. +`auth.message.data` identifies the intended builder and its `auth.message.slot` +is the proposal slot the preferences apply to. The builder MUST verify the +`auth` signature against the `validator_pubkey` path parameter and MUST reject +the request with a 400 response if `auth.message.data` does not match the +canonical form of its own URL (see +[URL canonicalization](#url-canonicalization)). If no preferences have been submitted, the builder MUST treat the proposer's `max_execution_payment` as `0`. @@ -138,7 +140,8 @@ builder MAY still serve a bid. If the validator chooses to authenticate its request, it constructs a `RequestAuthV1` with the following fields: -- `data`: MUST be set to the URL of the builder the request is intended for. +- `data`: MUST be set to the canonical form of the URL of the builder the + request is intended for (see [URL canonicalization](#url-canonicalization)). - `slot`: The proposal slot this request is authorized for, not the slot at which the request is signed or sent. @@ -150,6 +153,29 @@ The validator then constructs the `SignedRequestAuthV1` by signing the validator and discard requests from other parties (e.g. DDOS or replay attempts from competing builders). +#### URL canonicalization + +`data` is compared byte-for-byte, so both sides must derive the same bytes from +the same URL. `data` identifies the builder, not an API resource or a transport +detail, so its canonical form is just the scheme and host: + +- the scheme and host lowercased +- no port +- no path (including a bare trailing `/`), query, or fragment +- no userinfo + +`data` is the ASCII encoding of the canonical URL (internationalized hostnames +in their punycode form). The validator MUST apply these rules before signing, +and the builder MUST apply them to its own URL before comparing it against +`data`. For example: + +| URL | Canonical form | +| -------------------------------------- | ------------------------------- | +| `HTTPS://Builder.Example.com/` | `https://builder.example.com` | +| `https://builder.example.com:8080` | `https://builder.example.com` | +| `https://builder.example.com/bids?x=1` | `https://builder.example.com` | +| `https://bücher.example` | `https://xn--bcher-kva.example` | + ## Proposer Preferences *Note*: Validator registrations (`ValidatorRegistrationV1`) are **deprecated** diff --git a/types/gloas/request_auth.yaml b/types/gloas/request_auth.yaml index 016624e3..67f2f949 100644 --- a/types/gloas/request_auth.yaml +++ b/types/gloas/request_auth.yaml @@ -6,7 +6,11 @@ Gloas: data: type: string format: hex - description: "Opaque authentication data, up to `MAX_DATA_SIZE` bytes. Typically set to the URL of the builder the request is intended for." + description: >- + Opaque authentication data, up to `MAX_DATA_SIZE` bytes. MUST be + set to the ASCII encoding of the canonical form of the URL of the + builder the request is intended for (the lowercased scheme and host + only; no port, path, query, fragment, or userinfo). example: "0x68747470733a2f2f6275696c6465722e6578616d706c652e636f6d" pattern: "^0x[a-fA-F0-9]{0,8192}$" slot: From bd892d650df78f5205ad29ee7b7a89cb37bcfb5d Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 13 Jul 2026 20:43:40 -0700 Subject: [PATCH 04/28] Clarify request auth signing/verifying --- specs/gloas/builder.md | 51 ++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 6c4a8987..6b47aa4c 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -11,6 +11,7 @@ - [Per-request Validator Inputs](#per-request-validator-inputs) - [Proposer Preferences (Deprecation of Validator Registrations)](#proposer-preferences-deprecation-of-validator-registrations) - [Constructing a `SignedExecutionPayloadBid`](#constructing-a-signedexecutionpayloadbid) + - [Signing](#signing) - [Constructing a `SignedExecutionPayloadEnvelope`](#constructing-a-signedexecutionpayloadenvelope) @@ -149,18 +150,9 @@ signature against the `proposer_pubkey` path parameter, and check that `data` matches the canonical form of their own URL ([URL canonicalization][url-canonicalization]) and that `auth.message.slot` matches the proposal `slot` path parameter (see -[Constructing the `RequestAuthV1`][signed-request-auth]). If verification fails, -the builder MAY return a 401 response. - -```python -def verify_request_auth_signature( - signed_request_auth: SignedRequestAuthV1, - pubkey: BLSPubkey, -) -> bool: - domain = compute_domain(DOMAIN_REQUEST_AUTH) - signing_root = compute_signing_root(signed_request_auth.message, domain) - return bls.Verify(pubkey, signing_root, signed_request_auth.signature) -``` +[Constructing the `RequestAuthV1`][signed-request-auth]). The signature is +verified with [`verify_request_auth_signature`](#signing). If verification +fails, the builder MAY return a 401 response. If the request body is absent, the builder MAY still serve a bid. @@ -199,6 +191,40 @@ committing to pay the proposer the sum of the two. `bid.value` is deducted from the builder's staked collateral on-chain even when `bid.execution_payment` is also set. +### Signing + +All signature operations follow the [standard BLS operations][bls] interface +defined in `consensus-specs`. + +The [`SignedRequestAuthV1`][signed-request-auth] is an out-of-protocol Builder +API message, specific to this API and analogous to the now-deprecated +`ValidatorRegistrationV1`. It is signed and verified under +`DOMAIN_REQUEST_AUTH`. This domain MUST NOT be confused with +`DOMAIN_BEACON_BUILDER`, which is used for in-protocol builder messages defined +by the consensus specs. + +The validator signs a `RequestAuthV1` and the builder verifies the resulting +`SignedRequestAuthV1` under `DOMAIN_REQUEST_AUTH`: + +```python +def get_request_auth_signature( + request_auth: RequestAuthV1, + privkey: int, +) -> BLSSignature: + domain = compute_domain(DOMAIN_REQUEST_AUTH) + signing_root = compute_signing_root(request_auth, domain) + return bls.Sign(privkey, signing_root) + + +def verify_request_auth_signature( + signed_request_auth: SignedRequestAuthV1, + pubkey: BLSPubkey, +) -> bool: + domain = compute_domain(DOMAIN_REQUEST_AUTH) + signing_root = compute_signing_root(signed_request_auth.message, domain) + return bls.Verify(pubkey, signing_root, signed_request_auth.signature) +``` + ## Constructing a `SignedExecutionPayloadEnvelope` If the builder's [`SignedExecutionPayloadBid`][signed-execution-payload-bid] has @@ -212,6 +238,7 @@ The specification for a block builder to construct a [`SignedExecutionPayloadEnvelope`][signed-execution-payload-envelope] is documented in the [Gloas consensus specs][gloas-builder-specs]. +[bls]: https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/beacon-chain.md#bls-signatures [get-execution-payload-bid-api]: ./../../apis/builder/execution_payload_bid.yaml [gloas-builder-specs]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/builder.md [gloas-consensus-specs]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas From 3dadb3a370b64907740eb24edc2b45b1e7d984a5 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Tue, 14 Jul 2026 10:35:27 -0700 Subject: [PATCH 05/28] Define canonicalize() and apply it when signing/verifying RequestAuthV1 so cached auths are easily reusable --- specs/gloas/builder.md | 11 +++++++---- specs/gloas/validator.md | 32 +++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 6b47aa4c..49a59cb5 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -203,16 +203,17 @@ API message, specific to this API and analogous to the now-deprecated `DOMAIN_BEACON_BUILDER`, which is used for in-protocol builder messages defined by the consensus specs. -The validator signs a `RequestAuthV1` and the builder verifies the resulting -`SignedRequestAuthV1` under `DOMAIN_REQUEST_AUTH`: +Signing and verifying `canonicalize` `message.data`, so the signature is always +over the canonical builder URL ([URL canonicalization][url-canonicalization]). ```python def get_request_auth_signature( request_auth: RequestAuthV1, privkey: int, ) -> BLSSignature: + signed = RequestAuthV1(data=canonicalize(request_auth.data), slot=request_auth.slot) domain = compute_domain(DOMAIN_REQUEST_AUTH) - signing_root = compute_signing_root(request_auth, domain) + signing_root = compute_signing_root(signed, domain) return bls.Sign(privkey, signing_root) @@ -220,8 +221,10 @@ def verify_request_auth_signature( signed_request_auth: SignedRequestAuthV1, pubkey: BLSPubkey, ) -> bool: + message = signed_request_auth.message + signed = RequestAuthV1(data=canonicalize(message.data), slot=message.slot) domain = compute_domain(DOMAIN_REQUEST_AUTH) - signing_root = compute_signing_root(signed_request_auth.message, domain) + signing_root = compute_signing_root(signed, domain) return bls.Verify(pubkey, signing_root, signed_request_auth.signature) ``` diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index d5bbd611..379c299d 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -140,8 +140,10 @@ builder MAY still serve a bid. If the validator chooses to authenticate its request, it constructs a `RequestAuthV1` with the following fields: -- `data`: MUST be set to the canonical form of the URL of the builder the - request is intended for (see [URL canonicalization](#url-canonicalization)). +- `data`: the canonical form of the URL of the builder the request is for (see + [URL canonicalization](#url-canonicalization)). Because it identifies the + builder and not an endpoint, one `SignedRequestAuthV1` can authenticate the + proposer for both `getExecutionPayloadBid` and `submitBuilderPreferences`. - `slot`: The proposal slot this request is authorized for, not the slot at which the request is signed or sent. @@ -155,19 +157,16 @@ from competing builders). #### URL canonicalization -`data` is compared byte-for-byte, so both sides must derive the same bytes from -the same URL. `data` identifies the builder, not an API resource or a transport -detail, so its canonical form is just the scheme and host: +`data` is the builder's URL reduced to its canonical form: the scheme and host +only. The validator canonicalizes before signing, and the builder canonicalizes +its own URL to compare it against `data`. - the scheme and host lowercased - no port - no path (including a bare trailing `/`), query, or fragment - no userinfo -`data` is the ASCII encoding of the canonical URL (internationalized hostnames -in their punycode form). The validator MUST apply these rules before signing, -and the builder MUST apply them to its own URL before comparing it against -`data`. For example: +Internationalized hosts use their punycode form. For example: | URL | Canonical form | | -------------------------------------- | ------------------------------- | @@ -176,6 +175,21 @@ and the builder MUST apply them to its own URL before comparing it against | `https://builder.example.com/bids?x=1` | `https://builder.example.com` | | `https://bücher.example` | `https://xn--bcher-kva.example` | +```python +def canonicalize(url: str) -> str: + # Reduce a builder URL to its canonical identity: scheme and host only. + scheme, _, rest = url.partition("://") + # The authority ends at the first '/', '?', or '#'. + authority = rest + for separator in ("/", "?", "#"): + authority = authority.split(separator, 1)[0] + # Drop userinfo (up to and including the last '@') and any port. + host = authority.rsplit("@", 1)[-1].rsplit(":", 1)[0] + # Lowercase; internationalized hosts use their punycode (RFC 3492) form. + host = host.lower().encode("idna").decode("ascii") + return f"{scheme.lower()}://{host}" +``` + ## Proposer Preferences *Note*: Validator registrations (`ValidatorRegistrationV1`) are **deprecated** From 5078eabc63cf092007bb933ae16209087c053eba Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 15 Jul 2026 16:30:03 -0700 Subject: [PATCH 06/28] Adopt opaque RequestAuthV1.data, drop URL canonicalization, add Eth-Builder-Url header --- apis/builder/builder_preferences.yaml | 30 +++++++--- apis/builder/execution_payload_bid.yaml | 33 +++++++---- .../gloas/builder_preferences_request.json | 2 +- examples/gloas/signed_request_auth.json | 2 +- specs/gloas/builder.md | 48 +++++++++------ specs/gloas/validator.md | 58 ++++--------------- types/gloas/request_auth.yaml | 9 ++- 7 files changed, 92 insertions(+), 90 deletions(-) diff --git a/apis/builder/builder_preferences.yaml b/apis/builder/builder_preferences.yaml index 7b62dcc2..50db397a 100644 --- a/apis/builder/builder_preferences.yaml +++ b/apis/builder/builder_preferences.yaml @@ -11,15 +11,12 @@ post: builders have the preferences before the bid request arrives. The builder MUST verify the BLS signature in `auth` against - `validator_pubkey`, and check that `auth.message.data` - matches the canonical form of its own URL (the lowercased scheme and - host only; no port, path, query, fragment, or userinfo), with both - sides applying the same canonicalization before comparison. - `auth.message.slot` is the proposal slot the + `validator_pubkey`, and check that `auth.message.data` matches the value it + agreed with the proposer. `auth.message.slot` is the proposal slot the preferences apply to; the builder MUST reject preferences whose slot has - already passed. If the signature check fails, the builder MUST - return a 401 response. If the URL or slot check fails, the builder MUST - return a 400 response. + already passed. If the signature check fails, the builder MUST return a 401 + response. If the data or slot check fails, the builder MUST return a 400 + response. A success response (202) indicates that the preferences were accepted. If the preferences are invalid, then the builder MUST return an error response @@ -41,6 +38,17 @@ post: description: "The active consensus version to which the request body belongs. Required if the request body is SSZ encoded." schema: $ref: "../../builder-oapi.yaml#/components/schemas/ConsensusVersion" + - name: Eth-Builder-Url + in: header + required: false + description: | + The URL of the intended builder. When the request is sent through a + proxy, the proxy uses it to route the request. Only used in proxied + setups; end builders ignore it. + schema: + type: string + format: uri + example: "https://builder.example.com" requestBody: description: A `BuilderPreferencesRequestV1` containing the proposer's preferences and a `SignedRequestAuthV1` for authentication. required: true @@ -71,7 +79,11 @@ post: WrongBuilder: value: code: 400 - message: "auth.message.data does not match this builder's URL" + message: "auth.message.data does not match the value agreed with this builder" + UnknownBuilder: + value: + code: 400 + message: "Eth-Builder-Url does not resolve to a configured builder" InvalidPreferences: value: code: 400 diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index 05c4ad64..72ba27cf 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -19,8 +19,9 @@ post: and MUST NOT include an execution layer payment in the bid. The `SignedRequestAuthV1` body is optional. If it is present but malformed - or fails signature verification, the builder MAY return a 401 response. - If it is absent, the builder MAY still serve a bid, but builders MAY + or its `data` or `slot` does not match, the builder MAY return a 400 + response; if its signature fails to verify, the builder MAY return a 401 + response. If it is absent, the builder MAY still serve a bid, but builders MAY use the presence and validity of the `SignedRequestAuthV1` to apply per-validator policy (e.g. rate-limiting, prioritization, or refusing unauthenticated requests). @@ -88,17 +89,25 @@ post: request body belongs. Required if the request body is SSZ encoded. schema: $ref: "../../builder-oapi.yaml#/components/schemas/ConsensusVersion" + - name: Eth-Builder-Url + in: header + required: false + description: | + The URL of the intended builder. When the request is sent through a + proxy, the proxy uses it to route the request. Only used in proxied + setups; end builders ignore it. + schema: + type: string + format: uri + example: "https://builder.example.com" requestBody: description: | Optional `SignedRequestAuthV1` authenticating the request. If provided, the builder MAY verify the BLS signature against the validator pubkey - resolved from the `proposer_pubkey` path parameter, and check that - `data` matches the canonical form of its own URL (the lowercased - scheme and host only; no port, path, query, fragment, or userinfo) - and that - `auth.message.slot` matches the - `slot` path parameter (the proposal slot). If absent, the builder MAY - still serve a bid subject to its own policy. + resolved from the `proposer_pubkey` path parameter, and check that `data` + matches the value it agreed with the proposer and that `auth.message.slot` + matches the `slot` path parameter (the proposal slot). If absent, the + builder MAY still serve a bid subject to its own policy. required: false content: application/json: @@ -150,7 +159,11 @@ post: WrongBuilder: value: code: 400 - message: "Invalid SignedRequestAuthV1: auth.message.data does not match this builder's URL" + message: "Invalid SignedRequestAuthV1: auth.message.data does not match the value agreed with this builder" + UnknownBuilder: + value: + code: 400 + message: "Eth-Builder-Url does not resolve to a configured builder" "401": description: Authentication required. content: diff --git a/examples/gloas/builder_preferences_request.json b/examples/gloas/builder_preferences_request.json index 4fd016b2..fb12b597 100644 --- a/examples/gloas/builder_preferences_request.json +++ b/examples/gloas/builder_preferences_request.json @@ -5,7 +5,7 @@ }, "auth": { "message": { - "data": "0x68747470733a2f2f6275696c6465722e6578616d706c652e636f6d", + "data": "0x1234567890abcdef", "slot": "1" }, "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" diff --git a/examples/gloas/signed_request_auth.json b/examples/gloas/signed_request_auth.json index 5eab95dc..bdadfc0c 100644 --- a/examples/gloas/signed_request_auth.json +++ b/examples/gloas/signed_request_auth.json @@ -1,7 +1,7 @@ { "value": { "message": { - "data": "0x68747470733a2f2f6275696c6465722e6578616d706c652e636f6d", + "data": "0x1234567890abcdef", "slot": "1" }, "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 49a59cb5..28daa116 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -9,6 +9,7 @@ - [Builder Preferences](#builder-preferences) - [`max_execution_payment`](#max_execution_payment) - [Per-request Validator Inputs](#per-request-validator-inputs) + - [Routing through a proxy](#routing-through-a-proxy) - [Proposer Preferences (Deprecation of Validator Registrations)](#proposer-preferences-deprecation-of-validator-registrations) - [Constructing a `SignedExecutionPayloadBid`](#constructing-a-signedexecutionpayloadbid) - [Signing](#signing) @@ -105,12 +106,15 @@ object containing: will accept from this builder (in Gwei). - `auth`: A `SignedRequestAuthV1` authenticating the request. `auth.message.slot` is the proposal slot the preferences apply to. The builder - MUST check that `auth.message.data` matches the canonical form of its own URL - ([URL canonicalization][url-canonicalization]) and MUST verify the BLS - signature against the `validator_pubkey` path parameter. If either check - fails, the builder MUST return a 400 response. The builder MUST reject - preferences whose `auth.message.slot` has already passed, so that a replayed - request cannot roll preferences back to a stale value. + MUST verify the BLS signature against the `validator_pubkey` path parameter + and MUST check that `auth.message.data` matches the value it agreed with the + proposer, so that an unauthenticated or replayed request cannot skew a + proposer's preferences away from the value the proposer chose. If the + signature fails to verify, the builder MUST return a 401 response; if the + `auth.message.data` check fails, the builder MUST return a 400 response. The + builder MUST also reject, with a 400 response, preferences whose + `auth.message.slot` has already passed, so that a replayed request cannot roll + preferences back to a stale value. The builder SHOULD store the preferences for each proposer and apply the `max_execution_payment` constraint when constructing bids. If no preferences @@ -147,15 +151,25 @@ treat `max_execution_payment` as `0` or can choose to not serve the bid. If the request body is present, builders MAY verify the `SignedRequestAuthV1` signature against the `proposer_pubkey` path parameter, and check that `data` -matches the canonical form of their own URL -([URL canonicalization][url-canonicalization]) and that `auth.message.slot` +matches the value they agreed with the proposer and that `auth.message.slot` matches the proposal `slot` path parameter (see [Constructing the `RequestAuthV1`][signed-request-auth]). The signature is -verified with [`verify_request_auth_signature`](#signing). If verification -fails, the builder MAY return a 401 response. +verified with [`verify_request_auth_signature`](#signing). If the signature +fails to verify, the builder MAY return a 401 response; if the `data` or +`auth.message.slot` check fails, the builder MAY return a 400 response. If the request body is absent, the builder MAY still serve a bid. +### Routing through a proxy + +A proposer MAY reach a builder through a proxy (e.g. a sidecar) by supplying an +`Eth-Builder-Url` header set to the builder's URL. The header instructs the +proxy where to forward the request. End builders that receive the header SHOULD +ignore it. A proxy that cannot resolve `Eth-Builder-Url` to a configured builder +MUST return a 400 response. The header does not need to be signed, because +tampering with it only misroutes the request: an authenticated request delivered +to the wrong builder fails its authentication check. + ## Proposer Preferences (Deprecation of Validator Registrations) *Note*: `ValidatorRegistrationV1` is **deprecated** in favor of @@ -203,17 +217,18 @@ API message, specific to this API and analogous to the now-deprecated `DOMAIN_BEACON_BUILDER`, which is used for in-protocol builder messages defined by the consensus specs. -Signing and verifying `canonicalize` `message.data`, so the signature is always -over the canonical builder URL ([URL canonicalization][url-canonicalization]). +Signing and verification use the `RequestAuthV1` message exactly as serialized, +computing the signing root over its SSZ bytes. A beacon node or proxy that +forwards a `SignedRequestAuthV1` MUST pass its `message` and `signature` through +unchanged, so a builder verifies the same bytes the validator signed. ```python def get_request_auth_signature( request_auth: RequestAuthV1, privkey: int, ) -> BLSSignature: - signed = RequestAuthV1(data=canonicalize(request_auth.data), slot=request_auth.slot) domain = compute_domain(DOMAIN_REQUEST_AUTH) - signing_root = compute_signing_root(signed, domain) + signing_root = compute_signing_root(request_auth, domain) return bls.Sign(privkey, signing_root) @@ -221,10 +236,8 @@ def verify_request_auth_signature( signed_request_auth: SignedRequestAuthV1, pubkey: BLSPubkey, ) -> bool: - message = signed_request_auth.message - signed = RequestAuthV1(data=canonicalize(message.data), slot=message.slot) domain = compute_domain(DOMAIN_REQUEST_AUTH) - signing_root = compute_signing_root(signed, domain) + signing_root = compute_signing_root(signed_request_auth.message, domain) return bls.Verify(pubkey, signing_root, signed_request_auth.signature) ``` @@ -251,4 +264,3 @@ documented in the [Gloas consensus specs][gloas-builder-specs]. [signed-execution-payload-envelope]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadenvelope [signed-request-auth]: ./validator.md#signedrequestauthv1 [submit-builder-preferences-api]: ./../../apis/builder/builder_preferences.yaml -[url-canonicalization]: ./validator.md#url-canonicalization diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 379c299d..61480818 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -14,7 +14,6 @@ - [`max_execution_payment`](#max_execution_payment) - [Bid Request](#bid-request) - [Constructing the `RequestAuthV1`](#constructing-the-requestauthv1) - - [URL canonicalization](#url-canonicalization) - [Proposer Preferences](#proposer-preferences) - [Validating a `SignedExecutionPayloadBid`](#validating-a-signedexecutionpayloadbid) - [Block proposal](#block-proposal) @@ -100,12 +99,14 @@ The validator then constructs a `BuilderPreferencesRequestV1` with the `BuilderPreferencesV1` as `preferences` and a `SignedRequestAuthV1` as `auth`. The `SignedRequestAuthV1` is constructed as described in [Constructing the `RequestAuthV1`](#constructing-the-requestauthv1); its -`auth.message.data` identifies the intended builder and its `auth.message.slot` -is the proposal slot the preferences apply to. The builder MUST verify the -`auth` signature against the `validator_pubkey` path parameter and MUST reject -the request with a 400 response if `auth.message.data` does not match the -canonical form of its own URL (see -[URL canonicalization](#url-canonicalization)). +`auth.message.data` is the authentication data the builder expects and its +`auth.message.slot` is the proposal slot the preferences apply to. The builder +MUST verify the `auth` signature against the `validator_pubkey` path parameter, +returning a 401 response if it fails to verify, and MUST reject the request with +a 400 response if `auth.message.data` does not match the value it agreed with +the proposer. The builder MUST also reject, with a 400 response, a request whose +`auth.message.slot` has already passed, so that a replay cannot roll preferences +back to a stale value. If no preferences have been submitted, the builder MUST treat the proposer's `max_execution_payment` as `0`. @@ -140,10 +141,10 @@ builder MAY still serve a bid. If the validator chooses to authenticate its request, it constructs a `RequestAuthV1` with the following fields: -- `data`: the canonical form of the URL of the builder the request is for (see - [URL canonicalization](#url-canonicalization)). Because it identifies the - builder and not an endpoint, one `SignedRequestAuthV1` can authenticate the - proposer for both `getExecutionPayloadBid` and `submitBuilderPreferences`. +- `data`: opaque authentication data agreed with the builder out of band whose + meaning is left to the two parties. It is not tied to an endpoint, so one + `SignedRequestAuthV1` can authenticate the proposer for both + `getExecutionPayloadBid` and `submitBuilderPreferences`. - `slot`: The proposal slot this request is authorized for, not the slot at which the request is signed or sent. @@ -155,41 +156,6 @@ The validator then constructs the `SignedRequestAuthV1` by signing the validator and discard requests from other parties (e.g. DDOS or replay attempts from competing builders). -#### URL canonicalization - -`data` is the builder's URL reduced to its canonical form: the scheme and host -only. The validator canonicalizes before signing, and the builder canonicalizes -its own URL to compare it against `data`. - -- the scheme and host lowercased -- no port -- no path (including a bare trailing `/`), query, or fragment -- no userinfo - -Internationalized hosts use their punycode form. For example: - -| URL | Canonical form | -| -------------------------------------- | ------------------------------- | -| `HTTPS://Builder.Example.com/` | `https://builder.example.com` | -| `https://builder.example.com:8080` | `https://builder.example.com` | -| `https://builder.example.com/bids?x=1` | `https://builder.example.com` | -| `https://bücher.example` | `https://xn--bcher-kva.example` | - -```python -def canonicalize(url: str) -> str: - # Reduce a builder URL to its canonical identity: scheme and host only. - scheme, _, rest = url.partition("://") - # The authority ends at the first '/', '?', or '#'. - authority = rest - for separator in ("/", "?", "#"): - authority = authority.split(separator, 1)[0] - # Drop userinfo (up to and including the last '@') and any port. - host = authority.rsplit("@", 1)[-1].rsplit(":", 1)[0] - # Lowercase; internationalized hosts use their punycode (RFC 3492) form. - host = host.lower().encode("idna").decode("ascii") - return f"{scheme.lower()}://{host}" -``` - ## Proposer Preferences *Note*: Validator registrations (`ValidatorRegistrationV1`) are **deprecated** diff --git a/types/gloas/request_auth.yaml b/types/gloas/request_auth.yaml index 67f2f949..e67ce8d0 100644 --- a/types/gloas/request_auth.yaml +++ b/types/gloas/request_auth.yaml @@ -7,11 +7,10 @@ Gloas: type: string format: hex description: >- - Opaque authentication data, up to `MAX_DATA_SIZE` bytes. MUST be - set to the ASCII encoding of the canonical form of the URL of the - builder the request is intended for (the lowercased scheme and host - only; no port, path, query, fragment, or userinfo). - example: "0x68747470733a2f2f6275696c6465722e6578616d706c652e636f6d" + Opaque authentication data unique to the builder, agreed upon out of + band. The meaning of the up to `MAX_DATA_SIZE` bytes is left to the + two parties. + example: "0x1234567890abcdef" pattern: "^0x[a-fA-F0-9]{0,8192}$" slot: $ref: "../../beacon-apis/types/primitive.yaml#/Uint64" From 31712daba1e3f6ccc2abd774904d6556f57ba6c0 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 20 Jul 2026 13:50:33 -0700 Subject: [PATCH 07/28] SHOULD use builder URL as the default request auth data --- types/gloas/request_auth.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/types/gloas/request_auth.yaml b/types/gloas/request_auth.yaml index e67ce8d0..206fe533 100644 --- a/types/gloas/request_auth.yaml +++ b/types/gloas/request_auth.yaml @@ -9,7 +9,10 @@ Gloas: description: >- Opaque authentication data unique to the builder, agreed upon out of band. The meaning of the up to `MAX_DATA_SIZE` bytes is left to the - two parties. + two parties. When no value has been agreed out of band, implementations + SHOULD default to the builder's URL exactly as advertised, so proposers + with no prior relationship (and DVT operators sharing + static config) can construct an identical `data` deterministically. example: "0x1234567890abcdef" pattern: "^0x[a-fA-F0-9]{0,8192}$" slot: From e6f34b232d2e600f45a80a61f47c76b4b9faee7d Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 20 Jul 2026 14:52:22 -0700 Subject: [PATCH 08/28] remove Eth-Consensus-Version header requirement if type isn't fork versioned --- apis/builder/builder_preferences.yaml | 6 ------ apis/builder/execution_payload_bid.yaml | 8 -------- specs/gloas/builder.md | 4 ++-- specs/gloas/validator.md | 6 +++--- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/apis/builder/builder_preferences.yaml b/apis/builder/builder_preferences.yaml index 50db397a..4b0e7910 100644 --- a/apis/builder/builder_preferences.yaml +++ b/apis/builder/builder_preferences.yaml @@ -32,12 +32,6 @@ post: description: "The BLS public key of the validator expressing these preferences." schema: $ref: "../../beacon-apis/types/primitive.yaml#/Pubkey" - - name: Eth-Consensus-Version - in: header - required: true - description: "The active consensus version to which the request body belongs. Required if the request body is SSZ encoded." - schema: - $ref: "../../builder-oapi.yaml#/components/schemas/ConsensusVersion" - name: Eth-Builder-Url in: header required: false diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index 72ba27cf..68528719 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -81,14 +81,6 @@ post: type: integer format: int64 example: 10000 - - name: Eth-Consensus-Version - in: header - required: true - description: | - The active consensus version to which the `SignedRequestAuthV1` in the - request body belongs. Required if the request body is SSZ encoded. - schema: - $ref: "../../builder-oapi.yaml#/components/schemas/ConsensusVersion" - name: Eth-Builder-Url in: header required: false diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 28daa116..c0e3186a 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -141,8 +141,8 @@ Validators communicate per-request inputs to a builder on each - Optionally, a [`SignedRequestAuthV1`][signed-request-auth] in the request body used to authenticate the requesting validator. The body MAY be encoded as JSON (`Content-Type: application/json`) or SSZ - (`Content-Type: application/octet-stream`); when SSZ is used, the - `Eth-Consensus-Version` header MUST also be set. + (`Content-Type: application/octet-stream`); `RequestAuthV1` is not + fork-versioned, so no `Eth-Consensus-Version` header is required. The proposer's `max_execution_payment` is communicated exclusively via the [`submitBuilderPreferences`][submit-builder-preferences-api] endpoint. If no diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 61480818..b8d1035d 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -132,9 +132,9 @@ When calling [`getExecutionPayloadBid`][get-execution-payload-bid-api], the validator MAY send a [`SignedRequestAuthV1`](#signedrequestauthv1) as the request body to authenticate the request. The body MAY be encoded as JSON (`Content-Type: application/json`) or SSZ -(`Content-Type: application/octet-stream`); when SSZ is used, the validator MUST -also send the `Eth-Consensus-Version` header. If the body is omitted, the -builder MAY still serve a bid. +(`Content-Type: application/octet-stream`); `RequestAuthV1` is not +fork-versioned, so no `Eth-Consensus-Version` header is required. If the body is +omitted, the builder MAY still serve a bid. ### Constructing the `RequestAuthV1` From 4a602140b23e2a8925447cbdd66fe60747b07549 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 20 Jul 2026 16:06:02 -0700 Subject: [PATCH 09/28] housekeeping: - Eth-Builder-Url to submitSignedBeaconBlock - Specify clearly max_execution_payment in Gwei - additional clarity on what auth data SHOULD default to --- apis/builder/beacon_blocks.yaml | 10 ++++++++++ types/gloas/builder_preferences.yaml | 2 +- types/gloas/request_auth.yaml | 9 +++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apis/builder/beacon_blocks.yaml b/apis/builder/beacon_blocks.yaml index 6bbaede3..78cb8c33 100644 --- a/apis/builder/beacon_blocks.yaml +++ b/apis/builder/beacon_blocks.yaml @@ -22,6 +22,16 @@ post: required: true name: Eth-Consensus-Version description: "The active consensus version to which the block being submitted belongs." + - name: Eth-Builder-Url + in: header + required: false + description: | + The URL of the intended builder. When the request is sent through a + proxy, the proxy uses it to route the request. Only used in proxied + setups; end builders ignore it. + schema: + type: string + format: uri requestBody: description: A `SignedBeaconBlock`. required: true diff --git a/types/gloas/builder_preferences.yaml b/types/gloas/builder_preferences.yaml index 01b045ed..2b64dc56 100644 --- a/types/gloas/builder_preferences.yaml +++ b/types/gloas/builder_preferences.yaml @@ -6,7 +6,7 @@ Gloas: properties: max_execution_payment: $ref: "../../beacon-apis/types/primitive.yaml#/Uint64" - description: "Indicates the maximum amount that a proposer is willing to accept as an execution payment from the builder." + description: "The maximum amount, in Gwei, that a proposer is willing to accept as an execution payment from the builder." BuilderPreferencesRequestV1: type: object description: "A builder preferences submission containing the proposer's preferences and a signed request authentication." diff --git a/types/gloas/request_auth.yaml b/types/gloas/request_auth.yaml index 206fe533..edba9974 100644 --- a/types/gloas/request_auth.yaml +++ b/types/gloas/request_auth.yaml @@ -9,10 +9,11 @@ Gloas: description: >- Opaque authentication data unique to the builder, agreed upon out of band. The meaning of the up to `MAX_DATA_SIZE` bytes is left to the - two parties. When no value has been agreed out of band, implementations - SHOULD default to the builder's URL exactly as advertised, so proposers - with no prior relationship (and DVT operators sharing - static config) can construct an identical `data` deterministically. + two parties; the builder verifies the exact bytes. When no value has + been agreed out of band, implementations SHOULD default to the + builder's URL exactly as advertised, so proposers with no prior + relationship (and DVT operators sharing static config) can construct an + identical `data` deterministically. example: "0x1234567890abcdef" pattern: "^0x[a-fA-F0-9]{0,8192}$" slot: From 06ac9357da936699a99fba44f00e055c8d3f5a04 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 20 Jul 2026 21:46:43 -0700 Subject: [PATCH 10/28] add missing example --- apis/builder/beacon_blocks.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/apis/builder/beacon_blocks.yaml b/apis/builder/beacon_blocks.yaml index 78cb8c33..3dd426c3 100644 --- a/apis/builder/beacon_blocks.yaml +++ b/apis/builder/beacon_blocks.yaml @@ -32,6 +32,7 @@ post: schema: type: string format: uri + example: "https://builder.example.com" requestBody: description: A `SignedBeaconBlock`. required: true From 51545cf02a2131976e0baa87bf722ca2931986f6 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 22 Jul 2026 15:41:59 -0700 Subject: [PATCH 11/28] require and verify the SignedRequestAuthV1, drop explicit proxy support - the auth body on getExecutionPayloadBid is required and builders MUST verify it: signature failure is a 401, data/slot mismatch a 400, missing or malformed body a 400 -- the same auth story as submitBuilderPreferences; proposer duties are known an epoch ahead so auths pre-sign off the proposal hot path - delete the proxy section and the Eth-Builder-Url header from all endpoints; url and auth data are sufficient to support optional proxies - relax the no-preferences default: a served bid MUST honor the max_execution_payment cap from stored preferences, but without them the builder MAY serve any execution_payment; the proposer's locally configured per-builder limits are the backstop and discard bids that exceed them - data default convention: UTF-8 bytes of the builder's own advertised URL, hex pattern tightened to whole bytes - error surface aligned: 401 is signature failure only (InvalidAuth), 400 gains MissingBody, WrongBuilder renamed DataMismatch, 204 is any non-served bid; submitBuilderPreferences notes JSON/SSZ bodies need no Eth-Consensus-Version header (not fork-versioned) - unify the path parameter name to proposer_pubkey across both endpoints --- apis/builder/beacon_blocks.yaml | 11 ---- apis/builder/builder_preferences.yaml | 33 ++++-------- apis/builder/execution_payload_bid.yaml | 69 ++++++++++--------------- builder-oapi.yaml | 2 +- specs/gloas/builder.md | 63 ++++++++++------------ specs/gloas/validator.md | 45 ++++++++-------- types/gloas/request_auth.yaml | 14 ++--- 7 files changed, 95 insertions(+), 142 deletions(-) diff --git a/apis/builder/beacon_blocks.yaml b/apis/builder/beacon_blocks.yaml index 3dd426c3..6bbaede3 100644 --- a/apis/builder/beacon_blocks.yaml +++ b/apis/builder/beacon_blocks.yaml @@ -22,17 +22,6 @@ post: required: true name: Eth-Consensus-Version description: "The active consensus version to which the block being submitted belongs." - - name: Eth-Builder-Url - in: header - required: false - description: | - The URL of the intended builder. When the request is sent through a - proxy, the proxy uses it to route the request. Only used in proxied - setups; end builders ignore it. - schema: - type: string - format: uri - example: "https://builder.example.com" requestBody: description: A `SignedBeaconBlock`. required: true diff --git a/apis/builder/builder_preferences.yaml b/apis/builder/builder_preferences.yaml index 4b0e7910..fdd19b4a 100644 --- a/apis/builder/builder_preferences.yaml +++ b/apis/builder/builder_preferences.yaml @@ -10,8 +10,12 @@ post: which they will be proposing, as determined from `state.proposer_lookahead`, so that builders have the preferences before the bid request arrives. + The body MAY be encoded as JSON (`Content-Type: application/json`) or SSZ + (`Content-Type: application/octet-stream`); `BuilderPreferencesRequestV1` + is not fork-versioned, so no `Eth-Consensus-Version` header is required. + The builder MUST verify the BLS signature in `auth` against - `validator_pubkey`, and check that `auth.message.data` matches the value it + `proposer_pubkey`, and check that `auth.message.data` matches the value it agreed with the proposer. `auth.message.slot` is the proposal slot the preferences apply to; the builder MUST reject preferences whose slot has already passed. If the signature check fails, the builder MUST return a 401 @@ -26,23 +30,12 @@ post: tags: - Builder parameters: - - name: validator_pubkey + - name: proposer_pubkey in: path required: true - description: "The BLS public key of the validator expressing these preferences." + description: "The BLS public key of the proposer expressing these preferences." schema: $ref: "../../beacon-apis/types/primitive.yaml#/Pubkey" - - name: Eth-Builder-Url - in: header - required: false - description: | - The URL of the intended builder. When the request is sent through a - proxy, the proxy uses it to route the request. Only used in proxied - setups; end builders ignore it. - schema: - type: string - format: uri - example: "https://builder.example.com" requestBody: description: A `BuilderPreferencesRequestV1` containing the proposer's preferences and a `SignedRequestAuthV1` for authentication. required: true @@ -70,26 +63,22 @@ post: value: code: 400 message: "Invalid SignedRequestAuthV1: auth.message.slot has already passed" - WrongBuilder: - value: - code: 400 - message: "auth.message.data does not match the value agreed with this builder" - UnknownBuilder: + DataMismatch: value: code: 400 - message: "Eth-Builder-Url does not resolve to a configured builder" + message: "Invalid SignedRequestAuthV1: auth.message.data does not match the value agreed with this builder" InvalidPreferences: value: code: 400 message: "Invalid builder preferences: max_execution_payment malformed" "401": - description: Authentication required. + description: Authentication failed. content: application/json: schema: $ref: "../../builder-oapi.yaml#/components/schemas/ErrorMessage" examples: - SignatureVerificationFailed: + InvalidAuth: value: code: 401 message: "Invalid SignedRequestAuthV1: signature verification failed" diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index 68528719..d579caa6 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -10,28 +10,28 @@ post: - The hash of the execution layer block the proposer will build on. - The root of the beacon block the proposer will build on. - The public key of the proposer. - - Optionally, a `SignedRequestAuthV1` in the request body that - authenticates the request. The body MAY be encoded as JSON or SSZ. + - A `SignedRequestAuthV1` in the request body that authenticates the + request. The body MAY be encoded as JSON or SSZ. The proposer's `max_execution_payment` is communicated exclusively via the - `submitBuilderPreferences` endpoint. If no `BuilderPreferencesV1` have been - submitted for the proposer, the builder MUST treat `max_execution_payment` as `0` - and MUST NOT include an execution layer payment in the bid. + `submitBuilderPreferences` endpoint. Any bid the builder serves MUST honor + the `max_execution_payment` cap from stored preferences. Without stored + preferences the builder MAY serve a bid with any `execution_payment`; the + proposer's locally configured per-builder limits are the backstop: the + proposer discards any bid that exceeds them. - The `SignedRequestAuthV1` body is optional. If it is present but malformed - or its `data` or `slot` does not match, the builder MAY return a 400 - response; if its signature fails to verify, the builder MAY return a 401 - response. If it is absent, the builder MAY still serve a bid, but builders MAY - use the presence and validity of the `SignedRequestAuthV1` to apply - per-validator policy (e.g. rate-limiting, prioritization, or refusing - unauthenticated requests). + The `SignedRequestAuthV1` body is required. If the body is missing or + malformed, the request is invalid and the builder MUST return a 400 + response. If its `auth.message.data` or `auth.message.slot` does not match, + the builder MUST return a 400 response; if its signature fails to verify, + the builder MUST return a 401 response. Builders MAY use the authenticated + identity to apply per-validator policy (e.g. rate-limiting). The builder responds with a 200 response containing an execution payload bid if it can provide one. - If the builder is unable to produce a valid execution payload bid, then - the builder MUST return a 204 response. If the request is invalid, then the - builder MUST return an error response (400) with a description of the - validation failure. + A builder that does not serve a bid MUST return a 204 response. If the + request is invalid, then the builder MUST return an error response (400) + with a description of the validation failure. This API is applicable from Gloas fork onwards. tags: @@ -81,26 +81,15 @@ post: type: integer format: int64 example: 10000 - - name: Eth-Builder-Url - in: header - required: false - description: | - The URL of the intended builder. When the request is sent through a - proxy, the proxy uses it to route the request. Only used in proxied - setups; end builders ignore it. - schema: - type: string - format: uri - example: "https://builder.example.com" requestBody: description: | - Optional `SignedRequestAuthV1` authenticating the request. If provided, - the builder MAY verify the BLS signature against the validator pubkey - resolved from the `proposer_pubkey` path parameter, and check that `data` - matches the value it agreed with the proposer and that `auth.message.slot` - matches the `slot` path parameter (the proposal slot). If absent, the - builder MAY still serve a bid subject to its own policy. - required: false + A `SignedRequestAuthV1` authenticating the request. The builder MUST + verify the BLS signature against the validator pubkey resolved from the + `proposer_pubkey` path parameter, and MUST check that `auth.message.data` + matches the value it agreed with the proposer and that + `auth.message.slot` matches the `slot` path parameter (the proposal + slot). + required: true content: application/json: schema: @@ -148,25 +137,21 @@ post: value: code: 400 message: "Invalid SignedRequestAuthV1: auth.message.slot does not match the proposal slot in the request path" - WrongBuilder: + DataMismatch: value: code: 400 message: "Invalid SignedRequestAuthV1: auth.message.data does not match the value agreed with this builder" - UnknownBuilder: + MissingBody: value: code: 400 - message: "Eth-Builder-Url does not resolve to a configured builder" + message: "Invalid request: SignedRequestAuthV1 body is required" "401": - description: Authentication required. + description: Authentication failed. content: application/json: schema: $ref: "../../builder-oapi.yaml#/components/schemas/ErrorMessage" examples: - MissingAuth: - value: - code: 401 - message: "Missing SignedRequestAuthV1: this builder requires authenticated requests" InvalidAuth: value: code: 401 diff --git a/builder-oapi.yaml b/builder-oapi.yaml index 3e25135b..41a97653 100644 --- a/builder-oapi.yaml +++ b/builder-oapi.yaml @@ -61,7 +61,7 @@ paths: $ref: "./apis/builder/blinded_blocks.yaml" /eth/v2/builder/blinded_blocks: $ref: "./apis/builder/blinded_blocks_v2.yaml" - /eth/v1/builder/builder_preferences/{validator_pubkey}: + /eth/v1/builder/builder_preferences/{proposer_pubkey}: $ref: "./apis/builder/builder_preferences.yaml" /eth/v1/builder/status: $ref: "./apis/builder/status.yaml" diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index c0e3186a..71044400 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -9,7 +9,6 @@ - [Builder Preferences](#builder-preferences) - [`max_execution_payment`](#max_execution_payment) - [Per-request Validator Inputs](#per-request-validator-inputs) - - [Routing through a proxy](#routing-through-a-proxy) - [Proposer Preferences (Deprecation of Validator Registrations)](#proposer-preferences-deprecation-of-validator-registrations) - [Constructing a `SignedExecutionPayloadBid`](#constructing-a-signedexecutionpayloadbid) - [Signing](#signing) @@ -99,15 +98,15 @@ prior to the epoch in which they will be proposing, as determined from `state.proposer_lookahead`. The builder receives a `BuilderPreferencesRequestV1` object containing: -- `validator_pubkey`: The BLS public key of the validator submitting these +- `proposer_pubkey`: The BLS public key of the proposer submitting these preferences, passed as a path parameter. - `preferences`: A `BuilderPreferencesV1` with: - `max_execution_payment`: The maximum execution layer payment the proposer will accept from this builder (in Gwei). - `auth`: A `SignedRequestAuthV1` authenticating the request. `auth.message.slot` is the proposal slot the preferences apply to. The builder - MUST verify the BLS signature against the `validator_pubkey` path parameter - and MUST check that `auth.message.data` matches the value it agreed with the + MUST verify the BLS signature against the `proposer_pubkey` path parameter and + MUST check that `auth.message.data` matches the value it agreed with the proposer, so that an unauthenticated or replayed request cannot skew a proposer's preferences away from the value the proposer chose. If the signature fails to verify, the builder MUST return a 401 response; if the @@ -116,11 +115,11 @@ object containing: `auth.message.slot` has already passed, so that a replayed request cannot roll preferences back to a stale value. -The builder SHOULD store the preferences for each proposer and apply the -`max_execution_payment` constraint when constructing bids. If no preferences -have been submitted for a proposer, the builder MUST treat the proposer's -`max_execution_payment` as `0`. The builder can also choose to not serve the -bid. +The builder SHOULD store the preferences for each proposer and MUST honor the +`max_execution_payment` cap in any bid it serves. Without stored preferences it +MAY serve a bid with any `execution_payment`. The proposer's locally configured +per-builder limits are the backstop: the proposer discards any bid that exceeds +them. ### `max_execution_payment` @@ -138,37 +137,27 @@ reputation. Validators communicate per-request inputs to a builder on each [`getExecutionPayloadBid`][get-execution-payload-bid-api] call: -- Optionally, a [`SignedRequestAuthV1`][signed-request-auth] in the request body - used to authenticate the requesting validator. The body MAY be encoded as JSON - (`Content-Type: application/json`) or SSZ +- A [`SignedRequestAuthV1`][signed-request-auth] in the request body + authenticating the requesting validator. The body is required and MAY be + encoded as JSON (`Content-Type: application/json`) or SSZ (`Content-Type: application/octet-stream`); `RequestAuthV1` is not fork-versioned, so no `Eth-Consensus-Version` header is required. The proposer's `max_execution_payment` is communicated exclusively via the -[`submitBuilderPreferences`][submit-builder-preferences-api] endpoint. If no -`BuilderPreferencesV1` have been submitted for the proposer, the builder MUST -treat `max_execution_payment` as `0` or can choose to not serve the bid. +[`submitBuilderPreferences`][submit-builder-preferences-api] endpoint. A bid +MUST honor the `max_execution_payment` cap from stored preferences; without them +the builder MAY serve a bid with any `execution_payment`. -If the request body is present, builders MAY verify the `SignedRequestAuthV1` -signature against the `proposer_pubkey` path parameter, and check that `data` +Builders MUST verify the `SignedRequestAuthV1` signature against the +`proposer_pubkey` path parameter, and MUST check that `auth.message.data` matches the value they agreed with the proposer and that `auth.message.slot` matches the proposal `slot` path parameter (see [Constructing the `RequestAuthV1`][signed-request-auth]). The signature is verified with [`verify_request_auth_signature`](#signing). If the signature -fails to verify, the builder MAY return a 401 response; if the `data` or -`auth.message.slot` check fails, the builder MAY return a 400 response. - -If the request body is absent, the builder MAY still serve a bid. - -### Routing through a proxy - -A proposer MAY reach a builder through a proxy (e.g. a sidecar) by supplying an -`Eth-Builder-Url` header set to the builder's URL. The header instructs the -proxy where to forward the request. End builders that receive the header SHOULD -ignore it. A proxy that cannot resolve `Eth-Builder-Url` to a configured builder -MUST return a 400 response. The header does not need to be signed, because -tampering with it only misroutes the request: an authenticated request delivered -to the wrong builder fails its authentication check. +fails to verify, the builder MUST return a 401 response; if the +`auth.message.data` or `auth.message.slot` check fails, the builder MUST return +a 400 response. A missing or malformed body is an invalid request and the +builder MUST return a 400 response. ## Proposer Preferences (Deprecation of Validator Registrations) @@ -195,9 +184,9 @@ MUST set `bid.value` to the amount they are committing to pay. If the builder intends to pay the proposer via an execution layer payment, they MUST set `bid.execution_payment`. This value MUST NOT exceed the -`max_execution_payment` from the proposer's stored `BuilderPreferencesV1`. If no -`BuilderPreferencesV1` have been submitted, the builder MUST NOT include an -execution layer payment (i.e. MUST set `bid.execution_payment` to `0`). +`max_execution_payment` from the proposer's stored `BuilderPreferencesV1`. +Without stored preferences the builder MAY set any `bid.execution_payment`; the +proposer discards any bid that exceeds its locally configured limits. *Note*: `bid.value` and `bid.execution_payment` are not mutually exclusive. A builder MAY set both fields on a single bid; in that case the builder is @@ -218,9 +207,9 @@ API message, specific to this API and analogous to the now-deprecated by the consensus specs. Signing and verification use the `RequestAuthV1` message exactly as serialized, -computing the signing root over its SSZ bytes. A beacon node or proxy that -forwards a `SignedRequestAuthV1` MUST pass its `message` and `signature` through -unchanged, so a builder verifies the same bytes the validator signed. +computing the signing root over its SSZ bytes. A beacon node that forwards a +`SignedRequestAuthV1` MUST pass its `message` and `signature` through unchanged, +so a builder verifies the same bytes the validator signed. ```python def get_request_auth_signature( diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index b8d1035d..4c340028 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -91,9 +91,8 @@ The validator constructs a `BuilderPreferencesV1` with: accept from this builder. See [`max_execution_payment`](#max_execution_payment). -The validator's BLS public key is passed as the `validator_pubkey` path -parameter in the [`submitBuilderPreferences`][submit-builder-preferences-api] -API call. +The proposer's BLS public key is passed as the `proposer_pubkey` path parameter +in the [`submitBuilderPreferences`][submit-builder-preferences-api] API call. The validator then constructs a `BuilderPreferencesRequestV1` with the `BuilderPreferencesV1` as `preferences` and a `SignedRequestAuthV1` as `auth`. @@ -101,15 +100,17 @@ The `SignedRequestAuthV1` is constructed as described in [Constructing the `RequestAuthV1`](#constructing-the-requestauthv1); its `auth.message.data` is the authentication data the builder expects and its `auth.message.slot` is the proposal slot the preferences apply to. The builder -MUST verify the `auth` signature against the `validator_pubkey` path parameter, +MUST verify the `auth` signature against the `proposer_pubkey` path parameter, returning a 401 response if it fails to verify, and MUST reject the request with a 400 response if `auth.message.data` does not match the value it agreed with the proposer. The builder MUST also reject, with a 400 response, a request whose `auth.message.slot` has already passed, so that a replay cannot roll preferences back to a stale value. -If no preferences have been submitted, the builder MUST treat the proposer's -`max_execution_payment` as `0`. +A builder MUST honor the `max_execution_payment` cap in any bid it serves; +without stored preferences it MAY serve a bid with any `execution_payment`. The +proposer's locally configured per-builder limits are the backstop: the proposer +discards any bid that exceeds them. ### `max_execution_payment` @@ -122,29 +123,29 @@ payment amount from the builder. Proposers may adjust this parameter based on their level of trust in the builder's reliability and reputation. `max_execution_payment` is communicated exclusively via the -[`submitBuilderPreferences`][submit-builder-preferences-api] endpoint. If no -`BuilderPreferencesV1` have been submitted to a builder, that builder MUST NOT -include an execution layer payment in its bid. +[`submitBuilderPreferences`][submit-builder-preferences-api] endpoint. ## Bid Request When calling [`getExecutionPayloadBid`][get-execution-payload-bid-api], the -validator MAY send a [`SignedRequestAuthV1`](#signedrequestauthv1) as the +validator MUST send a [`SignedRequestAuthV1`](#signedrequestauthv1) as the request body to authenticate the request. The body MAY be encoded as JSON (`Content-Type: application/json`) or SSZ (`Content-Type: application/octet-stream`); `RequestAuthV1` is not -fork-versioned, so no `Eth-Consensus-Version` header is required. If the body is -omitted, the builder MAY still serve a bid. +fork-versioned, so no `Eth-Consensus-Version` header is required. Proposer +duties are known an epoch in advance, so the validator can sign the +`SignedRequestAuthV1` ahead of time, off the proposal hot path. ### Constructing the `RequestAuthV1` -If the validator chooses to authenticate its request, it constructs a -`RequestAuthV1` with the following fields: +The validator constructs a `RequestAuthV1` with the following fields: - `data`: opaque authentication data agreed with the builder out of band whose meaning is left to the two parties. It is not tied to an endpoint, so one `SignedRequestAuthV1` can authenticate the proposer for both - `getExecutionPayloadBid` and `submitBuilderPreferences`. + `getExecutionPayloadBid` and `submitBuilderPreferences`. When no value has + been agreed out of band, the validator SHOULD use the UTF-8 bytes of the + builder's own advertised URL, exactly as advertised. - `slot`: The proposal slot this request is authorized for, not the slot at which the request is signed or sent. @@ -215,11 +216,11 @@ def validate_bid( return verify_execution_payload_bid_signature(state, signed_bid) ``` -`max_execution_payment` is the value from the `BuilderPreferencesV1` the -validator submitted to this builder via -[`submitBuilderPreferences`][submit-builder-preferences-api]. Validators MUST -validate each bid against the `max_execution_payment` they submitted for that -builder. +`max_execution_payment` is the limit the validator has locally configured for +this builder, the same value it submits via +[`submitBuilderPreferences`][submit-builder-preferences-api] when it submits +preferences. Validators MUST validate each bid against that limit, whether or +not preferences were submitted. Note that the fee recipient specified in `bid.fee_recipient` does not necessarily correspond to the fee recipient of the execution payload. Even if a @@ -239,8 +240,8 @@ block on top of a beacon `state` must take the following actions: 1. Call upstream builder software to get a [`SignedExecutionPayloadBid`][signed-execution-payload-bid] using the [`getExecutionPayloadBid`][get-execution-payload-bid-api] API call. The - validator MAY send a `SignedRequestAuthV1` in the request body to - authenticate the request. + validator sends a `SignedRequestAuthV1` in the request body to authenticate + the request. 2. Assemble a `SignedBeaconBlock` according to the process outlined in the [Gloas validator specs][gloas-validator-specs] but with the best [`SignedExecutionPayloadBid`][signed-execution-payload-bid] from the prior diff --git a/types/gloas/request_auth.yaml b/types/gloas/request_auth.yaml index edba9974..ae97bb12 100644 --- a/types/gloas/request_auth.yaml +++ b/types/gloas/request_auth.yaml @@ -8,14 +8,14 @@ Gloas: format: hex description: >- Opaque authentication data unique to the builder, agreed upon out of - band. The meaning of the up to `MAX_DATA_SIZE` bytes is left to the - two parties; the builder verifies the exact bytes. When no value has - been agreed out of band, implementations SHOULD default to the - builder's URL exactly as advertised, so proposers with no prior - relationship (and DVT operators sharing static config) can construct an - identical `data` deterministically. + band. The meaning of the up to `MAX_DATA_SIZE` (4096) bytes is left + to the two parties; the builder checks the exact bytes when it + verifies. When no value has been agreed out of band, implementations + SHOULD default to the UTF-8 bytes of the builder's own advertised + URL, exactly as advertised, so proposers with no prior relationship + can construct an identical `data` deterministically. example: "0x1234567890abcdef" - pattern: "^0x[a-fA-F0-9]{0,8192}$" + pattern: "^0x(?:[a-fA-F0-9]{2}){0,4096}$" slot: $ref: "../../beacon-apis/types/primitive.yaml#/Uint64" description: "The proposal slot this request is authorized for." From 8eec2ebb6c14455026f835fe3106c4039166b40c Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Thu, 23 Jul 2026 11:36:56 -0700 Subject: [PATCH 12/28] Require Date-Milliseconds and X-Timeout-Ms headers --- apis/builder/execution_payload_bid.yaml | 20 ++++++++++++++------ specs/gloas/builder.md | 6 ++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index d579caa6..7a87a03a 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -12,6 +12,8 @@ post: - The public key of the proposer. - A `SignedRequestAuthV1` in the request body that authenticates the request. The body MAY be encoded as JSON or SSZ. + - A `Date-Milliseconds` header with the send time and an `X-Timeout-Ms` + header with the proposer's timeout for the request. The proposer's `max_execution_payment` is communicated exclusively via the `submitBuilderPreferences` endpoint. Any bid the builder serves MUST honor @@ -63,20 +65,22 @@ post: $ref: "../../beacon-apis/types/primitive.yaml#/Pubkey" - name: Date-Milliseconds in: header - required: false + required: true description: | - Optional header containing a Unix timestamp in milliseconds representing - the point-in-time the request was sent. This header can be used to measure - latency. + Unix timestamp in milliseconds at which the request was sent. The + builder compares it with its own receive time to estimate the transit + delay from proposer to builder. schema: type: integer format: int64 example: 1710338135000 - name: X-Timeout-Ms in: header - required: false + required: true description: | - Optional header containing the proposer's timeout for the request in milliseconds. + The proposer's timeout for the request in milliseconds, measured from + `Date-Milliseconds`: the builder MUST respond by `Date-Milliseconds` + plus `X-Timeout-Ms`, and the proposer discards later responses. schema: type: integer format: int64 @@ -145,6 +149,10 @@ post: value: code: 400 message: "Invalid request: SignedRequestAuthV1 body is required" + MissingHeader: + value: + code: 400 + message: "Invalid request: Date-Milliseconds and X-Timeout-Ms headers are required" "401": description: Authentication failed. content: diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 71044400..c0c3b75e 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -142,6 +142,12 @@ Validators communicate per-request inputs to a builder on each encoded as JSON (`Content-Type: application/json`) or SSZ (`Content-Type: application/octet-stream`); `RequestAuthV1` is not fork-versioned, so no `Eth-Consensus-Version` header is required. +- A required `Date-Milliseconds` header with the Unix timestamp in milliseconds + at which the request was sent, and a required `X-Timeout-Ms` header with the + proposer's timeout for the request, measured from `Date-Milliseconds`. The + timestamp lets the builder estimate the transit delay from proposer to + builder; the builder MUST respond by `Date-Milliseconds` plus `X-Timeout-Ms`, + and the proposer discards later responses. The proposer's `max_execution_payment` is communicated exclusively via the [`submitBuilderPreferences`][submit-builder-preferences-api] endpoint. A bid From 07b94de98d130eedf00317ecee9d03d8c79c2be0 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Fri, 24 Jul 2026 12:31:23 -0700 Subject: [PATCH 13/28] clarify the request auth signing root --- specs/gloas/builder.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index c0c3b75e..bbffc304 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -212,10 +212,10 @@ API message, specific to this API and analogous to the now-deprecated `DOMAIN_BEACON_BUILDER`, which is used for in-protocol builder messages defined by the consensus specs. -Signing and verification use the `RequestAuthV1` message exactly as serialized, -computing the signing root over its SSZ bytes. A beacon node that forwards a +Signing and verification compute the signing root with `compute_signing_root` +over the `RequestAuthV1` message, as shown below. A beacon node that forwards a `SignedRequestAuthV1` MUST pass its `message` and `signature` through unchanged, -so a builder verifies the same bytes the validator signed. +so a builder verifies exactly what the validator signed. ```python def get_request_auth_signature( From f99faf82a48cc9564de66c86d3c0257af94d98b3 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 27 Jul 2026 15:09:39 -0700 Subject: [PATCH 14/28] wording nitpicks --- apis/builder/beacon_blocks.yaml | 5 +++-- apis/builder/execution_payload_bid.yaml | 2 +- specs/gloas/validator.md | 11 ++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apis/builder/beacon_blocks.yaml b/apis/builder/beacon_blocks.yaml index 6bbaede3..d4b2baff 100644 --- a/apis/builder/beacon_blocks.yaml +++ b/apis/builder/beacon_blocks.yaml @@ -9,8 +9,9 @@ post: must return an error response (400) with a description of the validation failure. - After receiving 202, the proposer takes no further action on the block; the - builder is responsible for publishing the execution payload envelope. + After the builder returns 202, the proposer takes no further action on the + block; the builder is responsible for publishing the execution payload + envelope. This API is applicable from Gloas fork onwards. tags: diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index 7a87a03a..44835ae6 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -5,7 +5,7 @@ post: Requests a builder node to produce a valid execution payload bid, which can be integrated into a beacon block and signed. - The proposer sends a POST request to the builder with the following information: + The proposer's beacon node sends a POST request to the builder with the following information: - The slot for which the block should be proposed. - The hash of the execution layer block the proposer will build on. - The root of the beacon block the proposer will build on. diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 4c340028..d44bb01f 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -240,15 +240,16 @@ block on top of a beacon `state` must take the following actions: 1. Call upstream builder software to get a [`SignedExecutionPayloadBid`][signed-execution-payload-bid] using the [`getExecutionPayloadBid`][get-execution-payload-bid-api] API call. The - validator sends a `SignedRequestAuthV1` in the request body to authenticate - the request. + validator signs a `SignedRequestAuthV1` and the beacon node sends it + unchanged in the request body to authenticate the request. 2. Assemble a `SignedBeaconBlock` according to the process outlined in the [Gloas validator specs][gloas-validator-specs] but with the best [`SignedExecutionPayloadBid`][signed-execution-payload-bid] from the prior step. -3. The proposer returns the `SignedBeaconBlock` back to the upstream block - building software via [`submitSignedBeaconBlock`][submit-signed-beacon-block] - API call. +3. The validator publishes the `SignedBeaconBlock` to its beacon node, which + returns it to the upstream block building software via + [`submitSignedBeaconBlock`][submit-signed-beacon-block] API call. The + validator does not call builders directly. 4. The upstream block building software constructs the corresponding [`SignedExecutionPayloadEnvelope`][signed-execution-payload-envelope] and broadcasts it to the PTC committee. From e27dfe39cc3d46342b8c8f58afc091a9bfab317c Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 29 Jul 2026 09:53:46 -0700 Subject: [PATCH 15/28] Don't require Eth-Consensus-Version if JSON. Consistent with prior builder spec endpoints --- apis/builder/beacon_blocks.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apis/builder/beacon_blocks.yaml b/apis/builder/beacon_blocks.yaml index d4b2baff..1092976f 100644 --- a/apis/builder/beacon_blocks.yaml +++ b/apis/builder/beacon_blocks.yaml @@ -20,9 +20,9 @@ post: - in: header schema: $ref: "../../builder-oapi.yaml#/components/schemas/ConsensusVersion" - required: true + required: false name: Eth-Consensus-Version - description: "The active consensus version to which the block being submitted belongs." + description: "The active consensus version to which the block being submitted belongs. Required if request is SSZ encoded." requestBody: description: A `SignedBeaconBlock`. required: true From bc91f6f7ac650fc5f6d9ad7e31fa9c2cd860eeb8 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 29 Jul 2026 11:14:12 -0700 Subject: [PATCH 16/28] Revert "Don't require Eth-Consensus-Version if JSON. Consistent with prior" This reverts commit e27dfe39cc3d46342b8c8f58afc091a9bfab317c. --- apis/builder/beacon_blocks.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apis/builder/beacon_blocks.yaml b/apis/builder/beacon_blocks.yaml index 1092976f..d4b2baff 100644 --- a/apis/builder/beacon_blocks.yaml +++ b/apis/builder/beacon_blocks.yaml @@ -20,9 +20,9 @@ post: - in: header schema: $ref: "../../builder-oapi.yaml#/components/schemas/ConsensusVersion" - required: false + required: true name: Eth-Consensus-Version - description: "The active consensus version to which the block being submitted belongs. Required if request is SSZ encoded." + description: "The active consensus version to which the block being submitted belongs." requestBody: description: A `SignedBeaconBlock`. required: true From 25fa22b62398365355c03ac9ecb15eebd0e75559 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Thu, 30 Jul 2026 08:44:27 -0700 Subject: [PATCH 17/28] update wordlist.txt for CI --- wordlist.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/wordlist.txt b/wordlist.txt index a7d48150..df91640a 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -35,6 +35,7 @@ Gloas gloas Gwei PTC +UTF BuilderPreferencesV RequestAuthV SignedRequestAuthV From 36cefe4aa82b562e1cfdde2be0dba7b5b8b93254 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Fri, 31 Jul 2026 15:24:31 -0700 Subject: [PATCH 18/28] fork-version request auth and slim the builder preferences request Require the Eth-Consensus-Version header on getExecutionPayloadBid and submitBuilderPreferences, since RequestAuth and BuilderPreferencesRequest are fork-versioned, and drop the V1 type suffixes. --- apis/builder/builder_preferences.yaml | 38 ++++++----- apis/builder/execution_payload_bid.yaml | 24 ++++--- builder-oapi.yaml | 22 +++--- specs/gloas/builder.md | 60 ++++++++--------- specs/gloas/validator.md | 90 ++++++++++++------------- types/gloas/builder_preferences.yaml | 12 ++-- types/gloas/request_auth.yaml | 6 +- wordlist.txt | 6 +- 8 files changed, 133 insertions(+), 125 deletions(-) diff --git a/apis/builder/builder_preferences.yaml b/apis/builder/builder_preferences.yaml index fdd19b4a..e0a00b17 100644 --- a/apis/builder/builder_preferences.yaml +++ b/apis/builder/builder_preferences.yaml @@ -2,21 +2,21 @@ post: operationId: "submitBuilderPreferences" summary: Submit builder preferences for a proposer. description: | - Submits a proposer's `BuilderPreferencesRequestV1` to the builder, including + Submits a proposer's `BuilderPreferencesRequest` to the builder, including the `max_execution_payment` that the proposer is willing to accept from this - builder, authenticated via a `SignedRequestAuthV1`. + builder, authenticated via a `SignedRequestAuth`. Validators MAY call this endpoint in the epoch prior to the epoch in which they will be proposing, as determined from `state.proposer_lookahead`, so that builders have the preferences before the bid request arrives. The body MAY be encoded as JSON (`Content-Type: application/json`) or SSZ - (`Content-Type: application/octet-stream`); `BuilderPreferencesRequestV1` - is not fork-versioned, so no `Eth-Consensus-Version` header is required. + (`Content-Type: application/octet-stream`); `BuilderPreferencesRequest` + is fork-versioned, so the `Eth-Consensus-Version` header is required. - The builder MUST verify the BLS signature in `auth` against - `proposer_pubkey`, and check that `auth.message.data` matches the value it - agreed with the proposer. `auth.message.slot` is the proposal slot the + The builder MUST verify the BLS signature in `auth` against the + `proposer_pubkey` path parameter, and check that `auth.message.data` matches + the value it agreed with the proposer. `auth.message.slot` is the proposal slot the preferences apply to; the builder MUST reject preferences whose slot has already passed. If the signature check fails, the builder MUST return a 401 response. If the data or slot check fails, the builder MUST return a 400 @@ -33,22 +33,28 @@ post: - name: proposer_pubkey in: path required: true - description: "The BLS public key of the proposer expressing these preferences." + description: BLS public key of the proposer. schema: $ref: "../../beacon-apis/types/primitive.yaml#/Pubkey" + - in: header + schema: + $ref: "../../builder-oapi.yaml#/components/schemas/ConsensusVersion" + required: true + name: Eth-Consensus-Version + description: "The active consensus version to which the submitted preferences belong." requestBody: - description: A `BuilderPreferencesRequestV1` containing the proposer's preferences and a `SignedRequestAuthV1` for authentication. + description: A `BuilderPreferencesRequest` containing the proposer's preferences and a `SignedRequestAuth` for authentication. required: true content: application/json: schema: - $ref: "../../types/gloas/builder_preferences.yaml#/Gloas/BuilderPreferencesRequestV1" + $ref: "../../types/gloas/builder_preferences.yaml#/Gloas/BuilderPreferencesRequest" examples: - BuilderPreferencesRequestV1: - $ref: "../../builder-oapi.yaml#/components/examples/Gloas.BuilderPreferencesRequestV1" + BuilderPreferencesRequest: + $ref: "../../builder-oapi.yaml#/components/examples/Gloas.BuilderPreferencesRequest" application/octet-stream: schema: - description: "SSZ serialized `BuilderPreferencesRequestV1` bytes. Use content type header to indicate that SSZ data is contained in the request body." + description: "SSZ serialized `BuilderPreferencesRequest` bytes. Use content type header to indicate that SSZ data is contained in the request body." responses: "202": description: Success response. @@ -62,11 +68,11 @@ post: SlotMismatch: value: code: 400 - message: "Invalid SignedRequestAuthV1: auth.message.slot has already passed" + message: "Invalid SignedRequestAuth: auth.message.slot has already passed" DataMismatch: value: code: 400 - message: "Invalid SignedRequestAuthV1: auth.message.data does not match the value agreed with this builder" + message: "Invalid SignedRequestAuth: auth.message.data does not match the value agreed with this builder" InvalidPreferences: value: code: 400 @@ -81,6 +87,6 @@ post: InvalidAuth: value: code: 401 - message: "Invalid SignedRequestAuthV1: signature verification failed" + message: "Invalid SignedRequestAuth: signature verification failed" "500": $ref: "../../builder-oapi.yaml#/components/responses/InternalError" diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index 44835ae6..214a8495 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -10,7 +10,7 @@ post: - The hash of the execution layer block the proposer will build on. - The root of the beacon block the proposer will build on. - The public key of the proposer. - - A `SignedRequestAuthV1` in the request body that authenticates the + - A `SignedRequestAuth` in the request body that authenticates the request. The body MAY be encoded as JSON or SSZ. - A `Date-Milliseconds` header with the send time and an `X-Timeout-Ms` header with the proposer's timeout for the request. @@ -22,7 +22,7 @@ post: proposer's locally configured per-builder limits are the backstop: the proposer discards any bid that exceeds them. - The `SignedRequestAuthV1` body is required. If the body is missing or + The `SignedRequestAuth` body is required. If the body is missing or malformed, the request is invalid and the builder MUST return a 400 response. If its `auth.message.data` or `auth.message.slot` does not match, the builder MUST return a 400 response; if its signature fails to verify, @@ -85,9 +85,15 @@ post: type: integer format: int64 example: 10000 + - in: header + schema: + $ref: "../../builder-oapi.yaml#/components/schemas/ConsensusVersion" + required: true + name: Eth-Consensus-Version + description: "The active consensus version to which the request body belongs." requestBody: description: | - A `SignedRequestAuthV1` authenticating the request. The builder MUST + A `SignedRequestAuth` authenticating the request. The builder MUST verify the BLS signature against the validator pubkey resolved from the `proposer_pubkey` path parameter, and MUST check that `auth.message.data` matches the value it agreed with the proposer and that @@ -97,10 +103,10 @@ post: content: application/json: schema: - $ref: "../../types/gloas/request_auth.yaml#/Gloas/SignedRequestAuthV1" + $ref: "../../types/gloas/request_auth.yaml#/Gloas/SignedRequestAuth" application/octet-stream: schema: - description: "SSZ serialized `SignedRequestAuthV1` bytes. Use Content-Type header to indicate that SSZ data is contained in the request body." + description: "SSZ serialized `SignedRequestAuth` bytes. Use Content-Type header to indicate that SSZ data is contained in the request body." responses: "200": description: Success response. @@ -140,15 +146,15 @@ post: SlotMismatch: value: code: 400 - message: "Invalid SignedRequestAuthV1: auth.message.slot does not match the proposal slot in the request path" + message: "Invalid SignedRequestAuth: auth.message.slot does not match the proposal slot in the request path" DataMismatch: value: code: 400 - message: "Invalid SignedRequestAuthV1: auth.message.data does not match the value agreed with this builder" + message: "Invalid SignedRequestAuth: auth.message.data does not match the value agreed with this builder" MissingBody: value: code: 400 - message: "Invalid request: SignedRequestAuthV1 body is required" + message: "Invalid request: SignedRequestAuth body is required" MissingHeader: value: code: 400 @@ -163,7 +169,7 @@ post: InvalidAuth: value: code: 401 - message: "Invalid SignedRequestAuthV1: signature verification failed" + message: "Invalid SignedRequestAuth: signature verification failed" "406": $ref: "../../builder-oapi.yaml#/components/responses/NotAcceptable" "415": diff --git a/builder-oapi.yaml b/builder-oapi.yaml index 41a97653..62232e6e 100644 --- a/builder-oapi.yaml +++ b/builder-oapi.yaml @@ -108,14 +108,14 @@ components: $ref: "./types/fulu/blobs_bundle.yaml#/Fulu/BlobsBundle" Fulu.ExecutionPayloadAndBlobsBundle: $ref: "./types/fulu/execution_payload_and_blobs_bundle.yaml#/Fulu/ExecutionPayloadAndBlobsBundle" - Gloas.RequestAuthV1: - $ref: "./types/gloas/request_auth.yaml#/Gloas/RequestAuthV1" - Gloas.SignedRequestAuthV1: - $ref: "./types/gloas/request_auth.yaml#/Gloas/SignedRequestAuthV1" - Gloas.BuilderPreferencesV1: - $ref: "./types/gloas/builder_preferences.yaml#/Gloas/BuilderPreferencesV1" - Gloas.BuilderPreferencesRequestV1: - $ref: "./types/gloas/builder_preferences.yaml#/Gloas/BuilderPreferencesRequestV1" + Gloas.RequestAuth: + $ref: "./types/gloas/request_auth.yaml#/Gloas/RequestAuth" + Gloas.SignedRequestAuth: + $ref: "./types/gloas/request_auth.yaml#/Gloas/SignedRequestAuth" + Gloas.BuilderPreferences: + $ref: "./types/gloas/builder_preferences.yaml#/Gloas/BuilderPreferences" + Gloas.BuilderPreferencesRequest: + $ref: "./types/gloas/builder_preferences.yaml#/Gloas/BuilderPreferencesRequest" responses: InternalError: @@ -165,9 +165,9 @@ components: $ref: "./examples/fulu/signed_blinded_beacon_block.json" Fulu.SignedBuilderBid: $ref: "./examples/fulu/signed_builder_bid.json" - Gloas.SignedRequestAuthV1: + Gloas.SignedRequestAuth: $ref: "./examples/gloas/signed_request_auth.json" - Gloas.BuilderPreferencesV1: + Gloas.BuilderPreferences: $ref: "./examples/gloas/builder_preferences.json" - Gloas.BuilderPreferencesRequestV1: + Gloas.BuilderPreferencesRequest: $ref: "./examples/gloas/builder_preferences_request.json" \ No newline at end of file diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index bbffc304..5ac7254f 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -95,25 +95,23 @@ Validators MAY communicate their per-builder preferences ahead of the bid request by calling the [`submitBuilderPreferences`][submit-builder-preferences-api] API in the epoch prior to the epoch in which they will be proposing, as determined from -`state.proposer_lookahead`. The builder receives a `BuilderPreferencesRequestV1` -object containing: +`state.proposer_lookahead`. The proposer is identified by the `proposer_pubkey` +path parameter, and the builder receives a `BuilderPreferencesRequest` object +containing: -- `proposer_pubkey`: The BLS public key of the proposer submitting these - preferences, passed as a path parameter. -- `preferences`: A `BuilderPreferencesV1` with: +- `preferences`: A `BuilderPreferences` with: - `max_execution_payment`: The maximum execution layer payment the proposer will accept from this builder (in Gwei). -- `auth`: A `SignedRequestAuthV1` authenticating the request. - `auth.message.slot` is the proposal slot the preferences apply to. The builder - MUST verify the BLS signature against the `proposer_pubkey` path parameter and - MUST check that `auth.message.data` matches the value it agreed with the - proposer, so that an unauthenticated or replayed request cannot skew a - proposer's preferences away from the value the proposer chose. If the - signature fails to verify, the builder MUST return a 401 response; if the - `auth.message.data` check fails, the builder MUST return a 400 response. The - builder MUST also reject, with a 400 response, preferences whose - `auth.message.slot` has already passed, so that a replayed request cannot roll - preferences back to a stale value. +- `auth`: A `SignedRequestAuth` authenticating the request. `auth.message.slot` + is the proposal slot the preferences apply to. The builder MUST verify the BLS + signature against the `proposer_pubkey` path parameter and MUST check that + `auth.message.data` matches the value it agreed with the proposer, so that an + unauthenticated or replayed request cannot skew a proposer's preferences away + from the value the proposer chose. If the signature fails to verify, the + builder MUST return a 401 response; if the `auth.message.data` check fails, + the builder MUST return a 400 response. The builder MUST also reject, with a + 400 response, preferences whose `auth.message.slot` has already passed, so + that a replayed request cannot roll preferences back to a stale value. The builder SHOULD store the preferences for each proposer and MUST honor the `max_execution_payment` cap in any bid it serves. Without stored preferences it @@ -137,11 +135,11 @@ reputation. Validators communicate per-request inputs to a builder on each [`getExecutionPayloadBid`][get-execution-payload-bid-api] call: -- A [`SignedRequestAuthV1`][signed-request-auth] in the request body +- A [`SignedRequestAuth`][signed-request-auth] in the request body authenticating the requesting validator. The body is required and MAY be encoded as JSON (`Content-Type: application/json`) or SSZ - (`Content-Type: application/octet-stream`); `RequestAuthV1` is not - fork-versioned, so no `Eth-Consensus-Version` header is required. + (`Content-Type: application/octet-stream`); `RequestAuth` is fork-versioned, + so the `Eth-Consensus-Version` header is required. - A required `Date-Milliseconds` header with the Unix timestamp in milliseconds at which the request was sent, and a required `X-Timeout-Ms` header with the proposer's timeout for the request, measured from `Date-Milliseconds`. The @@ -154,11 +152,11 @@ The proposer's `max_execution_payment` is communicated exclusively via the MUST honor the `max_execution_payment` cap from stored preferences; without them the builder MAY serve a bid with any `execution_payment`. -Builders MUST verify the `SignedRequestAuthV1` signature against the +Builders MUST verify the `SignedRequestAuth` signature against the `proposer_pubkey` path parameter, and MUST check that `auth.message.data` matches the value they agreed with the proposer and that `auth.message.slot` matches the proposal `slot` path parameter (see -[Constructing the `RequestAuthV1`][signed-request-auth]). The signature is +[Constructing the `RequestAuth`][signed-request-auth]). The signature is verified with [`verify_request_auth_signature`](#signing). If the signature fails to verify, the builder MUST return a 401 response; if the `auth.message.data` or `auth.message.slot` check fails, the builder MUST return @@ -190,9 +188,9 @@ MUST set `bid.value` to the amount they are committing to pay. If the builder intends to pay the proposer via an execution layer payment, they MUST set `bid.execution_payment`. This value MUST NOT exceed the -`max_execution_payment` from the proposer's stored `BuilderPreferencesV1`. -Without stored preferences the builder MAY set any `bid.execution_payment`; the -proposer discards any bid that exceeds its locally configured limits. +`max_execution_payment` from the proposer's stored `BuilderPreferences`. Without +stored preferences the builder MAY set any `bid.execution_payment`; the proposer +discards any bid that exceeds its locally configured limits. *Note*: `bid.value` and `bid.execution_payment` are not mutually exclusive. A builder MAY set both fields on a single bid; in that case the builder is @@ -205,21 +203,21 @@ also set. All signature operations follow the [standard BLS operations][bls] interface defined in `consensus-specs`. -The [`SignedRequestAuthV1`][signed-request-auth] is an out-of-protocol Builder -API message, specific to this API and analogous to the now-deprecated +The [`SignedRequestAuth`][signed-request-auth] is an out-of-protocol Builder API +message, specific to this API and analogous to the now-deprecated `ValidatorRegistrationV1`. It is signed and verified under `DOMAIN_REQUEST_AUTH`. This domain MUST NOT be confused with `DOMAIN_BEACON_BUILDER`, which is used for in-protocol builder messages defined by the consensus specs. Signing and verification compute the signing root with `compute_signing_root` -over the `RequestAuthV1` message, as shown below. A beacon node that forwards a -`SignedRequestAuthV1` MUST pass its `message` and `signature` through unchanged, +over the `RequestAuth` message, as shown below. A beacon node that forwards a +`SignedRequestAuth` MUST pass its `message` and `signature` through unchanged, so a builder verifies exactly what the validator signed. ```python def get_request_auth_signature( - request_auth: RequestAuthV1, + request_auth: RequestAuth, privkey: int, ) -> BLSSignature: domain = compute_domain(DOMAIN_REQUEST_AUTH) @@ -228,7 +226,7 @@ def get_request_auth_signature( def verify_request_auth_signature( - signed_request_auth: SignedRequestAuthV1, + signed_request_auth: SignedRequestAuth, pubkey: BLSPubkey, ) -> bool: domain = compute_domain(DOMAIN_REQUEST_AUTH) @@ -257,5 +255,5 @@ documented in the [Gloas consensus specs][gloas-builder-specs]. [proposer-preferences-topic]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#new-proposer_preferences [signed-execution-payload-bid]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid [signed-execution-payload-envelope]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadenvelope -[signed-request-auth]: ./validator.md#signedrequestauthv1 +[signed-request-auth]: ./validator.md#signedrequestauth [submit-builder-preferences-api]: ./../../apis/builder/builder_preferences.yaml diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index d44bb01f..5d6e8048 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -6,14 +6,14 @@ - [Introduction](#introduction) - [Containers](#containers) - [New Containers](#new-containers) - - [`RequestAuthV1`](#requestauthv1) - - [`SignedRequestAuthV1`](#signedrequestauthv1) - - [`BuilderPreferencesV1`](#builderpreferencesv1) - - [`BuilderPreferencesRequestV1`](#builderpreferencesrequestv1) + - [`RequestAuth`](#requestauth) + - [`SignedRequestAuth`](#signedrequestauth) + - [`BuilderPreferences`](#builderpreferences) + - [`BuilderPreferencesRequest`](#builderpreferencesrequest) - [Submitting Builder Preferences](#submitting-builder-preferences) - [`max_execution_payment`](#max_execution_payment) - [Bid Request](#bid-request) - - [Constructing the `RequestAuthV1`](#constructing-the-requestauthv1) + - [Constructing the `RequestAuth`](#constructing-the-requestauth) - [Proposer Preferences](#proposer-preferences) - [Validating a `SignedExecutionPayloadBid`](#validating-a-signedexecutionpayloadbid) - [Block proposal](#block-proposal) @@ -39,66 +39,64 @@ corresponding to the included bid to the PTC committee. ### New Containers -#### `RequestAuthV1` +#### `RequestAuth` -`RequestAuthV1` is used to authenticate requests to a builder. This is useful so +`RequestAuth` is used to authenticate requests to a builder. This is useful so that other builders do not DDOS or run replay attacks on the builder. ```python -class RequestAuthV1(Container): +class RequestAuth(Container): data: ByteList[MAX_DATA_SIZE] slot: Slot ``` -#### `SignedRequestAuthV1` +#### `SignedRequestAuth` ```python -class SignedRequestAuthV1(Container): - message: RequestAuthV1 +class SignedRequestAuth(Container): + message: RequestAuth signature: BLSSignature ``` -#### `BuilderPreferencesV1` +#### `BuilderPreferences` -`BuilderPreferencesV1` communicates a proposer's per-builder preferences to a +`BuilderPreferences` communicates a proposer's per-builder preferences to a specific builder ahead of the bid request. ```python -class BuilderPreferencesV1(Container): +class BuilderPreferences(Container): max_execution_payment: Gwei ``` -#### `BuilderPreferencesRequestV1` +#### `BuilderPreferencesRequest` ```python -class BuilderPreferencesRequestV1(Container): - preferences: BuilderPreferencesV1 - auth: SignedRequestAuthV1 +class BuilderPreferencesRequest(Container): + auth: SignedRequestAuth + preferences: BuilderPreferences ``` ## Submitting Builder Preferences The validator MAY submit its -[`BuilderPreferencesRequestV1`](#builderpreferencesrequestv1) to each builder -via the [`submitBuilderPreferences`][submit-builder-preferences-api] API call in -the epoch prior to the epoch in which they will be proposing, as determined from +[`BuilderPreferencesRequest`](#builderpreferencesrequest) to each builder via +the [`submitBuilderPreferences`][submit-builder-preferences-api] API call in the +epoch prior to the epoch in which they will be proposing, as determined from `state.proposer_lookahead`. This ensures builders have the preferences before the bid request arrives. -The validator constructs a `BuilderPreferencesV1` with: +The validator constructs a `BuilderPreferences` with: - `max_execution_payment`: The maximum execution layer payment the proposer will accept from this builder. See [`max_execution_payment`](#max_execution_payment). -The proposer's BLS public key is passed as the `proposer_pubkey` path parameter -in the [`submitBuilderPreferences`][submit-builder-preferences-api] API call. - -The validator then constructs a `BuilderPreferencesRequestV1` with the -`BuilderPreferencesV1` as `preferences` and a `SignedRequestAuthV1` as `auth`. -The `SignedRequestAuthV1` is constructed as described in -[Constructing the `RequestAuthV1`](#constructing-the-requestauthv1); its -`auth.message.data` is the authentication data the builder expects and its +The validator then constructs a `BuilderPreferencesRequest` with the +`BuilderPreferences` as `preferences` and a `SignedRequestAuth` as `auth`, and +submits it to the [`submitBuilderPreferences`][submit-builder-preferences-api] +endpoint for its `proposer_pubkey`. The `SignedRequestAuth` is constructed as +described in [Constructing the `RequestAuth`](#constructing-the-requestauth); +its `auth.message.data` is the authentication data the builder expects and its `auth.message.slot` is the proposal slot the preferences apply to. The builder MUST verify the `auth` signature against the `proposer_pubkey` path parameter, returning a 401 response if it fails to verify, and MUST reject the request with @@ -128,21 +126,21 @@ their level of trust in the builder's reliability and reputation. ## Bid Request When calling [`getExecutionPayloadBid`][get-execution-payload-bid-api], the -validator MUST send a [`SignedRequestAuthV1`](#signedrequestauthv1) as the -request body to authenticate the request. The body MAY be encoded as JSON +validator MUST send a [`SignedRequestAuth`](#signedrequestauth) as the request +body to authenticate the request. The body MAY be encoded as JSON (`Content-Type: application/json`) or SSZ -(`Content-Type: application/octet-stream`); `RequestAuthV1` is not -fork-versioned, so no `Eth-Consensus-Version` header is required. Proposer -duties are known an epoch in advance, so the validator can sign the -`SignedRequestAuthV1` ahead of time, off the proposal hot path. +(`Content-Type: application/octet-stream`); `RequestAuth` is fork-versioned, so +the `Eth-Consensus-Version` header is required. Proposer duties are known an +epoch in advance, so the validator can sign the `SignedRequestAuth` ahead of +time, off the proposal hot path. -### Constructing the `RequestAuthV1` +### Constructing the `RequestAuth` -The validator constructs a `RequestAuthV1` with the following fields: +The validator constructs a `RequestAuth` with the following fields: - `data`: opaque authentication data agreed with the builder out of band whose meaning is left to the two parties. It is not tied to an endpoint, so one - `SignedRequestAuthV1` can authenticate the proposer for both + `SignedRequestAuth` can authenticate the proposer for both `getExecutionPayloadBid` and `submitBuilderPreferences`. When no value has been agreed out of band, the validator SHOULD use the UTF-8 bytes of the builder's own advertised URL, exactly as advertised. @@ -150,12 +148,12 @@ The validator constructs a `RequestAuthV1` with the following fields: which the request is signed or sent. The proposer's public key is already carried as a path parameter in the relevant -API request, so it does not need to be carried inside `RequestAuthV1`. +API request, so it does not need to be carried inside `RequestAuth`. -The validator then constructs the `SignedRequestAuthV1` by signing the -`RequestAuthV1`. The signature lets builders authenticate the requesting -validator and discard requests from other parties (e.g. DDOS or replay attempts -from competing builders). +The validator then constructs the `SignedRequestAuth` by signing the +`RequestAuth`. The signature lets builders authenticate the requesting validator +and discard requests from other parties (e.g. DDOS or replay attempts from +competing builders). ## Proposer Preferences @@ -240,8 +238,8 @@ block on top of a beacon `state` must take the following actions: 1. Call upstream builder software to get a [`SignedExecutionPayloadBid`][signed-execution-payload-bid] using the [`getExecutionPayloadBid`][get-execution-payload-bid-api] API call. The - validator signs a `SignedRequestAuthV1` and the beacon node sends it - unchanged in the request body to authenticate the request. + validator signs a `SignedRequestAuth` and the beacon node sends it unchanged + in the request body to authenticate the request. 2. Assemble a `SignedBeaconBlock` according to the process outlined in the [Gloas validator specs][gloas-validator-specs] but with the best [`SignedExecutionPayloadBid`][signed-execution-payload-bid] from the prior diff --git a/types/gloas/builder_preferences.yaml b/types/gloas/builder_preferences.yaml index 2b64dc56..83ee65ac 100644 --- a/types/gloas/builder_preferences.yaml +++ b/types/gloas/builder_preferences.yaml @@ -1,5 +1,5 @@ Gloas: - BuilderPreferencesV1: + BuilderPreferences: type: object description: "Per-builder preferences that a proposer can express." required: [max_execution_payment] @@ -7,12 +7,12 @@ Gloas: max_execution_payment: $ref: "../../beacon-apis/types/primitive.yaml#/Uint64" description: "The maximum amount, in Gwei, that a proposer is willing to accept as an execution payment from the builder." - BuilderPreferencesRequestV1: + BuilderPreferencesRequest: type: object description: "A builder preferences submission containing the proposer's preferences and a signed request authentication." - required: [preferences, auth] + required: [auth, preferences] properties: - preferences: - $ref: "#/Gloas/BuilderPreferencesV1" auth: - $ref: "./request_auth.yaml#/Gloas/SignedRequestAuthV1" + $ref: "./request_auth.yaml#/Gloas/SignedRequestAuth" + preferences: + $ref: "#/Gloas/BuilderPreferences" diff --git a/types/gloas/request_auth.yaml b/types/gloas/request_auth.yaml index ae97bb12..c4aa1472 100644 --- a/types/gloas/request_auth.yaml +++ b/types/gloas/request_auth.yaml @@ -1,5 +1,5 @@ Gloas: - RequestAuthV1: + RequestAuth: type: object required: [data, slot] properties: @@ -19,11 +19,11 @@ Gloas: slot: $ref: "../../beacon-apis/types/primitive.yaml#/Uint64" description: "The proposal slot this request is authorized for." - SignedRequestAuthV1: + SignedRequestAuth: type: object required: [message, signature] properties: message: - $ref: "#/Gloas/RequestAuthV1" + $ref: "#/Gloas/RequestAuth" signature: $ref: "../../beacon-apis/types/primitive.yaml#/Signature" diff --git a/wordlist.txt b/wordlist.txt index df91640a..629239b2 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -36,8 +36,8 @@ gloas Gwei PTC UTF -BuilderPreferencesV -RequestAuthV -SignedRequestAuthV +BuilderPreferences +RequestAuth +SignedRequestAuth submitBlindedBlockV ValidatorRegistrationsV \ No newline at end of file From 9a58e07ec1d921650264d708ef5930140da72bec Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 10 Aug 2026 10:29:46 -0700 Subject: [PATCH 19/28] preferences should be stored as (proposer, slot) --- specs/gloas/builder.md | 10 +++++----- specs/gloas/validator.md | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 5ac7254f..45dc8e9a 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -113,11 +113,11 @@ containing: 400 response, preferences whose `auth.message.slot` has already passed, so that a replayed request cannot roll preferences back to a stale value. -The builder SHOULD store the preferences for each proposer and MUST honor the -`max_execution_payment` cap in any bid it serves. Without stored preferences it -MAY serve a bid with any `execution_payment`. The proposer's locally configured -per-builder limits are the backstop: the proposer discards any bid that exceeds -them. +The builder SHOULD store the preferences per proposer per `auth.message.slot` +and MUST honor the `max_execution_payment` cap in any bid it serves for that +slot. Without preferences stored for the requested slot it MAY serve a bid with +any `execution_payment`. The proposer's locally configured per-builder limits are +the backstop: the proposer discards any bid that exceeds them. ### `max_execution_payment` diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 5d6e8048..9795014d 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -105,10 +105,10 @@ the proposer. The builder MUST also reject, with a 400 response, a request whose `auth.message.slot` has already passed, so that a replay cannot roll preferences back to a stale value. -A builder MUST honor the `max_execution_payment` cap in any bid it serves; -without stored preferences it MAY serve a bid with any `execution_payment`. The -proposer's locally configured per-builder limits are the backstop: the proposer -discards any bid that exceeds them. +A builder MUST honor the `max_execution_payment` cap in any bid it serves for a +slot it has stored preferences for; without them it MAY serve a bid with any +`execution_payment`. The proposer's locally configured per-builder limits are +the backstop: the proposer discards any bid that exceeds them. ### `max_execution_payment` From 72165af61ee367155aae04fa5911b46c7b287352 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 10 Aug 2026 14:54:56 -0700 Subject: [PATCH 20/28] missing 415 error type; clarity if fork header is missing or unknown --- apis/builder/builder_preferences.yaml | 2 ++ specs/gloas/builder.md | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/apis/builder/builder_preferences.yaml b/apis/builder/builder_preferences.yaml index e0a00b17..98bd29ac 100644 --- a/apis/builder/builder_preferences.yaml +++ b/apis/builder/builder_preferences.yaml @@ -88,5 +88,7 @@ post: value: code: 401 message: "Invalid SignedRequestAuth: signature verification failed" + "415": + $ref: "../../builder-oapi.yaml#/components/responses/UnsupportedMediaType" "500": $ref: "../../builder-oapi.yaml#/components/responses/InternalError" diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 45dc8e9a..1b39e750 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -147,6 +147,10 @@ Validators communicate per-request inputs to a builder on each builder; the builder MUST respond by `Date-Milliseconds` plus `X-Timeout-Ms`, and the proposer discards later responses. +The `Eth-Consensus-Version` header is required on every request that carries a +body, naming the fork of the body's type. The builder MUST return a 400 response +when the header is absent or names a fork it does not recognize. + The proposer's `max_execution_payment` is communicated exclusively via the [`submitBuilderPreferences`][submit-builder-preferences-api] endpoint. A bid MUST honor the `max_execution_payment` cap from stored preferences; without them From a5552d6f506557dd27179390f9597966dcdd468e Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 10 Aug 2026 15:57:25 -0700 Subject: [PATCH 21/28] a zero-length auth data is invalid Removes the last SSZ absence ambiguity: an empty data ByteList was the one remaining wire value that could read as "unset". It had no defined meaning either way, since omission already means derive from the builder's URL and auth is required on every request. --- specs/gloas/validator.md | 3 ++- types/gloas/request_auth.yaml | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 9795014d..2b70fb1b 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -143,7 +143,8 @@ The validator constructs a `RequestAuth` with the following fields: `SignedRequestAuth` can authenticate the proposer for both `getExecutionPayloadBid` and `submitBuilderPreferences`. When no value has been agreed out of band, the validator SHOULD use the UTF-8 bytes of the - builder's own advertised URL, exactly as advertised. + builder's own advertised URL, exactly as advertised. A zero-length `data` is + invalid. - `slot`: The proposal slot this request is authorized for, not the slot at which the request is signed or sent. diff --git a/types/gloas/request_auth.yaml b/types/gloas/request_auth.yaml index c4aa1472..c46b4e86 100644 --- a/types/gloas/request_auth.yaml +++ b/types/gloas/request_auth.yaml @@ -13,9 +13,10 @@ Gloas: verifies. When no value has been agreed out of band, implementations SHOULD default to the UTF-8 bytes of the builder's own advertised URL, exactly as advertised, so proposers with no prior relationship - can construct an identical `data` deterministically. + can construct an identical `data` deterministically. A zero-length + `data` is invalid. example: "0x1234567890abcdef" - pattern: "^0x(?:[a-fA-F0-9]{2}){0,4096}$" + pattern: "^0x(?:[a-fA-F0-9]{2}){1,4096}$" slot: $ref: "../../beacon-apis/types/primitive.yaml#/Uint64" description: "The proposal slot this request is authorized for." From 82fa5594f9b6820986fc3f975e38f0e6156c9259 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Fri, 14 Aug 2026 12:09:17 -0700 Subject: [PATCH 22/28] restore BuilderPreferencesRequest field order to [preferences, auth] --- examples/gloas/builder_preferences_request.ssz | Bin 151 -> 132 bytes examples/gloas/signed_request_auth.ssz | Bin 139 -> 120 bytes specs/gloas/validator.md | 2 +- types/gloas/builder_preferences.yaml | 6 +++--- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/gloas/builder_preferences_request.ssz b/examples/gloas/builder_preferences_request.ssz index e338bf00fb05355cfa62ff3a3d75fb5f680206f2..a6e15130df79851be5877afaa40a06ea77a984c5 100644 GIT binary patch delta 15 WcmbQv*uprWhC|3CtYX6Iv+n^YCEW}HwUol#O!P;8~IpH!NelagAbms*jSTac5gmz Date: Thu, 20 Aug 2026 19:16:54 +0100 Subject: [PATCH 23/28] Update apis/builder/execution_payload_bid.yaml Co-authored-by: Nico Flaig --- apis/builder/execution_payload_bid.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index 214a8495..db702c19 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -11,7 +11,7 @@ post: - The root of the beacon block the proposer will build on. - The public key of the proposer. - A `SignedRequestAuth` in the request body that authenticates the - request. The body MAY be encoded as JSON or SSZ. + request. - A `Date-Milliseconds` header with the send time and an `X-Timeout-Ms` header with the proposer's timeout for the request. From ce6251d23f09c27d8bc63b8bdb7a54bdd251a842 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Thu, 20 Aug 2026 11:20:02 -0700 Subject: [PATCH 24/28] drop the encoding note the schema already states --- apis/builder/builder_preferences.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apis/builder/builder_preferences.yaml b/apis/builder/builder_preferences.yaml index 98bd29ac..0233db6d 100644 --- a/apis/builder/builder_preferences.yaml +++ b/apis/builder/builder_preferences.yaml @@ -10,10 +10,6 @@ post: which they will be proposing, as determined from `state.proposer_lookahead`, so that builders have the preferences before the bid request arrives. - The body MAY be encoded as JSON (`Content-Type: application/json`) or SSZ - (`Content-Type: application/octet-stream`); `BuilderPreferencesRequest` - is fork-versioned, so the `Eth-Consensus-Version` header is required. - The builder MUST verify the BLS signature in `auth` against the `proposer_pubkey` path parameter, and check that `auth.message.data` matches the value it agreed with the proposer. `auth.message.slot` is the proposal slot the From 3979095271a09b4e0fa9581379145cfd6816ace5 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Thu, 20 Aug 2026 17:23:08 -0700 Subject: [PATCH 25/28] clarify min(execution_payment, max_execution_payment) clamp --- apis/builder/execution_payload_bid.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index db702c19..b8015d01 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -20,7 +20,9 @@ post: the `max_execution_payment` cap from stored preferences. Without stored preferences the builder MAY serve a bid with any `execution_payment`; the proposer's locally configured per-builder limits are the backstop: the - proposer discards any bid that exceeds them. + proposer values a bid at its `value` plus `min(execution_payment, + max_execution_payment)`, so payment above the cap adds nothing to the + bid's chances. The `SignedRequestAuth` body is required. If the body is missing or malformed, the request is invalid and the builder MUST return a 400 From 6f3058fcced5e90ba29c62ee84a4378710b98d1d Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Fri, 21 Aug 2026 11:55:46 -0700 Subject: [PATCH 26/28] Clarify builder SHOULD help disseminate block on 202 --- apis/builder/beacon_blocks.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apis/builder/beacon_blocks.yaml b/apis/builder/beacon_blocks.yaml index d4b2baff..aff7b563 100644 --- a/apis/builder/beacon_blocks.yaml +++ b/apis/builder/beacon_blocks.yaml @@ -2,17 +2,18 @@ post: operationId: "submitSignedBeaconBlock" summary: Submit a signed beacon block with the execution payload bid. description: | - Submits a `SignedBeaconBlock` to the builder, binding the proposer to the block. + Submits the proposer's `SignedBeaconBlock` to the builder whose bid it + commits to. + + This is how the builder learns that its bid won without waiting for the + block on gossip. The builder SHOULD help disseminate the block. The proposer + gossips the block itself either way, regardless of the reponse. A success response (202) indicates that the signed beacon block was valid. If the signed beacon block was invalid, then the builder must return an error response (400) with a description of the validation failure. - After the builder returns 202, the proposer takes no further action on the - block; the builder is responsible for publishing the execution payload - envelope. - This API is applicable from Gloas fork onwards. tags: - Builder From 4ad993826ca631d120891523ac580dab37ef6329 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Sun, 23 Aug 2026 12:00:31 -0700 Subject: [PATCH 27/28] - rename to BuilderRequestAuth, MAX_BUILDER_AUTH_DATA_SIZE, DOMAIN_BUILDER_REQUEST_AUTH - clarfications --- apis/builder/beacon_blocks.yaml | 2 +- apis/builder/builder_preferences.yaml | 10 +- apis/builder/execution_payload_bid.yaml | 18 ++-- builder-oapi.yaml | 12 +-- ....json => signed_builder_request_auth.json} | 0 ...th.ssz => signed_builder_request_auth.ssz} | Bin specs/gloas/builder.md | 87 +++++++++--------- specs/gloas/validator.md | 82 ++++++++--------- types/gloas/builder_preferences.yaml | 2 +- types/gloas/request_auth.yaml | 8 +- 10 files changed, 113 insertions(+), 108 deletions(-) rename examples/gloas/{signed_request_auth.json => signed_builder_request_auth.json} (100%) rename examples/gloas/{signed_request_auth.ssz => signed_builder_request_auth.ssz} (100%) diff --git a/apis/builder/beacon_blocks.yaml b/apis/builder/beacon_blocks.yaml index aff7b563..2e832645 100644 --- a/apis/builder/beacon_blocks.yaml +++ b/apis/builder/beacon_blocks.yaml @@ -7,7 +7,7 @@ post: This is how the builder learns that its bid won without waiting for the block on gossip. The builder SHOULD help disseminate the block. The proposer - gossips the block itself either way, regardless of the reponse. + gossips the block itself either way, regardless of the response. A success response (202) indicates that the signed beacon block was valid. If the signed beacon block was invalid, then the builder diff --git a/apis/builder/builder_preferences.yaml b/apis/builder/builder_preferences.yaml index 0233db6d..afe5673e 100644 --- a/apis/builder/builder_preferences.yaml +++ b/apis/builder/builder_preferences.yaml @@ -4,7 +4,7 @@ post: description: | Submits a proposer's `BuilderPreferencesRequest` to the builder, including the `max_execution_payment` that the proposer is willing to accept from this - builder, authenticated via a `SignedRequestAuth`. + builder, authenticated via a `SignedBuilderRequestAuth`. Validators MAY call this endpoint in the epoch prior to the epoch in which they will be proposing, as determined from `state.proposer_lookahead`, so that @@ -39,7 +39,7 @@ post: name: Eth-Consensus-Version description: "The active consensus version to which the submitted preferences belong." requestBody: - description: A `BuilderPreferencesRequest` containing the proposer's preferences and a `SignedRequestAuth` for authentication. + description: A `BuilderPreferencesRequest` containing the proposer's preferences and a `SignedBuilderRequestAuth` for authentication. required: true content: application/json: @@ -64,11 +64,11 @@ post: SlotMismatch: value: code: 400 - message: "Invalid SignedRequestAuth: auth.message.slot has already passed" + message: "Invalid SignedBuilderRequestAuth: auth.message.slot has already passed" DataMismatch: value: code: 400 - message: "Invalid SignedRequestAuth: auth.message.data does not match the value agreed with this builder" + message: "Invalid SignedBuilderRequestAuth: auth.message.data does not match the value agreed with this builder" InvalidPreferences: value: code: 400 @@ -83,7 +83,7 @@ post: InvalidAuth: value: code: 401 - message: "Invalid SignedRequestAuth: signature verification failed" + message: "Invalid SignedBuilderRequestAuth: signature verification failed" "415": $ref: "../../builder-oapi.yaml#/components/responses/UnsupportedMediaType" "500": diff --git a/apis/builder/execution_payload_bid.yaml b/apis/builder/execution_payload_bid.yaml index b8015d01..0333ce18 100644 --- a/apis/builder/execution_payload_bid.yaml +++ b/apis/builder/execution_payload_bid.yaml @@ -10,7 +10,7 @@ post: - The hash of the execution layer block the proposer will build on. - The root of the beacon block the proposer will build on. - The public key of the proposer. - - A `SignedRequestAuth` in the request body that authenticates the + - A `SignedBuilderRequestAuth` in the request body that authenticates the request. - A `Date-Milliseconds` header with the send time and an `X-Timeout-Ms` header with the proposer's timeout for the request. @@ -24,7 +24,7 @@ post: max_execution_payment)`, so payment above the cap adds nothing to the bid's chances. - The `SignedRequestAuth` body is required. If the body is missing or + The `SignedBuilderRequestAuth` body is required. If the body is missing or malformed, the request is invalid and the builder MUST return a 400 response. If its `auth.message.data` or `auth.message.slot` does not match, the builder MUST return a 400 response; if its signature fails to verify, @@ -95,7 +95,7 @@ post: description: "The active consensus version to which the request body belongs." requestBody: description: | - A `SignedRequestAuth` authenticating the request. The builder MUST + A `SignedBuilderRequestAuth` authenticating the request. The builder MUST verify the BLS signature against the validator pubkey resolved from the `proposer_pubkey` path parameter, and MUST check that `auth.message.data` matches the value it agreed with the proposer and that @@ -105,10 +105,10 @@ post: content: application/json: schema: - $ref: "../../types/gloas/request_auth.yaml#/Gloas/SignedRequestAuth" + $ref: "../../types/gloas/request_auth.yaml#/Gloas/SignedBuilderRequestAuth" application/octet-stream: schema: - description: "SSZ serialized `SignedRequestAuth` bytes. Use Content-Type header to indicate that SSZ data is contained in the request body." + description: "SSZ serialized `SignedBuilderRequestAuth` bytes. Use Content-Type header to indicate that SSZ data is contained in the request body." responses: "200": description: Success response. @@ -148,15 +148,15 @@ post: SlotMismatch: value: code: 400 - message: "Invalid SignedRequestAuth: auth.message.slot does not match the proposal slot in the request path" + message: "Invalid SignedBuilderRequestAuth: auth.message.slot does not match the proposal slot in the request path" DataMismatch: value: code: 400 - message: "Invalid SignedRequestAuth: auth.message.data does not match the value agreed with this builder" + message: "Invalid SignedBuilderRequestAuth: auth.message.data does not match the value agreed with this builder" MissingBody: value: code: 400 - message: "Invalid request: SignedRequestAuth body is required" + message: "Invalid request: SignedBuilderRequestAuth body is required" MissingHeader: value: code: 400 @@ -171,7 +171,7 @@ post: InvalidAuth: value: code: 401 - message: "Invalid SignedRequestAuth: signature verification failed" + message: "Invalid SignedBuilderRequestAuth: signature verification failed" "406": $ref: "../../builder-oapi.yaml#/components/responses/NotAcceptable" "415": diff --git a/builder-oapi.yaml b/builder-oapi.yaml index 62232e6e..de51014e 100644 --- a/builder-oapi.yaml +++ b/builder-oapi.yaml @@ -108,10 +108,10 @@ components: $ref: "./types/fulu/blobs_bundle.yaml#/Fulu/BlobsBundle" Fulu.ExecutionPayloadAndBlobsBundle: $ref: "./types/fulu/execution_payload_and_blobs_bundle.yaml#/Fulu/ExecutionPayloadAndBlobsBundle" - Gloas.RequestAuth: - $ref: "./types/gloas/request_auth.yaml#/Gloas/RequestAuth" - Gloas.SignedRequestAuth: - $ref: "./types/gloas/request_auth.yaml#/Gloas/SignedRequestAuth" + Gloas.BuilderRequestAuth: + $ref: "./types/gloas/request_auth.yaml#/Gloas/BuilderRequestAuth" + Gloas.SignedBuilderRequestAuth: + $ref: "./types/gloas/request_auth.yaml#/Gloas/SignedBuilderRequestAuth" Gloas.BuilderPreferences: $ref: "./types/gloas/builder_preferences.yaml#/Gloas/BuilderPreferences" Gloas.BuilderPreferencesRequest: @@ -165,8 +165,8 @@ components: $ref: "./examples/fulu/signed_blinded_beacon_block.json" Fulu.SignedBuilderBid: $ref: "./examples/fulu/signed_builder_bid.json" - Gloas.SignedRequestAuth: - $ref: "./examples/gloas/signed_request_auth.json" + Gloas.SignedBuilderRequestAuth: + $ref: "./examples/gloas/signed_builder_request_auth.json" Gloas.BuilderPreferences: $ref: "./examples/gloas/builder_preferences.json" Gloas.BuilderPreferencesRequest: diff --git a/examples/gloas/signed_request_auth.json b/examples/gloas/signed_builder_request_auth.json similarity index 100% rename from examples/gloas/signed_request_auth.json rename to examples/gloas/signed_builder_request_auth.json diff --git a/examples/gloas/signed_request_auth.ssz b/examples/gloas/signed_builder_request_auth.ssz similarity index 100% rename from examples/gloas/signed_request_auth.ssz rename to examples/gloas/signed_builder_request_auth.ssz diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 1b39e750..d7105497 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -27,11 +27,11 @@ describes how builders consume per-request inputs from validators and construct ## Constants -| Name | Value | -| ----------------------- | -------------------------- | -| `MAX_EXECUTION_PAYMENT` | `2**64 - 1` | -| `MAX_DATA_SIZE` | `4096` | -| `DOMAIN_REQUEST_AUTH` | `DomainType('0x0B000001')` | +| Name | Value | +| ----------------------------- | -------------------------- | +| `MAX_EXECUTION_PAYMENT` | `2**64 - 1` | +| `MAX_BUILDER_AUTH_DATA_SIZE` | `4096` | +| `DOMAIN_BUILDER_REQUEST_AUTH` | `DomainType('0x0B000001')` | ## Bidding @@ -102,22 +102,25 @@ containing: - `preferences`: A `BuilderPreferences` with: - `max_execution_payment`: The maximum execution layer payment the proposer will accept from this builder (in Gwei). -- `auth`: A `SignedRequestAuth` authenticating the request. `auth.message.slot` - is the proposal slot the preferences apply to. The builder MUST verify the BLS - signature against the `proposer_pubkey` path parameter and MUST check that - `auth.message.data` matches the value it agreed with the proposer, so that an - unauthenticated or replayed request cannot skew a proposer's preferences away - from the value the proposer chose. If the signature fails to verify, the - builder MUST return a 401 response; if the `auth.message.data` check fails, - the builder MUST return a 400 response. The builder MUST also reject, with a - 400 response, preferences whose `auth.message.slot` has already passed, so - that a replayed request cannot roll preferences back to a stale value. +- `auth`: A `SignedBuilderRequestAuth` authenticating the request. + `auth.message.slot` is the proposal slot the preferences apply to. The builder + MUST verify the BLS signature against the `proposer_pubkey` path parameter and + MUST check that `auth.message.data` matches the value it agreed with the + proposer, so that an unauthenticated or replayed request cannot skew a + proposer's preferences away from the value the proposer chose. If the + signature fails to verify, the builder MUST return a 401 response; if the + `auth.message.data` check fails, the builder MUST return a 400 response. The + builder MUST also reject, with a 400 response, preferences whose + `auth.message.slot` has already passed, so that a replayed request cannot roll + preferences back to a stale value. The builder SHOULD store the preferences per proposer per `auth.message.slot` and MUST honor the `max_execution_payment` cap in any bid it serves for that slot. Without preferences stored for the requested slot it MAY serve a bid with -any `execution_payment`. The proposer's locally configured per-builder limits are -the backstop: the proposer discards any bid that exceeds them. +any `execution_payment`. The proposer's locally configured per-builder limits +are the backstop: the proposer values a bid at its `value` plus +`min(execution_payment, max_execution_payment)`, so payment above the cap adds +nothing to the bid's chances. ### `max_execution_payment` @@ -135,11 +138,11 @@ reputation. Validators communicate per-request inputs to a builder on each [`getExecutionPayloadBid`][get-execution-payload-bid-api] call: -- A [`SignedRequestAuth`][signed-request-auth] in the request body +- A [`SignedBuilderRequestAuth`][signed-request-auth] in the request body authenticating the requesting validator. The body is required and MAY be encoded as JSON (`Content-Type: application/json`) or SSZ - (`Content-Type: application/octet-stream`); `RequestAuth` is fork-versioned, - so the `Eth-Consensus-Version` header is required. + (`Content-Type: application/octet-stream`); `BuilderRequestAuth` is + fork-versioned, so the `Eth-Consensus-Version` header is required. - A required `Date-Milliseconds` header with the Unix timestamp in milliseconds at which the request was sent, and a required `X-Timeout-Ms` header with the proposer's timeout for the request, measured from `Date-Milliseconds`. The @@ -156,13 +159,13 @@ The proposer's `max_execution_payment` is communicated exclusively via the MUST honor the `max_execution_payment` cap from stored preferences; without them the builder MAY serve a bid with any `execution_payment`. -Builders MUST verify the `SignedRequestAuth` signature against the +Builders MUST verify the `SignedBuilderRequestAuth` signature against the `proposer_pubkey` path parameter, and MUST check that `auth.message.data` matches the value they agreed with the proposer and that `auth.message.slot` matches the proposal `slot` path parameter (see -[Constructing the `RequestAuth`][signed-request-auth]). The signature is -verified with [`verify_request_auth_signature`](#signing). If the signature -fails to verify, the builder MUST return a 401 response; if the +[Constructing the `BuilderRequestAuth`][constructing-builder-request-auth]). The +signature is verified with [`verify_builder_request_auth_signature`](#signing). +If the signature fails to verify, the builder MUST return a 401 response; if the `auth.message.data` or `auth.message.slot` check fails, the builder MUST return a 400 response. A missing or malformed body is an invalid request and the builder MUST return a 400 response. @@ -193,8 +196,9 @@ MUST set `bid.value` to the amount they are committing to pay. If the builder intends to pay the proposer via an execution layer payment, they MUST set `bid.execution_payment`. This value MUST NOT exceed the `max_execution_payment` from the proposer's stored `BuilderPreferences`. Without -stored preferences the builder MAY set any `bid.execution_payment`; the proposer -discards any bid that exceeds its locally configured limits. +stored preferences the builder MAY set any `bid.execution_payment`; when valuing +the bid, the proposer counts the payment at no more than its locally configured +cap. *Note*: `bid.value` and `bid.execution_payment` are not mutually exclusive. A builder MAY set both fields on a single bid; in that case the builder is @@ -207,35 +211,35 @@ also set. All signature operations follow the [standard BLS operations][bls] interface defined in `consensus-specs`. -The [`SignedRequestAuth`][signed-request-auth] is an out-of-protocol Builder API -message, specific to this API and analogous to the now-deprecated +The [`SignedBuilderRequestAuth`][signed-request-auth] is an out-of-protocol +Builder API message, specific to this API and analogous to the now-deprecated `ValidatorRegistrationV1`. It is signed and verified under -`DOMAIN_REQUEST_AUTH`. This domain MUST NOT be confused with +`DOMAIN_BUILDER_REQUEST_AUTH`. This domain MUST NOT be confused with `DOMAIN_BEACON_BUILDER`, which is used for in-protocol builder messages defined by the consensus specs. Signing and verification compute the signing root with `compute_signing_root` -over the `RequestAuth` message, as shown below. A beacon node that forwards a -`SignedRequestAuth` MUST pass its `message` and `signature` through unchanged, -so a builder verifies exactly what the validator signed. +over the `BuilderRequestAuth` message, as shown below. A beacon node that +forwards a `SignedBuilderRequestAuth` MUST pass its `message` and `signature` +through unchanged, so a builder verifies exactly what the validator signed. ```python -def get_request_auth_signature( - request_auth: RequestAuth, +def get_builder_request_auth_signature( + request_auth: BuilderRequestAuth, privkey: int, ) -> BLSSignature: - domain = compute_domain(DOMAIN_REQUEST_AUTH) + domain = compute_domain(DOMAIN_BUILDER_REQUEST_AUTH) signing_root = compute_signing_root(request_auth, domain) return bls.Sign(privkey, signing_root) -def verify_request_auth_signature( - signed_request_auth: SignedRequestAuth, +def verify_builder_request_auth_signature( + signed_builder_request_auth: SignedBuilderRequestAuth, pubkey: BLSPubkey, ) -> bool: - domain = compute_domain(DOMAIN_REQUEST_AUTH) - signing_root = compute_signing_root(signed_request_auth.message, domain) - return bls.Verify(pubkey, signing_root, signed_request_auth.signature) + domain = compute_domain(DOMAIN_BUILDER_REQUEST_AUTH) + signing_root = compute_signing_root(signed_builder_request_auth.message, domain) + return bls.Verify(pubkey, signing_root, signed_builder_request_auth.signature) ``` ## Constructing a `SignedExecutionPayloadEnvelope` @@ -252,6 +256,7 @@ The specification for a block builder to construct a documented in the [Gloas consensus specs][gloas-builder-specs]. [bls]: https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/beacon-chain.md#bls-signatures +[constructing-builder-request-auth]: ./validator.md#constructing-the-builderrequestauth [get-execution-payload-bid-api]: ./../../apis/builder/execution_payload_bid.yaml [gloas-builder-specs]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/builder.md [gloas-consensus-specs]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas @@ -259,5 +264,5 @@ documented in the [Gloas consensus specs][gloas-builder-specs]. [proposer-preferences-topic]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#new-proposer_preferences [signed-execution-payload-bid]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid [signed-execution-payload-envelope]: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadenvelope -[signed-request-auth]: ./validator.md#signedrequestauth +[signed-request-auth]: ./validator.md#signedbuilderrequestauth [submit-builder-preferences-api]: ./../../apis/builder/builder_preferences.yaml diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index d3a3d1d6..5a7646b3 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -6,14 +6,14 @@ - [Introduction](#introduction) - [Containers](#containers) - [New Containers](#new-containers) - - [`RequestAuth`](#requestauth) - - [`SignedRequestAuth`](#signedrequestauth) + - [`BuilderRequestAuth`](#builderrequestauth) + - [`SignedBuilderRequestAuth`](#signedbuilderrequestauth) - [`BuilderPreferences`](#builderpreferences) - [`BuilderPreferencesRequest`](#builderpreferencesrequest) - [Submitting Builder Preferences](#submitting-builder-preferences) - [`max_execution_payment`](#max_execution_payment) - [Bid Request](#bid-request) - - [Constructing the `RequestAuth`](#constructing-the-requestauth) + - [Constructing the `BuilderRequestAuth`](#constructing-the-builderrequestauth) - [Proposer Preferences](#proposer-preferences) - [Validating a `SignedExecutionPayloadBid`](#validating-a-signedexecutionpayloadbid) - [Block proposal](#block-proposal) @@ -39,22 +39,22 @@ corresponding to the included bid to the PTC committee. ### New Containers -#### `RequestAuth` +#### `BuilderRequestAuth` -`RequestAuth` is used to authenticate requests to a builder. This is useful so -that other builders do not DDOS or run replay attacks on the builder. +`BuilderRequestAuth` is used to authenticate requests to a builder. This is +useful so that other builders do not DDOS or run replay attacks on the builder. ```python -class RequestAuth(Container): - data: ByteList[MAX_DATA_SIZE] +class BuilderRequestAuth(Container): + data: ByteList[MAX_BUILDER_AUTH_DATA_SIZE] slot: Slot ``` -#### `SignedRequestAuth` +#### `SignedBuilderRequestAuth` ```python -class SignedRequestAuth(Container): - message: RequestAuth +class SignedBuilderRequestAuth(Container): + message: BuilderRequestAuth signature: BLSSignature ``` @@ -73,7 +73,7 @@ class BuilderPreferences(Container): ```python class BuilderPreferencesRequest(Container): preferences: BuilderPreferences - auth: SignedRequestAuth + auth: SignedBuilderRequestAuth ``` ## Submitting Builder Preferences @@ -92,10 +92,11 @@ The validator constructs a `BuilderPreferences` with: [`max_execution_payment`](#max_execution_payment). The validator then constructs a `BuilderPreferencesRequest` with the -`BuilderPreferences` as `preferences` and a `SignedRequestAuth` as `auth`, and -submits it to the [`submitBuilderPreferences`][submit-builder-preferences-api] -endpoint for its `proposer_pubkey`. The `SignedRequestAuth` is constructed as -described in [Constructing the `RequestAuth`](#constructing-the-requestauth); +`BuilderPreferences` as `preferences` and a `SignedBuilderRequestAuth` as +`auth`, and submits it to the +[`submitBuilderPreferences`][submit-builder-preferences-api] endpoint for its +`proposer_pubkey`. The `SignedBuilderRequestAuth` is constructed as described in +[Constructing the `BuilderRequestAuth`](#constructing-the-builderrequestauth); its `auth.message.data` is the authentication data the builder expects and its `auth.message.slot` is the proposal slot the preferences apply to. The builder MUST verify the `auth` signature against the `proposer_pubkey` path parameter, @@ -108,7 +109,9 @@ back to a stale value. A builder MUST honor the `max_execution_payment` cap in any bid it serves for a slot it has stored preferences for; without them it MAY serve a bid with any `execution_payment`. The proposer's locally configured per-builder limits are -the backstop: the proposer discards any bid that exceeds them. +the backstop: the proposer values a bid at its `value` plus +`min(execution_payment, max_execution_payment)`, so payment above the cap adds +nothing to the bid's chances. ### `max_execution_payment` @@ -126,21 +129,21 @@ their level of trust in the builder's reliability and reputation. ## Bid Request When calling [`getExecutionPayloadBid`][get-execution-payload-bid-api], the -validator MUST send a [`SignedRequestAuth`](#signedrequestauth) as the request -body to authenticate the request. The body MAY be encoded as JSON +validator MUST send a [`SignedBuilderRequestAuth`](#signedbuilderrequestauth) as +the request body to authenticate the request. The body MAY be encoded as JSON (`Content-Type: application/json`) or SSZ -(`Content-Type: application/octet-stream`); `RequestAuth` is fork-versioned, so -the `Eth-Consensus-Version` header is required. Proposer duties are known an -epoch in advance, so the validator can sign the `SignedRequestAuth` ahead of -time, off the proposal hot path. +(`Content-Type: application/octet-stream`); `BuilderRequestAuth` is +fork-versioned, so the `Eth-Consensus-Version` header is required. Proposer +duties are known an epoch in advance, so the validator can sign the +`SignedBuilderRequestAuth` ahead of time, off the proposal hot path. -### Constructing the `RequestAuth` +### Constructing the `BuilderRequestAuth` -The validator constructs a `RequestAuth` with the following fields: +The validator constructs a `BuilderRequestAuth` with the following fields: - `data`: opaque authentication data agreed with the builder out of band whose meaning is left to the two parties. It is not tied to an endpoint, so one - `SignedRequestAuth` can authenticate the proposer for both + `SignedBuilderRequestAuth` can authenticate the proposer for both `getExecutionPayloadBid` and `submitBuilderPreferences`. When no value has been agreed out of band, the validator SHOULD use the UTF-8 bytes of the builder's own advertised URL, exactly as advertised. A zero-length `data` is @@ -149,12 +152,12 @@ The validator constructs a `RequestAuth` with the following fields: which the request is signed or sent. The proposer's public key is already carried as a path parameter in the relevant -API request, so it does not need to be carried inside `RequestAuth`. +API request, so it does not need to be carried inside `BuilderRequestAuth`. -The validator then constructs the `SignedRequestAuth` by signing the -`RequestAuth`. The signature lets builders authenticate the requesting validator -and discard requests from other parties (e.g. DDOS or replay attempts from -competing builders). +The validator then constructs the `SignedBuilderRequestAuth` by signing the +`BuilderRequestAuth`. The signature lets builders authenticate the requesting +validator and discard requests from other parties (e.g. DDOS or replay attempts +from competing builders). ## Proposer Preferences @@ -189,7 +192,6 @@ are also defined in the consensus specs. def validate_bid( state: BeaconState, proposer_preferences: ProposerPreferences, - max_execution_payment: uint64, signed_bid: SignedExecutionPayloadBid, fee_recipient: ExecutionAddress, ) -> bool: @@ -207,19 +209,17 @@ def validate_bid( assert bid.prev_randao == get_randao_mix(state, get_current_epoch(state)) assert bid.gas_limit <= proposer_preferences.gas_limit - assert bid.execution_payment <= max_execution_payment - if bid.value > 0: assert can_builder_cover_bid(state, bid.builder_index, bid.value) return verify_execution_payload_bid_signature(state, signed_bid) ``` -`max_execution_payment` is the limit the validator has locally configured for -this builder, the same value it submits via -[`submitBuilderPreferences`][submit-builder-preferences-api] when it submits -preferences. Validators MUST validate each bid against that limit, whether or -not preferences were submitted. +The validator's locally configured `max_execution_payment`, the same value it +submits via [`submitBuilderPreferences`][submit-builder-preferences-api] when it +submits preferences, is not a validity condition: it caps how much of +`bid.execution_payment` counts when the bid is valued, whether or not +preferences were submitted. Note that the fee recipient specified in `bid.fee_recipient` does not necessarily correspond to the fee recipient of the execution payload. Even if a @@ -239,8 +239,8 @@ block on top of a beacon `state` must take the following actions: 1. Call upstream builder software to get a [`SignedExecutionPayloadBid`][signed-execution-payload-bid] using the [`getExecutionPayloadBid`][get-execution-payload-bid-api] API call. The - validator signs a `SignedRequestAuth` and the beacon node sends it unchanged - in the request body to authenticate the request. + validator signs a `SignedBuilderRequestAuth` and the beacon node sends it + unchanged in the request body to authenticate the request. 2. Assemble a `SignedBeaconBlock` according to the process outlined in the [Gloas validator specs][gloas-validator-specs] but with the best [`SignedExecutionPayloadBid`][signed-execution-payload-bid] from the prior diff --git a/types/gloas/builder_preferences.yaml b/types/gloas/builder_preferences.yaml index 6af80011..0587e1d1 100644 --- a/types/gloas/builder_preferences.yaml +++ b/types/gloas/builder_preferences.yaml @@ -15,4 +15,4 @@ Gloas: preferences: $ref: "#/Gloas/BuilderPreferences" auth: - $ref: "./request_auth.yaml#/Gloas/SignedRequestAuth" + $ref: "./request_auth.yaml#/Gloas/SignedBuilderRequestAuth" diff --git a/types/gloas/request_auth.yaml b/types/gloas/request_auth.yaml index c46b4e86..e5e5e242 100644 --- a/types/gloas/request_auth.yaml +++ b/types/gloas/request_auth.yaml @@ -1,5 +1,5 @@ Gloas: - RequestAuth: + BuilderRequestAuth: type: object required: [data, slot] properties: @@ -8,7 +8,7 @@ Gloas: format: hex description: >- Opaque authentication data unique to the builder, agreed upon out of - band. The meaning of the up to `MAX_DATA_SIZE` (4096) bytes is left + band. The meaning of the up to `MAX_BUILDER_AUTH_DATA_SIZE` (4096) bytes is left to the two parties; the builder checks the exact bytes when it verifies. When no value has been agreed out of band, implementations SHOULD default to the UTF-8 bytes of the builder's own advertised @@ -20,11 +20,11 @@ Gloas: slot: $ref: "../../beacon-apis/types/primitive.yaml#/Uint64" description: "The proposal slot this request is authorized for." - SignedRequestAuth: + SignedBuilderRequestAuth: type: object required: [message, signature] properties: message: - $ref: "#/Gloas/RequestAuth" + $ref: "#/Gloas/BuilderRequestAuth" signature: $ref: "../../beacon-apis/types/primitive.yaml#/Signature" From 0bac9997b1dedad40910220883e63e0eb4f9aa63 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sun, 23 Aug 2026 21:42:25 +0100 Subject: [PATCH 28/28] clean up wordlist --- wordlist.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/wordlist.txt b/wordlist.txt index 629239b2..f57d5419 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -36,8 +36,5 @@ gloas Gwei PTC UTF -BuilderPreferences -RequestAuth -SignedRequestAuth submitBlindedBlockV ValidatorRegistrationsV \ No newline at end of file