Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
157a370
add per-key builders endpoints
JasonVranek Jul 26, 2026
db7dc28
Update types/builder_entry.yaml
JasonVranek Jul 26, 2026
eeec060
Add back enabled flag. When false, stops sourcing all builder bids
JasonVranek Jul 26, 2026
576ec6a
add per-key defaults, define GET resolution, fix entry identity
JasonVranek Jul 27, 2026
52ce114
scope entry uniqueness by role: request identity vs p2p policy
JasonVranek Jul 27, 2026
e2213e6
drop the default_ prefix from names
JasonVranek Jul 27, 2026
b647dd9
clarify that an entry means an entry in the builders array
JasonVranek Jul 31, 2026
655d731
track the builder-specs RequestAuth rename
JasonVranek Jul 31, 2026
a29a3f7
order BuilderConfig global fields before builders
JasonVranek Aug 2, 2026
8da225a
drop per-builder p2p bid preferences
JasonVranek Aug 3, 2026
d5b54fc
remove enabled flag
JasonVranek Aug 10, 2026
831b257
accept bids from a list of builder pubkeys
JasonVranek Aug 10, 2026
be0f46e
a zero-length auth_data is invalid
JasonVranek Aug 10, 2026
4c8db99
add required: [] for BuilderConfig
JasonVranek Aug 11, 2026
29b22b7
Update apis/builders.yaml
JasonVranek Aug 15, 2026
a20d511
Update apis/builders.yaml
JasonVranek Aug 15, 2026
bcb5776
Update types/builder_entry.yaml
JasonVranek Aug 15, 2026
aa0f2eb
explicitly say VC sets ommitted auth_data to url verbatim
JasonVranek Aug 15, 2026
491339a
Merge branch 'master' into validator-builders
JasonVranek Aug 15, 2026
ea83888
rename the route to builder_config and use getBuilderConfig,
JasonVranek Aug 18, 2026
5039201
Update types/builder_entry.yaml
JasonVranek Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions apis/builder_config.yaml
Comment thread
nflaig marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
get:
operationId: getBuilderConfig
summary: Get Builder Configuration
description: |
Get the builder configuration in effect for an individual validator public 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 own
configuration. An omitted `builders` list is returned resolved the same way, to the validator
client's globally configured builders, or `[]` if there are none. 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:
- Builder Config
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: GetBuilderConfigResponse
type: object
required: [data]
properties:
data:
$ref: "../keymanager-oapi.yaml#/components/schemas/BuilderConfig"
Comment thread
nflaig marked this conversation as resolved.
examples:
BuilderConfig:
value:
data:
min_bid: "10000000"
builder_boost_factor: "100"
builders:
Comment on lines +37 to +40

@nflaig nflaig Aug 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left that #88 (comment) before, worth noting that current keymanager apis return the pubkey in the response of any GET request

eg. curl -X GET http://localhost/eth/v1/validator/0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a/feerecipient

would return

{
  "data": {
    "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
    "ethaddress": "0xAbcF8e0d4e9587369b2301D0790347320302cc09"
  }
}

do we need to echo the pubkey just to be consistent? curious if @rolfyone or @james-prysm have an opinion on this, or maybe @yorickdowne is that any useful for eth-docker?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok either way, without it is also fine, no strong opinions here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also realized the pubkey field is optional and not required, so it's not even something tooling today can rely on for the existing apis

I did a quick AI analysis of how existing tooling uses the pubkey in the response, and seems like nobody does

so based on this, let's keep it out of the response for new apis going forward, it seems rather useless to echo back the pubkey that caller explicitly has to supply already

- url: "https://builder-a.example.com"
auth_data: "0x68747470733a2f2f6275696c6465722d612e6578616d706c652e636f6d"
builder_pubkeys: []
max_execution_payment: "0"
min_bid: "20000000"
builder_boost_factor: "100"
- url: "https://builder-b.example.com"
auth_data: "0x1234567890abcdef"
builder_pubkeys: ["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: setBuilderConfig
summary: Set Builder Configuration
description: |
Set the builder configuration for an individual validator public key. The submission replaces
the key's stored configuration in full; the server MUST NOT merge it with what was stored
before.

Each entry in the `builders` array MUST contain a `url`, and no two entries may share both
their `url` and their `auth_data`. The server MUST reject a submission that violates either
rule with a 400 rather than storing a partial list or silently dropping an entry.

A `POST` body of `{}` stores a configuration with no overrides, which resolves identically to
having none, so it is behaviorally equivalent to `DELETE`. An empty `builders` array is a
configuration that names no builders, not a removal.
security:
- bearerAuth: []
tags:
- Builder Config
parameters:
- in: path
name: pubkey
schema:
$ref: "../keymanager-oapi.yaml#/components/schemas/Pubkey"
required: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "../keymanager-oapi.yaml#/components/schemas/BuilderConfig"
examples:
BuilderConfig:
value:
min_bid: "10000000"
builder_boost_factor: "100"
builders:
- url: "https://builder-a.example.com"
max_execution_payment: "0"
- url: "https://builder-b.example.com"
builder_pubkeys: ["0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a"]
max_execution_payment: "0"
builder_boost_factor: "120"
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":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do you guys feel about this error code? we shouldn't return it if the validator key doesn't exist

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't that what we do for all others apis today? eg. https://ethereum.github.io/keymanager-APIs/?urls.primaryName=dev#/Fee%20Recipient/setFeeRecipient

Image

at least in lodestar we explicitly check this and return 404 if validator pubkey is not known

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client responses for an unknown pubkey

image

$ref: "../keymanager-oapi.yaml#/components/responses/NotFound"
"500":
$ref: "../keymanager-oapi.yaml#/components/responses/InternalError"

delete:
operationId: deleteBuilderConfig
summary: Delete Builder Configuration
description: |
Remove this public 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.

This is not the same as an empty `builders` list, which is a stored configuration saying this
key sources no builder-API bids. To stop using one builder while keeping the others, `POST` the
remaining entries of the `builders` array instead.
security:
- bearerAuth: []
tags:
- Builder Config
parameters:
- in: path
name: pubkey
schema:
$ref: "../keymanager-oapi.yaml#/components/schemas/Pubkey"
required: true
responses:
"204":
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: 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:
$ref: "../keymanager-oapi.yaml#/components/schemas/ErrorResponse"
"404":
$ref: "../keymanager-oapi.yaml#/components/responses/NotFound"
"500":
$ref: "../keymanager-oapi.yaml#/components/responses/InternalError"
8 changes: 8 additions & 0 deletions keymanager-oapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ servers:
default: "http://localhost/"

tags:
- name: Builder Config
description: Set of endpoints for management of builder configuration.
- name: Fee Recipient
description: Set of endpoints for management of fee recipient.
- name: Gas Limit
Expand All @@ -52,6 +54,8 @@ paths:
$ref: './apis/local_keystores.yaml'
/eth/v1/remotekeys:
$ref: './apis/remote_keystores.yaml'
/eth/v1/validator/{pubkey}/builder_config:
$ref: './apis/builder_config.yaml'
/eth/v1/validator/{pubkey}/feerecipient:
$ref: './apis/fee_recipient.yaml'
/eth/v1/validator/{pubkey}/gas_limit:
Expand All @@ -75,6 +79,10 @@ 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:
$ref: './types/fee_recipient.yaml'
GasLimit:
Expand Down
136 changes: 136 additions & 0 deletions types/builder_entry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
BuilderConfig:
type: object
description: |
How a validator public key sources blocks from builders.

An empty `builders` list requests bids from no builder, overriding any builders the validator
client is configured with globally; with a `builder_boost_factor` of `0` the key also prefers
its local build. This differs from having no configuration, which follows the validator client
in full.

`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 p2p bids.

The validator client resolves the configuration before it reaches the beacon node. An omitted
`builders` list resolves to the builders the validator client is configured with globally, or
to an empty list if there are none, and this key's defaults apply to those entries all the
same. Within an entry, 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` takes the validator client's configuration; an omitted `auth_data` is
derived from the entry's `url` as described in `BuilderEntry`; an omitted `builder_pubkeys`
resolves to the empty list.
required: []
properties:
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 its own, and to
p2p bids.
example: "10000000"
Comment thread
nflaig marked this conversation as resolved.
builder_boost_factor:
allOf:
Comment thread
nflaig marked this conversation as resolved.
- $ref: "../keymanager-oapi.yaml#/components/schemas/Uint64"
- description: |
This key's default `builder_boost_factor`. Applies to any entry that omits its own, and
to p2p bids.
Comment thread
nflaig marked this conversation as resolved.

* `0`: prefer the local payload unless an error makes it unviable.
* `100`: profit maximization; choose whichever pays more.
* `2**64 - 1`: prefer the bid unless an error or health check makes it unviable.
example: "100"
builders:
type: array
maxItems: 64 # MAX_BUILDER_ENTRIES
description: |
The builders this key sources bids from, replacing any the validator client is configured
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"

BuilderEntry:
type: object
description: |
One builder a validator public key may source blocks from.

No two entries may share both their `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`.

See [produceBlockV4](https://ethereum.github.io/beacon-APIs/#/Validator/produceBlockV4) for the
selection rule.
required: [url]
properties:
url:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this configuration seems to be fully tailored towards builders connected via builder-api, based on the original intend from ethereum/beacon-APIs#620, this doesn't seem to match up, I left a related comment here ethereum/beacon-APIs#625 (comment)

we also lose functionality we currently have, eg. the builder_boost_factor query param as defined in the beacon-api (on master branch) allows to specify how local payload vs. bids (from api or p2p) should be selected, but with this change, we have 0 ways to configure any bid selection strategy from p2p

maybe @potuz, @terence, or @james-prysm can clarify this, I thought this follows the prysm implementation but it doesn't at all match what potuz documented in ethereum/beacon-APIs#620 which I do prefer since it at least supported the full spectrum of builders, and not just the builder-api

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe we lose current functionality - we still can prefer local vs bids (from api or p2p) bids using builder_boost_factor right? The same way we do today.

It's just that the api builders as currently proposed enjoy per-builder configurability too, whereas for p2p values (min_bid, builder_boost_factor) apply globally.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes this comment is quite outdated, we've added a global builder_boost_factor and min_bid now which can be used to control p2p bids

it's possible to prefer local payload by configuring { "builders": [], "builder_boost_factor": "0" } and you can even configure local only (ie. not accept any bid even if local fails or is delayed) by setting the following { "builders": [], "min_bid": "18446744073709551615" }

type: string
format: uri
minLength: 1
maxLength: 2048 # MAX_BUILDER_URL_SIZE
description: |
The URL this entry's requests are sent to.
example: "https://builder.example.com"
auth_data:
Comment thread
nflaig marked this conversation as resolved.
type: string
pattern: "^0x(?:[a-fA-F0-9]{2}){1,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 `RequestAuth.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.
Comment on lines +86 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be simpler/better to just treat this auth_data field as optional? I assume custom auth data will not be set in the vast majority of cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thinking was if the key manager API required fully resolved fields, it simplified everything downstream since the request auth could be required by beacon api -> builder api and remove the need to reason about the cases where it's absent. If everyone used the default auth data they could just supply auth_data = builder_url as part of the keymanager call.

some motivation was talking with @james-prysm here where it was hard to reason about how to resolve configs when VCs have different defaults + keymanager fields were all optional

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does auth_data need to be required in the config though? we could still make it required in the call to the beacon node and default it to url if it's not explicitly set

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it isn't in the config, how could a node operator set their auth_data if they aren't using the default? I assumed it would have to be by manually inserting into the config which is what this PR helps make programmatic

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

operators can still set the auth_data if they wanna override the default (based on the url), but why does it need to be a required field to be configured by the user?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It definitely doesn't need to be required. I'm happy to make it and all of the fields that can as optional. It was just simpler if the key manager API supplied the VC with fully resolved values derived from the caller's config, so we don't have to reason about how missing fields in the key manager request body have to be resolved differently across all the different VC implementations

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do see your point but most users are just fine with the default that validator clients provide and each client will have their own defaults, eg. we currently don't force users to provide a gas limit in their config, and most don't and just go with whatever the client sets as default. so the vc can easily fill missing fields with default and supply all fields to the beacon node via api. but when it comes to configuring the validator client from a user perspective, I would prefer if users only have to configure the values they are interested in instead of having to provide the full set of options

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From your comment here, if we use builder_pubkey + the absence of a url as the way to tell the BN "use these config values when evaluating p2p bids from this builder" then I think it makes sense to switch all of these keymanager fields to optional, since you need to show the absence of something.

I'm pretty in favor of this. Not too sure who will want to set per-key, per-builder p2p bid settings but it is elegant that we can support both p2p bids / builder api bids symmetrically. (will build towards this if it sounds good)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we use builder_pubkey + the absence of a url as the way to tell the BN "use these config values when evaluating p2p bids from this builder"

if we want the mutually exclusive it could even be a config field like builder_id, which could be a url, or a pubkey, or even a builder index. but it's probably better to have separate fields, also when transmitting this to the beacon node, it would be good if we can support ssz encoding, and union or optional isn't widely supported and not part of standard spec, but for missing pubkey we can just set zero bytes for example

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed separate fields over builder_id since url and pubkey are complementary rather than strictly alternatives. In Terence's PoC the url is where you send and the pubkey is what you check came back, and a union can only carry one of those.

What I'm thinking is optional fields in keymanager and VC has to fully resolve before sending to BN. The one thing is we'll still need per-key min_bid and builder_boost_factor that apply to p2p bids from builders with no entry. From Luca's comment:

All per-key configurability in Vero is handled through the Keymanager API which is standardized across all CL/VC clients. This is the only way Vero allows node operators to specify certain things, like different fee recipient per validator key. I hate the idea of adding support for some kind of YAML file (that hasn't even been standardized) instead of doing this in a more standard way using the Keymanager API. We should seriously stop with these CL-client-specific YAML files, we have the standard Keymanager API for a reason.

So thinking to also add a way for the keymanager api to express the per-key p2p defaults.


Omitting it leaves the validator client to set it to the UTF-8 bytes of the entry's
`url` verbatim, since the builder requires authentication on every bid request. A
zero-length `auth_data` is invalid.
example: "0x68747470733a2f2f6275696c6465722e6578616d706c652e636f6d"
builder_pubkeys:
type: array
maxItems: 64 # MAX_BUILDER_PUBKEYS
description: |
The builder BLS public keys this entry accepts bids from. Empty or omitted accepts any
builder; otherwise a bid not signed by one of them MUST NOT be accepted.
items:
allOf:
- $ref: "../keymanager-oapi.yaml#/components/schemas/Pubkey"
- description: "A builder's BLS public key. _48-bytes, hex encoded with 0x prefix, case insensitive._"
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 other builder bids and the
local payload. Below `100` disfavors this builder, above `100` favors it.

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"
5 changes: 5 additions & 0 deletions wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ VPN
BLS
oapi
Vero
Gwei
uri
UTF
RequestAuth
produceBlockV
Loading