Skip to content

ePBS (EIP-7732) Support - #94

Open
shane-moore wants to merge 82 commits into
ssvlabs:mainfrom
shane-moore:feat/epbs
Open

ePBS (EIP-7732) Support#94
shane-moore wants to merge 82 commits into
ssvlabs:mainfrom
shane-moore:feat/epbs

Conversation

@shane-moore

Copy link
Copy Markdown
Collaborator

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.Index propagation through BeaconVote, the new PTC committee duty, the produceBlockV4 proposer flow (self-build vs external-builder variants), and the new SignedProposerPreferences broadcast.

@iurii-ssv iurii-ssv left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
@shane-moore

Copy link
Copy Markdown
Collaborator Author

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 😃

Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
@diegomrsantos

Copy link
Copy Markdown
Collaborator

I opened ethereum/EIPs#11684 to update the EIP-7732 Gloas summary against the current consensus-specs Gloas files:

ethereum/EIPs#11684

Since this SIP depends on those EIP-7732 details, I would appreciate review there as well.

@shane-moore

Copy link
Copy Markdown
Collaborator Author

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

the current top-level order mirrors upstream's grouping in gloas/validator.md: Attestation → Sync → Block proposal (with Broadcasting SignedProposerPreferences nested) → PTC. §1 Slot Timing already gives the temporal table. That said, I don't feel strongly here; happy to reorder timeline-first if SSV prefers.

…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>
@MatheusFranco99

Copy link
Copy Markdown
Contributor

@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 bid.block_hash pins exactly one payload, so there's nothing left to agree on. Running QBFT here just slows us down for something that is equivalent to the successful leader operator broadcasting the blinded envelope and the committee doing a threshold signature over it — the validation the others can do (BeaconBlockRoot matches the decided block, BuilderIndex == BUILDER_INDEX_SELF_BUILD) is the same in both designs, and PayloadRoot is unverifiable by them either way.

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 BeaconBlockRoot matches the decided root, from whoever has it. That's another argument that the primitive we need is "receive matching data + threshold-sign", not consensus.

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.

@shane-moore

Copy link
Copy Markdown
Collaborator Author

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

  • The one-consensus / fold direction was raised by @iurii-ssv in r3342402815 and set aside. On the extended-runner variant specifically: the proposer runner already uses both of its signing phases (RANDAO pre-consensus, block post-consensus), so an envelope phase needs its own carrier and partial-sig kind either way, and the duty trigger is already a continuation of the proposer flow.
  • Your leader-broadcast + threshold-signature design is Iurii's no-QBFT proposal, spec'd in full in r3361905725 and its gist: same dissemination-then-threshold-sign flow, same validation checks, plus the forgery analysis. He went with the QBFT variant for the current implementation (r3472349666).
  • The leader-rotation cost you describe was part of that discussion, not an oversight: see my r3364710775 (the envelope round's first leader may not be the block proposer; the miss is bounded, non-slashable, and self-build-only), and Gal called QBFT the correct call for now with timeout reductions as the way forward (r3460315536). The QBFT choice is a deliberate trade: accept a bounded miss in a narrow path rather than add a new top-level SSV message class that every client must adopt and keep byte-compatible. One correction to your framing: the block QBFT is also capped at round cut-off 2, so the "decided in round 3+" case cannot arise; the real exposure is only a block decided in round 2.

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 🤝

iurii-ssv added a commit to ssvlabs/ssv that referenced this pull request Aug 26, 2026
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 added a commit to ssvlabs/ssv-spec that referenced this pull request Aug 26, 2026
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)
@shane-moore

Copy link
Copy Markdown
Collaborator Author

@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 RoleProposerPreferences with RequestAuthPartialSig = 9, one single-entry packet per distinct auth root, per-root collection and reconstruction into a per-proposal-slot cache, the 8-entry builder-config sub-cap bounding a separate 8-root §7 budget, byte-identical entries across all n operators, and both consumption channels (produce body plus the optional ahead-of-time submit). Constants match your ssv-spec#632 branch: DomainBuilderRequestAuth = 0x0B000001, RequestAuthPartialSig = 9.

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.
iurii-ssv added a commit to ssvlabs/ssv that referenced this pull request Sep 2, 2026
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 iurii-ssv left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The updated local block building LGTM, left a couple suggestions to polish/finalize it below.

Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
shane-moore and others added 4 commits September 3, 2026 15:56
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
Comment thread sips/epbs_support.md

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

Suggested change
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.

iurii-ssv added a commit to ssvlabs/ssv that referenced this pull request Sep 7, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants