Skip to content

fix(message_validator): accept messages up to one second before their slot - #1315

Merged
shane-moore merged 1 commit into
sigp:epbsfrom
shane-moore:fix/early-message-margin
Sep 27, 2026
Merged

shane-moore merged 1 commit into
sigp:epbsfrom
shane-moore:fix/early-message-margin

Conversation

@shane-moore

Copy link
Copy Markdown
Member

Problem, Evidence, and Context

Change Overview

  • Every role now uses one early-arrival margin: CLOCK_ERROR_TOLERANCE + EARLY_MESSAGE_MARGIN (1.05 s), measured from the role's earliest arrival. Proposer preferences keep their epoch-aligned reference point, so their bound does not change.
  • Accepting early is safe. The signature collector keeps a network partial until local signing joins it, and QBFT buffers messages for instances that have not started.
  • Not changed: lateness, the 50 ms clock tolerance, ring sizes, and outbound validation (which has no timing check).

Risks, Trade-offs, and Mitigations

  • Side effect for monotonic-slot roles, which go-ssv also documents: a signer's early message for slot N+1 advances that signer's slot up to 1 s sooner. Its own slot-N messages that arrive afterwards are then ignored as SlotAlreadyAdvanced. This needs one validator to hold the same role in consecutive slots, plus a late or reordered slot-N message, and it affects only that signer. A test pins this behavior.
  • Clock skew can make only slot-tick sends arrive early: RANDAO, selection proofs, registrations and exits. Duties sent later in the slot, such as attestations, aggregates, sync contributions and PTC votes, are unaffected.
  • Mixed versions: both clients IGNORE early messages, so a different window changes forwarding only, not peer scoring.

Validation

  • Five tests failed with EarlySlotMessage before the change and pass after:
    • A Proposer RANDAO partial received 150 ms early, through the full partial-signature path.
    • A first-slot-of-epoch RANDAO received 150 ms early while the next epoch's proposers are not yet fetched.
    • Every non-preference role is accepted at exactly 1.05 s early and ignored 1 ns beyond.
    • Adjacent slots: an early N+1 RANDAO followed by the same signer's slot-N packet gives SlotAlreadyAdvanced. Another signer's slot-N packet, and the reverse order, are both accepted.
    • A committee QBFT proposal received 150 ms early is accepted.
  • The proposer-preferences boundary tests are unchanged and still pass. Applying the margin twice makes them fail, and adding it to lateness makes the existing lateness tests fail.
  • make cargo-fmt-check, make lint and make test (release, 993 passed) all pass.

Rollback

Revert the commit. There is no config, storage, or wire-format impact.

Additional Info / Next Steps

  • SIP-94 §7 leaves early-side tolerance for existing roles unspecified, so no SIP change is required.

🤖 Generated with Claude Code

… slot

RANDAO and selection-proof partials are sent once on the sender's slot
tick and never re-sent. A sender clock slightly ahead of the receiver's
made peers ignore them as early, so the duty was lost; go-ssv missed a
block this way (ssvlabs/ssv#3026).

Apply the one-second early margin, previously limited to proposer
preferences, to every role, matching ssvlabs/ssv#2901. Proposer
preferences keep their existing bound and lateness is unchanged.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (epbs@e9996e8). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             epbs    #1315   +/-   ##
=======================================
  Coverage        ?   80.79%           
=======================================
  Files           ?      179           
  Lines           ?    41191           
  Branches        ?        0           
=======================================
  Hits            ?    33281           
  Misses          ?     7910           
  Partials        ?        0           
Flag Coverage Δ
rust 80.79% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shane-moore

Copy link
Copy Markdown
Member Author

Review from Claude (Fable 5.1), head ef74b585.

lgtm. Checked the five-line production change against the consumers and the go-ssv source: the bound matches 3d03760de on epbs-gloas (proposer-preferences lead stays additive there, epoch-aligned here, same result); the collector stores network shares before local registration and cannot reconstruct until registered; the uninitialized QBFT instance buffers and replays; both cleanup cutoffs are lower bounds so slot-ahead entries survive; the stored_slot_count one-slot padding still holds since the 1.05 s early and 3.05 s late extensions sit at opposite ends of a slot (4.1 s total, under 6 s minimal and 12 s mainnet); and the first-slot RANDAO tolerance passes on early arrival because now <= slot.

One doc nit, not blocking: the EARLY_MESSAGE_MARGIN comment frames the per-signer slot-advance side effect as needing consecutive-slot duties. Sync committee members meet that every slot, and validate_qbft_message_by_duty_logic reads the same max_slot, so the signer's late slot-N QBFT messages are Ignored too, not only its partials. Same behavior existed inside the 50 ms window; a sentence saying so would stop a reader treating it as proposer-only.

@shane-moore

Copy link
Copy Markdown
Member Author

re the doc nit, agreed sync committee hits this every slot before boole and qbft reads the same max_slot, but the doc comment already covers every monotonic slot role and all previous slot messages so leaving the code as is. a slot N message is only dropped if the signer sent it after its own N+1 tick, which is already past that duty's deadline

@shane-moore
shane-moore merged commit 6f60de8 into sigp:epbs Sep 27, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants