Skip to content

Add a POST produceBlockV4 - #625

Open
potuz wants to merge 1 commit into
ethereum:masterfrom
potuz:builder_preferences
Open

Add a POST produceBlockV4#625
potuz wants to merge 1 commit into
ethereum:masterfrom
potuz:builder_preferences

Conversation

@potuz

@potuz potuz commented Jul 8, 2026

Copy link
Copy Markdown

This PR adds a POST version of produceBlockV4 in which the validator sends a list of BuilderPreferences objects. These object specify a per-builder list of preferences that helps the beacon node chose the right payload bid contained in the block to be produced. This solves issue #620.

@potuz
potuz force-pushed the builder_preferences branch from c28b8b8 to ca2cbed Compare July 8, 2026 16:04
Comment thread apis/validator/block.v4.yaml Outdated
This PR adds a POST version of produceBlockV4 in which the validator
sends a list of `BuilderPreferences` objects. These object specify a
per-builder list of preferences that helps the beacon node chose the
right payload bid contained in the block to be produced. This solves
issue ethereum#620.
@nflaig nflaig added the Gloas api's needed in Gloas fork. label Jul 8, 2026
@potuz
potuz force-pushed the builder_preferences branch from ca2cbed to fea32e4 Compare July 8, 2026 18:12
@eth2353

eth2353 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

I was unable to make ACDC earlier today but here is some feedback from my different points of view.


Vero validator client maintainer:

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.

Setting these various per-key per-builder preferences via the Keymanager API presents some challenges but I'd still prefer solving those over the YAML files.

Switching CL/VC clients should be easy and different YAML configurations make that unnecessarily hard, whereas the Keymanager API makes it trivial.


Staking-as-a-service provider / node operator:

We don't expect to use the advanced functionality described in #620 and I also expect 99% of other node operators not to.

In practice, I expect the vast majority of node operators will be running a much simpler configuration similar to one of these:

A) trustless P2P bids only (no CLI flags, this could be the default clients ship with)
B) trustless P2P bids + specified builder URLs (--builder-urls=http://... , similar to how relays URLs are provided today)
C) specified builder URLs only - I expect entities that censor transactions may want to use this kind of config (--builder-urls=... + perhaps something like --disable-p2p-bids ?). I don't think they'd be using a blocklist of builders for this purpose since it could easily be circumvented by non-censoring builders.

Will these 3 configurations actually cover 99% of the node operators? I don't know. But if they do, we should really avoid overcomplicating things.


I think we should consider making the 1% of super-advanced node operators that MAY want to run a ton of different builder configurations on different subsets of their keys to simply run a corresponding number of validator client instances with different configurations. If they need more than a few, that might require a bit of automation, but so would populating a YAML file, so I don't think that makes things significantly more difficult for them. Whereas supporting these per-key per-builder configuration settings does add a considerable amount of maintenance burden/tech debt across all CL/VC codebases.

Hope this makes sense, ePBS itself is honestly quite complex on its own already so I may have overlooked something.

@JasonVranek

Copy link
Copy Markdown

Agree with @eth2353 on the Keymanager API point, and tried to implement here ethereum/keymanager-APIs/pull/87 with those specs, per-key block-production preferences (fee recipient, gas limit, graffiti, builder list and constraints) as one atomic document per key. The schema follows the BuilderEntry shape from OffchainLabs/prysm#17124 which I think captures the real use cases well (e.g. the optional proxy endpoint, with the builder URL staying the signed identity), and adopts the preference semantics from this PR, including builder_boost_factor and its reserved values.

I understand this PR is for VC to BN but since the Keymanager API sits upstream (user/tool to VC) it needs to carry a superset of what the beacon api will use. So standardizing both makes sense + agreed we dont want to have to support N client-specific YAML dialects.

ofc the big open question is which knobs are truly per-builder, and which can be per-key only? e.g., max_execution_payment makes sense as per-builder but probably min_bid is more suitable as per-key. In the Keymanager PR I just defaulted to maximal flexibility for now.

@shane-moore

Copy link
Copy Markdown
Contributor

Cross-repository sync note from the SSV side: I raised a RequestAuthV1 construction and signing issue in builder-specs #165. Since this PR copies that type, could we make the following corresponding changes here?

I think builder-specs should remain normative for construction and signing. This PR only needs to preserve the resulting signed object across the Validator API boundary:

  1. In types/gloas/request_auth.yaml, specify that data contains the canonical ASCII bytes of the builder identity URL. These are the ByteList contents used by the signed SSZ object and are represented as hex in JSON. Also specify that slot is the proposal slot the authorization applies to, not the slot when it is signed or sent.
  2. In types/builder_preferences.yaml, require the beacon node to forward signed_request_auth without changing its decoded message or signature. In particular, it must not canonicalize or reconstruct message.data.

@potuz

potuz commented Jul 15, 2026

Copy link
Copy Markdown
Author

I think builder-specs should remain normative for construction and signing. This PR only needs to preserve the resulting signed object across the Validator API boundary:

Agree, will update (if this PR is the one adopted) when the builder API is stabilized, but I think the Auth data message should not be specified to be the URL but rather arbitrary data that can be negotiated off-band with the builder. Some builders would want to see here an URL, some others something more generic... there is no need to specify this data to be anything in particular.

@JasonVranek

Copy link
Copy Markdown

[...] I think the Auth data message should not be specified to be the URL but rather arbitrary data that can be negotiated off-band with the builder. Some builders would want to see here an URL, some others something more generic... there is no need to specify this data to be anything in particular.

Agreed with @shane-moore the canonicalizing should happen as the first step, not after a RequestAuthV1 was made. Also agree with @potuz that opaque auth data bytes is more expressive / future proof. An alt proposal is to drop all notions of "canonicalization" and just use auth data bytes as-is. 

Cross referencing to the ethereum/keymanager-APIs/pull/87 PR, a builder entry could look like this, which is unambiguous for VC/BN/ DVT setups etc.

{
  "url": "https://builder-a.example.com",
  "auth_data": "0x123123123....", // new
  "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
  "proxy": "http://side-car:9001",
  "max_execution_payment": "250000000",
  "min_bid": "10000000",
  "builder_boost_factor": "100"
}

The wrinkle is that before, a proxy could extract the builder url from the auth data to route. Since the SignedRequestAuth wouldn't be required to carry this anymore, either:

  1. the type could include the destination builder url (ugly)
  2. submitBuilderPreferences and getExecutionPayloadBid callers add an optional Eth-Builder-Url​ header. Note since auth_data is bilaterally agreed upon, an unsigned routing header is safe since a mis-routed request fails bc of the wrong auth_data.

wdyt about 2?

@shane-moore

Copy link
Copy Markdown
Contributor

From the SSV/DVT side, option 2 works. The same builder-provided auth_data for a validator-builder relationship would need to be configured across all operators so they construct the same RequestAuthV1 signing root.

The builder URL and proxy destination can remain unsigned routing metadata. If Eth-Builder-Url misroutes the request, the receiving builder should reject it because the exact signed auth_data does not match what it negotiated. So I do not think SSV requires the URL to be included in RequestAuthV1.

@JasonVranek

Copy link
Copy Markdown

Both PRs updated following this convention:

@michaelsproul

michaelsproul commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

A) trustless P2P bids only (no CLI flags, this could be the default clients ship with)
B) trustless P2P bids + specified builder URLs (--builder-urls=http://... , similar to how relays URLs are provided today)
C) specified builder URLs only - I expect entities that censor transactions may want to use this kind of config (--builder-urls=... + perhaps something like --disable-p2p-bids ?). I don't think they'd be using a blocklist of builders for this purpose since it could easily be circumvented by non-censoring builders.

Will these 3 configurations actually cover 99% of the node operators? I don't know. But if they do, we should really avoid overcomplicating things.

Don't we need a POST for use-cases B and C? We need to include the SignedRequestAuthV1, even if no other builder preferences are configured.

This thread on Eth R&D: https://discord.com/channels/595666850260713488/874767108809031740/1522112101714362448

@iurii-ssv

iurii-ssv commented Jul 16, 2026

Copy link
Copy Markdown

Following up on @shane-moore's earlier note from the SSV side — we reviewed the convention this thread converged on, now implemented in builder-specs #165 (opaque pre-agreed data signed exactly as serialized, unsigned Eth-Builder-Url routing, byte-preserving forwarding): LGTM, implementable by SSV without protocol headaches — and of the two models discussed, we prefer this one over deriving data from a canonicalized builder URL.

The reason for that preference: reconstructing a threshold signature requires byte-identical messages from every operator either way, so URL canonicalization only helps if every validator implementation and every builder normalize URLs bit-identically — a silent cross-implementation failure mode. Opaque bytes signed as-is turn it into plain config discipline: distribute the exact per-builder data bytes to all operators, done.

With data a pure function of static config and slot the proposal slot, all operators in an SSV cluster can independently construct the identical RequestAuthV1 at duty-discovery time (thanks to proposer lookahead), threshold-sign it ahead of the slot, and attach the reconstructed signature at proposal time. For us this is one new signing duty, same shape as validator registrations today but without the timestamp-determinism workarounds.

Putting on record the properties this relies on, so future revisions keep them:

  1. RequestAuthV1 contains no per-signer or time-of-signing fields (no pubkey, timestamp, or nonce) — N operators must be able to sign the identical message independently.
  2. Signing and verification operate on the message exactly as serialized, under the pinned DOMAIN_REQUEST_AUTH; no hop (VC, BN, proxy, builder) transforms message.data.
  3. The beacon node forwards signed_request_auth byte-for-byte unchanged — 👍 to mirroring the forwarding MUST from builder-specs Clarify until_epoch is exclusive #165 in types/builder_preferences.yaml when this PR syncs, as discussed above.
  4. The empty-url default entry ("one config applied to all validators") remains a fully supported mode — SSV clusters have no per-validator channel for builder preferences, so cluster-level defaults are how we'd use this API.

One suggestion: it would help to RECOMMEND a default value for data for the case where nothing was agreed out of band — the simplest being the builder's URL exactly as advertised. In practice most proposers will just copy builder URLs from a public list without ever talking to the builder (like relay lists today), and they need to know what bytes to sign. With a spec-suggested default, that plug-and-play case keeps working out of the box, while any builder and proposer that do have a relationship can still agree on whatever custom value they like.

One question: for the ahead-of-time channel (submitBuilderPreferences, which gates max_execution_payment) — is the expectation that VCs call builders directly in the epoch prior to proposing, possibly via the Eth-Builder-Url proxy pattern, or is a BN-mediated endpoint planned? This PR covers the just-in-time attach at produceBlockV4 only. Either works for us; we'd just like to plan the integration.

@chong-he chong-he 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.

Left a small comment below.

On a side note, this is about replacing the current GET to POST endpoint. Maybe we can rename the title a bit? Something like "replace GET with POST"

@@ -0,0 +1,63 @@
BuilderPreferences:

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 wonder should we put this file under the directory types/gloas/builder_preferences.yaml rather than types/builder_preferences.yaml? As BuilderPreferences is a Gloas thing

@JasonVranek

Copy link
Copy Markdown

4. The empty-url default entry ("one config applied to all validators")

ethereum/builder-specs@31712da adds clarity that the builder URL SHOULD be used as the default interpretation of the auth data bytes

authenticate the request. If omitted, requests to this builder are sent without
authentication.
url:
type: string

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.

Assuming this BuilderPreferences here is the same as the BuilderPreferencesV1 that is tentatively defined in the current builder-spec.. if we make this change, how are we supposed to ssz serialize / deserialize a BuilderPreferences object which now contains a field of type string?

@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.

@potuz this PR doesn't solve #620, see my comments (also cc @JasonVranek I left the comments here as this is easier to review than your PR, I think the points apply to both PRs though)

