Skip to content

feat(signature_collector): distinguishable no-threshold error for the PTC divergence metric #1079

Description

@shane-moore

Goal

Make signature_collector resolve a no-threshold collection (a still-pending collector evicted at cleanup) to a distinguishable error instead of the ambiguous QueueClosedError, so the PTC reconstruction-failure metric added in #1077 can isolate a clean observation_divergence rate instead of the coarse {reason="no_signature"} upper bound. Observability improvement, not a protocol requirement.

Context / motivation

Under the no-QBFT PTC design (#1077, milestone #6), a minority operator whose beacon-node observation never reaches threshold records a non-slashable, penalty-free silent miss. How often that happens is useful telemetry to watch (the question was raised in the SIP-94 thread), but it is not a normative SIP-94 §3 requirement, and nothing functional depends on it: #1077 ships a coarse {reason="no_signature"} counter that works today.

The blocker to a clean rate is in the collector: CollectionError::CollectionTimeout is dead code (never constructed; signature_collector/src/lib.rs:514), and sign_and_collect ends at Ok(result_rx.await?) (:294) with no timeout. So a minority/no-threshold operator blocks until the cleaner (:411-429, retains for_slot >= slot - SIGNATURE_COLLECTOR_RETAIN_SLOTS) evicts its collector, dropping the notifier -> RecvError -> QueueClosedError (:529-532) -- indistinguishable from a genuine channel close.

Blocked by: #1077 (the reconstruction-failure metric this would refine).

Suggested approach

Make eviction of a still-pending collector resolve its registered waiters to a distinguishable no-threshold error instead of QueueClosedError. Two shapes:

  • (a) clean protocol fix: change the notifier oneshot to carry Result<Arc<Signature>, CollectionError> and have the cleanup / State-drop path send Err(CollectionTimeout) (or a new NoThreshold variant) before dropping. Touches every collect_signature caller's success unwrap (Ok((*collector.await…)?).clone() -> handle the inner Result).
  • (b) lower blast radius: at eviction, detect that the collector still had registered notifiers and signal those waiters distinctly without changing the success payload type (a dedicated close-reason).

Either way, a no-threshold expiry must reach collect_signature's caller as a variant distinct from a genuine queue close. Pick at implementation time.

Then (reclassification in #1077's sign path): split the sign_payload_attestation error arm so CollectionTimeout / NoThreshold -> {reason="observation_divergence"} and QueueClosedError -> {reason="queue_closed"} (genuine close). The interim {reason="no_signature"} bucket dissolves.

Acceptance criteria

  • A test where shares never reach threshold (collector evicted) asserts the waiter receives the distinguishable no-threshold error, NOT QueueClosedError.
  • All existing collect_signature callers compile and pass (option (a) touches the success unwrap site).
  • cargo test -p signature_collector -p validator_store.

Notes

Tagged research because the path forward is open (two shapes with different blast radius, and whether the rate is worth the collector churn at all, since it is not a requirement). Not part of milestone #6: #1077 ships the coarse {reason="no_signature"} proxy in the meantime. Issues are directionally correct, not prescriptive; verify symbols at PR time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    epbsePBS / EIP-7732 / Gloas implementationresearchAnything with unclear path forward and likely needing Go-SSV involvement

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions