You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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)
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.
Candidate: gate behind Verify signed message with domain #625 merging; keep the GET path until then. Build the list from cluster builder config + the epoch-ahead auth cache.
Still open (gossip vs. "contacted"):Verify signed message with domain #625's BuilderPreferences are framed around builders that will be contacted — unclear how the knobs govern gossip-sourced enshrined bids for a trustless-only cluster; confirm before the GET→POST migration.
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): proposed — all 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.
✅ Resolved at the builder-specs level (Add version during build and expose a CLI #165): required SignedRequestAuthV1body, JSON or SSZ, noEth-Consensus-Version (the object is not fork-versioned), required Date-Milliseconds/X-Timeout-Ms headers on the bid request (BN→builder hop), proxy header dropped. The beacon-API (Verify signed message with domain #625) shape is concrete but pending merge.
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)
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.
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).
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.
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.
ekm/ssvsigner:DomainRequestAuth arm beside DomainApplicationBuilder (genesis-style domain, non-slashable), local + remote key managers; Web3Signer gap recorded with the other Gloas domains.
§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.
E1: build-source reason enum replacing the bool.
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, 0x0B000001 ≠ DOMAIN_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.
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 viaselfBuild()(BuilderIndex == BUILDER_INDEX_SELF_BUILD).A newer, still-open spec line adds a validator→builder authentication handshake and per-builder bid preferences:
BuilderPreferenceslist in the body.RequestAuthV1/SignedRequestAuthV1(the signed auth),BuilderPreferencesV1(max_execution_payment) and thesubmitBuilderPreferencesendpoint; builder-specs#165 clarifies slot/data/signing semantics./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
ProposerPreferences(consensus-spec)ValidatorRegistrationV1)BuilderPreferences(beacon-APIs#625)signed_request_auth?,url,builder_boost_factor,max_trusted_bid,min_bidBuilderPreferencesV1(builder-specs)submitBuilderPreferences)max_execution_payment(+auth)BuilderEntry(keymanager-APIs#87)url,auth_data,max_execution_payment,min_bid,builder_boost_factor,pubkey?Why the distributed-validator case is special
The only validator-signed artifact in this whole surface is
SignedRequestAuthV1, and its signed pre-image is justRequestAuthV1{ data, slot }— not the boost factor, bid caps, ormax_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:
2f+1; 3-of-4, 5-of-7, …) of partials over a byte-identical signing root.datais aByteList, so any divergence changes the root and the partials won't combine.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.dataalone grants nothing. It can live in ordinary — even on-chain — config.Design questions
A. The auth
datatokenA1. What kind of value is
data?A2. How do all operators produce byte-identical
data? (Hard requirement:datais hashed into the signing root, so any divergence breaks reconstruction.)databytes 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?
RoleProposerPreferences, a newRequestAuthPartialSigpartial-sig type (ssv-spec#632 extension). The §5 slot sub-runner freezes oneRequestAuthV1{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 forRoleProposerPreferencesgains 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?
DOMAIN_REQUEST_AUTH = DomainType('0x0B000001')is pinned on builder-specs master; signing/verification iscompute_signing_root(RequestAuthV1, compute_domain(DOMAIN_REQUEST_AUTH))— genesis-style likeDOMAIN_APPLICATION_BUILDER(networkGENESIS_FORK_VERSION, zero genesis-validators-root), and the object is not fork-versioned. Plumbing: extend ssv-spec#632 withDomainRequestAuth+RequestAuthPartialSig = PartialSigMsgType(9); ekm/ssvsigner get aDomainRequestAutharm besideDomainApplicationBuilder(non-slashable signing path).0x0B000001is one byte away from the beacon domain0x0B000000(DOMAIN_BEACON_BUILDER, SIP-94 §6) — keep the loud comment. Also unlikeDOMAIN_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?datais no longer endpoint-tied):submitBuilderPreferencesrejects auths whose slot has already passed (anti-replay/rollback),getExecutionPayloadBidrequires slot == the proposal-slot path param.B4. Quorum & the sub-quorum failure mode.
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-nrequirement stands: every operator holds the token config (not just a bare quorum) soffault-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
BuilderPreferenceslist (one entry per configured builder + optional empty-urldefault) and embedding cached auths.BuilderPreferencesare framed around builders that will be contacted — unclear how the knobs govern gossip-sourced enshrined bids for a trustless-only cluster; confirm before the GET→POST migration.C2.
submitBuilderPreferences(builder-specs).{data, slot}) as the bid request.max_execution_payment(D1/D2).C3. Transport.
SignedRequestAuthV1body, JSON or SSZ, noEth-Consensus-Version(the object is not fork-versioned), requiredDate-Milliseconds/X-Timeout-Msheaders on the bid request (BN→builder hop), proxy header dropped. The beacon-API (Verify signed message with domain #625) shape is concrete but pending merge.D. Non-signed config & governance
D1. Selection knobs (
builder_boost_factor,max_trusted_bid,min_bid) andmax_execution_paymentare cluster policy — a shared risk/reward decision.proposerChecker.CheckValueinprotocol/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"), andmax_execution_paymentresolves 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.D2. Config surface & distribution.
BuilderEntryvocabulary (url,auth_data,max_execution_payment,min_bid,builder_boost_factor, optionalpubkey), entries identified by(url, auth_data),auth_datadefaulting 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.databytes — 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."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,0x0B000001pinned on master.Builders accept a proposer-derived— 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.data(A1/A2)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 establishedspectypespattern; ekm/ssvsigner reach signing domains only viaspectypes).Phase 1 — spec-stable, implementable now:
DomainRequestAuth = 0x0b000001+RequestAuthPartialSig = PartialSigMsgType(9)+ golden tests.RequestAuthV1{data ByteList[4096], slot}/SignedRequestAuthV1inprotocol/v2/types/gloas(SSZ + JSON), round-trip/HTR tests with builder-specs'examples/gloas/signed_request_auth.jsonas fixture.auth_datadefaults to URL bytes;MaxBuilderEntriescap);docs/EXTERNAL_BUILDERS.mddocuments the all-nrequired-identical rule.DomainRequestAutharm besideDomainApplicationBuilder(genesis-style domain, non-slashable), local + remote key managers; Web3Signer gap recorded with the other Gloas domains.RequestAuthV1s, one single-rootRequestAuthPartialSigbroadcast 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 underRoleProposerPreferenceswith its own distinct-root budget (= the entry capMaxBuilderEntries = 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.RequestAuthV1{data, proposal_slot}freezing, one single-entry packet per builder; §7 table row for role 8 admitsRequestAuthPartialSig (9)with the auth root budget; the fork-gate rows are unchanged (auths carry post-fork proposal slots by construction); domain note (genesis-stylecompute_domain, not fork-versioned,0x0B000001≠DOMAIN_BEACON_BUILDER 0x0B000000); new security consideration mirroring "config divergence silently disables trustless builder bids" —auth_datadivergence silently disables direct-builder bids (fallback: enshrined floor).Phase 2 — gated on #625 merge + C1 resolution: §4 produce GET→POST (
beacon/goclientGloas proposer path): body assembled from config + the auth cache (entries without a reconstructed auth omitsigned_request_auth), GET fallback retained until BNs ship the POST; wireauth-unavailableinto E1.Phase 3 — gated on the C2 channel answer: direct SSV→builder
submitBuilderPreferencesclient 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; §5ProposerPreferencesgossip (fee-recipient/gas,ValidatorRegistrationV1deprecation); §3 PTC; §2 Gloas attestations; fork gating/retiming.References
BuilderPreferences), #580 (merged Gloas produce), #620 (motivation)DOMAIN_REQUEST_AUTH)/eth/v1/validator/config,BuilderEntry)