From 8a5da9ef0ac1f3133373b88f7f53beac1c0e7d87 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 23 Jul 2026 16:15:05 -0700 Subject: [PATCH 01/20] Add early RANDAO pre-consensus emission SIP --- sips/early_randao_preconsensus.md | 115 ++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 sips/early_randao_preconsensus.md diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md new file mode 100644 index 0000000..4993bc4 --- /dev/null +++ b/sips/early_randao_preconsensus.md @@ -0,0 +1,115 @@ +| Author | Title | Category | Status | Dependency SIP | Date | +| ------ | ----- | -------- | ------ | -------------- | ---- | +| Shane Moore | Early RANDAO Pre-Consensus Emission | Core | draft | ePBS (EIP-7732) Support | 2026-07-23 | + +[Discussion](https://github.com/ssvlabs/SIPs/discussions/100) + +**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 rules plus a bounded receive-side buffer. Activates at the Gloas-aligned SSV fork; the dependency on the ePBS SIP is activation coupling only. + +**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. Every failure mode falls back to today's in-slot exchange. Addresses [ssv-spec#373](https://github.com/ssvlabs/ssv-spec/issues/373). + +**Rationale & Design Goals** + +- 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`, 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 exist to keep honest shares out of that failure class. + +**Specification** + +Notation: `S` is a proposal slot; `slot_start(s)`, `SLOT_DURATION`, `epoch(s)` as usual; `fork_at_slot(s)` maps a slot to its scheduled SSV fork. REJECT penalizes the delivering peer; IGNORE drops without forwarding or penalty. + +| Constant | Value | +| -------- | ----- | +| `EARLY_RANDAO_LEAD` | 2 slots | +| `EARLY_RANDAO_CLOCK_TOLERANCE` | 1000 ms | +| `MAX_QUARANTINED_MESSAGES` | 4096 | + +**Qualifying message** + +A qualifying randao partial MUST satisfy all of: + +- 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: `fork_at_slot(S - EARLY_RANDAO_LEAD) == fork_at_slot(S)` and `epoch(S) >= GLOAS_FORK_EPOCH`. + +The predicate is a pure function of `S`, evaluated identically by producers and receivers, never re-evaluated against wall-clock time. Proposals in the first `EARLY_RANDAO_LEAD` slots of any SSV fork epoch are ineligible by construction. Containers violating the canonical form fall to existing structural rules (REJECT). Messages failing the predicate, and all non-randao messages, keep today's validation unchanged. + +**Producer behavior** + +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))`; +- 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; +- MAY emit immediately for a duty discovered inside the window; SHOULD emit multiple eligible duties in ascending slot order (correctness does not depend on it). + +Operators that never emit early remain fully conformant. + +**Message validation** + +Rule order: structural and canonical-form checks, then signatures, then the rules below. Validation state (duplicate counts, slot high-water marks) mutates only on acceptance or promotion, never on IGNORE. + +*Earliness.* A receiver MUST accept a qualifying message's timing iff + +```text +slot_start(msg.slot) - local_now <= EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RANDAO_CLOCK_TOLERANCE +``` + +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. + +*Slot ordering.* Qualifying 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). The exemption applies only after the qualifying-message checks pass; earliness, lateness, the duplicate limit of 1, and canonical form bound these messages instead. + +*Duty assignment.* Tri-state on the receiver's proposer-duty view for `epoch(msg.slot)`: + +- Known (a successfully completed fetch for the epoch is cached; an empty result is Known) and assigned at exactly `msg.slot`: accept. +- Known and not assigned: IGNORE. Never REJECT (receiver views can be stale or re-orged). +- Unknown (no successfully completed fetch cached; failures count as Unknown): IGNORE-AND-RETAIN, below. + +Receivers SHOULD hold next-epoch proposer duties by the tail of each epoch. + +**Unknown-duty retention (IGNORE-AND-RETAIN)** + +A message reaching the duty check in the Unknown state is retained locally before the gossip verdict IGNORE is returned: not forwarded, no penalty, but kept so the seen-cache mark cannot permanently discard it. + +- Admission: passed every rule except duty assignment, and `epoch(msg.slot)` is Unknown. +- Key: (operator, validator, `msg.slot`), at most 1 entry. Global capacity `MAX_QUARANTINED_MESSAGES` per node. +- Occupied key, distinct bytes: existing two-tier duplicate rule (REJECT from the same delivering peer, IGNORE otherwise); original retained; duplicate-count and ordering state not mutated; quota not recharged. +- Eviction when full: greatest `|msg.slot - current_slot|` first; ties by larger `msg.slot`, then higher validator index, then higher operator ID, then lexicographically greater message identifier. +- Expiry: delete an entry once a newly received copy would fail the lateness rule. +- Promotion: when the epoch's duty fetch completes, each entry is decided immediately: assigned at exactly `msg.slot` promotes, anything else deletes. A promoted message is processed exactly as a newly received accepted message, with all stateful rules applied at promotion time (a normally accepted interim copy for the same (signer, slot) makes the promoted entry the count-limit duplicate). +- Accounting: the retention quota mutates at retention time; ordinary validation state does not mutate on the IGNORE path. + +Shares that are neither promoted nor normally accepted MUST NOT be fed to signature collection or any cache. + +**Reconstruction and consumption** + +Unchanged. 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. + +**Test expectations** + +Cross-client vectors MUST cover: + +- earliness boundary at exactly `EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RANDAO_CLOCK_TOLERANCE` (accept) and beyond (IGNORE); +- Unknown-epoch retention, promotion, and deletion; Known-unassigned IGNORE; +- the two-direction ordering exemption with proposals at consecutive slots for one validator; +- occupied-key two-tier duplicates and distinct-bytes REJECT; +- the eligibility predicate at fork boundaries (first `EARLY_RANDAO_LEAD` slots of a fork epoch ineligible); +- restart cases, late mesh join, cross-epoch stamps. + +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 MUST demonstrate round-2 viability under Gloas timing, meaning block publication before the post-Gloas useful-block deadline. + +**Security Considerations** + +- Deliberately ineligible early emission only starves the attacker's own share (only the signer can produce the bytes). The clock tolerance, Unknown-state retention, and IGNORE-not-REJECT choices keep honest shares out of the permanent-loss class. +- After the gossip message-cache window (a few seconds), a stable origin cannot re-deliver a missed early share; receiver restarts and partitions are correlated events. Residual: a live round-1 leader without a reconstruction forces a round change. Mitigations: later emission inside the window, the unconditional in-slot emission (restarted-origin recovery), optional non-normative persistence of received shares, and the mandatory testnet measurement above. +- Early reconstruction widens the third-party reaction window between reveal knowability and block publication from roughly 2-4 s to up to 25 s, enabling adaptive bribery, censorship, or targeted DoS when withholding the proposal favors an adversary (one randao bit per affected slot, compounding across slots). Bounded by the existing single-proposal grinding bound; narrowable operationally via later emission, without a protocol change. +- An attacker meeting the retention preconditions (a committee operator sharing more than ~600 validators with the victim, during a duty-fetch failure) can fill the retention capacity and evict legitimate entries; cost is bounded to the evicted shares (round-change path). +- No amplification: unverifiable messages are never forwarded. Authentication cost is unchanged (operator signature at validation, BLS at consumption). A signer emitting two distinct containers for one (signer, slot) is provably misbehaving and attributable via its operator signature. +- No slashing surface: the randao object is not slashable and is a pure function of public data; early signing changes when it is signed, not what. From ecf7b3c9773e8b5b66a3a7faed469fcab44d47ba Mon Sep 17 00:00:00 2001 From: shane-moore Date: Fri, 24 Jul 2026 09:27:10 -0700 Subject: [PATCH 02/20] State stale-Known reorg loss bound and add test vector --- sips/early_randao_preconsensus.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 4993bc4..80bb5d6 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -74,6 +74,8 @@ inclusive on the accept side; strictly greater is IGNORE. This replaces the gene Receivers SHOULD hold next-epoch proposer duties by the tail of each epoch. +A Known view made stale by a boundary-adjacent reorg (including a provisional next-epoch view) IGNOREs an honest early share without retention. This loss class is intentional: the share is not recoverable (the in-slot re-emission is byte-identical, absorbed by the origin's publish-side deduplication and, from a restarted origin, by the receiver's seen-cache), and the cost is bounded by the round-change path, the same residual class as the round-change case in Security Considerations. Retention deliberately excludes Known-and-not-assigned; retaining it would remove the duty-fetch-failure precondition from the quarantine-exhaustion attack and would require re-evaluating the buffer on every duty-view change. Exposure is transient: receiver views converge within a slot or two of a reorg under normal operation (per-slot re-polling or dependent-root-triggered refetch). + **Unknown-duty retention (IGNORE-AND-RETAIN)** A message reaching the duty check in the Unknown state is retained locally before the gossip verdict IGNORE is returned: not forwarded, no penalty, but kept so the seen-cache mark cannot permanently discard it. @@ -98,6 +100,7 @@ Cross-client vectors MUST cover: - earliness boundary at exactly `EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RANDAO_CLOCK_TOLERANCE` (accept) and beyond (IGNORE); - Unknown-epoch retention, promotion, and deletion; Known-unassigned IGNORE; +- the stale-Known reorg path: a share IGNOREd under a pre-reorg Known view is not retained, an identical later copy is dropped by the seen-cache, and a subsequent duty-view update does not resurrect it; - the two-direction ordering exemption with proposals at consecutive slots for one validator; - occupied-key two-tier duplicates and distinct-bytes REJECT; - the eligibility predicate at fork boundaries (first `EARLY_RANDAO_LEAD` slots of a fork epoch ineligible); From 1cd0d6e4a27847dc79ffef2a28de60b068470e38 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Fri, 24 Jul 2026 09:44:16 -0700 Subject: [PATCH 03/20] Clarify per-epoch reuse reachability and evaluation time --- sips/early_randao_preconsensus.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 80bb5d6..521ff6d 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -94,6 +94,8 @@ Shares that are neither promoted nor normally accepted MUST NOT be fed to signat Unchanged. 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. + **Test expectations** Cross-client vectors MUST cover: @@ -101,6 +103,7 @@ Cross-client vectors MUST cover: - earliness boundary at exactly `EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RANDAO_CLOCK_TOLERANCE` (accept) and beyond (IGNORE); - Unknown-epoch retention, promotion, and deletion; Known-unassigned IGNORE; - the stale-Known reorg path: a share IGNOREd under a pre-reorg Known view is not retained, an identical later copy is dropped by the seen-cache, and a subsequent duty-view update does not resurrect it; +- per-epoch reuse: shares stamped `X` accepted under a duty-at-`X` view, the duty moves to `Y` in the same epoch, and the reconstruction serves the proposal at `Y`; - the two-direction ordering exemption with proposals at consecutive slots for one validator; - occupied-key two-tier duplicates and distinct-bytes REJECT; - the eligibility predicate at fork boundaries (first `EARLY_RANDAO_LEAD` slots of a fork epoch ineligible); From daf4c7751dee16eb1b686cb4c72a4f1ce5b21df4 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Fri, 24 Jul 2026 13:22:00 -0700 Subject: [PATCH 04/20] Derive retention capacity and attacker threshold, bound honest demand --- sips/early_randao_preconsensus.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 521ff6d..d45a2be 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -81,13 +81,15 @@ A Known view made stale by a boundary-adjacent reorg (including a provisional ne A message reaching the duty check in the Unknown state is retained locally before the gossip verdict IGNORE is returned: not forwarded, no penalty, but kept so the seen-cache mark cannot permanently discard it. - Admission: passed every rule except duty assignment, and `epoch(msg.slot)` is Unknown. -- Key: (operator, validator, `msg.slot`), at most 1 entry. Global capacity `MAX_QUARANTINED_MESSAGES` per node. +- Key: (operator, validator, `msg.slot`), at most 1 entry. Global capacity `MAX_QUARANTINED_MESSAGES` per node. Sizing: an entry is one signed message (~0.5 KB), bounding memory at ~2 MB. A receiver MAY scope retention to committees it participates in (retained shares are only ever consumed locally; promotion cannot retroactively forward). - Occupied key, distinct bytes: existing two-tier duplicate rule (REJECT from the same delivering peer, IGNORE otherwise); original retained; duplicate-count and ordering state not mutated; quota not recharged. - Eviction when full: greatest `|msg.slot - current_slot|` first; ties by larger `msg.slot`, then higher validator index, then higher operator ID, then lexicographically greater message identifier. - Expiry: delete an entry once a newly received copy would fail the lateness rule. - Promotion: when the epoch's duty fetch completes, each entry is decided immediately: assigned at exactly `msg.slot` promotes, anything else deletes. A promoted message is processed exactly as a newly received accepted message, with all stateful rules applied at promotion time (a normally accepted interim copy for the same (signer, slot) makes the promoted entry the count-limit duplicate). - Accounting: the retention quota mutates at retention time; ordinary validation state does not mutate on the IGNORE path. +Honest demand cannot approach the cap. At any instant only ~7 slot stamps are admissible (the 3-slot proposer lateness TTL behind, `EARLY_RANDAO_LEAD` ahead, the current slot, and tolerances), and an honest entry additionally requires a real proposer duty in an epoch Unknown to the receiver. That bounds honest entries by one SSV proposal per slot network-wide, times at most 13 committee operators, times the ~7-slot span: under 100 entries. The bound is independent of node size (admission is not scoped to local validators, so demand tracks the network's proposal rate) and of outage duration (the admissible window slides and expiry drains it). Reaching the cap requires fabricated slot stamps from a real signer (see Security Considerations). + Shares that are neither promoted nor normally accepted MUST NOT be fed to signature collection or any cache. **Reconstruction and consumption** @@ -116,6 +118,6 @@ Before mainnet activation guidance, testnet measurement MUST quantify early-shar - Deliberately ineligible early emission only starves the attacker's own share (only the signer can produce the bytes). The clock tolerance, Unknown-state retention, and IGNORE-not-REJECT choices keep honest shares out of the permanent-loss class. - After the gossip message-cache window (a few seconds), a stable origin cannot re-deliver a missed early share; receiver restarts and partitions are correlated events. Residual: a live round-1 leader without a reconstruction forces a round change. Mitigations: later emission inside the window, the unconditional in-slot emission (restarted-origin recovery), optional non-normative persistence of received shares, and the mandatory testnet measurement above. - Early reconstruction widens the third-party reaction window between reveal knowability and block publication from roughly 2-4 s to up to 25 s, enabling adaptive bribery, censorship, or targeted DoS when withholding the proposal favors an adversary (one randao bit per affected slot, compounding across slots). Bounded by the existing single-proposal grinding bound; narrowable operationally via later emission, without a protocol change. -- An attacker meeting the retention preconditions (a committee operator sharing more than ~600 validators with the victim, during a duty-fetch failure) can fill the retention capacity and evict legitimate entries; cost is bounded to the evicted shares (round-change path). +- An attacker meeting the retention preconditions (a committee operator sharing more than `MAX_QUARANTINED_MESSAGES` divided by the ~7 admissible slot stamps ≈ 600 validators with the victim, while the victim's view of the stamped epoch is Unknown; milder outages raise the threshold) can fill the retention capacity and evict legitimate entries; cost is bounded to the evicted shares (round-change path). - No amplification: unverifiable messages are never forwarded. Authentication cost is unchanged (operator signature at validation, BLS at consumption). A signer emitting two distinct containers for one (signer, slot) is provably misbehaving and attributable via its operator signature. - No slashing surface: the randao object is not slashable and is a pure function of public data; early signing changes when it is signed, not what. From 4e761cf3e27e087c1e3c658341d90c3f34c47a01 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Fri, 24 Jul 2026 13:46:50 -0700 Subject: [PATCH 05/20] Link ePBS dependency PR and note SIP-0 lifecycle coupling --- sips/early_randao_preconsensus.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index d45a2be..f1c439d 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -1,9 +1,11 @@ | Author | Title | Category | Status | Dependency SIP | Date | | ------ | ----- | -------- | ------ | -------------- | ---- | -| Shane Moore | Early RANDAO Pre-Consensus Emission | Core | draft | ePBS (EIP-7732) Support | 2026-07-23 | +| Shane Moore | Early RANDAO Pre-Consensus Emission | Core | draft | [ePBS (EIP-7732) Support (#94)](https://github.com/ssvlabs/SIPs/pull/94) | 2026-07-23 | [Discussion](https://github.com/ssvlabs/SIPs/discussions/100) +Depends on the ePBS SIP (currently [PR #94](https://github.com/ssvlabs/SIPs/pull/94)); per [SIP-0](sip0.md), this SIP cannot move to last-call before that SIP is approved, and is rejected if it is rejected (unless changed to remove the dependency). + **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 rules plus a bounded receive-side buffer. Activates at the Gloas-aligned SSV fork; the dependency on the ePBS SIP is activation coupling only. From 7afb541d50ddeeaa3a03331f7af1ac0eed361225 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Fri, 24 Jul 2026 13:49:11 -0700 Subject: [PATCH 06/20] Define SLOT_DURATION and cross-reference ePBS-defined symbols --- sips/early_randao_preconsensus.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index f1c439d..1b03623 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -16,14 +16,14 @@ Gloas moves the attestation deadline to 1/4 slot. RANDAO pre-consensus (sign, go **Rationale & Design Goals** -- 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`, which must reconstruct before the slot and is mutable. +- 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 exist to keep honest shares out of that failure class. **Specification** -Notation: `S` is a proposal slot; `slot_start(s)`, `SLOT_DURATION`, `epoch(s)` as usual; `fork_at_slot(s)` maps a slot to its scheduled SSV fork. REJECT penalizes the delivering peer; IGNORE drops without forwarding or penalty. +Notation: `S` is a proposal slot; `slot_start(s)` and `epoch(s)` as usual; `SLOT_DURATION` is the network's beacon-chain seconds-per-slot (unchanged at Gloas, which alters only intra-slot timing); `fork_at_slot(s)` maps a slot to its scheduled SSV fork. REJECT penalizes the delivering peer; IGNORE drops without forwarding or penalty. | Constant | Value | | -------- | ----- | @@ -38,7 +38,7 @@ A qualifying randao partial MUST satisfy all of: - 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: `fork_at_slot(S - EARLY_RANDAO_LEAD) == fork_at_slot(S)` and `epoch(S) >= GLOAS_FORK_EPOCH`. +- eligibility predicate: `fork_at_slot(S - EARLY_RANDAO_LEAD) == fork_at_slot(S)` and `epoch(S) >= GLOAS_FORK_EPOCH` (defined in 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. Proposals in the first `EARLY_RANDAO_LEAD` slots of any SSV fork epoch are ineligible by construction. Containers violating the canonical form fall to existing structural rules (REJECT). Messages failing the predicate, and all non-randao messages, keep today's validation unchanged. From c3d464064cf31db1c8b148628bf69a7b93c87f71 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Sun, 26 Jul 2026 19:39:14 -0700 Subject: [PATCH 07/20] Scope permanent-loss claims to the documented residual cases --- sips/early_randao_preconsensus.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 1b03623..4e73db5 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -12,14 +12,14 @@ Operators may emit their existing block-proposal RANDAO partial signature up to **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. Every failure mode falls back to today's in-slot exchange. Addresses [ssv-spec#373](https://github.com/ssvlabs/ssv-spec/issues/373). +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: that share cannot be re-served in-slot (see *Duty assignment*). Addresses [ssv-spec#373](https://github.com/ssvlabs/ssv-spec/issues/373). **Rationale & Design Goals** - 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 exist to keep honest shares out of that failure class. +- 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) and in Security Considerations. **Specification** @@ -117,8 +117,8 @@ Before mainnet activation guidance, testnet measurement MUST quantify early-shar **Security Considerations** -- Deliberately ineligible early emission only starves the attacker's own share (only the signer can produce the bytes). The clock tolerance, Unknown-state retention, and IGNORE-not-REJECT choices keep honest shares out of the permanent-loss class. -- After the gossip message-cache window (a few seconds), a stable origin cannot re-deliver a missed early share; receiver restarts and partitions are correlated events. Residual: a live round-1 leader without a reconstruction forces a round change. Mitigations: later emission inside the window, the unconditional in-slot emission (restarted-origin recovery), optional non-normative persistence of received shares, and the mandatory testnet measurement above. +- Deliberately ineligible early emission only starves the attacker's own share (only the signer can produce the bytes). The clock tolerance, Unknown-state retention, and IGNORE-not-REJECT choices narrow the permanent-loss class for honest shares but do not empty it: the stale-`Known` reorg path (see *Duty assignment*) and the residuals in the bullets below remain. +- After the gossip message-cache window (a few seconds), a stable origin cannot re-deliver a missed early share; receiver restarts and partitions are correlated events. A restart also drops unpromoted retained entries, since this SIP does not mandate persisting the retention buffer. Residual: a live round-1 leader without a reconstruction forces a round change. Mitigations: later emission inside the window, the unconditional in-slot emission (restarted-origin recovery), optional non-normative persistence of received shares, and the mandatory testnet measurement above. - Early reconstruction widens the third-party reaction window between reveal knowability and block publication from roughly 2-4 s to up to 25 s, enabling adaptive bribery, censorship, or targeted DoS when withholding the proposal favors an adversary (one randao bit per affected slot, compounding across slots). Bounded by the existing single-proposal grinding bound; narrowable operationally via later emission, without a protocol change. - An attacker meeting the retention preconditions (a committee operator sharing more than `MAX_QUARANTINED_MESSAGES` divided by the ~7 admissible slot stamps ≈ 600 validators with the victim, while the victim's view of the stamped epoch is Unknown; milder outages raise the threshold) can fill the retention capacity and evict legitimate entries; cost is bounded to the evicted shares (round-change path). - No amplification: unverifiable messages are never forwarded. Authentication cost is unchanged (operator signature at validation, BLS at consumption). A signer emitting two distinct containers for one (signer, slot) is provably misbehaving and attributable via its operator signature. From 0affc08e9ba4676d5a71176803988699971bbef1 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 27 Jul 2026 15:29:37 -0700 Subject: [PATCH 08/20] Pin validation ordering and stage authentication checks 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. --- sips/early_randao_preconsensus.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 4e73db5..ad4c4d0 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -56,9 +56,11 @@ Operators that never emit early remain fully conformant. **Message validation** -Rule order: structural and canonical-form checks, then signatures, then the rules below. Validation state (duplicate counts, slot high-water marks) mutates only on acceptance or promotion, never on IGNORE. +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; in particular, a candidate failing it under an Unknown duty view is REJECTed, never retained. 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. Validation state (duplicate counts, signer state, slot high-water marks, and epoch counters) mutates only on acceptance or promotion, never on IGNORE. -*Earliness.* A receiver MUST accept a qualifying message's timing iff +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 ```text slot_start(msg.slot) - local_now <= EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RANDAO_CLOCK_TOLERANCE @@ -66,12 +68,12 @@ slot_start(msg.slot) - local_now <= EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RA 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. -*Slot ordering.* Qualifying 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). The exemption applies only after the qualifying-message checks pass; earliness, lateness, the duplicate limit of 1, and canonical form bound these messages instead. +*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 on the receiver's proposer-duty view for `epoch(msg.slot)`: -- Known (a successfully completed fetch for the epoch is cached; an empty result is Known) and assigned at exactly `msg.slot`: accept. -- Known and not assigned: IGNORE. Never REJECT (receiver views can be stale or re-orged). +- Known (a successfully completed fetch for the epoch is cached; an empty result is Known) and assigned at exactly `msg.slot`: the duty-assignment check passes. +- Known and not assigned: the duty-assignment verdict is IGNORE, never REJECT (receiver views can be stale or re-orged); an independently failing rule may still determine the final verdict under the ordering allowance above. - Unknown (no successfully completed fetch cached; failures count as Unknown): IGNORE-AND-RETAIN, below. Receivers SHOULD hold next-epoch proposer duties by the tail of each epoch. @@ -82,7 +84,7 @@ A Known view made stale by a boundary-adjacent reorg (including a provisional ne A message reaching the duty check in the Unknown state is retained locally before the gossip verdict IGNORE is returned: not forwarded, no penalty, but kept so the seen-cache mark cannot permanently discard it. -- Admission: passed every rule except duty assignment, and `epoch(msg.slot)` is Unknown. +- Admission: operator-signature verification and every applicable message-validation rule other than duty assignment have passed, and `epoch(msg.slot)` is Unknown. - Key: (operator, validator, `msg.slot`), at most 1 entry. Global capacity `MAX_QUARANTINED_MESSAGES` per node. Sizing: an entry is one signed message (~0.5 KB), bounding memory at ~2 MB. A receiver MAY scope retention to committees it participates in (retained shares are only ever consumed locally; promotion cannot retroactively forward). - Occupied key, distinct bytes: existing two-tier duplicate rule (REJECT from the same delivering peer, IGNORE otherwise); original retained; duplicate-count and ordering state not mutated; quota not recharged. - Eviction when full: greatest `|msg.slot - current_slot|` first; ties by larger `msg.slot`, then higher validator index, then higher operator ID, then lexicographically greater message identifier. @@ -92,7 +94,7 @@ A message reaching the duty check in the Unknown state is retained locally befor Honest demand cannot approach the cap. At any instant only ~7 slot stamps are admissible (the 3-slot proposer lateness TTL behind, `EARLY_RANDAO_LEAD` ahead, the current slot, and tolerances), and an honest entry additionally requires a real proposer duty in an epoch Unknown to the receiver. That bounds honest entries by one SSV proposal per slot network-wide, times at most 13 committee operators, times the ~7-slot span: under 100 entries. The bound is independent of node size (admission is not scoped to local validators, so demand tracks the network's proposal rate) and of outage duration (the admissible window slides and expiry drains it). Reaching the cap requires fabricated slot stamps from a real signer (see Security Considerations). -Shares that are neither promoted nor normally accepted MUST NOT be fed to signature collection or any cache. +Shares that are neither promoted nor normally accepted MUST NOT be fed to signature collection or any downstream reconstruction cache. **Reconstruction and consumption** @@ -110,7 +112,10 @@ Cross-client vectors MUST cover: - per-epoch reuse: shares stamped `X` accepted under a duty-at-`X` view, the duty moves to `Y` in the same epoch, and the reconstruction serves the proposal at `Y`; - the two-direction ordering exemption with proposals at consecutive slots for one validator; - occupied-key two-tier duplicates and distinct-bytes REJECT; +- an operator-authenticated share whose BLS signature is invalid is discarded at consumption and contributes to no reconstructed reveal; reconstruction still succeeds from a valid threshold of honest shares; - the eligibility predicate at fork boundaries (first `EARLY_RANDAO_LEAD` slots of a fork epoch ineligible); +- multi-fault precedence: a structurally valid message failing both operator-signature verification and a non-retaining contextual check (e.g. invalid signature plus Known-unassigned, or invalid signature plus too-early) may produce either the contextual verdict or REJECT, but is never retained, accepted, forwarded, collected, or state-mutating; +- invalid operator signature with an Unknown duty view: REJECT, never retained; - restart cases, late mesh join, cross-epoch stamps. 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 MUST demonstrate round-2 viability under Gloas timing, meaning block publication before the post-Gloas useful-block deadline. @@ -121,5 +126,5 @@ Before mainnet activation guidance, testnet measurement MUST quantify early-shar - After the gossip message-cache window (a few seconds), a stable origin cannot re-deliver a missed early share; receiver restarts and partitions are correlated events. A restart also drops unpromoted retained entries, since this SIP does not mandate persisting the retention buffer. Residual: a live round-1 leader without a reconstruction forces a round change. Mitigations: later emission inside the window, the unconditional in-slot emission (restarted-origin recovery), optional non-normative persistence of received shares, and the mandatory testnet measurement above. - Early reconstruction widens the third-party reaction window between reveal knowability and block publication from roughly 2-4 s to up to 25 s, enabling adaptive bribery, censorship, or targeted DoS when withholding the proposal favors an adversary (one randao bit per affected slot, compounding across slots). Bounded by the existing single-proposal grinding bound; narrowable operationally via later emission, without a protocol change. - An attacker meeting the retention preconditions (a committee operator sharing more than `MAX_QUARANTINED_MESSAGES` divided by the ~7 admissible slot stamps ≈ 600 validators with the victim, while the victim's view of the stamped epoch is Unknown; milder outages raise the threshold) can fill the retention capacity and evict legitimate entries; cost is bounded to the evicted shares (round-change path). -- No amplification: unverifiable messages are never forwarded. Authentication cost is unchanged (operator signature at validation, BLS at consumption). A signer emitting two distinct containers for one (signer, slot) is provably misbehaving and attributable via its operator signature. +- No unauthenticated amplification: messages failing operator-signature verification are never forwarded or retained; authentication cost is unchanged (operator signature at validation, BLS at consumption). An operator-authenticated share whose BLS signature proves invalid at consumption is attributable and MUST be discarded without contributing to a reconstructed reveal. A signer emitting two distinct containers for one (signer, slot) is provably misbehaving and attributable via its operator signature. - No slashing surface: the randao object is not slashable and is a pure function of public data; early signing changes when it is signed, not what. From 60d143ea52d3ffcc27b7585687e580cd7a3f2753 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 27 Jul 2026 15:30:29 -0700 Subject: [PATCH 09/20] Make duty-view and retention behavior deterministic 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. --- sips/early_randao_preconsensus.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index ad4c4d0..9359e29 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -72,9 +72,9 @@ inclusive on the accept side; strictly greater is IGNORE. This replaces the gene *Duty assignment.* Tri-state on the receiver's proposer-duty view for `epoch(msg.slot)`: -- Known (a successfully completed fetch for the epoch is cached; an empty result is Known) and assigned at exactly `msg.slot`: the duty-assignment check passes. +- Known (a successfully completed fetch of the epoch's complete proposer schedule is cached; a locally filtered subset does not constitute a Known view) and assigned at exactly `msg.slot`: the duty-assignment check passes. - Known and not assigned: the duty-assignment verdict is IGNORE, never REJECT (receiver views can be stale or re-orged); an independently failing rule may still determine the final verdict under the ordering allowance above. -- Unknown (no successfully completed fetch cached; failures count as Unknown): IGNORE-AND-RETAIN, below. +- Unknown (no successfully completed fetch cached; failures count as Unknown): IGNORE; retained only if admitted under the rules below. Receivers SHOULD hold next-epoch proposer duties by the tail of each epoch. @@ -82,14 +82,15 @@ A Known view made stale by a boundary-adjacent reorg (including a provisional ne **Unknown-duty retention (IGNORE-AND-RETAIN)** -A message reaching the duty check in the Unknown state is retained locally before the gossip verdict IGNORE is returned: not forwarded, no penalty, but kept so the seen-cache mark cannot permanently discard it. +A candidate whose proposer-duty view is Unknown and that meets the admission conditions below is considered for retention under the occupied-key and capacity rules below; if admitted, it is retained locally before the gossip verdict IGNORE is returned: not forwarded, no penalty, but kept so the seen-cache mark cannot permanently discard it. - Admission: operator-signature verification and every applicable message-validation rule other than duty assignment have passed, and `epoch(msg.slot)` is Unknown. - Key: (operator, validator, `msg.slot`), at most 1 entry. Global capacity `MAX_QUARANTINED_MESSAGES` per node. Sizing: an entry is one signed message (~0.5 KB), bounding memory at ~2 MB. A receiver MAY scope retention to committees it participates in (retained shares are only ever consumed locally; promotion cannot retroactively forward). +- Occupied key, byte-identical content: IGNORE; the original entry and its metadata are kept unchanged. - Occupied key, distinct bytes: existing two-tier duplicate rule (REJECT from the same delivering peer, IGNORE otherwise); original retained; duplicate-count and ordering state not mutated; quota not recharged. -- Eviction when full: greatest `|msg.slot - current_slot|` first; ties by larger `msg.slot`, then higher validator index, then higher operator ID, then lexicographically greater message identifier. +- Eviction when full: the incoming candidate competes with the stored entries; the greatest `|msg.slot - current_slot|` is evicted first, ties by larger `msg.slot`, then higher validator index, then higher operator ID (the key fields make further ties impossible). A candidate that loses the comparison is not admitted (IGNORE). - Expiry: delete an entry once a newly received copy would fail the lateness rule. -- Promotion: when the epoch's duty fetch completes, each entry is decided immediately: assigned at exactly `msg.slot` promotes, anything else deletes. A promoted message is processed exactly as a newly received accepted message, with all stateful rules applied at promotion time (a normally accepted interim copy for the same (signer, slot) makes the promoted entry the count-limit duplicate). +- Promotion: when the epoch's duty fetch completes, each entry is decided immediately: an entry assigned at exactly `msg.slot` becomes a promotion candidate, anything else deletes. A candidate is reprocessed exactly as a newly received message, with all stateful rules applied at reprocessing time; it promotes only if that revalidation succeeds and is deleted otherwise (a normally accepted interim copy for the same (signer, slot) makes the candidate the count-limit duplicate, so it deletes). - Accounting: the retention quota mutates at retention time; ordinary validation state does not mutate on the IGNORE path. Honest demand cannot approach the cap. At any instant only ~7 slot stamps are admissible (the 3-slot proposer lateness TTL behind, `EARLY_RANDAO_LEAD` ahead, the current slot, and tolerances), and an honest entry additionally requires a real proposer duty in an epoch Unknown to the receiver. That bounds honest entries by one SSV proposal per slot network-wide, times at most 13 committee operators, times the ~7-slot span: under 100 entries. The bound is independent of node size (admission is not scoped to local validators, so demand tracks the network's proposal rate) and of outage duration (the admissible window slides and expiry drains it). Reaching the cap requires fabricated slot stamps from a real signer (see Security Considerations). @@ -109,9 +110,11 @@ Cross-client vectors MUST cover: - earliness boundary at exactly `EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RANDAO_CLOCK_TOLERANCE` (accept) and beyond (IGNORE); - Unknown-epoch retention, promotion, and deletion; Known-unassigned IGNORE; - the stale-Known reorg path: a share IGNOREd under a pre-reorg Known view is not retained, an identical later copy is dropped by the seen-cache, and a subsequent duty-view update does not resurrect it; -- per-epoch reuse: shares stamped `X` accepted under a duty-at-`X` view, the duty moves to `Y` in the same epoch, and the reconstruction serves the proposal at `Y`; +- same-epoch duty move: shares stamped `X` accepted under a duty-at-`X` view, the duty moves to `Y` in the same epoch, and the proposal at `Y` completes (served from the `X` reconstruction where the implementation reuses; via the in-slot exchange otherwise); - the two-direction ordering exemption with proposals at consecutive slots for one validator; -- occupied-key two-tier duplicates and distinct-bytes REJECT; +- occupied-key duplicates: byte-identical IGNORE-keep-original, distinct-bytes two-tier (same-peer REJECT, otherwise IGNORE); +- full-capacity eviction with the incoming candidate winning and losing the comparison; +- Known completeness: a complete fetched proposer schedule is Known; a view filtered to a local validator subset is not, and must not IGNORE an honest share as Known-unassigned; - an operator-authenticated share whose BLS signature is invalid is discarded at consumption and contributes to no reconstructed reveal; reconstruction still succeeds from a valid threshold of honest shares; - the eligibility predicate at fork boundaries (first `EARLY_RANDAO_LEAD` slots of a fork epoch ineligible); - multi-fault precedence: a structurally valid message failing both operator-signature verification and a non-retaining contextual check (e.g. invalid signature plus Known-unassigned, or invalid signature plus too-early) may produce either the contextual verdict or REJECT, but is never retained, accepted, forwarded, collected, or state-mutating; From 01a14f17a5657a90518dc7bdef49d73f1606eda4 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 27 Jul 2026 15:30:51 -0700 Subject: [PATCH 10/20] Gate activation on the Ethereum Gloas fork epoch The summary said the SIP activates at the Gloas-aligned SSV fork, but the ePBS SIP (#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. --- sips/early_randao_preconsensus.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 9359e29..04b1003 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -8,7 +8,7 @@ Depends on the ePBS SIP (currently [PR #94](https://github.com/ssvlabs/SIPs/pull **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 rules plus a bounded receive-side buffer. Activates at the Gloas-aligned SSV fork; the dependency on the ePBS SIP is activation coupling only. +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. **Motivation** @@ -38,9 +38,9 @@ A qualifying randao partial MUST satisfy all of: - 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: `fork_at_slot(S - EARLY_RANDAO_LEAD) == fork_at_slot(S)` and `epoch(S) >= GLOAS_FORK_EPOCH` (defined in the ePBS SIP, [#94](https://github.com/ssvlabs/SIPs/pull/94)). +- eligibility predicate: `S >= EARLY_RANDAO_LEAD`, `fork_at_slot(S - EARLY_RANDAO_LEAD) == fork_at_slot(S)`, 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. Proposals in the first `EARLY_RANDAO_LEAD` slots of any SSV fork epoch are ineligible by construction. Containers violating the canonical form fall to existing structural rules (REJECT). Messages failing the predicate, and all non-randao messages, keep today's validation unchanged. +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. `fork_at_slot` refers to the SSV fork schedule; the equality conjunct is window-boundary protection, not an activation gate: proposals in the first `EARLY_RANDAO_LEAD` slots of any SSV fork activation epoch are ineligible by construction, and the `S >= EARLY_RANDAO_LEAD` conjunct excludes the first `EARLY_RANDAO_LEAD` slots at genesis. 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. **Producer behavior** @@ -116,7 +116,8 @@ Cross-client vectors MUST cover: - full-capacity eviction with the incoming candidate winning and losing the comparison; - Known completeness: a complete fetched proposer schedule is Known; a view filtered to a local validator subset is not, and must not IGNORE an honest share as Known-unassigned; - an operator-authenticated share whose BLS signature is invalid is discarded at consumption and contributes to no reconstructed reveal; reconstruction still succeeds from a valid threshold of honest shares; -- the eligibility predicate at fork boundaries (first `EARLY_RANDAO_LEAD` slots of a fork epoch ineligible); +- the eligibility predicate at genesis and later fork boundaries (slots below `EARLY_RANDAO_LEAD` and the first `EARLY_RANDAO_LEAD` slots of a fork epoch ineligible); +- activation gating: `epoch(S)` immediately before `GLOAS_FORK_EPOCH` is ineligible; at a non-genesis `GLOAS_FORK_EPOCH` with no SSV fork scheduled there, eligible from the epoch's first slot (a genesis `GLOAS_FORK_EPOCH` is covered by the slots-below-`EARLY_RANDAO_LEAD` vector above); with an SSV fork scheduled there, its first `EARLY_RANDAO_LEAD` slots are ineligible; - multi-fault precedence: a structurally valid message failing both operator-signature verification and a non-retaining contextual check (e.g. invalid signature plus Known-unassigned, or invalid signature plus too-early) may produce either the contextual verdict or REJECT, but is never retained, accepted, forwarded, collected, or state-mutating; - invalid operator signature with an Unknown duty view: REJECT, never retained; - restart cases, late mesh join, cross-epoch stamps. From ab0ce628fc10ca350a5591b50a1f46a605f2a12e Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 27 Jul 2026 19:44:26 -0700 Subject: [PATCH 11/20] Drop the SSV fork conjunct from the eligibility predicate 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. --- sips/early_randao_preconsensus.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 04b1003..211e814 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -23,7 +23,7 @@ Gloas moves the attestation deadline to 1/4 slot. RANDAO pre-consensus (sign, go **Specification** -Notation: `S` is a proposal slot; `slot_start(s)` and `epoch(s)` as usual; `SLOT_DURATION` is the network's beacon-chain seconds-per-slot (unchanged at Gloas, which alters only intra-slot timing); `fork_at_slot(s)` maps a slot to its scheduled SSV fork. REJECT penalizes the delivering peer; IGNORE drops without forwarding or penalty. +Notation: `S` is a proposal slot; `slot_start(s)` and `epoch(s)` as usual; `SLOT_DURATION` is the network's beacon-chain seconds-per-slot (unchanged at Gloas, which alters only intra-slot timing). REJECT penalizes the delivering peer; IGNORE drops without forwarding or penalty. | Constant | Value | | -------- | ----- | @@ -38,9 +38,9 @@ A qualifying randao partial MUST satisfy all of: - 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`, `fork_at_slot(S - EARLY_RANDAO_LEAD) == fork_at_slot(S)`, 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)). +- 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. `fork_at_slot` refers to the SSV fork schedule; the equality conjunct is window-boundary protection, not an activation gate: proposals in the first `EARLY_RANDAO_LEAD` slots of any SSV fork activation epoch are ineligible by construction, and the `S >= EARLY_RANDAO_LEAD` conjunct excludes the first `EARLY_RANDAO_LEAD` slots at genesis. 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. +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. **Producer behavior** @@ -116,8 +116,8 @@ Cross-client vectors MUST cover: - full-capacity eviction with the incoming candidate winning and losing the comparison; - Known completeness: a complete fetched proposer schedule is Known; a view filtered to a local validator subset is not, and must not IGNORE an honest share as Known-unassigned; - an operator-authenticated share whose BLS signature is invalid is discarded at consumption and contributes to no reconstructed reveal; reconstruction still succeeds from a valid threshold of honest shares; -- the eligibility predicate at genesis and later fork boundaries (slots below `EARLY_RANDAO_LEAD` and the first `EARLY_RANDAO_LEAD` slots of a fork epoch ineligible); -- activation gating: `epoch(S)` immediately before `GLOAS_FORK_EPOCH` is ineligible; at a non-genesis `GLOAS_FORK_EPOCH` with no SSV fork scheduled there, eligible from the epoch's first slot (a genesis `GLOAS_FORK_EPOCH` is covered by the slots-below-`EARLY_RANDAO_LEAD` vector above); with an SSV fork scheduled there, its first `EARLY_RANDAO_LEAD` slots are ineligible; +- the eligibility predicate at genesis: slots below `EARLY_RANDAO_LEAD` are ineligible even when `GLOAS_FORK_EPOCH` is 0; +- activation gating: `epoch(S)` immediately before `GLOAS_FORK_EPOCH` is ineligible; a non-genesis `GLOAS_FORK_EPOCH` is eligible from its first slot, whose emission window starts before the fork, and an SSV fork scheduled anywhere does not change that; - multi-fault precedence: a structurally valid message failing both operator-signature verification and a non-retaining contextual check (e.g. invalid signature plus Known-unassigned, or invalid signature plus too-early) may produce either the contextual verdict or REJECT, but is never retained, accepted, forwarded, collected, or state-mutating; - invalid operator signature with an Unknown duty view: REJECT, never retained; - restart cases, late mesh join, cross-epoch stamps. From cd3a6f1472b7e422f34cfcc655c1eaab11528cbd Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 29 Jul 2026 08:02:37 -0700 Subject: [PATCH 12/20] Define duty-view authority by validator coverage --- sips/early_randao_preconsensus.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 211e814..30d4cf8 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -70,11 +70,13 @@ inclusive on the accept side; strictly greater is IGNORE. This replaces the gene *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 on the receiver's proposer-duty view for `epoch(msg.slot)`: +*Duty assignment.* Tri-state, evaluated per candidate against the receiver's proposer-duty view for `epoch(msg.slot)` and the message's validator: -- Known (a successfully completed fetch of the epoch's complete proposer schedule is cached; a locally filtered subset does not constitute a Known view) and assigned at exactly `msg.slot`: the duty-assignment check passes. +- Known (a cached view is authoritative for the candidate; see below) and assigned at exactly `msg.slot`: the duty-assignment check passes. - Known and not assigned: the duty-assignment verdict is IGNORE, never REJECT (receiver views can be stale or re-orged); an independently failing rule may still determine the final verdict under the ordering allowance above. -- Unknown (no successfully completed fetch cached; failures count as Unknown): IGNORE; retained only if admitted under the rules below. +- Unknown (otherwise): IGNORE; retained only if admitted under the rules below. + +A cached view is authoritative for a candidate iff a successfully completed proposer-duty fetch for `epoch(msg.slot)` is cached whose requested coverage includes the message's validator, evaluated against the receiver's current registry state: a fetch that predates the receiver's most recent validator-set change is not authoritative for validators affected by that change. A reorg does not revoke authority: a view made stale by a reorg remains Known, with the loss class described below. A complete-schedule fetch covers every validator, but a response without exactly `SLOTS_PER_EPOCH` distinct in-epoch slots is a fetch failure. A registry-filtered fetch covers exactly the validators it requested, even when the filtered result is empty; in particular, a view covering only the operator's own validators MUST NOT yield a not-assigned verdict for claimants outside it. Receivers SHOULD hold next-epoch proposer duties by the tail of each epoch. @@ -84,16 +86,16 @@ A Known view made stale by a boundary-adjacent reorg (including a provisional ne A candidate whose proposer-duty view is Unknown and that meets the admission conditions below is considered for retention under the occupied-key and capacity rules below; if admitted, it is retained locally before the gossip verdict IGNORE is returned: not forwarded, no penalty, but kept so the seen-cache mark cannot permanently discard it. -- Admission: operator-signature verification and every applicable message-validation rule other than duty assignment have passed, and `epoch(msg.slot)` is Unknown. +- Admission: operator-signature verification and every applicable message-validation rule other than duty assignment have passed, and the candidate's duty view is Unknown. - Key: (operator, validator, `msg.slot`), at most 1 entry. Global capacity `MAX_QUARANTINED_MESSAGES` per node. Sizing: an entry is one signed message (~0.5 KB), bounding memory at ~2 MB. A receiver MAY scope retention to committees it participates in (retained shares are only ever consumed locally; promotion cannot retroactively forward). - Occupied key, byte-identical content: IGNORE; the original entry and its metadata are kept unchanged. - Occupied key, distinct bytes: existing two-tier duplicate rule (REJECT from the same delivering peer, IGNORE otherwise); original retained; duplicate-count and ordering state not mutated; quota not recharged. - Eviction when full: the incoming candidate competes with the stored entries; the greatest `|msg.slot - current_slot|` is evicted first, ties by larger `msg.slot`, then higher validator index, then higher operator ID (the key fields make further ties impossible). A candidate that loses the comparison is not admitted (IGNORE). - Expiry: delete an entry once a newly received copy would fail the lateness rule. -- Promotion: when the epoch's duty fetch completes, each entry is decided immediately: an entry assigned at exactly `msg.slot` becomes a promotion candidate, anything else deletes. A candidate is reprocessed exactly as a newly received message, with all stateful rules applied at reprocessing time; it promotes only if that revalidation succeeds and is deleted otherwise (a normally accepted interim copy for the same (signer, slot) makes the candidate the count-limit duplicate, so it deletes). +- Promotion: when a newly installed view is authoritative for a retained entry, that entry is decided immediately: an entry assigned at exactly `msg.slot` becomes a promotion candidate, anything else deletes. A candidate is reprocessed exactly as a newly received message, with all stateful rules applied at reprocessing time; it promotes only if that revalidation succeeds and is deleted otherwise (a normally accepted interim copy for the same (signer, slot) makes the candidate the count-limit duplicate, so it deletes). - Accounting: the retention quota mutates at retention time; ordinary validation state does not mutate on the IGNORE path. -Honest demand cannot approach the cap. At any instant only ~7 slot stamps are admissible (the 3-slot proposer lateness TTL behind, `EARLY_RANDAO_LEAD` ahead, the current slot, and tolerances), and an honest entry additionally requires a real proposer duty in an epoch Unknown to the receiver. That bounds honest entries by one SSV proposal per slot network-wide, times at most 13 committee operators, times the ~7-slot span: under 100 entries. The bound is independent of node size (admission is not scoped to local validators, so demand tracks the network's proposal rate) and of outage duration (the admissible window slides and expiry drains it). Reaching the cap requires fabricated slot stamps from a real signer (see Security Considerations). +Honest demand cannot approach the cap. At any instant only ~7 slot stamps are admissible (the 3-slot proposer lateness TTL behind, `EARLY_RANDAO_LEAD` ahead, the current slot, and tolerances), and an honest entry additionally requires a real proposer duty whose duty view at the receiver is Unknown. That bounds honest entries by one SSV proposal per slot network-wide, times at most 13 committee operators, times the ~7-slot span: under 100 entries. The bound is independent of node size (admission is not scoped to local validators, so demand tracks the network's proposal rate) and of outage duration (the admissible window slides and expiry drains it). Reaching the cap requires fabricated slot stamps from a real signer (see Security Considerations). Shares that are neither promoted nor normally accepted MUST NOT be fed to signature collection or any downstream reconstruction cache. @@ -108,13 +110,13 @@ Unchanged. The reconstructed reveal is a per-epoch value: an implementation MAY Cross-client vectors MUST cover: - earliness boundary at exactly `EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RANDAO_CLOCK_TOLERANCE` (accept) and beyond (IGNORE); -- Unknown-epoch retention, promotion, and deletion; Known-unassigned IGNORE; +- Unknown-view retention, promotion, and deletion; Known-unassigned IGNORE; - the stale-Known reorg path: a share IGNOREd under a pre-reorg Known view is not retained, an identical later copy is dropped by the seen-cache, and a subsequent duty-view update does not resurrect it; - same-epoch duty move: shares stamped `X` accepted under a duty-at-`X` view, the duty moves to `Y` in the same epoch, and the proposal at `Y` completes (served from the `X` reconstruction where the implementation reuses; via the in-slot exchange otherwise); - the two-direction ordering exemption with proposals at consecutive slots for one validator; - occupied-key duplicates: byte-identical IGNORE-keep-original, distinct-bytes two-tier (same-peer REJECT, otherwise IGNORE); - full-capacity eviction with the incoming candidate winning and losing the comparison; -- Known completeness: a complete fetched proposer schedule is Known; a view filtered to a local validator subset is not, and must not IGNORE an honest share as Known-unassigned; +- Known coverage: a registry-filtered fetch covering every admissible claimant is authoritative, and a covered validator absent from its result is not-assigned; a view scoped to the operator's own validators is not authoritative for an out-of-coverage claimant and must not IGNORE its honest share as Known-unassigned; a claimant registered after the cached fetch takes the Unknown path until a refresh covers it; a complete-schedule response without exactly `SLOTS_PER_EPOCH` distinct in-epoch slots establishes no view; - an operator-authenticated share whose BLS signature is invalid is discarded at consumption and contributes to no reconstructed reveal; reconstruction still succeeds from a valid threshold of honest shares; - the eligibility predicate at genesis: slots below `EARLY_RANDAO_LEAD` are ineligible even when `GLOAS_FORK_EPOCH` is 0; - activation gating: `epoch(S)` immediately before `GLOAS_FORK_EPOCH` is ineligible; a non-genesis `GLOAS_FORK_EPOCH` is eligible from its first slot, whose emission window starts before the fork, and an SSV fork scheduled anywhere does not change that; From d326a08be509776d41cf96da484d0dae86d0ddfb Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 29 Jul 2026 08:35:45 -0700 Subject: [PATCH 13/20] Restrict retention admission to fetch outages --- sips/early_randao_preconsensus.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 30d4cf8..a74686e 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -19,7 +19,7 @@ Gloas moves the attestation deadline to 1/4 slot. RANDAO pre-consensus (sign, go - 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) and in Security Considerations. +- 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. **Specification** @@ -76,7 +76,7 @@ inclusive on the accept side; strictly greater is IGNORE. This replaces the gene - Known and not assigned: the duty-assignment verdict is IGNORE, never REJECT (receiver views can be stale or re-orged); an independently failing rule may still determine the final verdict under the ordering allowance above. - Unknown (otherwise): IGNORE; retained only if admitted under the rules below. -A cached view is authoritative for a candidate iff a successfully completed proposer-duty fetch for `epoch(msg.slot)` is cached whose requested coverage includes the message's validator, evaluated against the receiver's current registry state: a fetch that predates the receiver's most recent validator-set change is not authoritative for validators affected by that change. A reorg does not revoke authority: a view made stale by a reorg remains Known, with the loss class described below. A complete-schedule fetch covers every validator, but a response without exactly `SLOTS_PER_EPOCH` distinct in-epoch slots is a fetch failure. A registry-filtered fetch covers exactly the validators it requested, even when the filtered result is empty; in particular, a view covering only the operator's own validators MUST NOT yield a not-assigned verdict for claimants outside it. +A cached view is authoritative for a candidate iff a successfully completed proposer-duty fetch for `epoch(msg.slot)` is cached whose requested coverage includes the message's validator, evaluated against the receiver's current registry state: a fetch that predates the receiver's most recent validator-set change is not authoritative for validators affected by that change. A reorg does not revoke authority: a view made stale by a reorg remains Known, with the loss class described below. A complete-schedule fetch covers every validator, but a response without exactly `SLOTS_PER_EPOCH` distinct in-epoch slots is a fetch failure. A provisional response (one flagged execution-optimistic) installs no view. A registry-filtered fetch covers exactly the validators it requested, even when the filtered result is empty; in particular, a view covering only the operator's own validators MUST NOT yield a not-assigned verdict for claimants outside it. Receivers SHOULD hold next-epoch proposer duties by the tail of each epoch. @@ -86,7 +86,7 @@ A Known view made stale by a boundary-adjacent reorg (including a provisional ne A candidate whose proposer-duty view is Unknown and that meets the admission conditions below is considered for retention under the occupied-key and capacity rules below; if admitted, it is retained locally before the gossip verdict IGNORE is returned: not forwarded, no penalty, but kept so the seen-cache mark cannot permanently discard it. -- Admission: operator-signature verification and every applicable message-validation rule other than duty assignment have passed, and the candidate's duty view is Unknown. +- Admission: operator-signature verification and every applicable message-validation rule other than duty assignment have passed, the candidate's duty view is Unknown, and the most recent completed fetch attempt for `epoch(msg.slot)` did not succeed (or none has completed). Attempts are ordered by initiation, so a result arriving for a superseded attempt is discarded for this state; a provisional response counts as a succeeded attempt here, so a long provisional period cannot hold the epoch retention-eligible. A candidate left Unknown by a succeeding fetch (outside its coverage, or affected by a later validator-set change) is IGNOREd without retention: a refresh is already due, and the exposure is the same transient class as the reorg path. - Key: (operator, validator, `msg.slot`), at most 1 entry. Global capacity `MAX_QUARANTINED_MESSAGES` per node. Sizing: an entry is one signed message (~0.5 KB), bounding memory at ~2 MB. A receiver MAY scope retention to committees it participates in (retained shares are only ever consumed locally; promotion cannot retroactively forward). - Occupied key, byte-identical content: IGNORE; the original entry and its metadata are kept unchanged. - Occupied key, distinct bytes: existing two-tier duplicate rule (REJECT from the same delivering peer, IGNORE otherwise); original retained; duplicate-count and ordering state not mutated; quota not recharged. @@ -95,7 +95,7 @@ A candidate whose proposer-duty view is Unknown and that meets the admission con - Promotion: when a newly installed view is authoritative for a retained entry, that entry is decided immediately: an entry assigned at exactly `msg.slot` becomes a promotion candidate, anything else deletes. A candidate is reprocessed exactly as a newly received message, with all stateful rules applied at reprocessing time; it promotes only if that revalidation succeeds and is deleted otherwise (a normally accepted interim copy for the same (signer, slot) makes the candidate the count-limit duplicate, so it deletes). - Accounting: the retention quota mutates at retention time; ordinary validation state does not mutate on the IGNORE path. -Honest demand cannot approach the cap. At any instant only ~7 slot stamps are admissible (the 3-slot proposer lateness TTL behind, `EARLY_RANDAO_LEAD` ahead, the current slot, and tolerances), and an honest entry additionally requires a real proposer duty whose duty view at the receiver is Unknown. That bounds honest entries by one SSV proposal per slot network-wide, times at most 13 committee operators, times the ~7-slot span: under 100 entries. The bound is independent of node size (admission is not scoped to local validators, so demand tracks the network's proposal rate) and of outage duration (the admissible window slides and expiry drains it). Reaching the cap requires fabricated slot stamps from a real signer (see Security Considerations). +Honest demand cannot approach the cap. At any instant only ~7 slot stamps are admissible (the 3-slot proposer lateness TTL behind, `EARLY_RANDAO_LEAD` ahead, the current slot, and tolerances), and an honest entry additionally requires a real proposer duty in an epoch the receiver has not successfully fetched. That bounds honest entries by one SSV proposal per slot network-wide, times at most 13 committee operators, times the ~7-slot span: under 100 entries. The bound is independent of node size (admission is not scoped to local validators, so demand tracks the network's proposal rate) and of outage duration (the admissible window slides and expiry drains it). Reaching the cap requires fabricated slot stamps from a real signer (see Security Considerations). Shares that are neither promoted nor normally accepted MUST NOT be fed to signature collection or any downstream reconstruction cache. @@ -116,7 +116,8 @@ Cross-client vectors MUST cover: - the two-direction ordering exemption with proposals at consecutive slots for one validator; - occupied-key duplicates: byte-identical IGNORE-keep-original, distinct-bytes two-tier (same-peer REJECT, otherwise IGNORE); - full-capacity eviction with the incoming candidate winning and losing the comparison; -- Known coverage: a registry-filtered fetch covering every admissible claimant is authoritative, and a covered validator absent from its result is not-assigned; a view scoped to the operator's own validators is not authoritative for an out-of-coverage claimant and must not IGNORE its honest share as Known-unassigned; a claimant registered after the cached fetch takes the Unknown path until a refresh covers it; a complete-schedule response without exactly `SLOTS_PER_EPOCH` distinct in-epoch slots establishes no view; +- Known coverage: a registry-filtered fetch covering every admissible claimant is authoritative, and a covered validator absent from its result is not-assigned; a view scoped to the operator's own validators is not authoritative for an out-of-coverage claimant and must not IGNORE its honest share as Known-unassigned; a claimant registered after the cached fetch takes the Unknown path, without retention, until a refresh covers it; a complete-schedule response without exactly `SLOTS_PER_EPOCH` distinct in-epoch slots establishes no view; +- retention admission: a candidate in a never-fetched epoch retains and one in an epoch whose latest refresh failed retains, while one left Unknown by a succeeding fetch does not; a late failure result from a superseded attempt does not make the epoch retention-eligible; a provisional (execution-optimistic) response ends eligibility while installing no view; - an operator-authenticated share whose BLS signature is invalid is discarded at consumption and contributes to no reconstructed reveal; reconstruction still succeeds from a valid threshold of honest shares; - the eligibility predicate at genesis: slots below `EARLY_RANDAO_LEAD` are ineligible even when `GLOAS_FORK_EPOCH` is 0; - activation gating: `epoch(S)` immediately before `GLOAS_FORK_EPOCH` is ineligible; a non-genesis `GLOAS_FORK_EPOCH` is eligible from its first slot, whose emission window starts before the fork, and an SSV fork scheduled anywhere does not change that; @@ -128,9 +129,9 @@ Before mainnet activation guidance, testnet measurement MUST quantify early-shar **Security Considerations** -- Deliberately ineligible early emission only starves the attacker's own share (only the signer can produce the bytes). The clock tolerance, Unknown-state retention, and IGNORE-not-REJECT choices narrow the permanent-loss class for honest shares but do not empty it: the stale-`Known` reorg path (see *Duty assignment*) and the residuals in the bullets below remain. +- Deliberately ineligible early emission only starves the attacker's own share (only the signer can produce the bytes). The clock tolerance, Unknown-state retention, and IGNORE-not-REJECT choices narrow the permanent-loss class for honest shares but do not empty it: the stale-`Known` reorg path, the refresh-pending admission exclusion, and the residuals in the bullets below remain. - After the gossip message-cache window (a few seconds), a stable origin cannot re-deliver a missed early share; receiver restarts and partitions are correlated events. A restart also drops unpromoted retained entries, since this SIP does not mandate persisting the retention buffer. Residual: a live round-1 leader without a reconstruction forces a round change. Mitigations: later emission inside the window, the unconditional in-slot emission (restarted-origin recovery), optional non-normative persistence of received shares, and the mandatory testnet measurement above. - Early reconstruction widens the third-party reaction window between reveal knowability and block publication from roughly 2-4 s to up to 25 s, enabling adaptive bribery, censorship, or targeted DoS when withholding the proposal favors an adversary (one randao bit per affected slot, compounding across slots). Bounded by the existing single-proposal grinding bound; narrowable operationally via later emission, without a protocol change. -- An attacker meeting the retention preconditions (a committee operator sharing more than `MAX_QUARANTINED_MESSAGES` divided by the ~7 admissible slot stamps ≈ 600 validators with the victim, while the victim's view of the stamped epoch is Unknown; milder outages raise the threshold) can fill the retention capacity and evict legitimate entries; cost is bounded to the evicted shares (round-change path). +- An attacker meeting the retention preconditions (a committee operator sharing more than `MAX_QUARANTINED_MESSAGES` divided by the ~7 admissible slot stamps ≈ 600 validators with the victim, while the victim has no successful fetch for the stamped epoch; milder outages raise the threshold) can fill the retention capacity and evict legitimate entries; cost is bounded to the evicted shares (round-change path). - No unauthenticated amplification: messages failing operator-signature verification are never forwarded or retained; authentication cost is unchanged (operator signature at validation, BLS at consumption). An operator-authenticated share whose BLS signature proves invalid at consumption is attributable and MUST be discarded without contributing to a reconstructed reveal. A signer emitting two distinct containers for one (signer, slot) is provably misbehaving and attributable via its operator signature. - No slashing surface: the randao object is not slashable and is a pure function of public data; early signing changes when it is signed, not what. From 473dc0a619f91848e6eec69562eafe67e32a15d4 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 29 Jul 2026 09:10:58 -0700 Subject: [PATCH 14/20] Break eviction ties by validator pubkey --- sips/early_randao_preconsensus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index a74686e..0e60d40 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -90,7 +90,7 @@ A candidate whose proposer-duty view is Unknown and that meets the admission con - Key: (operator, validator, `msg.slot`), at most 1 entry. Global capacity `MAX_QUARANTINED_MESSAGES` per node. Sizing: an entry is one signed message (~0.5 KB), bounding memory at ~2 MB. A receiver MAY scope retention to committees it participates in (retained shares are only ever consumed locally; promotion cannot retroactively forward). - Occupied key, byte-identical content: IGNORE; the original entry and its metadata are kept unchanged. - Occupied key, distinct bytes: existing two-tier duplicate rule (REJECT from the same delivering peer, IGNORE otherwise); original retained; duplicate-count and ordering state not mutated; quota not recharged. -- Eviction when full: the incoming candidate competes with the stored entries; the greatest `|msg.slot - current_slot|` is evicted first, ties by larger `msg.slot`, then higher validator index, then higher operator ID (the key fields make further ties impossible). A candidate that loses the comparison is not admitted (IGNORE). +- Eviction when full: the incoming candidate competes with the stored entries; the greatest `|msg.slot - current_slot|` is evicted first, ties by larger `msg.slot`, then the lexicographically greater validator pubkey (over its canonical 48-byte compressed encoding), then higher operator ID (the key fields make further ties impossible). A candidate that loses the comparison is not admitted (IGNORE). - Expiry: delete an entry once a newly received copy would fail the lateness rule. - Promotion: when a newly installed view is authoritative for a retained entry, that entry is decided immediately: an entry assigned at exactly `msg.slot` becomes a promotion candidate, anything else deletes. A candidate is reprocessed exactly as a newly received message, with all stateful rules applied at reprocessing time; it promotes only if that revalidation succeeds and is deleted otherwise (a normally accepted interim copy for the same (signer, slot) makes the candidate the count-limit duplicate, so it deletes). - Accounting: the retention quota mutates at retention time; ordinary validation state does not mutate on the IGNORE path. From c859af659f6f3b0d9aaf386279a5f2e2c606e58e Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 30 Jul 2026 09:00:33 -0700 Subject: [PATCH 15/20] Clarify Early RANDAO activation timing --- sips/early_randao_preconsensus.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 0e60d40..4b4145d 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -8,7 +8,7 @@ Depends on the ePBS SIP (currently [PR #94](https://github.com/ssvlabs/SIPs/pull **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. +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. The rules activate by target proposal slot `S`: they apply when `epoch(S) >= GLOAS_FORK_EPOCH`, even if the permitted emission window for `S` begins before the fork in wall-clock time. The dependency on the ePBS SIP is activation coupling only. **Motivation** @@ -17,13 +17,13 @@ Gloas moves the attestation deadline to 1/4 slot. RANDAO pre-consensus (sign, go **Rationale & Design Goals** - 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 2-slot value is the maximum earliness receivers MUST support, not a gossip-latency requirement. Producers may emit later within that window, and operators need not emit simultaneously. At a participating receiver, the reveal becomes available once it has collected `2f+1` valid shares, so the latest share needed for quorum determines reconstruction time. Different producer schedules remain interoperable, but later schedules reduce the time saved before block proposal. The window also permits a simple slot-driven producer to make an initial attempt in the `S - 2` slot and retry in `S - 1` if the earlier local publication attempt fails. It does not guarantee a second network delivery after successful publication because byte-identical re-publications are gossip-deduplicated. A longer receiver window would widen reveal and stale-duty exposure without an identified need and would require protocol coordination. - 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. **Specification** -Notation: `S` is a proposal slot; `slot_start(s)` and `epoch(s)` as usual; `SLOT_DURATION` is the network's beacon-chain seconds-per-slot (unchanged at Gloas, which alters only intra-slot timing). REJECT penalizes the delivering peer; IGNORE drops without forwarding or penalty. +Notation: `S` is a proposal slot; `slot_start(s)` and `epoch(s)` as usual; for a non-genesis Gloas activation, `F` is the first slot of `GLOAS_FORK_EPOCH`; `SLOT_DURATION` is the network's beacon-chain seconds-per-slot (unchanged at Gloas, which alters only intra-slot timing). REJECT penalizes the delivering peer; IGNORE drops without forwarding or penalty. | Constant | Value | | -------- | ----- | @@ -40,7 +40,11 @@ A qualifying randao partial MUST satisfy all of: - 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. +The predicate is a pure function of `S`, evaluated identically by producers and receivers. It is not gated by the receiver's current epoch or by whether wall-clock time has reached the Gloas fork. The `S >= EARLY_RANDAO_LEAD` conjunct only keeps the producer's emission window well defined and excludes the first `EARLY_RANDAO_LEAD` slots at genesis. + +For a non-genesis activation, `F` and `F + 1` are eligible target slots. Their producer windows begin, by the producer's clock, at `slot_start(F - 2)` and `slot_start(F - 1)`, respectively. There is no activation warm-up: the first Gloas target slot receives the full early window. Because receiver timing includes `EARLY_RANDAO_CLOCK_TOLERANCE`, implementations MUST enable the receiver rules no later than `slot_start(F - 2) - EARLY_RANDAO_CLOCK_TOLERANCE` by the receiver's clock. This is the earliest receiver-local time at which a conforming partial for `F` can pass timing validation. + +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. 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. **Producer behavior** @@ -52,6 +56,8 @@ For a locally known proposer duty at eligible slot `S`, an operator: - 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; - MAY emit immediately for a duty discovered inside the window; SHOULD emit multiple eligible duties in ascending slot order (correctness does not depend on it). +For `S = F`, the interval begins at `slot_start(F - EARLY_RANDAO_LEAD)`, before wall-clock Gloas activation. Producer scheduling therefore needs to run before `F` to use the full window, but early emission itself remains optional. + Operators that never emit early remain fully conformant. **Message validation** @@ -120,7 +126,7 @@ Cross-client vectors MUST cover: - retention admission: a candidate in a never-fetched epoch retains and one in an epoch whose latest refresh failed retains, while one left Unknown by a succeeding fetch does not; a late failure result from a superseded attempt does not make the epoch retention-eligible; a provisional (execution-optimistic) response ends eligibility while installing no view; - an operator-authenticated share whose BLS signature is invalid is discarded at consumption and contributes to no reconstructed reveal; reconstruction still succeeds from a valid threshold of honest shares; - the eligibility predicate at genesis: slots below `EARLY_RANDAO_LEAD` are ineligible even when `GLOAS_FORK_EPOCH` is 0; -- activation gating: `epoch(S)` immediately before `GLOAS_FORK_EPOCH` is ineligible; a non-genesis `GLOAS_FORK_EPOCH` is eligible from its first slot, whose emission window starts before the fork, and an SSV fork scheduled anywhere does not change that; +- activation gating: `epoch(S)` immediately before `GLOAS_FORK_EPOCH` is ineligible; for a non-genesis `F`, an otherwise qualifying `msg.slot = F` received at receiver-local time `slot_start(F - 2) - EARLY_RANDAO_CLOCK_TOLERANCE` and `msg.slot = F + 1` received at `slot_start(F - 1) - EARLY_RANDAO_CLOCK_TOLERANCE` enter the Early RANDAO validation path even though local wall-clock time is before the fork; an SSV fork scheduled anywhere does not change that; - multi-fault precedence: a structurally valid message failing both operator-signature verification and a non-retaining contextual check (e.g. invalid signature plus Known-unassigned, or invalid signature plus too-early) may produce either the contextual verdict or REJECT, but is never retained, accepted, forwarded, collected, or state-mutating; - invalid operator signature with an Unknown duty view: REJECT, never retained; - restart cases, late mesh join, cross-epoch stamps. From a9394025776c09417a00003a03247818d3c82464 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 30 Jul 2026 13:52:32 -0700 Subject: [PATCH 16/20] Define complete duty authority and optional retention --- sips/early_randao_preconsensus.md | 56 +++++++++++++------------------ 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 4b4145d..e50c365 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -8,18 +8,18 @@ Depends on the ePBS SIP (currently [PR #94](https://github.com/ssvlabs/SIPs/pull **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. The rules activate by target proposal slot `S`: they apply when `epoch(S) >= GLOAS_FORK_EPOCH`, even if the permitted emission window for `S` begins before the fork in wall-clock time. The dependency on the ePBS SIP is activation coupling only. +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. Protocol changes are confined to message-validation timing, ordering, and duty-handling rules; receivers may additionally retain Unknown-duty candidates locally for later revalidation. The rules activate by target proposal slot `S`: they apply when `epoch(S) >= GLOAS_FORK_EPOCH`, even if the permitted emission window for `S` begins before the fork in wall-clock time. The dependency on the ePBS SIP is activation coupling only. **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: that share cannot be re-served in-slot (see *Duty assignment*). Addresses [ssv-spec#373](https://github.com/ssvlabs/ssv-spec/issues/373). +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 Unknown-duty retention*). Addresses [ssv-spec#373](https://github.com/ssvlabs/ssv-spec/issues/373). **Rationale & Design Goals** - 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 value is the maximum earliness receivers MUST support, not a gossip-latency requirement. Producers may emit later within that window, and operators need not emit simultaneously. At a participating receiver, the reveal becomes available once it has collected `2f+1` valid shares, so the latest share needed for quorum determines reconstruction time. Different producer schedules remain interoperable, but later schedules reduce the time saved before block proposal. The window also permits a simple slot-driven producer to make an initial attempt in the `S - 2` slot and retry in `S - 1` if the earlier local publication attempt fails. It does not guarantee a second network delivery after successful publication because byte-identical re-publications are gossip-deduplicated. A longer receiver window would widen reveal and stale-duty exposure without an identified need and would require protocol coordination. - 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. +- Because gossip duplicate-cache lifetimes can exceed the entire useful Early RANDAO window, a receiver that IGNOREs an early partial cannot rely on a byte-identical in-slot re-emission to recover that share. The dedicated clock tolerance narrows timing-induced loss, and optional Unknown-duty retention can recover some missing-view cases. Remaining losses are bounded by the existing round-change path. **Specification** @@ -29,7 +29,6 @@ Notation: `S` is a proposal slot; `slot_start(s)` and `epoch(s)` as usual; for a | -------- | ----- | | `EARLY_RANDAO_LEAD` | 2 slots | | `EARLY_RANDAO_CLOCK_TOLERANCE` | 1000 ms | -| `MAX_QUARANTINED_MESSAGES` | 4096 | **Qualifying message** @@ -62,7 +61,7 @@ Operators that never emit early remain fully conformant. **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; in particular, a candidate failing it under an Unknown duty view is REJECTed, never retained. 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. Validation state (duplicate counts, signer state, slot high-water marks, and epoch counters) mutates only on acceptance or promotion, never on IGNORE. +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. 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. @@ -78,30 +77,23 @@ inclusive on the accept side; strictly greater is IGNORE. This replaces the gene *Duty assignment.* Tri-state, evaluated per candidate against the receiver's proposer-duty view for `epoch(msg.slot)` and the message's validator: -- Known (a cached view is authoritative for the candidate; see below) and assigned at exactly `msg.slot`: the duty-assignment check passes. +- Known (a retained view is authoritative for the candidate; see below) and assigned at exactly `msg.slot`: the duty-assignment check passes. - Known and not assigned: the duty-assignment verdict is IGNORE, never REJECT (receiver views can be stale or re-orged); an independently failing rule may still determine the final verdict under the ordering allowance above. -- Unknown (otherwise): IGNORE; retained only if admitted under the rules below. +- Unknown (otherwise): IGNORE. A receiver MAY additionally retain the candidate locally under the optional rules below. -A cached view is authoritative for a candidate iff a successfully completed proposer-duty fetch for `epoch(msg.slot)` is cached whose requested coverage includes the message's validator, evaluated against the receiver's current registry state: a fetch that predates the receiver's most recent validator-set change is not authoritative for validators affected by that change. A reorg does not revoke authority: a view made stale by a reorg remains Known, with the loss class described below. A complete-schedule fetch covers every validator, but a response without exactly `SLOTS_PER_EPOCH` distinct in-epoch slots is a fetch failure. A provisional response (one flagged execution-optimistic) installs no view. A registry-filtered fetch covers exactly the validators it requested, even when the filtered result is empty; in particular, a view covering only the operator's own validators MUST NOT yield a not-assigned verdict for claimants outside it. +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. Receivers SHOULD hold next-epoch proposer duties by the tail of each epoch. -A Known view made stale by a boundary-adjacent reorg (including a provisional next-epoch view) IGNOREs an honest early share without retention. This loss class is intentional: the share is not recoverable (the in-slot re-emission is byte-identical, absorbed by the origin's publish-side deduplication and, from a restarted origin, by the receiver's seen-cache), and the cost is bounded by the round-change path, the same residual class as the round-change case in Security Considerations. Retention deliberately excludes Known-and-not-assigned; retaining it would remove the duty-fetch-failure precondition from the quarantine-exhaustion attack and would require re-evaluating the buffer on every duty-view change. Exposure is transient: receiver views converge within a slot or two of a reorg under normal operation (per-slot re-polling or dependent-root-triggered refetch). +A retained complete schedule can become stale after a boundary-adjacent reorg while remaining Known. It can therefore yield Known-and-not-assigned and IGNORE an honest early share. This is an intentional residual: the original gossip verdict cannot be retroactively changed, a byte-identical in-slot re-emission may be duplicate-suppressed, and the cost is bounded by the existing round-change path. Receivers SHOULD refresh proposer duties promptly after detecting a change, but this SIP does not require recovery from a stale Known view. -**Unknown-duty retention (IGNORE-AND-RETAIN)** +**Optional Unknown-duty retention** -A candidate whose proposer-duty view is Unknown and that meets the admission conditions below is considered for retention under the occupied-key and capacity rules below; if admitted, it is retained locally before the gossip verdict IGNORE is returned: not forwarded, no penalty, but kept so the seen-cache mark cannot permanently discard it. +A receiver MAY retain an Early RANDAO candidate whose proposer-duty view is Unknown for possible later promotion. Retention is local implementation policy and does not change the gossip verdict, which remains IGNORE. A receiver that does not retain the candidate remains conformant. -- Admission: operator-signature verification and every applicable message-validation rule other than duty assignment have passed, the candidate's duty view is Unknown, and the most recent completed fetch attempt for `epoch(msg.slot)` did not succeed (or none has completed). Attempts are ordered by initiation, so a result arriving for a superseded attempt is discarded for this state; a provisional response counts as a succeeded attempt here, so a long provisional period cannot hold the epoch retention-eligible. A candidate left Unknown by a succeeding fetch (outside its coverage, or affected by a later validator-set change) is IGNOREd without retention: a refresh is already due, and the exposure is the same transient class as the reorg path. -- Key: (operator, validator, `msg.slot`), at most 1 entry. Global capacity `MAX_QUARANTINED_MESSAGES` per node. Sizing: an entry is one signed message (~0.5 KB), bounding memory at ~2 MB. A receiver MAY scope retention to committees it participates in (retained shares are only ever consumed locally; promotion cannot retroactively forward). -- Occupied key, byte-identical content: IGNORE; the original entry and its metadata are kept unchanged. -- Occupied key, distinct bytes: existing two-tier duplicate rule (REJECT from the same delivering peer, IGNORE otherwise); original retained; duplicate-count and ordering state not mutated; quota not recharged. -- Eviction when full: the incoming candidate competes with the stored entries; the greatest `|msg.slot - current_slot|` is evicted first, ties by larger `msg.slot`, then the lexicographically greater validator pubkey (over its canonical 48-byte compressed encoding), then higher operator ID (the key fields make further ties impossible). A candidate that loses the comparison is not admitted (IGNORE). -- Expiry: delete an entry once a newly received copy would fail the lateness rule. -- Promotion: when a newly installed view is authoritative for a retained entry, that entry is decided immediately: an entry assigned at exactly `msg.slot` becomes a promotion candidate, anything else deletes. A candidate is reprocessed exactly as a newly received message, with all stateful rules applied at reprocessing time; it promotes only if that revalidation succeeds and is deleted otherwise (a normally accepted interim copy for the same (signer, slot) makes the candidate the count-limit duplicate, so it deletes). -- Accounting: the retention quota mutates at retention time; ordinary validation state does not mutate on the IGNORE path. +Before retention, the candidate MUST pass operator-signature verification and every applicable message-validation rule other than duty assignment. Initial retention MUST NOT forward the original message, penalize the delivering peer, feed the candidate to signature collection or any downstream reconstruction cache, or mutate ordinary validation state. -Honest demand cannot approach the cap. At any instant only ~7 slot stamps are admissible (the 3-slot proposer lateness TTL behind, `EARLY_RANDAO_LEAD` ahead, the current slot, and tolerances), and an honest entry additionally requires a real proposer duty in an epoch the receiver has not successfully fetched. That bounds honest entries by one SSV proposal per slot network-wide, times at most 13 committee operators, times the ~7-slot span: under 100 entries. The bound is independent of node size (admission is not scoped to local validators, so demand tracks the network's proposal rate) and of outage duration (the admissible window slides and expiry drains it). Reaching the cap requires fabricated slot stamps from a real signer (see Security Considerations). +If a retained candidate is later considered for promotion, it MUST be reprocessed as if newly received under the receiver's then-current duty view, with every stateful rule applied at that time. It may be accepted and consumed only if full revalidation succeeds; otherwise it MUST be discarded. Promotion MUST NOT retroactively forward the original message or change its original gossip verdict. An implementation may attempt promotion before or after `S`; the ordinary lateness rule determines whether the candidate can still be accepted. Capacity, keying, duplicate handling within the retention store, eviction, expiry, persistence, and earlier discard are implementation-specific. Shares that are neither promoted nor normally accepted MUST NOT be fed to signature collection or any downstream reconstruction cache. @@ -116,28 +108,26 @@ Unchanged. The reconstructed reveal is a per-epoch value: an implementation MAY Cross-client vectors MUST cover: - earliness boundary at exactly `EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RANDAO_CLOCK_TOLERANCE` (accept) and beyond (IGNORE); -- Unknown-view retention, promotion, and deletion; Known-unassigned IGNORE; -- the stale-Known reorg path: a share IGNOREd under a pre-reorg Known view is not retained, an identical later copy is dropped by the seen-cache, and a subsequent duty-view update does not resurrect it; +- Known-assigned pass, Known-unassigned IGNORE, and an otherwise-valid Unknown candidate producing the same initial IGNORE gossip verdict whether optional retention is enabled or not; +- the stale-Known reorg path: an honest share is IGNOREd as Known-and-not-assigned under a stale complete schedule, and a later replacement schedule does not retroactively change the original gossip verdict; - same-epoch duty move: shares stamped `X` accepted under a duty-at-`X` view, the duty moves to `Y` in the same epoch, and the proposal at `Y` completes (served from the `X` reconstruction where the implementation reuses; via the in-slot exchange otherwise); - the two-direction ordering exemption with proposals at consecutive slots for one validator; -- occupied-key duplicates: byte-identical IGNORE-keep-original, distinct-bytes two-tier (same-peer REJECT, otherwise IGNORE); -- full-capacity eviction with the incoming candidate winning and losing the comparison; -- Known coverage: a registry-filtered fetch covering every admissible claimant is authoritative, and a covered validator absent from its result is not-assigned; a view scoped to the operator's own validators is not authoritative for an out-of-coverage claimant and must not IGNORE its honest share as Known-unassigned; a claimant registered after the cached fetch takes the Unknown path, without retention, until a refresh covers it; a complete-schedule response without exactly `SLOTS_PER_EPOCH` distinct in-epoch slots establishes no view; -- retention admission: a candidate in a never-fetched epoch retains and one in an epoch whose latest refresh failed retains, while one left Unknown by a succeeding fetch does not; a late failure result from a superseded attempt does not make the epoch retention-eligible; a provisional (execution-optimistic) response ends eligibility while installing no view; +- Known authority: a retained schedule with exactly `SLOTS_PER_EPOCH` duties over distinct in-epoch slots is authoritative for every validator; a missing, duplicate-slot, or out-of-epoch entry establishes no new view; local validator-set changes and failed or malformed refreshes do not invalidate a prior authoritative view; - an operator-authenticated share whose BLS signature is invalid is discarded at consumption and contributes to no reconstructed reveal; reconstruction still succeeds from a valid threshold of honest shares; - the eligibility predicate at genesis: slots below `EARLY_RANDAO_LEAD` are ineligible even when `GLOAS_FORK_EPOCH` is 0; - activation gating: `epoch(S)` immediately before `GLOAS_FORK_EPOCH` is ineligible; for a non-genesis `F`, an otherwise qualifying `msg.slot = F` received at receiver-local time `slot_start(F - 2) - EARLY_RANDAO_CLOCK_TOLERANCE` and `msg.slot = F + 1` received at `slot_start(F - 1) - EARLY_RANDAO_CLOCK_TOLERANCE` enter the Early RANDAO validation path even though local wall-clock time is before the fork; an SSV fork scheduled anywhere does not change that; - multi-fault precedence: a structurally valid message failing both operator-signature verification and a non-retaining contextual check (e.g. invalid signature plus Known-unassigned, or invalid signature plus too-early) may produce either the contextual verdict or REJECT, but is never retained, accepted, forwarded, collected, or state-mutating; -- invalid operator signature with an Unknown duty view: REJECT, never retained; -- restart cases, late mesh join, cross-epoch stamps. +- late mesh join and cross-epoch stamps. -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 MUST demonstrate round-2 viability under Gloas timing, meaning block publication before the post-Gloas useful-block deadline. +Implementations that provide optional retention SHOULD additionally test that initial retention leaves the gossip verdict and ordinary validation state unchanged, candidates with invalid operator signatures are never retained, unpromoted candidates never reach signature collection, promotion performs full revalidation, and candidates that fail revalidation are discarded. + +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. **Security Considerations** -- Deliberately ineligible early emission only starves the attacker's own share (only the signer can produce the bytes). The clock tolerance, Unknown-state retention, and IGNORE-not-REJECT choices narrow the permanent-loss class for honest shares but do not empty it: the stale-`Known` reorg path, the refresh-pending admission exclusion, and the residuals in the bullets below remain. -- After the gossip message-cache window (a few seconds), a stable origin cannot re-deliver a missed early share; receiver restarts and partitions are correlated events. A restart also drops unpromoted retained entries, since this SIP does not mandate persisting the retention buffer. Residual: a live round-1 leader without a reconstruction forces a round change. Mitigations: later emission inside the window, the unconditional in-slot emission (restarted-origin recovery), optional non-normative persistence of received shares, and the mandatory testnet measurement above. +- 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 Unknown-view losses. Stale Known views and Unknown candidates that are not retained or are discarded before promotion remain residual loss cases. +- Gossip duplicate-cache lifetimes can exceed the entire useful Early RANDAO window, so a receiver that has already seen and IGNOREd an early share cannot rely on byte-identical re-emission for recovery. A receiver restart can both clear its seen state and drop any in-memory retained candidates; restarts and partitions are correlated events. Residual: a live round-1 leader without a reconstruction forces a round change. Mitigations: later emission inside the window, unconditional in-slot emission where duplicate state does not suppress it, optional retention, and the mandatory testnet measurement above. - Early reconstruction widens the third-party reaction window between reveal knowability and block publication from roughly 2-4 s to up to 25 s, enabling adaptive bribery, censorship, or targeted DoS when withholding the proposal favors an adversary (one randao bit per affected slot, compounding across slots). Bounded by the existing single-proposal grinding bound; narrowable operationally via later emission, without a protocol change. -- An attacker meeting the retention preconditions (a committee operator sharing more than `MAX_QUARANTINED_MESSAGES` divided by the ~7 admissible slot stamps ≈ 600 validators with the victim, while the victim has no successful fetch for the stamped epoch; milder outages raise the threshold) can fill the retention capacity and evict legitimate entries; cost is bounded to the evicted shares (round-change path). -- No unauthenticated amplification: messages failing operator-signature verification are never forwarded or retained; authentication cost is unchanged (operator signature at validation, BLS at consumption). An operator-authenticated share whose BLS signature proves invalid at consumption is attributable and MUST be discarded without contributing to a reconstructed reveal. A signer emitting two distinct containers for one (signer, slot) is provably misbehaving and attributable via its operator signature. +- Optional retention exposes local memory and revalidation work to operator-authenticated candidates. Implementations that enable it are responsible for bounding local resource use and MAY scope retention to committees they participate in. Capacity, eviction, and persistence choices do not affect wire conformance. +- No unauthenticated retention amplification: messages failing operator-signature verification are never forwarded or retained. An operator-authenticated share whose BLS signature proves invalid at consumption is attributable and MUST be discarded without contributing to a reconstructed reveal. A signer emitting two distinct containers for one (signer, slot) is provably misbehaving and attributable via its operator signature. - No slashing surface: the randao object is not slashable and is a pure function of public data; early signing changes when it is signed, not what. From cce6fbebd5c24445470abd094232921a0266a96c Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 30 Jul 2026 15:27:40 -0700 Subject: [PATCH 17/20] Clarify Early RANDAO candidate scope --- sips/early_randao_preconsensus.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index e50c365..2c9d243 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -63,6 +63,8 @@ Operators that never emit early remain fully conformant. 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. + 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 @@ -108,6 +110,8 @@ Unchanged. The reconstructed reveal is a per-epoch value: an implementation MAY Cross-client vectors MUST cover: - earliness boundary at exactly `EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RANDAO_CLOCK_TOLERANCE` (accept) and beyond (IGNORE); +- candidate scope: an eligible in-slot `RandaoPartialSig` under an Unknown duty view produces the same initial IGNORE verdict as an otherwise-identical early copy; +- message-kind scoping: a non-RANDAO Proposer-role message two slots early remains outside the candidate path and receives the existing timing IGNORE without the Early RANDAO ordering exemption; - Known-assigned pass, Known-unassigned IGNORE, and an otherwise-valid Unknown candidate producing the same initial IGNORE gossip verdict whether optional retention is enabled or not; - the stale-Known reorg path: an honest share is IGNOREd as Known-and-not-assigned under a stale complete schedule, and a later replacement schedule does not retroactively change the original gossip verdict; - same-epoch duty move: shares stamped `X` accepted under a duty-at-`X` view, the duty moves to `Y` in the same epoch, and the proposal at `Y` completes (served from the `X` reconstruction where the implementation reuses; via the in-slot exchange otherwise); From b29db38a8a4b073c34c3d68b24df2f881177c992 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 30 Jul 2026 18:00:28 -0700 Subject: [PATCH 18/20] Recommend one-slot Early RANDAO lead --- sips/early_randao_preconsensus.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 2c9d243..09e1ab8 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -8,7 +8,7 @@ Depends on the ePBS SIP (currently [PR #94](https://github.com/ssvlabs/SIPs/pull **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. Protocol changes are confined to message-validation timing, ordering, and duty-handling rules; receivers may additionally retain Unknown-duty candidates locally for later revalidation. The rules activate by target proposal slot `S`: they apply when `epoch(S) >= GLOAS_FORK_EPOCH`, even if the permitted emission window for `S` begins before the fork in wall-clock time. The dependency on the ePBS SIP is activation coupling only. +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. Within the maximum window, an early-emitting producer's recommended first attempt is at `slot_start(S - 1)`, with the ordinary in-slot path still invoked at `S`. Protocol changes are confined to message-validation timing, ordering, and duty-handling rules; receivers may additionally retain Unknown-duty candidates locally for later revalidation. The rules activate by target proposal slot `S`: they apply when `epoch(S) >= GLOAS_FORK_EPOCH`, even if the permitted emission window for `S` begins before the fork in wall-clock time. The dependency on the ePBS SIP is activation coupling only. **Motivation** @@ -17,7 +17,9 @@ Gloas moves the attestation deadline to 1/4 slot. RANDAO pre-consensus (sign, go **Rationale & Design Goals** - 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 value is the maximum earliness receivers MUST support, not a gossip-latency requirement. Producers may emit later within that window, and operators need not emit simultaneously. At a participating receiver, the reveal becomes available once it has collected `2f+1` valid shares, so the latest share needed for quorum determines reconstruction time. Different producer schedules remain interoperable, but later schedules reduce the time saved before block proposal. The window also permits a simple slot-driven producer to make an initial attempt in the `S - 2` slot and retry in `S - 1` if the earlier local publication attempt fails. It does not guarantee a second network delivery after successful publication because byte-identical re-publications are gossip-deduplicated. A longer receiver window would widen reveal and stale-duty exposure without an identified need and would require protocol coordination. +- The 2-slot value is the maximum earliness receivers MUST support, not a gossip-latency requirement. Different producer schedules remain interoperable, but both reconstruction and third-party knowability begin only after `2f+1` shares, so the common producer policies determine typical readiness and exposure. A longer receiver window would widen reveal and stale-duty exposure without an identified need and would require protocol coordination. +- The recommended `S - 1` policy gives a full slot for gossip and reconstruction while reducing reveal exposure relative to emission near the receiver boundary. The SIP does not coordinate a second pre-slot attempt. Earlier and later policies remain conformant and are compared by the mandatory testnet measurement before mainnet activation guidance. +- A locally reported publication success does not prove peer delivery, and byte-identical re-publications may be gossip-deduplicated. The in-slot path is therefore a best-effort fallback, while reuse of cached reconstructions or signing artifacts is an implementation optimization. - 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 duplicate-cache lifetimes can exceed the entire useful Early RANDAO window, a receiver that IGNOREs an early partial cannot rely on a byte-identical in-slot re-emission to recover that share. The dedicated clock tolerance narrows timing-induced loss, and optional Unknown-duty retention can recover some missing-view cases. Remaining losses are bounded by the existing round-change path. @@ -28,6 +30,7 @@ Notation: `S` is a proposal slot; `slot_start(s)` and `epoch(s)` as usual; for a | Constant | Value | | -------- | ----- | | `EARLY_RANDAO_LEAD` | 2 slots | +| `EARLY_RANDAO_RECOMMENDED_LEAD` | `SLOT_DURATION` (12 s for a 12 s slot) | | `EARLY_RANDAO_CLOCK_TOLERANCE` | 1000 ms | **Qualifying message** @@ -51,11 +54,12 @@ 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))`; - 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; -- MAY emit immediately for a duty discovered inside the window; SHOULD emit multiple eligible duties in ascending slot order (correctness does not depend on it). +- 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)`; +- SHOULD still invoke the existing in-slot emission path at `S` regardless of its locally recorded early outcome; this is a best-effort fallback, not proof of redelivery, and a running origin's identical re-publish is absorbed by its own gossip layer (expected, not an error), while a restarted origin's re-publish aids recovery; +- MAY serve an already reconstructed reveal to the in-slot consumer without waiting for repeated local-share signing, envelope signing, or publication; implementations MAY reuse cached valid signing artifacts or schedule the emission attempt independently; +- MAY emit immediately for a duty first discovered after the recommended first-attempt time; SHOULD emit multiple eligible duties in ascending slot order (correctness does not depend on it). -For `S = F`, the interval begins at `slot_start(F - EARLY_RANDAO_LEAD)`, before wall-clock Gloas activation. Producer scheduling therefore needs to run before `F` to use the full window, but early emission itself remains optional. +For `S = F`, the interval begins at `slot_start(F - EARLY_RANDAO_LEAD)`, and the recommended producer time is `slot_start(F - 1)`, both before wall-clock Gloas activation. Producer scheduling therefore needs to run before `F` to follow the recommendation, but early emission itself remains optional. Operators that never emit early remain fully conformant. @@ -75,6 +79,8 @@ slot_start(msg.slot) - local_now <= EARLY_RANDAO_LEAD * SLOT_DURATION + EARLY_RA 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. + *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: @@ -101,7 +107,7 @@ Shares that are neither promoted nor normally accepted MUST NOT be fed to signat **Reconstruction and consumption** -Unchanged. 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. +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. @@ -125,13 +131,15 @@ Cross-client vectors MUST cover: Implementations that provide optional retention SHOULD additionally test that initial retention leaves the gossip verdict and ordinary validation state unchanged, candidates with invalid operator signatures are never retained, unpromoted candidates never reach signature collection, promotion performs full revalidation, and candidates that fail revalidation are discarded. -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. +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. **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 Unknown-view losses. Stale Known views and Unknown candidates that are not retained or are discarded before promotion remain residual loss cases. - Gossip duplicate-cache lifetimes can exceed the entire useful Early RANDAO window, so a receiver that has already seen and IGNOREd an early share cannot rely on byte-identical re-emission for recovery. A receiver restart can both clear its seen state and drop any in-memory retained candidates; restarts and partitions are correlated events. Residual: a live round-1 leader without a reconstruction forces a round change. Mitigations: later emission inside the window, unconditional in-slot emission where duplicate state does not suppress it, optional retention, and the mandatory testnet measurement above. -- Early reconstruction widens the third-party reaction window between reveal knowability and block publication from roughly 2-4 s to up to 25 s, enabling adaptive bribery, censorship, or targeted DoS when withholding the proposal favors an adversary (one randao bit per affected slot, compounding across slots). Bounded by the existing single-proposal grinding bound; narrowable operationally via later emission, without a protocol change. +- Under `EARLY_RANDAO_RECOMMENDED_LEAD`, a homogeneous cluster may make the reveal knowable close to one slot before `S`, compared with roughly 2-4 s today. The wider reaction window enables adaptive bribery, censorship, or targeted DoS when withholding the proposal favors an adversary (one randao bit per affected slot, compounding across slots). It is bounded by the existing single-proposal grinding bound and can be adjusted via producer policy without changing the receiver window. - Optional retention exposes local memory and revalidation work to operator-authenticated candidates. Implementations that enable it are responsible for bounding local resource use and MAY scope retention to committees they participate in. Capacity, eviction, and persistence choices do not affect wire conformance. - No unauthenticated retention amplification: messages failing operator-signature verification are never forwarded or retained. An operator-authenticated share whose BLS signature proves invalid at consumption is attributable and MUST be discarded without contributing to a reconstructed reveal. A signer emitting two distinct containers for one (signer, slot) is provably misbehaving and attributable via its operator signature. - No slashing surface: the randao object is not slashable and is a pure function of public data; early signing changes when it is signed, not what. From c13987a24c73fe348416468e79a29adbb41651f8 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 30 Jul 2026 19:24:38 -0700 Subject: [PATCH 19/20] Clarify Early RANDAO duplicate handling --- sips/early_randao_preconsensus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 09e1ab8..80fc645 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -20,7 +20,7 @@ Gloas moves the attestation deadline to 1/4 slot. RANDAO pre-consensus (sign, go - The 2-slot value is the maximum earliness receivers MUST support, not a gossip-latency requirement. Different producer schedules remain interoperable, but both reconstruction and third-party knowability begin only after `2f+1` shares, so the common producer policies determine typical readiness and exposure. A longer receiver window would widen reveal and stale-duty exposure without an identified need and would require protocol coordination. - The recommended `S - 1` policy gives a full slot for gossip and reconstruction while reducing reveal exposure relative to emission near the receiver boundary. The SIP does not coordinate a second pre-slot attempt. Earlier and later policies remain conformant and are compared by the mandatory testnet measurement before mainnet activation guidance. - A locally reported publication success does not prove peer delivery, and byte-identical re-publications may be gossip-deduplicated. The in-slot path is therefore a best-effort fallback, while reuse of cached reconstructions or signing artifacts is an implementation optimization. -- 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. +- The per-(signer, slot) duplicate limit stays 1. Byte-identical copies share a content-derived gossip message identifier and are normally removed before application validation. Distinct operator-authenticated bytes for the same (`MessageID`, signer, slot) key normally have different gossip identifiers and are bounded by the existing one-message limit for each pre-consensus or post-consensus class. - Because gossip duplicate-cache lifetimes can exceed the entire useful Early RANDAO window, a receiver that IGNOREs an early partial cannot rely on a byte-identical in-slot re-emission to recover that share. The dedicated clock tolerance narrows timing-induced loss, and optional Unknown-duty retention can recover some missing-view cases. Remaining losses are bounded by the existing round-change path. **Specification** From 8a098c9ea7c21a738d8043d7695642ae0cdd49fd Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 30 Jul 2026 19:39:23 -0700 Subject: [PATCH 20/20] Describe Early RANDAO constants --- sips/early_randao_preconsensus.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sips/early_randao_preconsensus.md b/sips/early_randao_preconsensus.md index 80fc645..7ab2f52 100644 --- a/sips/early_randao_preconsensus.md +++ b/sips/early_randao_preconsensus.md @@ -27,11 +27,11 @@ Gloas moves the attestation deadline to 1/4 slot. RANDAO pre-consensus (sign, go Notation: `S` is a proposal slot; `slot_start(s)` and `epoch(s)` as usual; for a non-genesis Gloas activation, `F` is the first slot of `GLOAS_FORK_EPOCH`; `SLOT_DURATION` is the network's beacon-chain seconds-per-slot (unchanged at Gloas, which alters only intra-slot timing). REJECT penalizes the delivering peer; IGNORE drops without forwarding or penalty. -| Constant | Value | -| -------- | ----- | -| `EARLY_RANDAO_LEAD` | 2 slots | -| `EARLY_RANDAO_RECOMMENDED_LEAD` | `SLOT_DURATION` (12 s for a 12 s slot) | -| `EARLY_RANDAO_CLOCK_TOLERANCE` | 1000 ms | +| Constant | Value | Description | +| -------- | ----- | ----------- | +| `EARLY_RANDAO_LEAD` | 2 slots | Maximum permitted emission lead and base receiver earliness allowance | +| `EARLY_RANDAO_RECOMMENDED_LEAD` | `SLOT_DURATION` (12 s for a 12 s slot) | Recommended lead for an early producer's first publication attempt | +| `EARLY_RANDAO_CLOCK_TOLERANCE` | 1000 ms | Additional receiver allowance for honest clock disparity at the earliest emission boundary | **Qualifying message**