Skip to content

ePBS (EIP-7732) Support#94

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

ePBS (EIP-7732) Support#94
shane-moore wants to merge 32 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.

Copy link
Copy Markdown

@iurii-ssv iurii-ssv left a comment

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>
Copy link
Copy Markdown

@iurii-ssv iurii-ssv left a comment

Choose a reason for hiding this comment

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

Second pass on e01800e. My first-round items are all resolved — thanks Shane.

Inline comments below cover the remaining structural issues. Summary by severity:

Blockers for v1 ship:

  • B1 (L180) — §4 BlockContents-path runner behavior unspecified
  • B2 (L136) — §3 PTC PartialSigMsgType not declared

Important:

  • I3 + I4 (L30) — Share.FeeRecipientAddress deprecated in SSV node; DefaultGasLimit differs between ssv-spec (30M) and node (36M)
  • I5 (L209) — §5 re-emission timing risk near proposal slot (§5 × §4 interaction)

Nits:

  • M2 (L100) — math.MaxUint64 sentinel rationale not explained
  • N1 (L182) — line-range link to GetBlockData() against main will drift
  • N2 (L157) — RunnerRole numbering gap choice not explained

Already in existing threads (no new inline needed):

  • I2 §3 fork-choice asymmetry — see my L138 reply. Linked gist updated to firmly recommend Basic no-QBFT over the Pre-Consensus appendix: the appendix queries BN at ~65% (vs Basic at ~73%) to leave gossip headroom for derivation, but that creates a ~10% slot F-bias window (~1.2s) where late-arriving envelopes are actively signed as wrong-False against the Gloas spec's 75%-anchored truth-claim semantics. Basic has only a ~2% F-bias window. SIP §3 (QBFT) has the largest F-bias (~8-25%) plus an active wrong-T attack surface from the Byzantine leader — both alternatives structurally dominate it.
  • M1 §5 dependent_root phrasing & multi-checkpoint emission timing — see my L30 reply

Still pending the SSV-team decision: L186 self-build envelope path (a) vs (b) — see my L186 reply for my lean.

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
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md
Comment thread sips/epbs_support.md Outdated

Slot is omitted because it is already pinned by the QBFT instance (same pattern as `BeaconVote`); only the observation-dependent fields need consensus. One QBFT round covers all of the cluster's local PTC-assigned validators for the slot (committee-scoped, same as `CommitteeRunner` and `AggregatorCommitteeRunner`), rather than one QBFT per validator. As a consequence, a cluster's local PTC validators in a slot contribute one shared observation to the network-wide tally rather than independent ones, which is a deliberate liveness choice of this committee-scoped design. The False-vote / missed-vote equivalence holds for `payload_present` only: `blob_data_available = False` votes are additionally counted in `should_build_on_full` via `payload_data_availability(..., available=False)`, so the cluster's shared observation carries slightly different fork-choice weight across the two boolean fields. At signing time, each operator reconstructs the full `PayloadAttestationData` (slot injected from the duty) and produces one partial signature per local PTC validator under `DOMAIN_PTC_ATTESTER` (domain epoch = `compute_epoch_at_slot(duty.slot)`), because each `PayloadAttestationMessage` on the wire ships a validator-specific signature verified against that validator's pubkey. All partial signatures broadcast together in a single `PartialSignatureMessages` container with `Type = PostConsensusPartialSig` (reused from the existing post-consensus path; the runner role `RolePTCCommittee` is the dispatch discriminator). After reconstruction, one `PayloadAttestationMessage` per validator is submitted to the beacon node.

The value check should reject zero `BeaconBlockRoot` (a null root cannot refer to a real block). `PayloadPresent` and `BlobDataAvailable` are observation-dependent booleans and are not compared against the local BN view (see Security Considerations); `BeaconBlockRoot` is likewise not checked against the BN's head for the slot, matching existing `BeaconVote.BlockRoot` handling. PTC attestations are not in the beacon chain slashing predicate, so no slashability call is required.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The Gloas spec changed after this SIP’s current pin. Current consensus-specs master adds one more PTC gossip check: the block referenced by data.beacon_block_root must be at data.slot.

This is weaker than checking the root against the local BN head. It does not ask whether the operator agrees this root is canonical; it only checks that the root belongs to the slot being signed. Since SSV injects data.slot from the duty but takes BeaconBlockRoot from the QBFT value, a bad leader could otherwise make the cluster sign a PTC message for slot S with a block root from another slot. The message would have a valid SSV signature, but gossip would ignore it under the current Gloas rule, wasting every local PTC vote covered by this QBFT round.

Can we add a value check that, when the block is known locally, verifies block.slot == duty.slot before signing? If the block is not known locally, the SIP should say whether the runner waits, requests the block, or accepts that the network may ignore the resulting message.

Reference: https://github.com/ethereum/consensus-specs/blob/6370819a35e9558822ef024126cc09ee3666827d/specs/gloas/p2p-interface.md#L346-L347

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed the new rule: at 6370819a the payload_attestation_message gossip validations add an IGNORE-level block.slot == data.slot check, absent at our current 5898db97e pin.

Going with accept-and-sign (your third option). The "block known locally" branch doesn't really have a referent at the SSV layer: the runner is a validator client with no block store, it reconstructs PayloadAttestationData from the QBFT-decided value plus the duty, so checking block.slot == duty.slot would mean an extra beacon-node lookup-by-root on the hot path near 75% (and the BeaconNode interface has no by-root method today).

The exposure is small: data.slot is duty-pinned and only BeaconBlockRoot is leader-supplied, so a slot-mismatched root only arises under a faulty leader, and the cost is that round's PTC votes ignored by peers, non-slashable and equivalent to a missed vote on the PTC_SIZE/2 tally. Not worth a per-slot BN round-trip to defend.

Documented in c5417cb: a Security Considerations entry plus a value-check note in §3. Since the rule postdates our pin, it's flagged to fold into §3 when we next bump consensus-specs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I see the tradeoff, and I agree we probably do not want every PTC round to depend on a blocking BN header lookup near the 75% cutoff.

One nuance: I do not think the check has to be all or nothing. SSV may already know the root slot from BN block events or a local root to slot cache. In that case, if the operator can determine that the decided BeaconBlockRoot belongs to a different slot than duty.slot, I think the implementation should reject before signing.

Maybe §3 can frame this as a recommended check when the information is available, without making a late BN lookup mandatory. Something like:

The value check should reject `BeaconBlockRoot` when the operator can determine that the referenced block's slot differs from the duty slot, for example from a local root to slot cache or a timely BN header lookup. If the block slot is not known before the PTC signing deadline, the runner may continue without this check; in that case, a slot mismatched root will be signed but ignored by peers.

That still preserves the latency tradeoff you described, but avoids signing values that the implementation already knows the network will ignore.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think this complexity goes away if we were to go with the Basic (non-QBFT) variant from #94 (comment) ? The problem doesn't fully dissolve, but it doesn't require any extra handling.

Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md
Comment thread sips/epbs_support.md Outdated
PTC beacon_block_root slot-binding gossip check (consensus-specs #5281)
is now part of the pinned snapshot; dropped the 'added after this SIP's
pinned snapshot' caveat in Security Considerations. All other Gloas
commits since the prior pin are below the SIP's abstraction or
unrelated (bid construction, should_build_on_full guard, proposer
boost, Fulu deposit removal).
State that the self-build envelope duty must target publication before
get_payload_due_ms (PAYLOAD_DUE_BPS, 75%) so PTC validators observe it
before the cutoff, with the block-to-payload budget window and the
payload_present=False / empty-parent consequence. Broaden the
envelope-missed Security Considerations entry to cover late publication
as a second cause of the same bounded degradation.
Recast the §6 QBFT-proposal paragraph and the envelope-missed Security
Considerations entry: only the §4-block builder originates a publishable
value, but under QBFT round-change a later leader can carry the justified
blinded value to decision without holding the full bytes. Publication is
therefore by content-match (the Publication paragraph already states
this), and the liveness risk is the matching-envelope operator failing
before publication, not the deciding leader.
Distinguish the POST body for the two self-build modes (per Diego's
review): stateless publishes SignedExecutionPayloadEnvelopeContents
(envelope + blobs + KZG proofs, since the publishing operator holds the
side data from BlockContents), stateful publishes a bare
SignedExecutionPayloadEnvelope (the BN already cached the side data).
Also hyperlink the PR #580 reference for consistency.
- drop settled head_v2 and PTC-surface watchlist items (merged + released)
- drop non-normative builder_boost_factor note
- add §3 envelope-arrival SSE signals for the PTC fetch/QBFT cutoff
- repoint proposer-duties-v2 link to the released spec
- fix envelope endpoint paths to #580's current plural form
@diegomrsantos
Copy link
Copy Markdown
Collaborator

One small suggestion before this is finalized: can we add an Acknowledgements section?

The review process changed the SIP quite a bit, and it would be good for the document to reflect the people who helped review and shape it, without making everyone a listed author. You can use my handle there.

Something like:

## Acknowledgements

Thanks to @diegomrsantos and @iurii-ssv for review, design discussion, and feedback on the Gloas integration, including PTC handling, proposer preferences, and self build envelope signing.

Comment thread sips/epbs_support.md
Comment on lines +290 to +292
#### QBFT proposal

Each operator constructs `BlindedExecutionPayloadEnvelope` from its local BN's envelope (`PayloadRoot = hash_tree_root(envelope.payload)`, other fields verbatim) and proposes the SSZ-encoded form in `EnvelopeConsensusData.DataSSZ`. Only an operator whose BN built the §4-decided block holds an envelope with a matching `BeaconBlockRoot` and a `PayloadRoot` backed by real full bytes, so only such an operator originates a publishable value in the first round. The QBFT value is the blinded envelope, so under round-change a later-round leader can re-propose that justified value without holding the full bytes; the decided value, and which operator can publish it, are independent of who leads the deciding round (publication is by content-match, see Publication).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe I'm missing something, but I think we might need further adjustments on §6 - here is the concise summary of what I figured (with assertive AI tone mixed in):

Proposal: decide the blinded envelope inside §4 and drop §6's QBFT entirely.

§6's envelope-signing QBFT looks redundant. The value it agrees on is already fully
pinned by the §4 decision, so a second consensus only distributes it — and §4's QBFT
already does distribution.

The envelope is pinned at §4 time. The self-build ExecutionPayloadBid carried in
the §4 block commits to block_hash, blob_kzg_commitments, and
execution_requests_root (gloas/beacon-chain.md).
Once §4 decides and the block is signed, the payload the proposer may later reveal is
fixed (the envelope reveal must satisfy those commitments). So
payload_root = hash_tree_root(payload) is final the moment §4 decides — there is exactly
one valid envelope, and nothing left to agree, only to distribute.

A standalone §6 QBFT is structurally degenerate. Only the operator whose BN built the
§4-decided block (the winning-bid proposer, "A") can originate a valid §6 value: every
other operator's envelope carries a different beacon_block_root and fails the value
check, and only A holds the bytes to compute payload_root. Consequences:

  • §6 can only make progress in a round led by A, so we'd have to special-case §6's leader
    selection to seed A as round-1 leader (RoundRobinProposer otherwise picks by
    height/round, unrelated to who built the block).
  • Even then it's fragile: a later-round leader can only re-propose A's value if A already
    proposed it and it was prepared in an earlier round (carried in the round-change
    justification). Non-A leaders can never originate it — strictly weaker than a normal
    proposal-type QBFT where any leader proposes its own valid candidate.

§4 has none of this: every operator's BN produces its own valid (block, envelope) pair,
so any §4 leader originates a valid value in any round — a healthy QBFT. The degeneracy
exists only because the §4 decision already fixed which operator holds the valid value;
re-running consensus over it can only ever be seeded by that one operator.

Proposed shape. On the self-build path, A proposes
(block, BlindedExecutionPayloadEnvelope) as the §4 value. The blinded form is small —
that's why it was introduced for §6 in the first place — so it rides in §4 just as well,
including under §4 round-changes via the justified value. §6 then reduces to: sign the
§4-agreed blinded root under DOMAIN_BEACON_BUILDER, reconstruct, and A publishes at the
reveal deadline — no QBFT. (We could even fold the envelope partial-sig into §4's
post-consensus, leaving §6 as pure publication.) Trust model (payload_root
leader-trusted, no content validation) and 1-of-N publication are unchanged.

Cost. §4's decided value grows by the small blinded envelope and becomes
self-build-conditional; stateful self-build fetches the envelope at §4 time (one extra
GET, just moved earlier from §6). In exchange we drop an entire consensus instance on the
tightest deadline in the slot — with no liveness tradeoff, since the value is pinned by the
already-signed §4 block.

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.

3 participants