node MUST forward to the builder when requesting a bid so that the builder can
authenticate the request. If omitted, requests to this builder are sent without
authentication.
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 PR deviates quite a lot from what you mentioned in #620

so first of all the BuilderPreferences as defined here do not support p2p builders at all, the builder_id concept is missing from this PR. Can you explain why we no longer wanna support this?

Comment on lines +12 to +13
external builders. The `builder_boost_factor` is expressed per builder in the request body
rather than as a query parameter.

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

The builder_boost_factor is expressed per builder

today we use this parameter as a global flag (applied to all builders) to let the operator decide their block production strategy

eg. you can set builder_boost_factor=0 which basically says "always pick local unless the EL fails to produce the payload", or builder_boost_factor=90 which says "pick local payload if builder payload(s) are only marginally more valuable"

having builder_boost_factor and also min_bid expressed per builder works with directly connected builders via api, but how does that work with p2p builders?

I think it would be good to have a way to globally apply builder boost factor and min bid

in #620 there is the following statement

The default BuilderPreferences can be sent explicitly as a default to the endpoint or just be assigned to the builder_id being the max uint.

this seems to suggest a way to do this? I don't see it expressed in this PR though

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Definitely an oversight on my side since I based 630 on 625.

To match the current conventions in beacon 630 / builder 165 / keymanager 88, p2p bids would be configurable per-key, per-builder like: "validator X accepts p2p bids from builder Y only above this min_bid, weighted by this builder_boost_factor." I'm personally not sure that's worth it. p2p bids are entirely trustless payments so a Gwei from one p2p builder is the same as a Gwei from another, and there's no trust dimension left to express per builder. The thing you might genuinely want to differentiate on is censorship but you can't read that off a bid. If the node operator's goal is to "blacklist" specific p2p builders, the builders could always get around by rotating keys. Also most builders worth configuring individually would likely also be reachable over the builder API, where the full knob set already applies.

I'm more partial to per-key p2p preferences. The pragmatic version is to bring back something like the builder_boost_factor query param on produceBlockV4 and treat these as per-key p2p knobs. Then either VCs hold them in their own config as a global, or we extend setBuilders here to carry them per key.

Curious what others think.

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

"validator X accepts p2p bids from builder Y only above this min_bid, weighted by this builder_boost_factor." I'm personally not sure that's worth it

yes I don't think it's worth it but neither is that worth it on the builder-api, this is not used today and will not be useful after gloas, builder boost factor and also min-bid is very hard to configure even just for local vs. builder payload, having this per builder doesn't seem very useful, but I rather implement that at this point and trying to debate it. But I don't see why we don't wanna support the full spectrum, all that seems to be required is add a builder_pubkey field, and make url optional and if configured the beacon-node reaches out to the builder via api too + enforcing same policy to the bids of that builder on p2p

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Following this thread, I updated #630 and #88 to offer the same config knobs for p2p bids. tl;dr is including builder_pubkey and excluding url means your BuilderEntry supplies min_bid and builder_boost_factor constraints on the p2p bids coming from builder_pubkey so the BN can apply them when determining the winning bid. Keymanager then needed to change to allow defaults to be configured.

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.

was thinking about this more and getting more convinced about your point of not needing per-BuilderEntry for p2p builders, maybe that also keeps the configuration simpler for users if we only allow global builder_boost_factor and min_bid to affect p2p bids while for directly connected builders we keep the per-builder configs. So since you made changes to support global parameters now, we might be fine with those only and can avoid overloading the usage of builder_pubkey.

one of the reasons is also the following p2p rule

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

since builder_boost_factor and min_bid are local only, it would be kinda random based on how bids are propagated, so even if you boost a certain builder's bid by 100x, it might not even reach your node due to the rule above, so it might be better from a user perspective to see p2p bids as essentially a single (the highest value) bid as coming from a single builder (not sure that framing makes sense).

The use cases outlined by potuz in #620 sound pretty complete to me already, so if we can cover those which I think we can now since we added the global parameters, then we are kinda getting close to the final design we want.

image

@JasonVranek maybe you can double check this

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