Skip to content

Add gloas builder flow: produceBlockV4, builder preferences, block forwarding - #630

Merged
nflaig merged 29 commits into
ethereum:masterfrom
JasonVranek:update-produceBlockV4
Aug 24, 2026
Merged

Add gloas builder flow: produceBlockV4, builder preferences, block forwarding#630
nflaig merged 29 commits into
ethereum:masterfrom
JasonVranek:update-produceBlockV4

Conversation

@JasonVranek

@JasonVranek JasonVranek commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The Beacon API side of the Gloas builder flow, aligned with ethereum/builder-specs#165 and ethereum/keymanager-APIs#88 so the three specs describe one proposer flow. Supersedes #625.

  • produceBlockV4 takes a required BuilderConfig body: a builders list with one BuilderEntry per builder-API bid request, plus a top-level min_bid and builder_boost_factor that apply to p2p bids. A bid is valued at value + min(execution_payment, max_execution_payment), surviving bids are weighted by their builder_boost_factor and compared without division, and the local build's value is weighted by 100.
  • submitBuilderPreferences (POST /eth/v1/validator/builder_preferences) lets the validator client push BuilderPreferencesEntry objects, batched across its proposers, so the beacon node can submit each builder's max_execution_payment cap ahead of the slot.
  • Block forwarding: produceBlockV4 returns Eth-Builder-Url when a builder-API bid wins; publishBlockV2 echoes it so the beacon node forwards the signed block to that builder via submitSignedBeaconBlock, letting it learn the result without waiting on gossip.
  • Both request bodies support JSON and SSZ and require Eth-Consensus-Version; builder failures are handled per entry so one unusable builder does not fail the block-production request or prevent other preference submissions.

Reference implementation: ChainSafe/lodestar#9832

Comment thread CHANGES.md Outdated
Comment thread apis/validator/block.v4.yaml Outdated
description: |
An optional proxy URL. When set, the beacon node sends the request to this URL instead of
to `url`, and sets the `Eth-Builder-Url` header to `url` so the proxy can forward the
request to the intended builder.

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.

can you explain what's the use case of this, what's the purpose of the proxy in this setup and why can't the CL call the builder directly?

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.

updated the language in f8d2943

An optional proxy URL for reaching this builder. Some deployments place a sidecar between
the beacon node and its builders; when proxy is set the beacon node sends the request to
this URL and sets the Eth-Builder-Url header to url so the sidecar can forward it to the
intended builder. When proxy is unset the beacon node contacts the builder directly at
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.

that doesn't really answer my question, what I wanna know is what does the proxy do in addition to the CL client in gloas when it comes to the PBS pipeline and why should be enshrine the concept of such a proxy into the protocol?

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 plan post-gloas is for the BN to do muxing and bid selection. If we assume those are the only roles the builder-facing layer will ever need, the proxy is unnecessary, but I don't think that assumption holds. PBS itself came from out-of-protocol experimentation, not bc the protocol predicted it.

The BN exposes a fixed bid selection policy using min_bid, max_execution_payment, builder_boost_factor (at least in this PR). Out-of-protocol services may have different policy reqs beyond that set, e.g. (acknowledging my bias) commit-boost lets a proposer make commitments, so the winning bid must also be commitment-honoring, which the BN knobs can't express but a proxy in the request path can.

These out-of-protocol experiments have a chicken and egg problem. To eventually become enshrined they have to run in the wild first, and if the only way to do that is forking a CL, it either kills the experimentation or incentivize forked clients. I'm advocating to keep the experimentation zone open (and ofc optional)

@nflaig nflaig Jul 25, 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.

PBS itself came from out-of-protocol experimentation, not bc the protocol predicted it.

well I don't think mev-boost should even exist as a sidecar, CL clients are already 90% there in terms of functionality since the merge, all we needed to add is multiplexing and bid selection

commit-boost lets a proposer make commitments

doesn't commit-boost need access to the bls keys of the proposer in that case anyways? so it could sign the auth request itself

but anyways, my concerns are addressed since we got rid of the concept of a proxy, while allowing such a "proxy" (whether that means relay or sidecar) to exist and work if operators wanna use it

james-prysm added a commit to OffchainLabs/prysm that referenced this pull request Jul 21, 2026
Comment thread apis/validator/block.v4.yaml Outdated
Comment thread apis/validator/block.v4.yaml Outdated
Comment thread apis/beacon/blocks/blocks.v2.yaml
Maximum trusted execution-layer payment, in Gwei, accepted from this builder. Bids
promising a trusted payment above this value MUST be rejected.
example: "1000000000"
min_bid:

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.

