Skip to content

Deprecate prior and add /validator/config endpoint#87

Open
JasonVranek wants to merge 4 commits into
ethereum:masterfrom
Commit-Boost:validator-config
Open

Deprecate prior and add /validator/config endpoint#87
JasonVranek wants to merge 4 commits into
ethereum:masterfrom
Commit-Boost:validator-config

Conversation

@JasonVranek

@JasonVranek JasonVranek commented Jul 9, 2026

Copy link
Copy Markdown

Add /eth/v1/validator/config: atomic per-key management of block-production preferences

Motivation

With ePBS, proposers can talk to builders directly instead of through relay lists configured in a sidecar. The validator client becomes the owner of a per-key builder list with per-builder constraints: which builders to request bids from, an optional expected bid signer, a trusted-payment cap (max_execution_payment), bid floors, boost factors, and an optional proxy to route requests through. The VC must own this list because it pre-signs the request authentications that bind each request to a builder's canonical URL.

Today that configuration only exists as static per-client config files (see the example in OffchainLabs/prysm#17124). Defining many builder entries across hundreds or thousands of keys in a static file is verbose and error-prone, and downstream software (e.g. eth-docker, dappnode, etc) would need to maintain a different config dialect per client. The keymanager API already solves this class of problem for fee recipient, gas limit and graffiti, but as three separate endpoint families, and the ePBS builder preference set is about to grow well past what per-field endpoints can sanely express.

What this PR does

  • Adds one endpoint, /eth/v1/validator/config (GET / POST), managing a validator's full block-production preferences (fee recipient, gas limit, graffiti, builder preferences) as one atomic per-key document.
  • Deprecates all 9 operations across the feerecipient, gas_limit and graffiti endpoint families. This endpoint absorbs their responsibilities and replaces three partial-write surfaces with a single consistent one.

The document model

Per key: ValidatorConfig = {fee_recipient?, target_gas_limit?, graffiti?, builder?}. The builder block holds enabled, the builders[] list, and per-key fallback values. Each BuilderEntry carries url (required; the signed identity), and optional pubkey, proxy, max_execution_payment, min_bid, builder_boost_factor.

Every field is optional. Resolution order is normative: BuilderEntry field, then the per-key BuilderConfig fallback, then the same two levels within default_config, then the client default. Key-level fields resolve document, then default_config, then client default.

Monetary values are Gwei as decimal strings. builder_boost_factor semantics (including the reserved 0 / 100 / 2**64 - 1 values) follow ethereum/beacon-APIs#625; max_execution_payment naming follows the builder-specs BuilderPreferencesRequestV1.

Example: GET /eth/v1/validator/config

Returns the configured fragments plus the read-only default_config, so tooling can distinguish an explicit per-key value from an inherited default (and can resolve effective values locally):

{
  "data": {
    "default_config": {
      "fee_recipient": "0x8943545177806ED17B9F23F0a21ee5948eCaa776",
      "target_gas_limit": "30000000",
      "graffiti": "example graffiti",
      "builder": {
        "enabled": true,
        "builders": [
          { "url": "https://builder-a.example.com" },
          { "url": "https://builder-b.example.com" }
        ],
        "max_execution_payment": "1000000000"
      }
    },
    "configs": {
      "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": {
        "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3",
        "target_gas_limit": "45000000",
        "builder": {
          "enabled": true,
          "builders": [
            {
              "url": "https://builder-a.example.com",
              "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
              "proxy": "http://side-car:9001",
              "max_execution_payment": "250000000",
              "min_bid": "10000000",
              "builder_boost_factor": "100"
            }
          ],
          "max_execution_payment": "500000000"
        }
      },
      "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c": {
        "builder": { "enabled": false }
      }
    }
  }
}

Example: POST /eth/v1/validator/config

Full-replace per key. The third key shows the delete idiom: an empty document clears the key back to defaults, so there is no separate DELETE operation.

{
  "configs": {
    "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": {
      "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3",
      "builder": {
        "enabled": true,
        "builders": [
          { "url": "https://builder-a.example.com", "max_execution_payment": "250000000" }
        ]
      }
    },
    "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c": {
      "builder": { "enabled": false }
    },
    "0xb0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7": {}
  }
}

Response, per-key statuses keyed by the same pubkeys:

{
  "data": {
    "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": { "status": "set" },
    "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c": {
      "status": "error",
      "message": "builder.builders[0].url is not a valid URL"
    },
    "0xb0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7": { "status": "set" }
  }
}

Key semantics

  • POST is full-replace per key, never a merge. An absent field is unset and inherits from default_config. An empty document {} clears the key entirely (this is the delete).
  • Keys not present in the request are untouched, so large key sets can be split across multiple requests but servers should be able to accept batches covering their full key set in one request (thousand-key batches are expected).
  • Atomicity is per key, not per batch. Each key's document applies fully or not at all; keys that validate successfully are applied even if others in the batch fail, with the outcome reported per key. Keys are independent in protocol terms, so batch transactionality would only make bulk reconciliation more fragile.
  • GET returns configured fragments, not resolved values. Reconciling tools need to know what they set; effective values are derivable client-side from default_config plus the normative resolution order.
  • Unknown fields are rejected everywhere (additionalProperties: false throughout): inside a document as a per-key error, in the envelope as a 400. A typo'd preference must fail loudly, not be silently ignored. default_config is read-only through this API by schema construction.

Relationship to other work

Notes for reviewers

  • default_config is deliberately read-only in this version: it is the minimal static floor a bare VC needs, and management tooling owns the per-key layer. Writable defaults can be added later without breaking changes.
  • Spec info.version is bumped to 2.0.0-dev to signal the breaking deprecation; happy to adjust to the repo's release conventions.

@james-prysm
james-prysm self-requested a review July 10, 2026 21:28
Comment thread apis/validator_config.yaml Outdated
Comment on lines +133 to +137
configs:
type: object
description: |
Mapping of validator public key (hex encoded with 0x prefix) to the full
`ValidatorConfig` document to set for that key.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Would it be possible this one to be the validator's index, rather than the public key?

In cases like DVs it's a bit better to have references of the index, rather than the public key, as the public keys the VC sees are partial keys, rather than the full key, as seen on the CL side. The current request which is with similar intent, POST /eth/v1/validator/prepare_beacon_proposer uses validator index. So my assumption is it won't be a big hurdle to use validator index here as well?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't have a strong opinion here but it seems like the key manager clients that normally deal with just public keys would require a BN query to resolve the mapping from pubkey -> validator index before making the call. It would be good to know if this change would negatively impact any other software.

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.

You could do index for active keys but for new additions you wouldn't have an index, so if you wanted to support indices you'd still need to support pubkey as well unless you want inconsistent apis i think...

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.

Operationally pubkeys make much more sense for this. Node operators can pre-configure keys in batches at any time, including before they become active. With indexes, operators would need to continuously monitor the validator statuses and update their configs one-by-one or in small batches, quite impractical and unnecessarily complex.

Can't DVs work around this the same way they have worked around the current Keymanager API endpoints that are also using the pubkey?

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 agree with paul here, I think it's preferable to use public key here

Comment thread apis/validator_config.yaml Outdated
type: string
description: |
- set: the document was validated and applied atomically
- not_found: the key is not known to the server

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.

we should permit configs for keys not found i think, its not clear that if this is saying it's not set

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.

we should allow for preprovisioning

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added pre-provisioning and as a consequence this requires using pubkeys and not validator indices

description: |
- set: the document was validated and applied atomically
- not_found: the key is not known to the server
- error: the document failed validation or could not be applied; no change

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.

what should we do for unknown fields, silently accept ( client specific) or error here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

removed additionalProperties: false for more leniency to match the rest of the keymanager API. Unrecognized fields should be ignored and invalid values for recognized fields should error

description: |
Sets the block-production preferences of one or more keys. For each key in `request.configs`,
the submitted document REPLACES the key's entire configured fragment: the server MUST NOT merge
it with previously configured values. A field absent from the submitted document is unset and

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.

a present builder object replaces default_config's builder entirely; inheritance applies only to absent top-level fields

right now prysm is looking at " BuilderEntry → BuilderConfig → default_config → client default" inheritance
in prysm it's whole object reading instead of field level reading . do we have to do inheritance by field level?

if builder config is set at the public key level then it replaces defaults in its entirety only if we are missing a field we fall back down the hierachy, not sure if others have feels about this

@JasonVranek JasonVranek Jul 20, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

BuilderEntry → BuilderConfig → default_config → client default

on the same page with this, I just tried disambiguating it in 8052f47

- keep pubkey as the config key (since no index exists for
  not-yet-active keys)
- pre-provisioning: a config may be set for an unmanaged key
- accept unrecognized fields to match the rest of the keymanager API
- disambiguate per-builder field resolution
@yorickdowne

Copy link
Copy Markdown

I am all for having this grab bag of options. Eth Docker may not configure every single one out of the gate; and, it's good to have it available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants