Add early RANDAO pre-consensus emission SIP - #101
Conversation
iurii-ssv
left a comment
There was a problem hiding this comment.
LGTM overall, the suggestions below are mostly minor clarifications.
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.
|
Pushed some commits tightening the validation contract, from some findings while implementing the receiver rules in anchor:
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
left a comment
There was a problem hiding this comment.
Good work @shane-moore !!
First review batch :)
|
|
||
| **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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 = Fis eligible, and its producer window opens atslot_start(F - 2).S = F + 1is eligible, and its window opens atslot_start(F - 1).- Target slots before
Fremain 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.
There was a problem hiding this comment.
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.
|
|
||
| - 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| - 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| | Constant | Value | | ||
| | -------- | ----- | | ||
| | `EARLY_RANDAO_LEAD` | 2 slots | | ||
| | `EARLY_RANDAO_CLOCK_TOLERANCE` | 1000 ms | | ||
| | `MAX_QUARANTINED_MESSAGES` | 4096 | |
There was a problem hiding this comment.
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.
| - 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)). |
There was a problem hiding this comment.
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$ ?
There was a problem hiding this comment.
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.
| - 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. |
There was a problem hiding this comment.
the first
EARLY_RANDAO_LEADslots 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?
There was a problem hiding this comment.
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))`; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| - 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
|
||
| - 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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Added some potential clarifications to consider
| - 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. |
There was a problem hiding this comment.
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.
GalRogozinski
left a comment
There was a problem hiding this comment.
Looks good! Just a clarity request
|
|
||
| Operators that never emit early remain fully conformant. | ||
|
|
||
| **Message validation** |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.)
|
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:
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:
|
|
|
||
| **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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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)`; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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.
|
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.
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. |
Operators may emit the existing Proposer-duty RANDAO partial signature up to 2 slots before the proposal slot, so clusters can reconstruct
randao_revealbefore 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 whenepoch(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