min_bid and builder_boost_factor are defined per entry, but they feed a single global decision (which bid wins the slot). How have you interpreted for this

Per-builder reading: each entry's values judge only that builder's bid.
Proposer-level reading: they express one overall policy — but then entries can disagree and a collapse rule is needed (first? max?).

hese pick different blocks from identical configs.

┌───────────────────────────────────────────────┬───────────────────────────┬───────────────────────────┬────────────────────┐
│                Interpretation                 │         A's test          │         B's test          │       Winner       │
├───────────────────────────────────────────────┼───────────────────────────┼───────────────────────────┼────────────────────┤
│ Per-builder                                   │ 90 × 100% = 90 < 100, out │ 60 × 200% = 120 > 100, in │ B (a 60-value bid) │
├───────────────────────────────────────────────┼───────────────────────────┼───────────────────────────┼────────────────────┤
│ Proposer-level, collapse = first entry (100%) │ 90 < 100, out             │ 60 < 100, out             │ local              │
├───────────────────────────────────────────────┼───────────────────────────┼───────────────────────────┼────────────────────┤
│ Proposer-level, collapse = max (200%)         │ 180 > 100, in             │ 120 > 100, in             │ A (best raw bid)   │
└───────────────────────────────────────────────┴───────────────────────────┴───────────────────────────┴────────────────────┘

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.

my understanding was the per-builder reading. the keymanager API describes the normative resolution order from BuilderEntry → BuilderConfig → default_config → client default per field so by the time we're at the bid selection process, the BN should have received all of the per-builder resolved preferences. So there shouldn't be a notion of a "global" min_bid at that point, but earlier upstream the VC could have effectively resolved a global min_bid from its default_config. still the comparisons should all be per-builder. in other words the 'collapse rule' isn't needed bc it should have collapsed upstream at the VC upon keymanager config

if that makes sense I can make that more explicit in the spec

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.

sounds good interested what other cls 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.

There's an important nuance here..

by the time we're at the bid selection process, the BN should have received all of the per-builder resolved preferences … the comparisons should all be per-builder

That holds for bids the BN pulls directly via getExecutionPayloadBid — each arrives together with its entry, so the BN can rank by the per-builder boosted value. But I don't think it holds for p2p (gossip) bids, and it's the table's headline example that breaks.

Gossip bids are pre-filtered by the consensus-specs gossip rule

[IGNORE] this bid is the highest value bid seen for the tuple (bid.slot, bid.parent_block_hash, bid.parent_block_root)

So a node retains only the single highest-value bid per tuple; lower bids are IGNOREd at validation and never cached. Now re-read the per-builder example assuming A and B are gossip bids for the same tuple:

A B
raw value 90 60
boost 100% 200%
boosted 90 120 → wins

The table says B (the 60-value bid) wins. But under the gossip rule, B was IGNOREd the moment A (value 90) was seen for that tuple — it's never in the cache, so per-builder boost (which is only known later at proposal time) can't resurrect it. In other words, builder_boost_factor and min_bid can't reorder among gossip bids, because the gossip layer has already collapsed them to one before selection runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes the boost is kinda irrelevant for p2p bids since they are filtered by nodes that don't know the proposer's value.

Comment thread apis/validator/builder_preferences.yaml Outdated
Comment thread apis/validator/block.v4.yaml
proxy routing

- BuilderEntry mirrors the keymanager config: url, auth,
  max_execution_payment, min_bid, and builder_boost_factor required,
  pubkey the sole optional verification pin; entries arrive fully
  resolved and the beacon node never substitutes defaults
- no uniqueness rule here: multiple entries may share a url (the
  keymanager enforces the pair rule at write time), one
  getExecutionPayloadBid call per entry, malformed entries ignored
- produceBlockV4 and submitBuilderPreferences accept SSZ request bodies:
  BuilderEntryV1 / BuilderPreferenceEntryV1 containers with
  MAX_BUILDER_ENTRIES (64) and MAX_BUILDER_URL_SIZE (2048) defined on
  the BuilderEntry type, all-zero pubkey means unset in either encoding,
  maxItems mirrors the SSZ cap, 415 for unsupported media types
- bid evaluation made precise: min_bid floors the bid's total payment
  (value plus execution_payment), max_execution_payment caps the trusted
  component, and builder_boost_factor applies to the total after both
  checks
- proxy fields and Eth-Builder-Proxy headers removed; the
  Eth-Builder-Url
  winner echo stays, with the stateless-setup rationale stated
- request_auth data matches the builder-specs twin (UTF-8 URL default,
  whole-byte hex pattern); builder-API path parameter renamed to
  proposer_pubkey
@nflaig nflaig added the Gloas api's needed in Gloas fork. label Jul 25, 2026
Comment thread apis/validator/block.v4.yaml Outdated
Comment thread apis/validator/block.v4.yaml Outdated
Comment thread apis/validator/block.v4.yaml Outdated
required: true
name: Eth-Consensus-Version
description: "The active consensus version to which the block being submitted belongs."
- name: Eth-Builder-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.

do we really need this header? if I understand correctly the sole purpose of this is so that the beacon node knows where to submit it via submitSignedBeaconBlock but the beacon node should know this anyways, since it got the bid from that builder via api

the only case where the beacon node doesn't know this is if it's a different node (eg. in a multi-node setup) if the primary node that sourced the bid from the builder via api went offline during the proposal flow but this seems like an edge case and even then, publishing only via p2p is a fine fallback behavior that is sufficient anyways most of the time

that said, we can keep the header if people wanna have it, just questionable to me if it's useful enough

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.

this was solely for the multi-node support, not opinionated on it but it seemed like DVT teams wanted. Will leave for now and address most of the fat trimming comments

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 not feel strongly about removing it, also I think when I commented there was in addition a Eth-Proxy-Url header which is now gone, so this single header, if useful for DVT teams, might be worth keeping

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.

+1 questioning the usefulness of this header, both here and in the produceBlockV4 response. I'd personally prefer removing it unless there is a good reason to keep it.

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.

after implementing it, the header makes it easier on the submission side on the beacon node, avoids having to implement a lookup cache to "remember" to which builder to submit the bid to, it also acts as a signal whether the beacon node should forward the beacon block to a builder

Comment thread types/gloas/builder_preference_entry.yaml Outdated
Comment thread apis/validator/block.v4.yaml Outdated

@nflaig nflaig left a comment

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.

did another quick pass, I feel like mostly this needs to be trimmed a lot, if details regarding bid selection are already part of the builder-specs there is no need to repeat that here, I don't really think the beacon-api should care about this at all, the builder-specs is not part of the required specs, so we should avoid leaking so much off protocol details into the spec here, especially when it comes to configs like max_execution_payment, min bid and boost factor we can consider keeping documented, similar to how we had it for builder_boost_factor query param

Comment thread apis/validator/block.v4.yaml Outdated
Comment thread apis/validator/block.v4.yaml Outdated
Comment thread apis/validator/block.v4.yaml Outdated
Comment thread types/gloas/request_auth.yaml Outdated
Comment thread apis/beacon/blocks/blocks.v2.yaml Outdated
Comment thread dictionary.dic
Comment thread types/gloas/builder_preference_entry.yaml Outdated
Comment thread apis/validator/builder_preferences.yaml Outdated
selection

Reference the Builder API containers at a pinned commit with
anchors instead of restating them (will need to bump pin later);
BuilderEntryV1 has no Builder API definition, so it stays defined here
and now says why.

Bid selection: all candidates are compared in Gwei and the highest
boosted value wins.

Behaviour: the beacon node MAY consider a p2p bid rather than always
doing so; an entry the beacon node cannot use never fails the request;
each entry's auth.message.slot is the slot the request is for.

Also: revert the accidental dictionary.dic edit,
add the new endpoint and the Eth-Builder-Url header to CHANGES.md, and
document the builder-bid path in validator-flow.md.
Comment thread types/gloas/builder_entry.yaml Outdated
semantics

New Eth-Execution-Payload-Source response header (local, builder, p2p).
A self-built block with include_payload=false and a p2p bid win were
indistinguishable: both return a bare BeaconBlock with no
Eth-Builder-Url, but the validator publishes the envelope itself on the
first and must not on the second.

Defaults: default_min_bid and default_builder_boost_factor query
parameters cover bids that match no BuilderEntry. No default value
is asserted.

Bid selection is one rule over both channels: reject on min_bid or
max_execution_payment, boost every survivor, the highest boosted bid
competes with the local build, and the local build wins a tie.

Entries: url and builder_pubkey act independently, so the three legal
combinations are enumerated rather than left to inference.
Comment thread beacon-node-oapi.yaml Outdated
Comment on lines +582 to +583
`builder` when a builder-API bid won, and `p2p` when a p2p bid won. Required in response so
client can determine whether it must publish the execution payload envelope itself.

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.

Required in response so client can determine whether it must publish the execution payload envelope itself.

this shouldn't be required, you can check bid.builder_index === BUILDER_INDEX_SELF_BUILD to figure out if it's self-build and whether or not you need to publish the execution payload envelope yourself

is there another egde case I am missing? generally for the validator client it shouldn't matter from where the builder bid is sourced

aside that, I kinda like the idea of this header, it's useful metadata, so I am not really opposed to adding it, could be a separate pr though, I don't really see how it's related to this one

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.

will remove from this PR

`builder_pubkey` does two different jobs depending on whether the entry
has a `url`: on a url entry it filters the returned bid (a builder-API
bid not signed by it is rejected), on a url-less entry it names the
builder whose p2p bids the entry applies to. The old rule gave an entry
one combined identity and forbade a `builder_pubkey` appearing twice,
which was wrong both ways: it rejected a builder bidding into two relays
(two entries naming it), and accepted two entries sharing a url and auth
but expecting different pubkeys, which produce identical requests.

Split into two scoped rules. An entry with a url is a bid request,
unique by (url, auth.data); several may share a url with different data.
An entry with no url supplies p2p policy, unique by builder_pubkey. p2p
policy now comes only from url-less entries, so p2p matching is scoped
to them and the requestBody note follows suit.
default_builder_boost_factor values, so drop the two query parameters.
james-prysm
james-prysm previously approved these changes Aug 20, 2026

@james-prysm james-prysm left a comment

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.

ship it

@KaloyanTanev KaloyanTanev left a comment

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.

All good on Obol's side, as long as a beacon node accepts a submission of a signed block with a builder URL different than what it provided as an unsigned payload earlier. And of course, utilises this new URL it has received upon submission.

Comment thread types/gloas/request_auth.yaml Outdated
Comment thread types/gloas/builder_entry.yaml Outdated
Comment on lines +46 to +55
- name: Eth-Builder-Url
in: header
required: false
description: |
The `url` of the winning builder, as returned by `POST /eth/v4/validator/blocks/{slot}`.
When that endpoint returns this header the validator client MUST echo it here, so the beacon
node can forward the signed block to the same builder and the builder can release the
payload without waiting for gossip. The echo is what lets a beacon node that did not serve
the block-production request still forward it. Omitted for a self-built block or a block won
by a p2p bid.

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 can be implementation specific but we should probably not blindly call any url that is provided here, otherwise this creates a potential SSRF path

this isn't that critical since generally the beacon api is not to be exposed publicly but there are providers like rescue node or in general rpc nodes that could be vulnerable to this if not handled correctly

cc @jshufro might be relevant for you

- update boost logic
- SSRF advisory
Comment thread wordlist.txt Outdated
Comment thread wordlist.txt Outdated
@JasonVranek JasonVranek changed the title Update Beacon API for parity with Builder API and KeyManager API Add gloas builder flow: produceBlockV4, builder preferences, block forwarding Aug 24, 2026
the block-production request still forward it. Omitted for a self-built block or a block won
by a p2p bid.

The URL is untrusted input and a server-side request forgery (SSRF) risk: the beacon

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.

as a side note, lodestar will only forward urls that have previously been authenticated by a proposer, I expect even in DVT setups that either produceBlockV4 or submitBuilderPreferences has authenticated the url beforehand on all beacon nodes

Comment thread types/gloas/builder_preferences_entry.yaml Outdated
Comment thread types/gloas/builder_entry.yaml Outdated
Comment thread types/gloas/builder_entry.yaml Outdated
Co-authored-by: Nico Flaig <nflaig@protonmail.com>

@nflaig nflaig left a comment

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.

LGTM

@nflaig
nflaig merged commit 159622d into ethereum:master Aug 24, 2026
3 checks passed
@nflaig nflaig mentioned this pull request Aug 24, 2026
eth2353 added a commit to eth2353/beacon-APIs that referenced this pull request Aug 24, 2026
iurii-ssv added a commit to ssvlabs/ssv that referenced this pull request Aug 27, 2026
produceBlockV4 became POST-only with a required BuilderConfig body in
ethereum/beacon-APIs#630 (merged 2026-08-24); Teku already dropped the GET.
The node sent a bare GET whenever no direct builder was configured — always,
today — so Gloas block production fails against any spec-current beacon node
(only Lighthouse/Lodestar/Prysm still keep the GET, hiding it for now).

Always POST the BuilderConfig body: the direct-builder overlay when
configured, else a neutral local-build config (empty builders,
builder_boost_factor 100 — SSV's neutral, so p2p bids compete at par with the
local build). The per-node GET fallback stays for pre-#630 nodes, only on a
404/405. Docs and comments describing the old "unconfigured -> GET" behavior
are updated to match.

Reported in #3002; completes the POST-first flip deferred in this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gloas api's needed in Gloas fork.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants