ePBS (EIP-7732) Support - #94
Conversation
iurii-ssv
left a comment
There was a problem hiding this comment.
Appreciate this effort! Doing first pass, just started looking into Gloas, forgive AI-heavy commentary (seems relevant though).
Nit: would be nice to list/organize the duties affected/added by the actual slot timeline (eg. "Proposer Preferences Duty", should come first, then "Modified Proposer Duty", then "Modified Attestation Duty", etc.)
thanks for taking a look! will resolve all the comments this week 😃 |
|
I opened ethereum/EIPs#11684 to update the EIP-7732 Gloas summary against the current consensus-specs Gloas files: Since this SIP depends on those EIP-7732 details, I would appreciate review there as well. |
the current top-level order mirrors upstream's grouping in |
…erences Pin updated from f1371480c4 to upstream master HEAD following PR review feedback from iurii-ssv and diegomrsantos. Net changes in the Proposer Preferences section: ProposerPreferences now carries dependent_root, bid handshake matches on (proposal_slot, dependent_root), gossip rule is first-valid-per-tuple, new Security Considerations entry on too-early publication. PTC paragraph also tightened to distinguish PAYLOAD_ATTESTATION_DUE_BPS from PAYLOAD_DUE_BPS. Slot Timing, Attestation Duty, and Proposer Duty sections unchanged at target pin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@shane-moore On §6, I don't get why we need a consensus step and I'm not sure about its benefits as well. The value has already been decided in the previous duty: the §4 block carries the signed bid, and QBFT actually makes it worse in one concrete case: the leader rotation. Only the operator whose BN built the decided block can propose an admissible envelope, but if the §4 block was decided in round 2, the envelope duty's round-1 leader is an operator that can't even propose — a guaranteed dead round inside the tightest window in the SIP, and with the round cut-off of 2, a block decided in round 3+ can never reach its only viable leader. While I strongly suggest this design, I think we should acknowledge that this introduces a new pattern in SSV: state shared across duties. The envelope duty needs outputs of the §4 duty to operate — the decided block root to validate against, and (in the leader-broadcast framing) the expected data. Though note that it doesn't actually matter who proposed the §4 block (though we can and it's good to validate the operator ID who created and signed the msg): the committee just needs to receive, via gossip, an envelope whose Taking the questioning one step further: why isn't this an extended proposer duty instead of a second duty? When we're building the block locally, the SSV duty simply wouldn't stop at publishing the signed block — the operators continue into reviewing the envelope data and signing it as an extra phase of the same runner. When the block is externally built, the duty terminates at post-consensus as it does today. That would make the cross-duty state internal to one runner instead of a new coordination surface between two. |
|
@MatheusFranco99 great thoughts, appreciate you pressure-testing §6! Good news is this exact axis got a full round in June, linking so we can build on it rather than re-derive:
TLDR: my position is that growing the SSV protocol surface with a new message class isn't justified by the self-build case, which should be rare on mainnet, especially with p2p bids as the real fallback for off-protocol bids. Curious where you land with that context in hand 🤝 |
Node-side surface for Gloas (ePBS) Payload Timeliness Committee duties, per SIP ssvlabs/SIPs#94: - types/gloas: PayloadAttestationData (42B SSZ) and PayloadAttestationMessage (146B SSZ), plus JSON-only PTCDuty, matching the beacon-APIs Gloas schemas; SSZ round-trip and golden JSON wire-format tests. - goclient: hand-rolled HTTP for the three PTC endpoints (get duties, produce data, submit messages). go-eth2-client has no Gloas provider yet, so these are direct requests until it is rebased. - beacon: standalone PTCCalls interface (folded into BeaconNode with the PTC runner later) plus regenerated MockPTCCalls; GoClient conformance asserted in tests.
Per SIP ssvlabs/SIPs#94, add the wire constants for the new ePBS duties: - domains DomainBeaconBuilder (0x0B), DomainPTCAttester (0x0C), DomainProposerPreferences (0x0D) - beacon + runner roles for PTC attester, proposer preferences, and envelope builder (7/8/9), wired through MapDutyToRunnerRole and the String() methods - partial-sig types PTCAttesterPartialSig (7) and ProposerPreferencesPartialSig (8)
|
@iurii-ssv The head commit (afb5284) specifies the builder request auth extension, and it is essentially your design record from ssvlabs/ssv#2962 adopted into the SIP: rides |
A fresh-eyes review found §6 and §7 disagreeing on what first-valid means: §6 said operators store the first dissemination passing the decision-binding checks, while §7 records the first structurally valid message and classifies the binding checks as runner concerns, making §6's reading unreachable. §6 now matches §7: the first §7-valid dissemination is stored, and the runner abstains if the stored envelope fails its own decision bindings. Also makes the §7 decode rule explicit about the inner blinded container (undecodable bytes must not consume the slot's dissemination budget), bounds it by the SSV message data cap rather than an undefined progressive-container maximum, and broadens the Security Considerations residual: any well-formed first dissemination from a Byzantine member costs the reveal, whether or not it passes the binding checks.
Node-side surface for Gloas (ePBS) Payload Timeliness Committee duties, per SIP ssvlabs/SIPs#94: - types/gloas: PayloadAttestationData (42B SSZ) and PayloadAttestationMessage (146B SSZ), plus JSON-only PTCDuty, matching the beacon-APIs Gloas schemas; SSZ round-trip and golden JSON wire-format tests. - goclient: hand-rolled HTTP for the three PTC endpoints (get duties, produce data, submit messages). go-eth2-client has no Gloas provider yet, so these are direct requests until it is rebased. - beacon: standalone PTCCalls interface (folded into BeaconNode with the PTC runner later) plus regenerated MockPTCCalls; GoClient conformance asserted in tests.
iurii-ssv
left a comment
There was a problem hiding this comment.
The updated local block building LGTM, left a couple suggestions to polish/finalize it below.
Anchor implemented this in sigp/anchor#1288 and validated it on a Gloas devnet, so the rule is known implementable before it is written down here. Section 7 keyed the dissemination dedup on (MessageID, slot), so the first structurally valid carrier was the only one an operator would forward or consider. One committee member broadcasting a well-formed but decision-unbound envelope ahead of the builder therefore made every operator abstain, costing the cluster its reveal. Dedup is now one per (MessageID, signer, slot), and section 6 signs the first candidate whose envelope satisfies the four bindings rather than the first that arrives, continuing to the payload-due cutoff. The previous commit reconciled section 6 to section 7 after a review found them disagreeing about what first-valid meant. That resolved the conflict in the wrong direction: section 6's original content-based reading was correct, and section 7's per-slot dedup was what made it unreachable. Two rules that were implicit are now stated, because an implementation that reads them differently is exploitable or unbounded. Recording must follow the signature check, or a forged carrier claiming another operator's identity consumes that operator's budget and suppresses its honest message. And forwarding is bounded by committee size, which holds only because the signature check is scoped to committee members. Security Considerations narrows accordingly: the binding-failing carrier is no longer a liveness risk, and what remains is a binding-passing forgery that differs only in the unchecked PayloadRoot, splitting operators across two roots by node-local arrival order. That case tolerates no Byzantine member when ExecutionRequests is empty, and the named hardening is now sign-all relative to the new rule rather than to first-valid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJP7vu2EGTFknFswZpjNjJ
Section 6 named a sole disseminator and publisher without saying how an operator learns it is that one. "The QBFT leader" does not work: after a round change the deciding leader re-proposes an earlier leader's block, so its beacon node did not build it. An operator is now the builder operator when the block in its own produce response carries the decided root, the local test section 4 already uses for the Eth-Builder-Url echo, which is round-change safe. The envelope now comes from the operator's own BlockContents, obtained by calling produceBlockV4 with include_payload=true, so no beacon-node call sits between the decision and the dissemination. The stateful branch is dropped: the envelope GET answers only on the node that built the block and the publish POST rejects a body produced elsewhere, so it forces node affinity on any operator with more than one beacon node. Publication is the Contents form to every beacon node, matching the section 4 block publish redundancy.
The previous commit deleted it, leaving a SHOULD on the inline produce variant with no alternative described, so a client that fetches the envelope from its beacon node had no path in the text at all. That is the common case today: a validator client can own the fetch and hand the envelope to the SSV layer to sign, which leaves the SSV side no way to choose the inline variant on its own. Both paths are described again, with the node affinity stated as the reason to prefer the inline one rather than as grounds for removing the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJP7vu2EGTFknFswZpjNjJ
The previous commit spelled out the beacon-node fetch as a separate allowed path, which SHOULD already grants. Keeping both said the same thing twice and read as an argument rather than a rule. The affinity cost now sits with the SHOULD as its justification, and the restatement is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJP7vu2EGTFknFswZpjNjJ
|
|
||
| ### A forged dissemination can cost a self-build reveal, never a wrong payload | ||
|
|
||
| A `BlindedExecutionPayloadEnvelope` is cheap to forge: the four bound fields are public once the [§4](#4-modified-proposer-duty) block is, and `PayloadRoot` is the one field the [§6](#6-new-duty-envelope-signing-self-build-path) checks cannot verify. Safety holds regardless: a publishable envelope needs the payload bytes behind its `PayloadRoot`, and finding a payload for a fabricated root is a hash preimage, so a forgery can never put a wrong payload on chain. The residual is liveness, and it narrowed to one case. Per-signer dedup and content-based selection ([§6](#6-new-duty-envelope-signing-self-build-path)) let operators skip a binding-failing carrier and sign the honest envelope behind it. A binding-passing forgery still splits them: it can differ from the honest envelope only in the unchecked `PayloadRoot`, selection follows arrival order, and arrival order is node-local, so the cluster can divide across two roots with neither reaching threshold and the slot's reveal missed. With empty `ExecutionRequests` every committee member can construct one, so this path tolerates no Byzantine member. The trigger is narrow (self-build path, an intentionally Byzantine committee operator) and the loss is the bounded, non-slashable miss above, so this SIP accepts it. A known hardening exists if the trade worsens: sign every admitted dissemination rather than only the first that binds, so the honest root always reaches threshold; safe because the envelope signature (`DOMAIN_BEACON_BUILDER`) appears in no Gloas slashing predicate. |
There was a problem hiding this comment.
Nit: the opening clause says all four bound fields are public, but the entry itself later relies on ExecutionRequests not being public unless empty ("With empty ExecutionRequests every committee member can construct one"). The bid carries only execution_requests_root, so before the reveal the contents are known to the builder operator alone, or to everyone when the block has no requests. Suggest:
| A `BlindedExecutionPayloadEnvelope` is cheap to forge: the four bound fields are public once the [§4](#4-modified-proposer-duty) block is, and `PayloadRoot` is the one field the [§6](#6-new-duty-envelope-signing-self-build-path) checks cannot verify. Safety holds regardless: a publishable envelope needs the payload bytes behind its `PayloadRoot`, and finding a payload for a fabricated root is a hash preimage, so a forgery can never put a wrong payload on chain. The residual is liveness, and it narrowed to one case. Per-signer dedup and content-based selection ([§6](#6-new-duty-envelope-signing-self-build-path)) let operators skip a binding-failing carrier and sign the honest envelope behind it. A binding-passing forgery still splits them: it can differ from the honest envelope only in the unchecked `PayloadRoot`, selection follows arrival order, and arrival order is node-local, so the cluster can divide across two roots with neither reaching threshold and the slot's reveal missed. With empty `ExecutionRequests` every committee member can construct one, so this path tolerates no Byzantine member. The trigger is narrow (self-build path, an intentionally Byzantine committee operator) and the loss is the bounded, non-slashable miss above, so this SIP accepts it. A known hardening exists if the trade worsens: sign every admitted dissemination rather than only the first that binds, so the honest root always reaches threshold; safe because the envelope signature (`DOMAIN_BEACON_BUILDER`) appears in no Gloas slashing predicate. | |
| A `BlindedExecutionPayloadEnvelope` is cheap to forge: three of the four bound fields are public once the [§4](#4-modified-proposer-duty) block is, the fourth (`ExecutionRequests`) is public only when the block carries none, since the bid holds just its root, and `PayloadRoot` is the one field the [§6](#6-new-duty-envelope-signing-self-build-path) checks cannot verify. Safety holds regardless: a publishable envelope needs the payload bytes behind its `PayloadRoot`, and finding a payload for a fabricated root is a hash preimage, so a forgery can never put a wrong payload on chain. The residual is liveness, and it narrowed to one case. Per-signer dedup and content-based selection ([§6](#6-new-duty-envelope-signing-self-build-path)) let operators skip a binding-failing carrier and sign the honest envelope behind it. A binding-passing forgery still splits them: it can differ from the honest envelope only in the unchecked `PayloadRoot`, selection follows arrival order, and arrival order is node-local, so the cluster can divide across two roots with neither reaching threshold and the slot's reveal missed. With empty `ExecutionRequests` every committee member can construct one, so this path tolerates no Byzantine member. The trigger is narrow (self-build path, an intentionally Byzantine committee operator) and the loss is the bounded, non-slashable miss above, so this SIP accepts it. A known hardening exists if the trade worsens: sign every admitted dissemination rather than only the first that binds, so the honest root always reaches threshold; safe because the envelope signature (`DOMAIN_BEACON_BUILDER`) appears in no Gloas slashing predicate. |
Node-side surface for Gloas (ePBS) Payload Timeliness Committee duties, per SIP ssvlabs/SIPs#94: - types/gloas: PayloadAttestationData (42B SSZ) and PayloadAttestationMessage (146B SSZ), plus JSON-only PTCDuty, matching the beacon-APIs Gloas schemas; SSZ round-trip and golden JSON wire-format tests. - goclient: hand-rolled HTTP for the three PTC endpoints (get duties, produce data, submit messages). go-eth2-client has no Gloas provider yet, so these are direct requests until it is rebased. - beacon: standalone PTCCalls interface (folded into BeaconNode with the PTC runner later) plus regenerated MockPTCCalls; GoClient conformance asserted in tests.
This SIP describes the ssv spec changes needed to keep operators performing validator duties correctly after ePBS (EIP-7732) lands in the consensus layer Gloas fork. Covers earlier slot deadlines,
AttestationData.Indexpropagation throughBeaconVote, the new PTC committee duty, theproduceBlockV4proposer flow (self-build vs external-builder variants), and the newSignedProposerPreferencesbroadcast.