From 157a3701e675feb85653891abb246b9b021071ec Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Sun, 26 Jul 2026 11:03:29 -0700 Subject: [PATCH 1/6] add per-key builders endpoints Adds GET/POST/DELETE on /eth/v1/validator/{pubkey}/builders, letting an operator configure which external builders a validator key sources bids from, and with what per-builder limits. An alternative to #87, which managed one atomic per-key document at /eth/v1/validator/config and deprecated the fee recipient, gas limit and graffiti endpoints. This branch is cut from master, so those three are untouched, and each concern keeps its own endpoint. POST replaces the key's list and requires at least one entry; DELETE removes the configuration so the key follows the validator client's own, exactly as a key that was never configured does. Entries are identified by their (url, auth_data) pair, unique per key. The entry describes what each value means to the operator. The bid selection rule itself lives in beacon-APIs, since only the beacon node can implement it. --- apis/validator_builders.yaml | 153 +++++++++++++++++++++++++++++++++++ keymanager-oapi.yaml | 6 ++ types/builder_entry.yaml | 73 +++++++++++++++++ wordlist.txt | 5 ++ 4 files changed, 237 insertions(+) create mode 100644 apis/validator_builders.yaml create mode 100644 types/builder_entry.yaml diff --git a/apis/validator_builders.yaml b/apis/validator_builders.yaml new file mode 100644 index 0000000..ada98ed --- /dev/null +++ b/apis/validator_builders.yaml @@ -0,0 +1,153 @@ +get: + operationId: getBuilders + summary: Get Builders + description: | + List the builders configured for an individual validator key. A key with no builder + configuration returns an empty list, and follows the validator client's own configuration. + security: + - bearerAuth: [] + tags: + - Builders + parameters: + - in: path + name: pubkey + schema: + $ref: "../keymanager-oapi.yaml#/components/schemas/Pubkey" + required: true + responses: + "200": + description: success response + content: + application/json: + schema: + title: ListBuildersResponse + type: object + required: [data] + properties: + data: + type: array + items: + $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderEntry" + examples: + Builders: + value: + data: + - url: "https://builder-a.example.com" + auth_data: "0x68747470733a2f2f6275696c6465722d612e6578616d706c652e636f6d" + max_execution_payment: "0" + min_bid: "0" + builder_boost_factor: "100" + - url: "https://builder-b.example.com" + auth_data: "0x1234567890abcdef" + builder_pubkey: "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + max_execution_payment: "250000000" + min_bid: "10000000" + builder_boost_factor: "100" + "400": + $ref: "../keymanager-oapi.yaml#/components/responses/BadRequest" + "401": + $ref: "../keymanager-oapi.yaml#/components/responses/Unauthorized" + "403": + $ref: "../keymanager-oapi.yaml#/components/responses/Forbidden" + "404": + $ref: "../keymanager-oapi.yaml#/components/responses/NotFound" + "500": + $ref: "../keymanager-oapi.yaml#/components/responses/InternalError" + +post: + operationId: setBuilders + summary: Set Builders + description: | + Set the builders for an individual validator key. The submitted list replaces the key's stored + list in full; the server MUST NOT merge it with previously stored entries. + + The list MUST contain at least one entry, and MUST NOT contain two entries with the same `url` + and the same `auth_data`. The server MUST reject either with a 400 rather than storing a partial + list or silently dropping an entry. + + Because at least one entry is required, `POST` can narrow a key's builders but cannot remove the + configuration; use `DELETE` for that. + security: + - bearerAuth: [] + tags: + - Builders + parameters: + - in: path + name: pubkey + schema: + $ref: "../keymanager-oapi.yaml#/components/schemas/Pubkey" + required: true + requestBody: + required: true + content: + application/json: + schema: + title: SetBuildersRequest + type: object + required: [builders] + properties: + builders: + type: array + minItems: 1 + maxItems: 64 # MAX_BUILDER_ENTRIES + items: + $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderEntry" + examples: + Builders: + value: + builders: + - url: "https://builder-a.example.com" + auth_data: "0x68747470733a2f2f6275696c6465722d612e6578616d706c652e636f6d" + max_execution_payment: "0" + min_bid: "0" + builder_boost_factor: "100" + responses: + "202": + description: successfully updated + "400": + $ref: "../keymanager-oapi.yaml#/components/responses/BadRequest" + "401": + $ref: "../keymanager-oapi.yaml#/components/responses/Unauthorized" + "403": + $ref: "../keymanager-oapi.yaml#/components/responses/Forbidden" + "404": + $ref: "../keymanager-oapi.yaml#/components/responses/NotFound" + "500": + $ref: "../keymanager-oapi.yaml#/components/responses/InternalError" + +delete: + operationId: deleteBuilders + summary: Delete Configured Builders + description: | + Remove this key's builder configuration entirely. The key then follows the validator client's + own configuration, in accordance with the startup of the validator client, exactly as a key that + was never configured does. + + To stop using one builder while keeping the others, `POST` the remaining entries instead. + security: + - bearerAuth: [] + tags: + - Builders + parameters: + - in: path + name: pubkey + schema: + $ref: "../keymanager-oapi.yaml#/components/schemas/Pubkey" + required: true + responses: + "204": + description: Successfully removed the builders, or there were no builders set for the requested public key. + "400": + $ref: "../keymanager-oapi.yaml#/components/responses/BadRequest" + "401": + $ref: "../keymanager-oapi.yaml#/components/responses/Unauthorized" + "403": + description: Builders were found, but cannot be removed. This may be because they were in configuration files that cannot be updated. + content: + application/json: + schema: + $ref: "../keymanager-oapi.yaml#/components/schemas/ErrorResponse" + "404": + $ref: "../keymanager-oapi.yaml#/components/responses/NotFound" + "500": + $ref: "../keymanager-oapi.yaml#/components/responses/InternalError" diff --git a/keymanager-oapi.yaml b/keymanager-oapi.yaml index a5422ab..1462506 100644 --- a/keymanager-oapi.yaml +++ b/keymanager-oapi.yaml @@ -34,6 +34,8 @@ servers: default: "http://localhost/" tags: + - name: Builders + description: Set of endpoints for management of per-key builders. - name: Fee Recipient description: Set of endpoints for management of fee recipient. - name: Gas Limit @@ -52,6 +54,8 @@ paths: $ref: './apis/local_keystores.yaml' /eth/v1/remotekeys: $ref: './apis/remote_keystores.yaml' + /eth/v1/validator/{pubkey}/builders: + $ref: './apis/validator_builders.yaml' /eth/v1/validator/{pubkey}/feerecipient: $ref: './apis/fee_recipient.yaml' /eth/v1/validator/{pubkey}/gas_limit: @@ -75,6 +79,8 @@ components: $ref: './types/eth_address.yaml' Keystore: $ref: './types/keystore.yaml' + BuilderEntry: + $ref: './types/builder_entry.yaml#/BuilderEntry' FeeRecipient: $ref: './types/fee_recipient.yaml' GasLimit: diff --git a/types/builder_entry.yaml b/types/builder_entry.yaml new file mode 100644 index 0000000..ff87010 --- /dev/null +++ b/types/builder_entry.yaml @@ -0,0 +1,73 @@ +BuilderEntry: + type: object + description: | + One builder a validator key may source blocks from. An entry is identified by its + (`url`, `auth_data`) pair: `url` compared as the exact string, `auth_data` as its decoded bytes, + so hex case does not distinguish entries. Several entries MAY share a `url`, each with different + `auth_data`, and one request is made per entry. + + Every field except `builder_pubkey` is required: an entry carries every preference that applies + to this builder, and none are inherited. The beacon node applies these preferences when it + selects a bid; see [produceBlockV4](https://ethereum.github.io/beacon-APIs/#/Validator/produceBlockV4) for the selection rule. + required: [url, auth_data, max_execution_payment, min_bid, builder_boost_factor] + properties: + url: + type: string + format: uri + maxLength: 2048 # MAX_BUILDER_URL_SIZE + description: | + The URL this entry's requests are sent to. + example: "https://builder.example.com" + auth_data: + type: string + pattern: "^0x(?:[a-fA-F0-9]{2}){0,4096}$" + description: | + Opaque data used to authenticate this validator's requests to the builder. Agreed with the + builder out of band and placed verbatim in the `RequestAuthV1.data` the validator signs. The + builder checks the signature and the exact bytes. Its meaning is left to the two parties + (for example, a shared secret). At most `MAX_DATA_SIZE` (4096) bytes. + + The keymanager treats this value as opaque bytes: it only ever compares `auth_data` values + for equality, and never parses or inspects them. When no value has been agreed with the + builder out of band, the caller SHOULD set `auth_data` to the UTF-8 bytes of the builder's + own advertised URL, exactly as advertised, hex encoded: it is deterministic, distinct per + builder, and requires no coordination between the signers of a distributed validator. That + is purely a convention about what tooling writes into the field. + example: "0x68747470733a2f2f6275696c6465722e6578616d706c652e636f6d" + builder_pubkey: + description: | + The builder's BLS public key, distinct from the validator `pubkey` in the request path. When + set, it binds this builder's trusted execution payments to its on-chain key. _48-bytes, hex + encoded with 0x prefix, case insensitive._ + allOf: + - $ref: "../keymanager-oapi.yaml#/components/schemas/Pubkey" + max_execution_payment: + allOf: + - $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64" + - description: | + A ceiling, in Gwei, on the trusted execution payment accepted from this builder. The + remainder of its payment is expected in the trustless (in-protocol) form. + + `0` accepts no execution payments from this builder, requiring the whole payment to be + trustless. `MAX_EXECUTION_PAYMENT` (`2**64 - 1`) accepts any amount. + example: "250000000" + min_bid: + allOf: + - $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64" + - description: | + A floor, in Gwei, on the total payment accepted from this builder, counting a bid's + `value` plus its `execution_payment`. + example: "10000000" + builder_boost_factor: + allOf: + - $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64" + - description: | + Percentage multiplier weighting this builder's bid against the other candidates. Below + `100` favours the local payload, above `100` favours this builder. + + Values that encode common preferences: + + * `0`: prefer the local payload unless an error makes it unviable. + * `100`: profit maximization; choose whichever pays more. + * `2**64 - 1`: prefer this builder unless an error or health check makes it unviable. + example: "100" diff --git a/wordlist.txt b/wordlist.txt index 2156f88..0e186f4 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -19,3 +19,8 @@ VPN BLS oapi Vero +Gwei +uri +UTF +RequestAuthV +produceBlockV From db7dc28c2266099fe8b666a28d6382ddeb39ed1b Mon Sep 17 00:00:00 2001 From: JasonVranek Date: Sun, 26 Jul 2026 23:25:29 +0100 Subject: [PATCH 2/6] Update types/builder_entry.yaml Co-authored-by: Luca | Serenita <70237279+eth2353@users.noreply.github.com> --- types/builder_entry.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/builder_entry.yaml b/types/builder_entry.yaml index ff87010..6c27fa3 100644 --- a/types/builder_entry.yaml +++ b/types/builder_entry.yaml @@ -62,7 +62,7 @@ BuilderEntry: allOf: - $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64" - description: | - Percentage multiplier weighting this builder's bid against the other candidates. Below + Percentage multiplier weighting this builder's bid against the local payload. Below `100` favours the local payload, above `100` favours this builder. Values that encode common preferences: From eeec060bd8ef775ace4343e539e544ee2f0bba30 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Sun, 26 Jul 2026 16:45:25 -0700 Subject: [PATCH 3/6] Add back enabled flag. When false, stops sourcing all builder bids regardless of VC's global config --- apis/validator_builders.yaml | 43 ++++++++++++++++-------------------- keymanager-oapi.yaml | 2 ++ types/builder_entry.yaml | 29 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/apis/validator_builders.yaml b/apis/validator_builders.yaml index ada98ed..78e2cc5 100644 --- a/apis/validator_builders.yaml +++ b/apis/validator_builders.yaml @@ -2,8 +2,9 @@ get: operationId: getBuilders summary: Get Builders description: | - List the builders configured for an individual validator key. A key with no builder - configuration returns an empty list, and follows the validator client's own configuration. + Get the builder configuration in effect for an individual validator key. A key with no + configuration of its own returns the validator client's, in accordance with the startup of the + validator client. security: - bearerAuth: [] tags: @@ -20,18 +21,18 @@ get: content: application/json: schema: - title: ListBuildersResponse + title: GetBuildersResponse type: object required: [data] properties: data: - type: array - items: - $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderEntry" + $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderConfig" examples: Builders: value: data: + enabled: true + builders: - url: "https://builder-a.example.com" auth_data: "0x68747470733a2f2f6275696c6465722d612e6578616d706c652e636f6d" max_execution_payment: "0" @@ -58,15 +59,15 @@ post: operationId: setBuilders summary: Set Builders description: | - Set the builders for an individual validator key. The submitted list replaces the key's stored - list in full; the server MUST NOT merge it with previously stored entries. + Set the builder configuration for an individual validator key. The submission replaces the + key's stored configuration in full; the server MUST NOT merge it with what was stored before. - The list MUST contain at least one entry, and MUST NOT contain two entries with the same `url` - and the same `auth_data`. The server MUST reject either with a 400 rather than storing a partial - list or silently dropping an entry. + When present, `builders` MUST contain at least one entry, and MUST NOT contain two entries with + the same `url` and the same `auth_data`. The server MUST reject either with a 400 rather than + storing a partial list or silently dropping an entry. - Because at least one entry is required, `POST` can narrow a key's builders but cannot remove the - configuration; use `DELETE` for that. + `POST` always leaves the key configured; use `DELETE` to remove the configuration so the key + follows the validator client again. security: - bearerAuth: [] tags: @@ -82,19 +83,11 @@ post: content: application/json: schema: - title: SetBuildersRequest - type: object - required: [builders] - properties: - builders: - type: array - minItems: 1 - maxItems: 64 # MAX_BUILDER_ENTRIES - items: - $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderEntry" + $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderConfig" examples: Builders: value: + enabled: true builders: - url: "https://builder-a.example.com" auth_data: "0x68747470733a2f2f6275696c6465722d612e6578616d706c652e636f6d" @@ -123,7 +116,9 @@ delete: own configuration, in accordance with the startup of the validator client, exactly as a key that was never configured does. - To stop using one builder while keeping the others, `POST` the remaining entries instead. + This is not the same as `enabled: false`, which keeps a configuration saying this key sources no + builder bids at all. To stop using one builder while keeping the others, `POST` the remaining + entries instead. security: - bearerAuth: [] tags: diff --git a/keymanager-oapi.yaml b/keymanager-oapi.yaml index 1462506..cecc57b 100644 --- a/keymanager-oapi.yaml +++ b/keymanager-oapi.yaml @@ -79,6 +79,8 @@ components: $ref: './types/eth_address.yaml' Keystore: $ref: './types/keystore.yaml' + BuilderConfig: + $ref: './types/builder_entry.yaml#/BuilderConfig' BuilderEntry: $ref: './types/builder_entry.yaml#/BuilderEntry' FeeRecipient: diff --git a/types/builder_entry.yaml b/types/builder_entry.yaml index 6c27fa3..f353217 100644 --- a/types/builder_entry.yaml +++ b/types/builder_entry.yaml @@ -1,3 +1,32 @@ +BuilderConfig: + type: object + description: | + How a validator key sources blocks from builders. + + `enabled: false` stops this key sourcing builder bids at all, including from any builders the + validator client is configured with globally. This is how a single key opts out of an otherwise + global builder setup, and it is distinct from having no configuration, which follows the + validator client in full. + + When `enabled` is `true`, `builders` names the builders to use. Omit it to use whichever + builders the validator client is configured with. + required: [enabled] + properties: + enabled: + type: boolean + description: | + Whether blocks may be sourced from builders for this key. + example: true + builders: + type: array + minItems: 1 + maxItems: 64 # MAX_BUILDER_ENTRIES + description: | + The builders this key sources bids from, replacing any the validator client is configured + with. Omit the field rather than sending an empty array. + items: + $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderEntry" + BuilderEntry: type: object description: | From 576ec6ab6586dda849da98ca02f5dc3c672e0aa9 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Sun, 26 Jul 2026 20:58:14 -0700 Subject: [PATCH 4/6] add per-key defaults, define GET resolution, fix entry identity Defaults: default_min_bid and default_builder_boost_factor on BuilderConfig. An entry that omits min_bid or builder_boost_factor takes the key's default, so one value covers every builder without being repeated per entry, and the same values apply to bids from builders no entry identifies. Resolution order is stated as entry value, then the key's default, then the validator client's own configuration. Without the middle step an operator who set a default and omitted the field on entries would silently get the validator client's value instead. GET returns the effective configuration with omitted values resolved, matching getGasLimit and getGraffiti --- apis/validator_builders.yaml | 40 +++++++++++++-------- keymanager-oapi.yaml | 2 +- types/builder_entry.yaml | 69 ++++++++++++++++++++++++++---------- 3 files changed, 78 insertions(+), 33 deletions(-) diff --git a/apis/validator_builders.yaml b/apis/validator_builders.yaml index 78e2cc5..ee5abf7 100644 --- a/apis/validator_builders.yaml +++ b/apis/validator_builders.yaml @@ -2,9 +2,11 @@ get: operationId: getBuilders summary: Get Builders description: | - Get the builder configuration in effect for an individual validator key. A key with no - configuration of its own returns the validator client's, in accordance with the startup of the - validator client. + Get the builder configuration in effect for an individual validator key, with omitted values + resolved: an entry that omits a field is returned with the value that will be used for it, and a + key with no configuration of its own is returned with the validator client's. A returned + configuration is therefore fully resolved, so re-submitting it with `POST` fixes those values + rather than leaving them to follow the defaults. security: - bearerAuth: [] tags: @@ -28,10 +30,12 @@ get: data: $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderConfig" examples: - Builders: + BuilderConfig: value: data: enabled: true + default_min_bid: "10000000" + default_builder_boost_factor: "100" builders: - url: "https://builder-a.example.com" auth_data: "0x68747470733a2f2f6275696c6465722d612e6578616d706c652e636f6d" @@ -44,6 +48,10 @@ get: max_execution_payment: "250000000" min_bid: "10000000" builder_boost_factor: "100" + - builder_pubkey: "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a" + max_execution_payment: "0" + min_bid: "20000000" + builder_boost_factor: "120" "400": $ref: "../keymanager-oapi.yaml#/components/responses/BadRequest" "401": @@ -62,12 +70,14 @@ post: Set the builder configuration for an individual validator key. The submission replaces the key's stored configuration in full; the server MUST NOT merge it with what was stored before. - When present, `builders` MUST contain at least one entry, and MUST NOT contain two entries with - the same `url` and the same `auth_data`. The server MUST reject either with a 400 rather than - storing a partial list or silently dropping an entry. + Each entry MUST contain at least one of `url` and `builder_pubkey`. No two entries may share the + same `url`, `auth_data` and `builder_pubkey`, and no `builder_pubkey` may appear twice. The + server MUST reject any of these with a 400 rather than storing a partial list or silently + dropping an entry. `POST` always leaves the key configured; use `DELETE` to remove the configuration so the key - follows the validator client again. + follows the validator client again. An empty `builders` array is a configuration that names no + builders, not a removal. security: - bearerAuth: [] tags: @@ -85,15 +95,17 @@ post: schema: $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderConfig" examples: - Builders: + BuilderConfig: value: enabled: true + default_min_bid: "10000000" + default_builder_boost_factor: "100" builders: - url: "https://builder-a.example.com" - auth_data: "0x68747470733a2f2f6275696c6465722d612e6578616d706c652e636f6d" max_execution_payment: "0" - min_bid: "0" - builder_boost_factor: "100" + - builder_pubkey: "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a" + max_execution_payment: "0" + builder_boost_factor: "120" responses: "202": description: successfully updated @@ -131,13 +143,13 @@ delete: required: true responses: "204": - description: Successfully removed the builders, or there were no builders set for the requested public key. + description: Successfully removed the builder configuration, or there was none set for the requested public key. "400": $ref: "../keymanager-oapi.yaml#/components/responses/BadRequest" "401": $ref: "../keymanager-oapi.yaml#/components/responses/Unauthorized" "403": - description: Builders were found, but cannot be removed. This may be because they were in configuration files that cannot be updated. + description: A builder configuration was found, but cannot be removed. This may be because it was in configuration files that cannot be updated. content: application/json: schema: diff --git a/keymanager-oapi.yaml b/keymanager-oapi.yaml index cecc57b..5a1e84f 100644 --- a/keymanager-oapi.yaml +++ b/keymanager-oapi.yaml @@ -35,7 +35,7 @@ servers: tags: - name: Builders - description: Set of endpoints for management of per-key builders. + description: Set of endpoints for management of builders. - name: Fee Recipient description: Set of endpoints for management of fee recipient. - name: Gas Limit diff --git a/types/builder_entry.yaml b/types/builder_entry.yaml index f353217..661d4ff 100644 --- a/types/builder_entry.yaml +++ b/types/builder_entry.yaml @@ -5,11 +5,20 @@ BuilderConfig: `enabled: false` stops this key sourcing builder bids at all, including from any builders the validator client is configured with globally. This is how a single key opts out of an otherwise - global builder setup, and it is distinct from having no configuration, which follows the - validator client in full. + global builder setup, and differs from having no configuration, which follows the validator + client in full. - When `enabled` is `true`, `builders` names the builders to use. Omit it to use whichever - builders the validator client is configured with. + When `enabled` is `true`, `builders` selects which builders this key uses. + + `default_min_bid` and `default_builder_boost_factor` are this key's defaults. An entry that + omits `min_bid` or `builder_boost_factor` takes the corresponding default, so one value covers + every builder without being repeated on each entry. + + The validator client resolves every entry before it reaches the beacon node. An omitted + `min_bid` or `builder_boost_factor` takes this key's default, and then the validator client's + own configuration; an omitted `max_execution_payment` or `auth_data` takes the validator + client's configuration. An omitted `url` or `builder_pubkey` is not filled in: its absence is + what tells the beacon node that the entry requests no bid, or supplies no p2p policy. required: [enabled] properties: enabled: @@ -19,33 +28,53 @@ BuilderConfig: example: true builders: type: array - minItems: 1 maxItems: 64 # MAX_BUILDER_ENTRIES description: | The builders this key sources bids from, replacing any the validator client is configured - with. Omit the field rather than sending an empty array. + with. Omit this field to use the validator client's builders instead. Send an empty array to + use none, so no bid is requested from any builder and p2p bids remain this key's only + source. items: $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderEntry" + default_min_bid: + allOf: + - $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64" + - description: | + This key's default `min_bid`, in Gwei. Applies to any entry that omits `min_bid`, and + to a bid from a builder no entry identifies. + example: "10000000" + default_builder_boost_factor: + allOf: + - $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64" + - description: | + This key's default `builder_boost_factor`. Applies to any entry that omits + `builder_boost_factor`, and to a bid from a builder no entry identifies. + example: "100" BuilderEntry: type: object description: | - One builder a validator key may source blocks from. An entry is identified by its - (`url`, `auth_data`) pair: `url` compared as the exact string, `auth_data` as its decoded bytes, - so hex case does not distinguish entries. Several entries MAY share a `url`, each with different - `auth_data`, and one request is made per entry. + One builder a validator key may source blocks from. At least one of `url` and `builder_pubkey` + MUST be present. + + Entries are distinguished by `url`, `auth_data` and `builder_pubkey` taken together, where an + absent field matches only another absent field. `url` is compared as the exact string and + `auth_data` as the decoded bytes, so hex case does not distinguish entries, and an omitted + `auth_data` is compared as the value the validator client would derive for it. Several entries + MAY share a `url` with different `auth_data`, and one request is made per entry, since several + builders may be reachable at one `url`. A `builder_pubkey` MUST NOT appear on more than one + entry, since two entries naming the same builder would leave its p2p bids subject to both. - Every field except `builder_pubkey` is required: an entry carries every preference that applies - to this builder, and none are inherited. The beacon node applies these preferences when it - selects a bid; see [produceBlockV4](https://ethereum.github.io/beacon-APIs/#/Validator/produceBlockV4) for the selection rule. - required: [url, auth_data, max_execution_payment, min_bid, builder_boost_factor] + See [produceBlockV4](https://ethereum.github.io/beacon-APIs/#/Validator/produceBlockV4) for the + selection rule. properties: url: type: string format: uri maxLength: 2048 # MAX_BUILDER_URL_SIZE description: | - The URL this entry's requests are sent to. + The URL this entry's requests are sent to. Omit it to supply policy for this builder's p2p + bids without requesting one. example: "https://builder.example.com" auth_data: type: string @@ -62,12 +91,16 @@ BuilderEntry: own advertised URL, exactly as advertised, hex encoded: it is deterministic, distinct per builder, and requires no coordination between the signers of a distributed validator. That is purely a convention about what tooling writes into the field. + + Omitting it on an entry that contains a `url` leaves the validator client to derive it the + same way, since the builder requires authentication on every bid request. An entry with no + `url` requests no bid, so it needs no `auth_data`. example: "0x68747470733a2f2f6275696c6465722e6578616d706c652e636f6d" builder_pubkey: description: | The builder's BLS public key, distinct from the validator `pubkey` in the request path. When - set, it binds this builder's trusted execution payments to its on-chain key. _48-bytes, hex - encoded with 0x prefix, case insensitive._ + set, a builder-API bid not signed by it MUST NOT be accepted, and this entry also applies to + that builder's p2p bids. _48-bytes, hex encoded with 0x prefix, case insensitive._ allOf: - $ref: "../keymanager-oapi.yaml#/components/schemas/Pubkey" max_execution_payment: @@ -92,7 +125,7 @@ BuilderEntry: - $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64" - description: | Percentage multiplier weighting this builder's bid against the local payload. Below - `100` favours the local payload, above `100` favours this builder. + `100` favors the local payload, above `100` favors this builder. Values that encode common preferences: From 52ce1145a415214059d048526119ecc19080d80c Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 27 Jul 2026 11:31:45 -0700 Subject: [PATCH 5/6] scope entry uniqueness by role: request identity vs p2p policy Mirror of the #630 beacon-APIs change. `builder_pubkey` filters the response on an entry with a `url` and names the p2p builder on one without, so the POST 400 rules and the identity paragraph split in two: entries with a url are unique by (url, auth_data), entries without one by builder_pubkey. This unblocks a builder bidding into several relays, which the old "builder_pubkey may not appear twice" rule rejected. --- apis/validator_builders.yaml | 8 ++++---- types/builder_entry.yaml | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apis/validator_builders.yaml b/apis/validator_builders.yaml index ee5abf7..c45f546 100644 --- a/apis/validator_builders.yaml +++ b/apis/validator_builders.yaml @@ -70,10 +70,10 @@ post: Set the builder configuration for an individual validator key. The submission replaces the key's stored configuration in full; the server MUST NOT merge it with what was stored before. - Each entry MUST contain at least one of `url` and `builder_pubkey`. No two entries may share the - same `url`, `auth_data` and `builder_pubkey`, and no `builder_pubkey` may appear twice. The - server MUST reject any of these with a 400 rather than storing a partial list or silently - dropping an entry. + Each entry MUST contain at least one of `url` and `builder_pubkey`. No two entries that contain + a `url` may share both that `url` and their `auth_data`, and no two entries without one may + share a `builder_pubkey`. The server MUST reject any of these with a 400 rather than storing a + partial list or silently dropping an entry. `POST` always leaves the key configured; use `DELETE` to remove the configuration so the key follows the validator client again. An empty `builders` array is a configuration that names no diff --git a/types/builder_entry.yaml b/types/builder_entry.yaml index 661d4ff..f16ed83 100644 --- a/types/builder_entry.yaml +++ b/types/builder_entry.yaml @@ -57,13 +57,13 @@ BuilderEntry: One builder a validator key may source blocks from. At least one of `url` and `builder_pubkey` MUST be present. - Entries are distinguished by `url`, `auth_data` and `builder_pubkey` taken together, where an - absent field matches only another absent field. `url` is compared as the exact string and - `auth_data` as the decoded bytes, so hex case does not distinguish entries, and an omitted - `auth_data` is compared as the value the validator client would derive for it. Several entries - MAY share a `url` with different `auth_data`, and one request is made per entry, since several - builders may be reachable at one `url`. A `builder_pubkey` MUST NOT appear on more than one - entry, since two entries naming the same builder would leave its p2p bids subject to both. + An entry that contains a `url` is a bid request, and no two of them may share both that `url` + and their `auth_data`. `url` is compared as the exact string and `auth_data` as the decoded + bytes, so hex case does not distinguish entries, and an omitted `auth_data` is compared as the + value the validator client would derive for it. One request is made per entry, so several + entries MAY share a `url` with different `auth_data`, since several builders may be reachable + at one `url`. An entry with no `url` supplies p2p policy instead, and no two of them may share + a `builder_pubkey`. See [produceBlockV4](https://ethereum.github.io/beacon-APIs/#/Validator/produceBlockV4) for the selection rule. @@ -98,9 +98,10 @@ BuilderEntry: example: "0x68747470733a2f2f6275696c6465722e6578616d706c652e636f6d" builder_pubkey: description: | - The builder's BLS public key, distinct from the validator `pubkey` in the request path. When - set, a builder-API bid not signed by it MUST NOT be accepted, and this entry also applies to - that builder's p2p bids. _48-bytes, hex encoded with 0x prefix, case insensitive._ + The builder's BLS public key, distinct from the validator `pubkey` in the request path. On + an entry with a `url` it filters the response: a builder-API bid not signed by it MUST NOT + be accepted. On an entry without one it identifies the builder whose p2p bids this entry + applies to. _48-bytes, hex encoded with 0x prefix, case insensitive._ allOf: - $ref: "../keymanager-oapi.yaml#/components/schemas/Pubkey" max_execution_payment: From e2213e6d642d68aa3a5e3e8496e6e1cc717366bc Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Mon, 27 Jul 2026 11:39:39 -0700 Subject: [PATCH 6/6] drop the default_ prefix from names --- apis/validator_builders.yaml | 8 ++++---- types/builder_entry.yaml | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apis/validator_builders.yaml b/apis/validator_builders.yaml index c45f546..552b82d 100644 --- a/apis/validator_builders.yaml +++ b/apis/validator_builders.yaml @@ -34,8 +34,8 @@ get: value: data: enabled: true - default_min_bid: "10000000" - default_builder_boost_factor: "100" + min_bid: "10000000" + builder_boost_factor: "100" builders: - url: "https://builder-a.example.com" auth_data: "0x68747470733a2f2f6275696c6465722d612e6578616d706c652e636f6d" @@ -98,8 +98,8 @@ post: BuilderConfig: value: enabled: true - default_min_bid: "10000000" - default_builder_boost_factor: "100" + min_bid: "10000000" + builder_boost_factor: "100" builders: - url: "https://builder-a.example.com" max_execution_payment: "0" diff --git a/types/builder_entry.yaml b/types/builder_entry.yaml index f16ed83..aa8e957 100644 --- a/types/builder_entry.yaml +++ b/types/builder_entry.yaml @@ -10,9 +10,9 @@ BuilderConfig: When `enabled` is `true`, `builders` selects which builders this key uses. - `default_min_bid` and `default_builder_boost_factor` are this key's defaults. An entry that - omits `min_bid` or `builder_boost_factor` takes the corresponding default, so one value covers - every builder without being repeated on each entry. + `BuilderConfig` carries a `min_bid` and a `builder_boost_factor` that are this key's defaults. + An entry that omits its own takes the default, so one value covers every builder without being + repeated on each entry, and the defaults also apply to bids from builders that no entry names. The validator client resolves every entry before it reaches the beacon node. An omitted `min_bid` or `builder_boost_factor` takes this key's default, and then the validator client's @@ -36,19 +36,19 @@ BuilderConfig: source. items: $ref: "../keymanager-oapi.yaml#/components/schemas/BuilderEntry" - default_min_bid: + min_bid: allOf: - $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64" - description: | - This key's default `min_bid`, in Gwei. Applies to any entry that omits `min_bid`, and - to a bid from a builder no entry identifies. + This key's default `min_bid`, in Gwei. Applies to any entry that omits its own, and to + a bid from a builder no entry identifies. example: "10000000" - default_builder_boost_factor: + builder_boost_factor: allOf: - $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64" - description: | - This key's default `builder_boost_factor`. Applies to any entry that omits - `builder_boost_factor`, and to a bid from a builder no entry identifies. + This key's default `builder_boost_factor`. Applies to any entry that omits its own, and + to a bid from a builder no entry identifies. example: "100" BuilderEntry: