Skip to content

Add early RANDAO pre-consensus emission SIP - #101

Open
shane-moore wants to merge 22 commits into
ssvlabs:mainfrom
shane-moore:feat/early-randao-preconsensus
Open

Add early RANDAO pre-consensus emission SIP#101
shane-moore wants to merge 22 commits into
ssvlabs:mainfrom
shane-moore:feat/early-randao-preconsensus

Conversation

@shane-moore

@shane-moore shane-moore commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Operators may emit the existing Proposer-duty RANDAO partial signature up to 2 slots before the proposal slot, so clusters can reconstruct randao_reveal before the slot starts instead of inside the post-Gloas ~3s block-production budget.

No new duty, role, message kind, domain, topic, or container is introduced. Protocol changes are limited to target-slot activation, message-validation timing, message-kind-scoped ordering, and complete-schedule duty handling. Receivers MAY additionally retain an otherwise-valid Unknown-duty candidate locally, but retention is optional and does not change the initial IGNORE gossip verdict.

The rules activate from the stamped proposal slot S: they apply when epoch(S) >= GLOAS_FORK_EPOCH, including when the permitted window for the first Gloas slots begins before the fork in wall-clock time. The SSV fork schedule is not an input.

Rationale and design discussion: #100
Addresses: ssvlabs/ssv-spec#373

iurii-ssv
iurii-ssv previously approved these changes Jul 24, 2026

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

LGTM overall, the suggestions below are mostly minor clarifications.

Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
iurii-ssv
iurii-ssv previously approved these changes Jul 25, 2026

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

Just two non-blocking nits.

Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
iurii-ssv
iurii-ssv previously approved these changes Jul 27, 2026

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

LGTM!

Found while implementing the receiver rules in Anchor: existing client
pipelines run contextual checks before signature verification, and a strict
signatures-first order would drop the duplicate-count shield in front of RSA.
Structural and canonical-form checks now run first and define the Early
RANDAO candidate; for candidates the signature-vs-contextual order is
implementation-defined, short-circuiting only on non-retaining verdicts, and
any outcome that retains, accepts, forwards, or mutates state requires a
valid operator signature (invalid signature plus Unknown duty is a pinned
REJECT). Checks are staged: RSA gates retention and forwarding, BLS gates
consumption. Vectors added for the pinned and flexible cases.
Pins receiver-divergence points found while mapping the rules onto Anchor's
duty tracker and quarantine design. Known now requires the epoch's complete
fetched proposer schedule (a locally filtered subset is not Known, so an
honest share cannot be IGNOREd as Known-unassigned by a partial view).
Retention is admission-conditioned rather than unconditional; the
byte-identical occupied-key case keeps the original entry, full-capacity
eviction has the incoming candidate compete under an exact total order (the
unreachable fourth tiebreak is dropped), and promotion is restated via
promotion candidates so an entry cannot both promote and fail revalidation.
Vectors added for each case.
The summary said the SIP activates at the Gloas-aligned SSV fork, but the
ePBS SIP (ssvlabs#94, checked at d5197bc) defines no SSV-network fork; its
validation rules gate on epoch(msg.slot) >= GLOAS_FORK_EPOCH. This SIP now
matches that model explicitly: activation is Ethereum-gated by the
epoch(S) >= GLOAS_FORK_EPOCH conjunct, the SSV fork-equality conjunct is
window-boundary protection only (it keeps the 2-slot emission window from
spanning any scheduled SSV fork), and a new S >= EARLY_RANDAO_LEAD conjunct
resolves the genesis underflow normatively, surfaced by the slot-zero unit
test in the Anchor implementation. Vectors spell out the boundary outcomes.
@shane-moore

shane-moore commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed some commits tightening the validation contract, from some findings while implementing the receiver rules in anchor:

  1. 0affc08 Pin validation ordering and stage authentication checks. The previous text required signatures strictly before the contextual rules. Anchor's pipeline (and, from our reading, go-ssv's) runs the cheap contextual checks first, and reordering would cost the duplicate-count shield in front of RSA verification. Now: structural and canonical-form checks MUST run first and define an "Early RANDAO candidate"; for candidates the signature-vs-contextual order is implementation-defined, short-circuiting only on non-retaining verdicts; any outcome that retains, accepts, forwards, or mutates state MUST first pass operator-signature verification (invalid signature plus Unknown duty is a pinned REJECT, never retained). Staging is explicit: RSA gates retention and forwarding, BLS gates consumption.

  2. 60d143e Make duty-view and retention behavior deterministic. Pins points where two conformant receivers could diverge: Known requires the epoch's complete fetched proposer schedule (Anchor's duty tracker holds a locally filtered subset, which must not count as Known); retention is admission-conditioned; a byte-identical occupied-key duplicate keeps the original entry; full-capacity eviction has the incoming candidate compete under an exact total order; promotion is restated via promotion candidates (revalidate, promote only on success).

Test vectors added for each change.

The predicate required the SSV fork at S - EARLY_RANDAO_LEAD to equal the SSV
fork at S, making the first two slots of every SSV fork activation epoch
ineligible. It protects against nothing. Every fork-sensitive artifact of a
partial signature (gossip topic, domain, role gating) is derived from the
message's stamped slot rather than from emission time, so a message emitted
inside a window that spans a fork activation is published, validated, and
consumed entirely under the fork active at S. The SIP already relied on this
for the Ethereum boundary, where the first slot of GLOAS_FORK_EPOCH is
eligible and its window starts pre-fork; forbidding the same thing for SSV
forks was inconsistent.

Eligibility is now S >= EARLY_RANDAO_LEAD and epoch(S) >= GLOAS_FORK_EPOCH,
a pure function of the slot and the Ethereum fork schedule.

@MatheusFranco99 MatheusFranco99 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good work @shane-moore !!
First review batch :)

Comment thread sips/early_randao_preconsensus.md Outdated

**Summary**

Operators may emit their existing block-proposal RANDAO partial signature up to 2 slots before the proposal slot, so the cluster reconstructs `randao_reveal` before the slot starts instead of inside the post-Gloas ~3s block-production budget. No new duty, role, message kind, domain, topic, or container: the existing Proposer-duty `RandaoPartialSig` message is emitted earlier, stamped with the proposal slot as today. Changes are confined to message-validation timing, ordering, and duty-handling rules plus a bounded receive-side buffer. Activates at `GLOAS_FORK_EPOCH`, the same Ethereum gate the ePBS SIP uses for its own validation rules; the dependency on the ePBS SIP is activation coupling only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

up to 2 slots before the proposal slot

Hmm, just by curiosity, why 2 slots, exactly?
Also, is there an edge case, like first epoch slot, in which one can't do 2 slots or even 1 in advance?
And if so how should them behave?
Possibly, clear edge-cases are the 1st and 2nd slots of the fork, right?

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.

Good catch. The previous wording compressed three separate points.

EARLY_RANDAO_LEAD = 2 is the maximum earliness receivers must support, not an assertion that gossip needs two slots. The window gives a producer an opportunity during S - 2 and another during S - 1 if the earlier local publication attempt fails. Conforming clients may emit at different points in that window. Receivers accumulate shares asynchronously and reconstruct at 2f+1, so different schedules remain compatible, but the latest share needed for quorum determines when the reveal becomes available. We did not select a longer receiver window because it would increase reveal and stale-duty exposure without an identified need.

S >= EARLY_RANDAO_LEAD is only a genesis underflow guard for calculating S - 2. It excludes absolute slots 0 and 1, not the first two slots of every epoch or the Gloas activation epoch.

Let F be the first slot of a non-genesis GLOAS_FORK_EPOCH. There is intentionally no activation warm-up:

  • S = F is eligible, and its producer window opens at slot_start(F - 2).
  • S = F + 1 is eligible, and its window opens at slot_start(F - 1).
  • Target slots before F remain ineligible.

The separate 500 ms producer delay remains guidance inside those windows. Because receiver timing also includes the clock tolerance, receiver validation and quarantine must be active no later than receiver-local slot_start(F - 2) - EARLY_RANDAO_CLOCK_TOLERANCE.

Commit c859af6 now makes this target-slot activation model explicit in the summary, rationale, qualifying-message rules, producer behavior, and cross-client test expectations. We also aligned Anchor's receiver, quarantine, and producer issues so implementations do not accidentally introduce a current-epoch gate.

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.

Correction to part of my earlier reply: the activation model described there is unchanged, but the two-slot value is no longer used to prescribe an S - 2 attempt followed by an S - 1 retry.

It remains the maximum receiver window. The recommended producer attempt is now exactly S - 1; the 500 ms offset and coordinated second pre-slot attempt were removed.

Addressed in b29db38.

Comment thread sips/early_randao_preconsensus.md Outdated

- No new duty: RANDAO always has an in-slot consumer (the Proposer duty), and a separate duty would still need the in-slot pre-consensus as its fallback. This differs from `ProposerPreferences` (defined in the ePBS SIP, [#94](https://github.com/ssvlabs/SIPs/pull/94)), which must reconstruct before the slot and is mutable.
- The 2-slot window is chosen for operational margin, not gossip latency. The window is the fork-pinned receiver rule; the emission moment inside it is producer policy, tunable post-fork without a protocol change.
- The per-(signer, slot) duplicate limit stays 1. Each logical partial has exactly one valid byte encoding, gossip message identifiers are content-derived, and gossip layers deduplicate before validation, so a second copy either never reaches validation or is the receiver's first copy.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

gossip layers deduplicate before validation, so a second copy either never reaches validation or is the receiver's first copy.

Just to add: byzantine duplicates with a different signature for the same (duty, signer) won't be dropped by the naive gossip hash deduplication, but it will be by our app message validation logic that accepts only 1 partial beacon signature per signer per type (pre- or post- consensus) and per duty.

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.

Good point. A byte-identical copy shares the original content-derived gossip ID, but changing the inner partial signature and re-signing the envelope normally produces a different gossip ID. That variant reaches application validation, where the existing one-message limit for the (MessageID, signer, slot) key and pre-consensus or post-consensus class stops it.

I narrowed the rationale to distinguish those cases. The duplicate limit itself does not need to change.

Addressed in c13987a.

Comment thread sips/early_randao_preconsensus.md Outdated
- No new duty: RANDAO always has an in-slot consumer (the Proposer duty), and a separate duty would still need the in-slot pre-consensus as its fallback. This differs from `ProposerPreferences` (defined in the ePBS SIP, [#94](https://github.com/ssvlabs/SIPs/pull/94)), which must reconstruct before the slot and is mutable.
- The 2-slot window is chosen for operational margin, not gossip latency. The window is the fork-pinned receiver rule; the emission moment inside it is producer policy, tunable post-fork without a protocol change.
- The per-(signer, slot) duplicate limit stays 1. Each logical partial has exactly one valid byte encoding, gossip message identifiers are content-derived, and gossip layers deduplicate before validation, so a second copy either never reaches validation or is the receiver's first copy.
- Because gossip layers never unmark a seen message, an IGNORE of an early partial permanently discards that operator's share for the window. The Unknown-duty retention rule and the dedicated clock tolerance narrow that failure class for honest shares without eliminating it; the residual cases are documented in *Duty assignment* (stale-`Known` reorg), the retention admission rule (refresh-pending losses), and Security Considerations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

never unmark a seen message

I think they do so, but I also think we are just fine in here since it should be a window of 1 or 2 epochs. We just need to confirm.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Confirmed — they do expire, but with a wide margin: go-ssv's msgIDCacheTTL is 6.4 min (~32 slots) against this SIP's 24 s window. So "never unmark" is stronger than what implementations do; the conclusion holds, and stating the margin would be more convincing than the absolute.

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, thanks. I replaced both absolute claims, "never unmark" and "a few seconds," with the narrower statement that gossip duplicate-cache lifetimes can exceed the entire useful Early RANDAO window. The recovery wording now says the in-slot copy may be suppressed, since restart and local cache state matter.

Addressed in a939402.

Comment thread sips/early_randao_preconsensus.md Outdated
Comment on lines +28 to +32
| Constant | Value |
| -------- | ----- |
| `EARLY_RANDAO_LEAD` | 2 slots |
| `EARLY_RANDAO_CLOCK_TOLERANCE` | 1000 ms |
| `MAX_QUARANTINED_MESSAGES` | 4096 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it could be useful to add a third column for a short description for each so the reading flow provides more intuition, what do you think?
For example EARLY_RANDAO_CLOCK_TOLERANCE appears only after some time in the document.

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.

Agreed, addressed in 8a098c9.

- Proposer-role `MessageID`; type `RandaoPartialSig`; exactly one `PartialSignatureMessage` entry;
- `slot` = the proposal slot `S`; signed object `SSZUint64(epoch(S))` under `DOMAIN_RANDAO`, domain epoch `epoch(S)`;
- canonical SSZ; deterministic BLS share signature; deterministic RSA (PKCS#1 v1.5) operator signature; exactly one outer signer, equal to the embedded operator ID;
- eligibility predicate: `S >= EARLY_RANDAO_LEAD` and `epoch(S) >= GLOAS_FORK_EPOCH` (the Ethereum Gloas fork epoch, as used by the ePBS SIP, [#94](https://github.com/ssvlabs/SIPs/pull/94)).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

S >= EARLY_RANDAO_LEAD

Hmm, I got confused here... the slot number should be above 2? Or it was more in the sense:

  • First enabled slot ever: $S - \text{[GLOAS FORK EPOCH.first slot]} \geq 2$?
  • Or distance to duty slot: $0 \leq S (duty.slot) - now.slot \leq 2$ ?

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.

Answered this together with the two-slot and fork-boundary questions here. In short, S >= EARLY_RANDAO_LEAD is only the absolute-slot genesis guard; the allowed distance from the current time is enforced separately by the producer and receiver timing rules.

Comment thread sips/early_randao_preconsensus.md Outdated
- canonical SSZ; deterministic BLS share signature; deterministic RSA (PKCS#1 v1.5) operator signature; exactly one outer signer, equal to the embedded operator ID;
- eligibility predicate: `S >= EARLY_RANDAO_LEAD` and `epoch(S) >= GLOAS_FORK_EPOCH` (the Ethereum Gloas fork epoch, as used by the ePBS SIP, [#94](https://github.com/ssvlabs/SIPs/pull/94)).

The predicate is a pure function of `S`, evaluated identically by producers and receivers, never re-evaluated against wall-clock time. Activation is Ethereum-gated by `epoch(S) >= GLOAS_FORK_EPOCH`, matching the ePBS SIP's own validation gating; the `S >= EARLY_RANDAO_LEAD` conjunct keeps the producer's emission window well defined and excludes the first `EARLY_RANDAO_LEAD` slots at genesis. The SSV fork schedule plays no part, and an emission window that spans an SSV fork activation is fine: every fork-sensitive artifact of a partial signature (gossip topic, domain, role gating) is derived from the message's stamped slot rather than from the moment it was emitted, so such a message is published, validated, and consumed entirely under the fork active at `S`. The same holds for a window spanning the Ethereum fork boundary, which this predicate likewise permits. Containers violating the canonical form fall to existing structural rules (REJECT); BLS-share validity is not evaluated during message validation (see Message validation). Messages failing the predicate, and all non-randao messages, keep today's validation unchanged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the first EARLY_RANDAO_LEAD slots at genesis

Ah, got it. But does it really make sense to do so? Shouldn't it be the first two of the activation epoch?

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.

Covered this in the consolidated response here. We are keeping the genesis guard and not excluding the first two activation-epoch slots: F and F + 1 are eligible target slots, including their pre-fork emission windows.


For a locally known proposer duty at eligible slot `S`, an operator:

- MAY broadcast its qualifying randao partial at any wall-clock time in `[slot_start(S - EARLY_RANDAO_LEAD), slot_start(S))`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

But what about the first slot of the fork?
Is there a warm-up, or, for this case, it would end up just sending on its slot and that's it?

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.

Covered this in the consolidated response here. There is no warm-up: target slot F may emit from F - 2, so receiver support must be active before wall-clock Gloas.

Comment thread sips/early_randao_preconsensus.md Outdated
- MAY broadcast its qualifying randao partial at any wall-clock time in `[slot_start(S - EARLY_RANDAO_LEAD), slot_start(S))`;
- MUST NOT broadcast it before `slot_start(S - EARLY_RANDAO_LEAD)` by its own clock;
- SHOULD delay emission at least 500 ms past `slot_start(S - EARLY_RANDAO_LEAD)` (assumed maximum pairwise honest clock disparity: 1 s);
- SHOULD still execute the existing in-slot emission at `S` unconditionally; a running origin's identical re-publish is absorbed by its own gossip layer (expected, not an error), and a restarted origin's re-publish aids recovery;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wait, so it always also try to emit again once the slot starts?
I think there could be a problem here since re-creating a message and BLS-signing it takes time. Only if we have it cached...
But maybe a flag about the message being published well or not is enough, no? What do you think?

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.

Yes, the ordinary in-slot path remains a SHOULD regardless of the recorded early outcome.

A local published flag is not enough to suppress it. Local enqueue, gossip publication, and peer receipt are distinct states, and successful local publication does not prove that the intended peers received the share.

The revised text also makes clear that a completed reconstruction MAY be returned immediately to the in-slot consumer without waiting for repeated signing or publication. Implementations MAY reuse cached valid signing artifacts or schedule emission independently.

Addressed in b29db38.

Comment thread sips/early_randao_preconsensus.md Outdated

- MAY broadcast its qualifying randao partial at any wall-clock time in `[slot_start(S - EARLY_RANDAO_LEAD), slot_start(S))`;
- MUST NOT broadcast it before `slot_start(S - EARLY_RANDAO_LEAD)` by its own clock;
- SHOULD delay emission at least 500 ms past `slot_start(S - EARLY_RANDAO_LEAD)` (assumed maximum pairwise honest clock disparity: 1 s);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, sorry I got lost in here. What is the purpose of doing that? Trying to let the receiver accept the msg due to the possibility of it having an earlier clock time and still being in the duty slot - 3? We can align msg validation for that rather than the logic (as we already do if I'm not mistaken)

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.

You are right. The 500 ms value was additional producer-side slack, while EARLY_RANDAO_CLOCK_TOLERANCE = 1000 ms already handles the assumed clock disparity at the legal receiver boundary. Once the recommended emission moved to S - 1, the 500 ms offset no longer served a protocol purpose.

I removed it. The producer now SHOULD make its first early attempt at slot_start(S - 1), while the two-slot receiver window and 1000 ms receiver tolerance remain unchanged. I am open to another recommended lead if you think there is a better choice.

Addressed in b29db38.

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

Added some potential clarifications to consider

Comment thread sips/early_randao_preconsensus.md
Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
- No new duty: RANDAO always has an in-slot consumer (the Proposer duty), and a separate duty would still need the in-slot pre-consensus as its fallback. This differs from `ProposerPreferences` (defined in the ePBS SIP, [#94](https://github.com/ssvlabs/SIPs/pull/94)), which must reconstruct before the slot and is mutable.
- The 2-slot window is chosen for operational margin, not gossip latency. The window is the fork-pinned receiver rule; the emission moment inside it is producer policy, tunable post-fork without a protocol change.
- The per-(signer, slot) duplicate limit stays 1. Each logical partial has exactly one valid byte encoding, gossip message identifiers are content-derived, and gossip layers deduplicate before validation, so a second copy either never reaches validation or is the receiver's first copy.
- Because gossip layers never unmark a seen message, an IGNORE of an early partial permanently discards that operator's share for the window. The Unknown-duty retention rule and the dedicated clock tolerance narrow that failure class for honest shares without eliminating it; the residual cases are documented in *Duty assignment* (stale-`Known` reorg), the retention admission rule (refresh-pending losses), and Security Considerations.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Confirmed — they do expire, but with a wide margin: go-ssv's msgIDCacheTTL is 6.4 min (~32 slots) against this SIP's 24 s window. So "never unmark" is stronger than what implementations do; the conclusion holds, and stating the margin would be more convincing than the absolute.

iurii-ssv
iurii-ssv previously approved these changes Jul 31, 2026

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

Just a minor followup.

Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated
Comment thread sips/early_randao_preconsensus.md Outdated

@GalRogozinski GalRogozinski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good! Just a clarity request


Operators that never emit early remain fully conformant.

**Message validation**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I find this whole section very hard to read. Maybe make a list of new rules and changed rules


Either duty-assignment IGNORE is eligible for the optional local retention below.

A proposer-duty view is authoritative for an epoch iff the receiver retains a complete schedule containing exactly `SLOTS_PER_EPOCH` duties, one for every distinct slot in that epoch. Such a view is authoritative for every validator in the epoch, independent of the receiver's local SSV registry. A schedule that fails this shape establishes no new view. A newly retained complete schedule replaces the prior view; a failed or malformed refresh and a local validator-set change do not invalidate a previously retained authoritative view. A reorg likewise does not revoke authority until a replacement complete schedule is retained, so a stale view remains Known with the loss class described below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two conformant receivers can disagree after a refresh race: view replacement has no freshness rule.

Fix: bind installation to fetch recency — e.g. "A receiver MUST NOT replace an installed view with a schedule produced by a request issued before the request that produced the installed view" (or equivalently, key replacement to a newer observed dependent root).

"A newly retained complete schedule replaces the prior view" places no ordering constraint on which complete schedule wins. With overlapping refreshes — or two beacon nodes where one lags a reorg — an older complete response can arrive last and reinstate the pre-reorg schedule after the replacement was already installed. A receiver that suppresses stale responses then answers Known-and-assigned where one installing in completion order answers Known-and-not-assigned → IGNORE, for the same honest share. Distinct from the documented pre-replacement staleness interval: this permits rollback after replacement, and view flapping under multi-BN setups.


**Security Considerations**

- Deliberately ineligible early emission only starves the attacker's own share (only the signer can produce the bytes). The clock tolerance and IGNORE-not-REJECT choices narrow the permanent-loss class for honest shares, and optional retention can further narrow duty-view losses. Candidates that are not retained or are discarded before promotion remain residual loss cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Registry lag is an unnamed permanent-loss class, and retention cannot cover it.

Fix: name it in the residual-loss enumeration: a receiver whose SSV registry has not yet indexed the message's validator drops the share at the existing unknown-validator/committee semantics checks; retention is unavailable by construction (it requires duty assignment to be the only non-passing check), and duplicate suppression can block the byte-identical in-slot copy — bounded by the same round-change fallback as stale-Known.

The SIP carefully enumerates its residual losses (stale-Known reorg, restarts, eviction), but this class is only implicitly inside "candidates that are not retained ... remain residual loss cases." Concrete case: a newly registered validator's first proposal, with the early share arriving before every committee peer has synced the registry. (The same lag also drops the duty's QBFT messages under the legacy proposer check — see the PR-level comment on duty-staleness policies.)

@GalRogozinski

Copy link
Copy Markdown
Contributor

Design discussion (non-blocking for this PR): we now have three duty-staleness policies — should a follow-up unify them?

With this SIP, the stack answers "my duty view might be stale — what do I do?" three different ways:

  1. ePBS SIP roles (7/8/9): skip the assignment check during a detected validator-set/dependent-root change window — fail-open, safe because those roles carry duty-count and distinct-signing-root caps.
  2. This SIP (randao candidates): stale complete schedule stays authoritative — fail-closed IGNORE, with optional retention as the rescue. Justified: the proposer role has no per-epoch duty limit, so the assignment check is the only spam bound on the widened early window, and fail-open under an unboundedly long Unknown (BN outage) would remove it.
  3. Legacy proposer QBFT/post-consensus (same MessageID as the randao share): unspecified — go-ssv happens to accept under an unfetched epoch.

The asymmetry between 2 and 3 is partly principled (one-shot pre-slot shares vs an interactive protocol that self-heals via round changes), but it produces transient disagreements within one proposer flow (e.g. duty-fetch outage: randao share IGNOREd while the same duty's QBFT proposal is accepted; registry lag: the reverse). Nothing breaks — but nothing says the split is intentional either, and an implementer unifying the two checks would silently change wire behavior.

Two asks, both satisfiable outside this PR:

  • In this SIP: one sentence at the tri-state stating the split is intentional and why (the message-economics argument above), so it isn't "fixed" by accident.
  • As a follow-up (here or in the Early RANDAO Pre-Consensus Emission #100 discussion): should the legacy proposer check adopt the complete-schedule authoritative view? It is the best-specified of the three (defined authority, registry-independence, defined replacement), and would fix the pre-existing registry-lag drop of a just-registered validator's QBFT messages — but a naive copy would import fail-closed Unknown into QBFT, a regression under BN outages, so it needs its own design pass rather than inclusion here.


**Message validation**

Validation of a potential Early RANDAO message begins with the structural and canonical-form checks, which MUST run first; a structurally and canonically valid randao container satisfying the eligibility predicate is an Early RANDAO candidate, and the rules below apply to candidates only (all other messages keep today's validation unchanged). For a candidate, implementations MAY order operator-signature verification and the non-mutating contextual checks below (timing, slot ordering, duty assignment, duplicate limits) according to local denial-of-service policy, and MAY short-circuit on a contextual verdict that neither retains nor accepts the message. Any outcome that retains, accepts, forwards, feeds signature collection, or mutates ordinary validation state MUST first pass operator-signature verification. If a candidate fails both operator-signature verification and a non-retaining contextual check, either the contextual verdict or the signature-verification REJECT is conformant, but the candidate is never retained. Ordinary validation state (duplicate counts, signer state, slot high-water marks, and epoch counters) mutates only on acceptance or successful promotion, never on IGNORE or initial retention.

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could this lead to unbounded, unpenalized distinct-byte injection? Duplicate counts mutate only on acceptance, and under an Unknown/not-assigned view every copy is IGNOREd — so a committee operator can re-sign the envelope over arbitrarily many distinct BLS fields (unchecked at validation) for one (signer, slot). Each copy costs an RSA verification, gives no scoring feedback, and is retention-eligible with store dedup left to implementations — contradicting line 23's "bounded by the existing one-message limit". Fix: count authenticated distinct containers seen (REJECT the second), cap retention at one entry per key, and add a candidate-path duplicate vector (none exists).


Validation of a potential Early RANDAO message begins with the structural and canonical-form checks, which MUST run first; a structurally and canonically valid randao container satisfying the eligibility predicate is an Early RANDAO candidate, and the rules below apply to candidates only (all other messages keep today's validation unchanged). For a candidate, implementations MAY order operator-signature verification and the non-mutating contextual checks below (timing, slot ordering, duty assignment, duplicate limits) according to local denial-of-service policy, and MAY short-circuit on a contextual verdict that neither retains nor accepts the message. Any outcome that retains, accepts, forwards, feeds signature collection, or mutates ordinary validation state MUST first pass operator-signature verification. If a candidate fails both operator-signature verification and a non-retaining contextual check, either the contextual verdict or the signature-verification REJECT is conformant, but the candidate is never retained. Ordinary validation state (duplicate counts, signer state, slot high-water marks, and epoch counters) mutates only on acceptance or successful promotion, never on IGNORE or initial retention.

Candidate classification depends on message structure and the stamped target slot, not on whether a copy arrived before `slot_start(S)`. Consequently, the duty tri-state replaces existing RANDAO duty handling for every eligible target slot, including ordinary in-slot copies and later copies still within the existing Proposer lateness window.

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happens to relay when the duty view is Unknown? In-slot copies are now candidates, so an Unknown-view receiver IGNOREs — and stops forwarding — in-slot randao it forwards today: one node's BN gap becomes a mesh propagation hole. That makes "falls back to today's in-slot exchange" circular (the fallback hits the same tri-state that failed the early copy) and regresses never-early operators (line 64), whose shares Unknown-view peers no longer relay. Either make Unknown forward-but-don't-consume (unlike Known-not-assigned, where IGNORE is justified), or state the relay consequence, stratify the testnet measurement by duty-view state, and pin it with a vector.


Reconstruction semantics are unchanged. A completed reconstruction MAY be served immediately to the in-slot consumer; invoking the best-effort in-slot emission path does not require the consumer to wait for repeated signing or publication. The reconstructed reveal is a per-epoch value: an implementation MAY serve a proposal at slot `Y` from a reconstruction built via shares stamped for slot `X` of the same epoch, provided every contributing share was promoted or normally accepted. Wire messages remain per-slot-stamped.

"Promoted or normally accepted" is evaluated at receipt or promotion time against the receiver's then-current duty view and is never re-evaluated at consumption. The cross-slot case is reachable two ways: a validator with two proposals in the same epoch, and a reorg that moves a proposal from `X` to `Y` after `X`-stamped shares were accepted under the pre-reorg view; the allowance exists so a completed early collection survives the shift. The signed object is identical for `X` and `Y` (`SSZUint64(epoch)`), so reuse has no cryptographic effect; implementations that key collection by signing root exercise it naturally, and implementations that require exact-slot consumption remain conformant.

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need a per-signer dedup rule? In this duty-move case receivers hold two accepted shares from one signer for the epoch (X- and Y-stamped, identical inner signature and root). A root-keyed collector counting entries toward 2f+1 hits false quorum — 4 ops, f=1: 2 signers × 2 stamps = 4 entries vs threshold 3 — and reconstructs an invalid reveal that fails only at the BN. One sentence fixes it: a reconstruction counts at most one share per signer per signing root (a second replaces, never accumulates); the duty-move vector can then assert 2f+1 distinct signers over a mixed X/Y set.


Checks are staged: operator-signature verification gates retention, acceptance, forwarding, and state mutation; BLS-share validity gates consumption. A candidate is fully qualifying only once every applicable check has passed; honest producers emit qualifying messages by construction.

*Earliness.* A receiver MUST accept a candidate's timing iff

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we consider bounding only the early side? "MUST accept a candidate's timing iff" holds for any already-started slot — even ten epochs late — so one conformant reading has the formula override the lateness TTL and accept/forward arbitrarily late randao, leaving the past-slot half of the ordering exemption unbounded. "MUST NOT reject as too early iff …", with lateness applying independently, removes the ambiguity; the vectors cover only the early boundary, so add a late one.


`EARLY_RANDAO_CLOCK_TOLERANCE` is receiver-side headroom covering the assumed maximum 1 s pairwise honest clock disparity at the producer's legal boundary. It does not shift the recommended producer time.

*Slot ordering.* Candidate randao partials are exempt from the per-(operator, `MessageID`) highest-seen-slot rule in both directions: they MUST NOT be rejected for a slot below the high-water mark, and they MUST NOT advance the high-water mark applied to other message classes on the same `MessageID` (randao shares its `MessageID` with the proposal's consensus and post-consensus traffic). Final acceptance still requires the remaining qualifying-message checks; earliness, lateness, the duplicate limit of 1, and canonical form bound these messages instead.

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any reason "slot high-water marks" is still in line 68's mutate-on-acceptance list? I

t contradicts the MUST NOT here: an early candidate for slot 11 accepted during slot 9 either advances the mark, rejecting still-live slot-10 consensus/post-consensus traffic on the same MessageID under the unchanged rule, or it doesn't, and implementations diverge.

State that candidates neither read nor write any high-water mark (scope line 68's list to non-candidates). The consecutive-slots vector is randao-vs-randao only; this cross-class case is what the MUST NOT actually regulates.


*Slot ordering.* Candidate randao partials are exempt from the per-(operator, `MessageID`) highest-seen-slot rule in both directions: they MUST NOT be rejected for a slot below the high-water mark, and they MUST NOT advance the high-water mark applied to other message classes on the same `MessageID` (randao shares its `MessageID` with the proposal's consensus and post-consensus traffic). Final acceptance still requires the remaining qualifying-message checks; earliness, lateness, the duplicate limit of 1, and canonical form bound these messages instead.

*Duty assignment.* Tri-state, evaluated per candidate against the receiver's proposer-duty view for `epoch(msg.slot)` and the message's validator:

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to handle a validator that can't be resolved to an index?

Schedules key by validator_index, the MessageID by pubkey, so even with an authoritative view a just-registered validator is neither assigned nor provably not-assigned, and no branch is specified. I
It matters: pre-existing unknown-validator checks may REJECT (penalizing an honest share) where the tri-state says IGNORE, and retention eligibility differs (duty assignment must be the only failing check).

State precedence where existing rules resolve first with IGNORE not REJECT for candidates, or classify as Unknown, and narrow line 94's registry-independence claim to schedule authority.


- MAY broadcast its qualifying randao partial at any wall-clock time in `[slot_start(S - EARLY_RANDAO_LEAD), slot_start(S))`;
- MUST NOT broadcast it before `slot_start(S - EARLY_RANDAO_LEAD)` by its own clock;
- if it elects to emit early and knows the duty by the recommended time, SHOULD make its first local publication attempt at `slot_start(S - EARLY_RANDAO_RECOMMENDED_LEAD)`, equivalent to `slot_start(S - 1)`;

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth considering an intra-slot offset?

slot_start(S - 1) makes every early emitter fire at a slot boundary, competing with S-1 block propagation and the compressed post-Gloas attestation deadline. slot_start(S - 1) + SLOT_DURATION/2 still leaves ~6 s for gossip and reconstruction, halves the reveal-exposure window, and avoids the burst; at minimum add intra-slot offset to the mandatory lead comparison.


inclusive on the accept side; strictly greater is IGNORE. This replaces the generic clock tolerance for this rule only. Lateness is unchanged (existing Proposer-role TTL against `msg.slot`). All other message classes keep their existing allowances.

`EARLY_RANDAO_CLOCK_TOLERANCE` is receiver-side headroom covering the assumed maximum 1 s pairwise honest clock disparity at the producer's legal boundary. It does not shift the recommended producer time.

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should the tolerance match how the bound is stated?

"1 s pairwise" is exactly covered by 1000 ms, but clock bounds are usually per node vs true time, and ±1 s per node gives up to 2 s producer-receiver skew, IGNOREing honest boundary emissions.

Pin one: ±500 ms per node, or tolerance = twice the per-node bound. Only boundary emitters are exposed (recommended-lead producers have a full slot of slack).


Producer implementations that emit early SHOULD test the recommended `S - 1` attempt, an injected local publication failure followed by the in-slot path, duty discovery after the recommended time, unconditional invocation of the in-slot path with and without a completed reconstruction, and completion of a cached in-slot consumer without waiting for repeated signing or publication.

Before mainnet activation guidance, testnet measurement MUST quantify early-share miss incidence at duty start (stratified by emission lead, receiver restarts, partition duration, leader round, and whether receiver retention was enabled), record promotion timing and outcome where retention is enabled, and MUST demonstrate round-2 viability under Gloas timing, meaning block publication before the post-Gloas useful-block deadline. The emission-lead comparison MUST include `EARLY_RANDAO_RECOMMENDED_LEAD`, at least one earlier lead, and at least one later lead, and MUST record threshold-ready time relative to `slot_start(S)` so the resulting reveal-exposure window is explicit.

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any reason "the post-Gloas useful-block deadline" isn't pinned to a definition?

It sits inside a MUST that gates mainnet activation guidance, so it needs a citable value or a cross-reference into the ePBS SIP (#94) — the same treatment GLOAS_FORK_EPOCH got.


**Motivation**

Gloas moves the attestation deadline to 1/4 slot. RANDAO pre-consensus (sign, gossip, collect 2f+1, reconstruct) is the first serial step of block production and costs a gossip round trip at slot start. The signed object, `SSZUint64(epoch)` under `DOMAIN_RANDAO`, is a pure function of the proposal slot's epoch, so it can be signed and exchanged at any time and a re-org can never invalidate it. Failure modes fall back to today's in-slot exchange, except at a receiver that already IGNOREd the early copy and did not retain it: gossip duplicate caching may suppress the byte-identical in-slot copy for the remainder of the useful duty window (see *Duty assignment* and *Optional duty-assignment retention*). Addresses [ssv-spec#373](https://github.com/ssvlabs/ssv-spec/issues/373).

@momosh-ssv momosh-ssv Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Might be worth softening "a re-org can never invalidate it"?

A reorg can move the validator's assignment out of the epoch entirely, which is exactly why the reuse allowance in Reconstruction and consumption is epoch-scoped. "A re-org within the epoch can never invalidate it" matches the Specification.

@MatheusFranco99

Copy link
Copy Markdown
Contributor

Following up on the message validation duty-staleness design.

I initially thought accepting a message under a missing duty view made no sense — without the duty you can't process it. But that reasoning has two issues: most nodes validating a message are relays that don't actually process it (non-committee), and on a committee node an accepted message just waits in the queue until the duty fetch lands. We already do exactly this on purpose btw in ssv node: the first-slot-of-epoch carve-out for RANDAO messages accepts while duties "are still being fetched." Meanwhile IGNORE is not as harmless as gossipsub's redundancy suggests, because the staleness causes here may be correlated (e.g. a common BN that impacts two operators).

On the other hand, accepting cannot be as simple to prevent spamming.
So my proposal for the proposer role is a tri-state:

  • Known beacon state and assigned → pass.
  • Known and not assigned → IGNORE (you have real evidence; receiver views can be stale, so never REJECT).
  • Unknown → accept, backstopped by a new per-epoch duty cap for the proposer role (it's the one role missing from dutyLimit today).

Two honest caveats. First, unlike the registration/aggregator caps, a proposer cap is probabilistic rather than structural — a validator can draw more than 2 proposals in an epoch, it's just vanishingly rare — so the rule should state that, and the value (2, or 4 to be generous) is a judgment call (I think it's fine increasing up to the max possible, only for the proposer role ofc). Also, the cap fallback only ever binds while the view is Unknown (and soon we will have EIP-7917). Second, forwarding under Unknown is safe reputation-wise: duty-assignment failures are IGNORE-class everywhere, and every REJECT-class check (signatures, structure, counts) needs no duty view, so we never relay something a peer would penalize us for.

One consequence worth making explicit: this cap removes the stated justification for the early-RANDAO rule's fail-closed Unknown ("the assignment check is the only spam bound"). With the cap in place, the same tri-state can govern the RANDAO share and the legacy QBFT/post-consensus path — resolving the asymmetry between the two within one proposer flow, rather than documenting it as intentional. And EIP-7917's in-state proposer lookahead makes the Known side stronger over time: a complete schedule two epochs ahead, pinned by a dependent root, shrinks Unknown to deep-reorg and outage windows.

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