Skip to content

ePBS/Gloas: external-builder authentication & per-builder bid preferences — design questions #2962

Description

@iurii-ssv

Type: design / tracking → implementation plan (upstream refs re-checked 2026-07-28)
Related: #2901 (node-side ePBS impl — these changes will be added there), ssvlabs/SIPs#94, ssvlabs/ssv-spec#632

Context

#2901 implements node-side ePBS §2–§6. For block production (§4) it uses the merged, GET produce endpoint
GET /eth/v4/validator/blocks/{slot}?…&include_payload=false (beacon-APIs#580) with no builder preferences and no builder authentication — external-builder selection is entirely beacon-node-internal, and SSV only observes the outcome via selfBuild() (BuilderIndex == BUILDER_INDEX_SELF_BUILD).

A newer, still-open spec line adds a validator→builder authentication handshake and per-builder bid preferences:

  • beacon-APIs#625 — replaces GET produceBlockV4 with a POST that carries a BuilderPreferences list in the body.
  • builder-specs — defines RequestAuthV1 / SignedRequestAuthV1 (the signed auth), BuilderPreferencesV1 (max_execution_payment) and the submitBuilderPreferences endpoint; builder-specs#165 clarifies slot/data/signing semantics.
  • keymanager-APIs#87 — /eth/v1/validator/config: the atomic per-key config document (BuilderEntry{url, auth_data, max_execution_payment, min_bid, builder_boost_factor, pubkey?}) regular VCs will manage this surface with. SSV doesn't serve the keymanager API, but D2 adopts its vocabulary.

None of this is in #2901 yet — and SIP-94's upstream watchlist anticipates exactly this work: "If adopted, update §4's endpoint and specify the distributed request-auth signing duty once builder-specs#165 settles the signed object and forwarding contract." This issue started as the design-question capture; as of 2026-07-28 most questions are settled upstream — resolutions are annotated inline below, and the remainder is phased in the implementation plan at the bottom, including the SIP-94 amendment that watchlist item calls for.

This is an opt-in, off-protocol overlay

Everything in this issue lives in beacon-APIs / builder-specs, not the consensus spec — by design: trusted/off-protocol payments and direct-builder auth are deliberately kept out of consensus and handled at the client/builder-API layer ("trusted payments don't need to be enshrined into consensus", per the ePBS trustless-payments discussion). The enshrined flow — p2p bids from staked builders, trustless on-chain payment, with local self-build as the always-available floor — is self-sufficient, and #2901 already produces valid Gloas proposals on it.

So this whole surface is an opt-in enhancement, not on the critical path for ePBS support: a cluster that never adopts it still proposes valid blocks — trustless bids or self-build — just without direct-builder connections or trusted (off-protocol) payments, and the enshrined path stays the fallback whenever the overlay fails or a builder is unavailable. Prioritize accordingly (phase-2 value capture).

Don't confuse the four "preferences" objects

Object Hop Fields Signed? Status in SSV
ProposerPreferences (consensus-spec) gossip topic fee_recipient, gas_limit, … yes (own domain) done — §5 in #2901 (deprecates ValidatorRegistrationV1)
BuilderPreferences (beacon-APIs#625) VC → beacon node signed_request_auth?, url, builder_boost_factor, max_trusted_bid, min_bid only the nested auth this issue (phase 2)
BuilderPreferencesV1 (builder-specs) VC → builder (submitBuilderPreferences) max_execution_payment (+ auth) only the nested auth this issue (phase 3)
BuilderEntry (keymanager-APIs#87) operator → VC (config, not wire) url, auth_data, max_execution_payment, min_bid, builder_boost_factor, pubkey? no this issue — D2's config vocabulary (phase 1)

Why the distributed-validator case is special

The only validator-signed artifact in this whole surface is SignedRequestAuthV1, and its signed pre-image is just RequestAuthV1{ data, slot }not the boost factor, bid caps, or max_execution_payment. The builder verifies that signature against the (group) validator_pubkey, so in SSV it must be a threshold BLS signature reconstructed from operator partials.

Consequences that shape every question below:

  • Reconstruction needs a quorum (2f+1; 3-of-4, 5-of-7, …) of partials over a byte-identical signing root. data is a ByteList, so any divergence changes the root and the partials won't combine.
  • The unsigned fields (builder_boost_factor, max_trusted_bid, min_bid, max_execution_payment, url) need no signing round — each operator's BN applies them locally — but they still want cluster-consistent config for deterministic QBFT and coherent economics.
  • The token is a public label (by default), not a secret or credential: it binds the signature to a builder+slot, but the BLS signature is the actual authenticator, so possession of data alone grants nothing. It can live in ordinary — even on-chain — config.

Design questions

A. The auth data token

A1. What kind of value is data?

  • Resolved (builder-specs#165): opaque bytes agreed out of band, signed exactly as serialized — decoupled from the URL, with a spec-recommended default when nothing was agreed: the UTF-8 bytes of the builder's advertised URL, exactly as advertised. The authenticity-not-confidentiality reading was confirmed (anti-DDoS/replay; bid/payload privacy is the separate commit-reveal + PTC path), so the token stays a public label in the default case; a builder that insists on minting a custom (even secret) value is now a spec-supported variant, and for SSV it's the same config-distribution problem either way (see A2/D2).

A2. How do all operators produce byte-identical data? (Hard requirement: data is hashed into the signing root, so any divergence breaks reconstruction.)

  • Resolved (builder-specs#165 + the Verify signed message with domain #625 thread): no canonicalization anywhere — the earlier canonical-byte-form ask is superseded. Byte-identity is achieved by distributing the exact per-builder data bytes to all operators as config (the position we took upstream: canonicalization would only help if every implementation normalized bit-identically — a silent cross-implementation failure mode; opaque bytes signed as-is turn it into plain config discipline), backed by byte-preserving forwarding MUSTs at every hop (VC → BN → builder). D2's on-chain single-source-of-truth remains a legitimate distribution channel for those exact bytes.

B. Threshold-signing mechanics

B1. When/where does the auth get signed, and by which runner?

  • Decided (SSV-side; see implementation plan): ride the §5 dispatcher — same RoleProposerPreferences, a new RequestAuthPartialSig partial-sig type (ssv-spec#632 extension). The §5 slot sub-runner freezes one RequestAuthV1{data, proposal_slot} per configured builder next to its preference, broadcasts one single-root partial per builder (preserving the §5 single-message wire rule), collects them in a separate partial-sig container, and on quorum reconstructs into a per-proposal-slot auth cache consumed by §4 (phase 2) and the AOT submit (phase 3). Rationale: the cadence is inherently identical (proposer-lookahead-driven, epoch-ahead, reorg/indices-change rechecks), so riding §5 reuses the handler's emission machinery and the dispatcher's stash/replay + re-emission idempotency wholesale, with zero new scheduler, role, queue, or tracer surface and only +1 wire constant for Anchor to match; a dedicated role would duplicate all of that for no cadence or semantic gain. Isolation is kept by construction: the §5 duty outcome stays preference-defined, auth partials live in their own container, and auth failures surface via metrics (reconstruction + E1 consumption), never in the §5 path. This is also the SIP-94-conformant shape: single-entry packets keep §7's non-committee one-entry rule (REJECT above 1) intact, and the §7 validation-table row for RoleProposerPreferences gains a second admitted partial-sig type with its own distinct-root budget — a SIP amendment, not a new role row (the §5 cap of 4 distinct preference roots is SIP-normative and stays).

B2. What is the exact signing root/domain, and what wire plumbing does it need?

  • Resolved: DOMAIN_REQUEST_AUTH = DomainType('0x0B000001') is pinned on builder-specs master; signing/verification is compute_signing_root(RequestAuthV1, compute_domain(DOMAIN_REQUEST_AUTH)) — genesis-style like DOMAIN_APPLICATION_BUILDER (network GENESIS_FORK_VERSION, zero genesis-validators-root), and the object is not fork-versioned. Plumbing: extend ssv-spec#632 with DomainRequestAuth + RequestAuthPartialSig = PartialSigMsgType(9); ekm/ssvsigner get a DomainRequestAuth arm beside DomainApplicationBuilder (non-slashable signing path). ⚠️ 0x0B000001 is one byte away from the beacon domain 0x0B000000 (DOMAIN_BEACON_BUILDER, SIP-94 §6) — keep the loud comment. Also unlike DOMAIN_PROPOSER_PREFERENCES — fork-versioned at the proposal slot per SIP-94 §5 — this one is genesis-style and fork-agnostic, so the ekm arm must not reuse the fork-versioned signing path.

B3. What slot goes in RequestAuthV1.slot?

  • Resolved (builder-specs#165): the proposal slot, explicitly — "not the slot at which the request is signed or sent". Add version during build and expose a CLI #165 also spells out that proposer duties are known an epoch ahead so the auth can be pre-signed off the hot path, and that one auth covers both channels (data is no longer endpoint-tied): submitBuilderPreferences rejects auths whose slot has already passed (anti-replay/rollback), getExecutionPayloadBid requires slot == the proposal-slot path param.

B4. Quorum & the sub-quorum failure mode.

  • Updated (builder-specs#165): the auth body is now required on getExecutionPayloadBid (400 without it) — the "unauthenticated bid" degrade tier no longer exists. Sub-quorum on a builder's auth ⇒ that builder is simply not contactable for the slot; the fallback is the enshrined floor (gossip bids / self-build), and the proposal is never blocked on the overlay. The all-n requirement stands: every operator holds the token config (not just a bare quorum) so f fault-tolerance is preserved on the auth path too.

C. API adoption

C1. produceBlockV4 GET → POST (beacon-APIs#625). §4 currently uses GET. Adopting the POST means constructing the BuilderPreferences list (one entry per configured builder + optional empty-url default) and embedding cached auths.

C2. submitBuilderPreferences (builder-specs).

  • Candidate: yes, phase 3; reuse the same cached auth (same {data, slot}) as the bid request.
  • Update: Add version during build and expose a CLI #165 dropped explicit proxy support (Jul 22), implying the AOT submit is a direct VC→builder call; our upstream question (direct vs BN-mediated) is still unanswered — re-ping. Who submits (SSV): proposedall operators redundantly (matches the §4/§6 publish pattern; idempotent at the builder keyed by the group pubkey), safe given required-identical max_execution_payment (D1/D2).

C3. Transport.

D. Non-signed config & governance

D1. Selection knobs (builder_boost_factor, max_trusted_bid, min_bid) and max_execution_payment are cluster policy — a shared risk/reward decision.

  • Candidate: cluster-level config, required consistent across operators for deterministic QBFT and coherent economics, even though signing doesn't force it.
  • Finding (code-checked): divergence is consensus-safe — the proposer value-check (proposerChecker.CheckValue in protocol/v2/ssv/value_check.go) validates only role/validator/slot + slashing, never the knobs or the bid, so non-leaders ratify the leader's block regardless of their own config. Divergence doesn't stall consensus; it makes the effective policy non-deterministic ("whoever leads"), and max_execution_payment resolves last-write-wins at the builder (C2). "Required consistent" is thus for determinism/economics, not safety — contingent on the current lenient check: if a future Gloas value-check starts validating bids against these knobs, divergence flips to a consensus-stall risk, so tightening that check must be paired with enforced config consistency.
  • Update (builder-specs#165): the builder-side default flipped — without stored preferences a builder MAY pay anything; the proposer's locally configured per-builder caps are the backstop (applied at bid selection). This raises the stakes on cluster-consistent caps but doesn't change the safety analysis above.

D2. Config surface & distribution.

  • Decided (phase 1): cluster-level operator config adopting keymanager-APIs#87's BuilderEntry vocabulary (url, auth_data, max_execution_payment, min_bid, builder_boost_factor, optional pubkey), entries identified by (url, auth_data), auth_data defaulting to the URL bytes when omitted; documented as required-identical across all n operators (B4), with a shared entry cap (MaxBuilderEntries) that also bounds the message-validation budget.
  • Finding: since the token is non-secret in the default case (A1), an on-chain / contract-derived value remains a legitimate single-source-of-truth for the exact data bytes — the strongest long-term answer to A2's byte-identity requirement; revisit after phase 1.

E. Observability / fallback

E1. Distinguish self-build causes. selfBuild() today can't tell "self-build because economics" from "self-build because auth/quorum failed."

  • Candidate (phase 1 stages it): widen the §4 build-source telemetry from bool to a reason (no-bid / economics / auth-unavailable); the auth dimension wires in with the POST migration (phase 2), fed by the per-slot auth cache ("valid auth available for builder X @ slot S?"). Silent degradation is the main risk — a cluster can propose fine while quietly never reaching its preferred builder.

Load-bearing assumptions (revisit if these change)

  • Proposal-slot semantics (B3)resolved, explicit in Add version during build and expose a CLI #165.
  • Signing domain/root (B2)resolved, 0x0B000001 pinned on master.
  • Builders accept a proposer-derived data (A1/A2)resolved, amended: opaque pre-agreed bytes with URL-bytes default; a builder minting its own (even secret) token is now a supported variant and reduces to the same config-distribution discipline.
  • Value-check stays lenient (D1). Unchanged: divergent unsigned knobs are consensus-safe only while the proposer value-check ignores them; tightening it turns divergence into a stall risk.
  • Verify signed message with domain #625 lands with body transport (C1/C3). Still open; additionally its auth-optionality wording must sync to Add version during build and expose a CLI #165's required-auth stance.
  • Add version during build and expose a CLI #165's required-auth stance survives to merge (B4). If it flips back to optional, the degrade path regains an unauthenticated tier — harmless for us either way.
  • Auth-availability is observable (E1). Unchanged; the reason enum is staged in phase 1 so selfBuild() stops being a bare bool.

Implementation plan (SSV)

All phases land on epbs-gloas (bundled into #2901). Wire constants go through ssv-spec#632 (the established spectypes pattern; ekm/ssvsigner reach signing domains only via spectypes).

Phase 1 — spec-stable, implementable now:

  1. ssv-spec#632 extension: DomainRequestAuth = 0x0b000001 + RequestAuthPartialSig = PartialSigMsgType(9) + golden tests.
  2. Wire types: RequestAuthV1{data ByteList[4096], slot} / SignedRequestAuthV1 in protocol/v2/types/gloas (SSZ + JSON), round-trip/HTR tests with builder-specs' examples/gloas/signed_request_auth.json as fixture.
  3. Cluster builder config (D2): operator-YAML section in keymanager#87 vocabulary; parse/validate (auth_data defaults to URL bytes; MaxBuilderEntries cap); docs/EXTERNAL_BUILDERS.md documents the all-n required-identical rule.
  4. ekm/ssvsigner: DomainRequestAuth arm beside DomainApplicationBuilder (genesis-style domain, non-slashable), local + remote key managers; Web3Signer gap recorded with the other Gloas domains.
  5. §5 dispatcher extension (B1): per-builder frozen RequestAuthV1s, one single-root RequestAuthPartialSig broadcast per builder, separate partial-sig container, reconstruct → per-proposal-slot auth cache (evicted with the sub-runners); dispatcher stash/replay generalized by msg type. Message validation: admit the new type under RoleProposerPreferences with its own distinct-root budget (= the entry cap MaxBuilderEntries = 8: auth roots are startup-config-derived and dependent_root-independent, so no in-window churn headroom exists and every extra admitted root would be pure attack surface for non-opted-in clusters) and the same future-slot allowance as §5.
  6. E1: build-source reason enum replacing the bool.
  7. SIP-94 amendment draft (fulfils the SIP's watchlist item; lands in SIPs#94 once builder-specs#165 settles): specify the distributed request-auth signing duty as a §5 extension — per-builder RequestAuthV1{data, proposal_slot} freezing, one single-entry packet per builder; §7 table row for role 8 admits RequestAuthPartialSig (9) with the auth root budget; the fork-gate rows are unchanged (auths carry post-fork proposal slots by construction); domain note (genesis-style compute_domain, not fork-versioned, 0x0B000001DOMAIN_BEACON_BUILDER 0x0B000000); new security consideration mirroring "config divergence silently disables trustless builder bids" — auth_data divergence silently disables direct-builder bids (fallback: enshrined floor).

Phase 2 — gated on #625 merge + C1 resolution: §4 produce GET→POST (beacon/goclient Gloas proposer path): body assembled from config + the auth cache (entries without a reconstructed auth omit signed_request_auth), GET fallback retained until BNs ship the POST; wire auth-unavailable into E1.

Phase 3 — gated on the C2 channel answer: direct SSV→builder submitBuilderPreferences client reusing the cached auth (epoch-prior window, same as §5 emission); all-operator redundant submit; per-builder failure metrics.

Testing: unit + golden vectors per phase (SSZ/HTR/domain against builder-specs examples); §5 message-validation suite extended for the second type; e2e deferred until a CL ships the #625 POST and a Gloas builder exists — devnet items tracked with #2920.

Out of scope (already in #2901)

§4 GET produce/publish + selfBuild() detection; §6 self-build execution-payload envelope duty; §5 ProposerPreferences gossip (fee-recipient/gas, ValidatorRegistrationV1 deprecation); §3 PTC; §2 Gloas attestations; fork gating/retiming.

References

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions