Skip to content

Gloas: external builder API (payload bid selection and block production) - #9757

Open
ethDreamer wants to merge 13 commits into
sigp:unstablefrom
ethDreamer:gloas-builder-api
Open

Gloas: external builder API (payload bid selection and block production)#9757
ethDreamer wants to merge 13 commits into
sigp:unstablefrom
ethDreamer:gloas-builder-api

Conversation

@ethDreamer

@ethDreamer ethDreamer commented Aug 6, 2026

Copy link
Copy Markdown
Member

Issue Addressed

Closes #9590.

Adds support for the Gloas (ePBS) external builder API: the flow by which a proposer solicits
execution payload bids from off-protocol builders, ranks them against its local build and
gossip-relayed bids, commits to a winner, and forwards the signed block to the winning builder so it
reveals the execution payload envelope.

Implements the relevant parts of:

  • beacon-APIs #630produceBlockV4, builder-preferences endpoint, Eth-Builder-Url
  • builder-specs #165getExecutionPayloadBid, submitBuilderPreferences, submitSignedBeaconBlock, request auth
  • consensus-specs Gloas p2p / state transition for ExecutionPayloadBid

Proposed Changes

A Gloas proposer now considers three payload sources — its local EL build, gossip bids, and bids
fetched directly from configured builders — selects the most profitable eligible one, and (for a
direct builder) forwards the signed block back for envelope reveal.

The branch is organized as a bottom-up, dependency-ordered stack of commits so it can be reviewed one
layer at a time:

  1. consensus/types — the builder request-auth signing domain
    (compute_domain(DOMAIN_REQUEST_AUTH)) for authenticating Gloas builder-API requests.
  2. builder_types — shared SSZ/JSON wire types (builder config & entries, proposer preferences,
    request auth, builder URL).
  3. eth2 — beacon-node HTTP client methods for produceBlockV4, submitting builder preferences,
    and the Eth-Builder-Url response/echo header.
  4. builder_client — the Gloas Builder API HTTP client and a stateless Builders service that
    fans getExecutionPayloadBid / submitBuilderPreferences / submitSignedBeaconBlock out across
    a proposer's configured builders. The pre-Gloas relay client is relocated here.
  5. execution_layer — adopt the relocated pre-Gloas client; drop dead error variants.
  6. beacon_chain (verification) — direct-builder bid verification and gossip-bid refinements,
    sharing the state-dependent consistency checks.
  7. beacon_chain (selection + production) — a unified BidCandidate /
    BidSource { Local, Gossip, Direct } model and select_payload_bid, wired into Gloas block
    production. All bid value math lives on BidCandidate, computed on demand: ranking is the trusted
    value scaled by builder_boost_factor, in wei, so the local EL block value compares directly and
    builder_boost_factor == u64::MAX ("always prefer") is a plain multiply. min_bid is a ranking
    tier — a below-floor bid wins when it's the only option (e.g. the local build failed) but loses to
    any floor-clearing candidate. Direct-bid BLS verification runs on a blocking thread (off the async
    executor), and gossip bids are re-validated against the production state at selection time so a bid
    that has gone stale since gossip verification can't win selection and then fail block processing.
  8. client — construct the Builders service (honoring --builder-user-agent /
    --builder-disable-ssz) and wire it into the chain.
  9. network — gossip validation and peer scoring for payload bids and proposer preferences.
  10. http_apiproduceBlockV4, POST /eth/v1/validator/builder_preferences, and the
    Eth-Builder-Url publish round-trip. The winning builder's URL travels back to the VC as a
    response header and is echoed on publish, so forwarding the signed block works even when the
    publishing beacon node isn't the one that produced it.
  11. validator_client (signing) — sign builder request-auth and preferences.
  12. validator_client (config) — builder configuration store + docs.
  13. validator_client (services) — the builder-preferences submission service and v4 block
    production / publish.

Additional Info

Best reviewed commit-by-commit: the commits form a bottom-up dependency layering (types → wire types
→ clients → beacon_chain → node / network / API → validator_client), each a coherent per-crate
change.

@ethDreamer
ethDreamer requested a review from jxs as a code owner August 6, 2026 02:03
@ethDreamer ethDreamer added ready-for-review The code is ready for review gloas labels Aug 6, 2026
@mergify

mergify Bot commented Aug 7, 2026

Copy link
Copy Markdown

Some required checks have failed. Could you please take a look @ethDreamer? 🙏

@mergify mergify Bot added waiting-on-author The reviewer has suggested changes and awaits thier implementation. ready-for-review The code is ready for review and removed ready-for-review The code is ready for review waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Aug 7, 2026
@eserilev

eserilev commented Aug 9, 2026

Copy link
Copy Markdown
Member

thanks for breaking it up into commits like this! im wondering if you would be willing to convert this into stacked PRs?

We did this here: #9764

I think it makes reviewing big PRs much easier and hopefully more maintainable for the author. Lmk what you think

Comment thread beacon_node/http_api/src/publish_blocks.rs
```yaml
# Global bid policy: applies to p2p (gossip) bids, and is the default for any
# builder below that omits the corresponding field.
min_bid: 0 # gwei — reject any bid whose total payment is below this

@dapplion dapplion Aug 10, 2026

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.

🤖 automated (dapplion's agent): min_bid ranks as a tier. It does not reject. A bid below the floor still wins when it is the only candidate. The behavior is correct. This line and line 49 are wrong.

.and(warp::path("validator"))
.and(warp::path("builder_preferences"))
.and(warp::path::end())
.and(warp::header::<ForkName>(CONSENSUS_VERSION_HEADER))

@dapplion dapplion Aug 10, 2026

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.

🤖 automated (dapplion's agent): This makes Eth-Consensus-Version required, but the comment above says the body is not fork-versioned and uses no such header. post_validator_blocks_v4 omits it. One of the two must change.

/// viable option — the local build failed and every bid is under the floor — instead of missing
/// the slot. Whenever *any* candidate clears the floor (the local build always does), the
/// below-floor ones lose regardless of value, exactly as a hard filter would.
fn rank_key(&self) -> (bool, bool, Uint256, bool) {

@dapplion dapplion Aug 10, 2026

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.

🤖 automated (dapplion's agent): builder_boost_factor == u64::MAX no longer means always prefer the builder. A plain multiply ranks a zero-value bid at 0, so any non-zero local build wins. This differs from the pre-Gloas contract. Document it.

.unwrap_or_else(|| definition.url.to_default_auth_data());
// Omit any builder we cannot sign for, logging the error, rather than failing the
// whole config.
let auth = match sign(auth_data).await {

@dapplion dapplion Aug 10, 2026

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.

🤖 automated (dapplion's agent): This awaits one signature per builder, in sequence. On a cold cache this is up to 64 serial remote-signer round trips, on the proposal critical path. The signatures are independent, so use join_all.

match response {
Ok(Some(GloasBidResponse {
bid,
ssz_response: _,

@dapplion dapplion Aug 10, 2026

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.

🤖 automated (dapplion's agent): ssz_response is dropped here and forward_signed_block hardcodes false, so every submission is JSON. The doc comment on GloasBidResponse says the opposite. Thread it through, or delete the field.

direct.builder_boost_factor,
direct.max_execution_payment,
direct.min_bid,
direct.builder_url.expose_full().to_string(),

@dapplion dapplion Aug 10, 2026

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.

🤖 automated (dapplion's agent): expose_full() puts the full builder URL into a response header that travels BN to VC to BN in cleartext. An API key in the URL reaches every proxy log. Document that credentials belong in auth_data.

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

Labels

gloas ready-for-review The code is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants