Skip to content

epbs builder api endpoints - #505

Open
JasonVranek wants to merge 6 commits into
mainfrom
epbs-pr1
Open

JasonVranek wants to merge 6 commits into
mainfrom
epbs-pr1

Conversation

@JasonVranek

@JasonVranek JasonVranek commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

This PR adds the core functionality for the three new builder-API endpoints and later PRs will extend it.

What it does

  • POST /eth/v1/builder/execution_payload_bid/{slot}/{parent_hash}/{parent_root}/{proposer_pubkey}: sends the request to the relay its auth data names and returns that relay's bid as 200, the builder's own 400 or 401, or 204 when it has no bid or fails (timeout, 5xx, unreachable, undecodable). Commit-Boost does not validate or rank the bid: the beacon node checks it against the builder registry and the key's builder config, and each request names exactly one builder.
  • POST /eth/v1/builder/builder_preferences/{proposer_pubkey}: forwards to the relay its auth data names and returns that builder's answer (202, or its 400/401).
  • POST /eth/v1/builder/beacon_blocks: forwards the SSZ block bytes unparsed to every configured relay and answers 202 when one accepts (only the winning builder does). A non-SSZ body is a 415.

Decisions

  • PBS routes are unchanged to remain backwards compatible through the fork
  • Routing to relays is done using auth data
  • For timing, Commit-Boost derives an absolute deadline from the beacon node's Date-Milliseconds and X-Timeout-Ms headers, and reserves proposer_deadline_buffer_ms (default 50) back for the return trip.

Left to later PRs

  • URL-form auth data, forwarding to builders outside the config, SSRF guard.
  • cb-km, a tool that writes validators' builder configs from the Commit-Boost config (including max_execution_payment with an explicit unclamped setting), and the config fields only it reads.
  • Bid streaming

Configuring it by hand

Until cb-km lands, each validator key's builder config is written through the validator client's keymanager API (keymanager-APIs PR88). For every builder, add an entry whose url is Commit-Boost's own URL and whose auth_data is the hex of the builder's hostname bytes (echo 0x$(printf builder-a.example.com | xxd -p | tr -d '\n')). A key without builder config sends Commit-Boost's own hostname, which matches no relay, so its bid requests get 400. The new docs/get_started/epbs.md page walks through it with a worked example.

Testing

  • Kurtosis devnets with the docs' manual config against the client releases Sepolia's Gloas upgrade uses: Lodestar v1.49.0, Prysm v7.2.0 and Lighthouse v8.3.0-rc.0 built from Commit-Boost's bid in 17 of 17 slots. Nimbus v26.10.0's validator client stores the config but its beacon node does not request bids; Teku 26.9.1 and Grandine 3.0.0-rc.0 do not implement the builder config endpoint yet.

Pin the lighthouse crates to sigp/lighthouse unstable at 31d8cfd, whose
gloas containers hash as EIP-7495 progressive containers, matching the
consensus specs. Mirror lighthouse's own [patch.crates-io] entries for the
progressive ssz stack (a consumer does not inherit a dependency's patches)
and pin the blstrs_plus patch to an explicit rev.

lighthouse dropped `TestRandom` for an `arbitrary` generator, so
`TestRandomSeed` now draws from `arbitrary::Arbitrary`. Validated BLS
points do not randomize under `arbitrary`, so tests that need a real key
use `BlsSecretKey::random()`. Also adapt to `ForkName::Heze` and to
`ExecutionRequests` no longer implementing `ssz::Decode`.
Tests discovered a free port, dropped the listener and let the server
rebind it, leaving a window where another process could take the port.
PbsService and SigningService gain `run_with_listener`, the mock SSV
servers take a bound listener, and the legacy suites hand their listeners
straight to the server. Also add `wait_for_ready`, which polls /status
instead of sleeping a fixed 100 ms.
From the Gloas fork the beacon node calls three builder-API endpoints on
Commit-Boost instead of get_header and get_payload:

- POST /eth/v1/builder/execution_payload_bid/{slot}/{parent_hash}/{parent_root}/{proposer_pubkey}
  sends the request to the one relay its auth data names and returns that
  relay's bid (200), its 400 or 401, or 204 when it has no bid or fails.
  Commit-Boost does not validate or rank the bid: the beacon node checks it,
  and each request names exactly one builder.
- POST /eth/v1/builder/builder_preferences/{proposer_pubkey} forwards to the
  relay the auth data names and returns that builder's answer.
- POST /eth/v1/builder/beacon_blocks forwards the SSZ block bytes unparsed to
  every configured relay and answers 202 when one accepts.

Requests must carry Eth-Consensus-Version: gloas. A body without
Content-Type is JSON and a request without Accept gets JSON, as
builder-specs requires. Bid requests need Date-Milliseconds and
X-Timeout-Ms; Commit-Boost clamps the deadline to one slot, keeps
proposer_deadline_buffer_ms back for the return trip and sends the rest to
the builder as its own X-Timeout-Ms, or returns 204 when nothing is left.

Routing follows builder-specs #168: auth data matches a relay when it equals
the lowercase hostname of the relay URL. The first match in config order wins
and unmatched auth data is a 400. Commit-Boost never verifies auth data; the
builder does. Relay-supplied amounts are saturated.

The new operator page (docs/get_started/epbs.md) covers writing each
validator key's builder config through the keymanager API, with a
copy-paste call, plus routing, timing, metrics and troubleshooting.
Unreleased, shipping from v0.12.0-rc1.
@JasonVranek
JasonVranek requested a review from a team October 6, 2026 04:53
Comment thread crates/pbs/src/utils.rs Outdated
Comment thread crates/pbs/src/routes/execution_payload_bid.rs Outdated
Comment thread crates/common/src/pbs/types/mod.rs Outdated
Comment thread crates/pbs/src/routes/execution_payload_bid.rs Outdated
Comment thread crates/pbs/src/routes/execution_payload_bid.rs Outdated
Comment thread crates/pbs/src/utils.rs Outdated
Comment thread crates/pbs/src/utils.rs Outdated
Comment thread crates/pbs/src/routes/execution_payload_bid.rs Outdated
Comment thread crates/pbs/src/routes/builder_preferences.rs Outdated
Comment thread crates/pbs/src/utils.rs
Comment on lines +34 to +55
pub(crate) async fn send_to_relay(
req: reqwest::RequestBuilder,
relay: &RelayClient,
tag: &str,
) -> Result<(reqwest::Response, Duration), PbsError> {
let start_request = Instant::now();
let res = match req.send().await {
Ok(res) => res,
Err(err) => {
RELAY_STATUS_CODE.with_label_values(&[TIMEOUT_ERROR_CODE_STR, tag, &relay.id]).inc();
return Err(err.into());
}
};

let request_latency = start_request.elapsed();
RELAY_LATENCY.with_label_values(&[tag, &relay.id]).observe(request_latency.as_secs_f64());

let code = res.status();
RELAY_STATUS_CODE.with_label_values(&[code.as_str(), tag, &relay.id]).inc();

Ok((res, request_latency))
}

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.

get_header and get_payload routes also do similar work, but inline
can be reused to remove duplicate code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

agreed but would rather leave PBS untouched so nothing can break and then deprecate PBS routes post fork / adopt this for /status

/// `/eth/v1/builder/execution_payload_bid/{slot}/{parent_hash}/{parent_root}/
/// {proposer_pubkey}`
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GetExecutionPayloadBidParams {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

the naming of this is super confusing, its a POST called Get.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

early on in the builder spec timeline, the BuilderRequestAuth was just a header, then some advocated for it to be part of the body

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SubmitBuilderPreferencesParams {
/// The public key of the proposer expressing these preferences
pub proposer_pubkey: BlsPublicKey,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

out of interest, why is the proposer pubkey specified as a path parameter and not a field on the request?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

also part of builder spec, i believe the reasoning was it was to remain as similar to the get_header path params

Comment thread crates/common/src/wire.rs Outdated
Comment thread crates/pbs/src/utils.rs Outdated
Comment thread crates/pbs/src/utils.rs
Err(PbsClientError::AuthDataMismatch)
}
}
}

@vladimir-ea vladimir-ea Oct 6, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this seems a bit fragile? if I understand correctly, the beacon node must encode into the auth data the URL of a relay that is configured for the proposer pubkey?

why do we have the requirement that the relay is configured in commit boost?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

the BN doesn't build the auth data, it just sends whatever the VC's config held and signed. The spec says if auth data isn't specified, VCs should default to using the url's lowercase hostname. So if any node operator mistakenly set's only url = cb.example.com but omits auth_data, then the VC default will set auth_data = cb.example.com and CB can't dial the request properly and must 400.

so there's a couple cases:

  1. url = cb.example.com and auth_data = builder.example.com -> CB dials correctly
  2. url = auth_data = cb.example.com -> CB 400s
  3. url = cb.example.com and auth_data = junk -> CB 400s

In a follow up PR we'll have the CB CLI tool to programmatically set auth_data correctly via KeyManager API to be in case 1. Since the CLI tool sources from CB's config resolve_addressed_relay would find it. That being said, I agree we don't need to be this restrictive. We may end up in case 1 if another tool is used to call KeyManager API or the user manually enters or something. Also in a followup PR we relaxed this to try to dial to what auth_data holds even if it's not known to CB's config with a guard for SSRF.


// Only the winner accepts, so one 202 across the broadcast is success
if accepted == 0 {
return Err(PbsClientError::NoBuilderResponse);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I am not entirely convinced relays will do anything with these requests - they have no particular interest in trusting what the proposer sends vs. observing what is gossiped on p2p - so we should be careful not to treat an apparent failure as an actual problem.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

the beacon blocks are signed, but if you're referring to relays/builders waiting long enough to know the block won't get reorged out then agree / agree with your posts on discord that it isn't particularly useful info to get back a success when your BN gossips it anyways

This branch has not been deployed

No deployments
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.

3 participants