Skip to content

fix(v4): make ready-signal canonicalization a pure function of the sequenced bytes (V2)#1823

Merged
omersadika merged 3 commits into
mainfrom
fix/v4-ready-signal-determinism
Jul 14, 2026
Merged

fix(v4): make ready-signal canonicalization a pure function of the sequenced bytes (V2)#1823
omersadika merged 3 commits into
mainfrom
fix/v4-ready-signal-determinism

Conversation

@omersadika

Copy link
Copy Markdown
Contributor

Track A consensus-determinism fix from the protocol-v4 gate list (see dev-docs/plans/v4-activation-gate-list.md). Precondition for V1's freeze determinism.

The fork

record_epoch_mpc_data_ready_signal canonicalized the persisted validated_peers with a weight closure that consulted the local validator_mpc_data_announcements table: a next-epoch joiner (zero current-committee weight) was kept only if present in that table, otherwise dropped.

That table is populated for a relayed joiner announcement only after verify_joiner_announcement passes against the installed JoinerPubkeyProvider — a ~5s Sui-poll product installed on wall-clock time (buffered until then). So two honest validators processing the same sequenced ready signal persisted different canonical validated_peers depending on their provider-install timing: validator A (provider installed) keeps joiner J and can freeze it; validator B (provider lagging) drops J and excludes it. Divergent frozen set → divergent next-epoch committee mpc_data → forked reconfiguration participant set / handoff cert. This is exactly the taint freeze_mpc_data_if_first documents it avoids ("we deliberately do NOT read the local announcement table here") — laundered one step earlier, at receive-time canonicalization.

The fix — pure function of the signal bytes

canonicalize_ready_signal_peers no longer consults any local table:

  • Keep every deduped (peer, hash) pair — including zero-weight joiners — instead of dropping non-committee peers via the announced-table lookup. A joiner is frozen only when a stake-quorum of signers attest it in the tally (compute_freeze_partition); kept non-committee names are inert downstream (a garbage name never reaches a stake-quorum → lands in excluded, which is consulted only by committee-member key — verified).
  • Measure quorum coverage on current-committee weight only, so a sparse signal still can't push the freeze trigger.
  • A deterministic length cap (K × current committee size, K=4) bounds byzantine garbage padding; the deduped length is identical across honest validators, so the cap decision is itself consensus-deterministic.

Diagnostics non_committee_droppednon_committee_kept; new OverCap outcome.

Tests

Joiner retained in Accept while coverage stays committee-only; a signal of only zero-weight peers → BelowQuorumCoverage; over-cap dropped as a pure function of input length; the existing dedup/coverage/mixed-padding tests updated for the keep-not-drop semantics. All green.

Spec

validator-mpc-data-announcements.md: receive-time canonicalization added to the ready-signal semantics and to invariant 1 (the "pure function of the consensus sequence" property now explicitly covers canonicalization, not just the freeze tally).

Validation note

Per CLAUDE.md's reconfiguration/epoch-boundary rule, the determinism property (two validators with divergent provider timing persist byte-identical signals and compute the identical freeze partition) is validated by the cluster suite — the fault-injection scenario (force the provider absent on one node) is the pre-merge gate.

🤖 Generated with Claude Code

…quenced bytes (V2)

record_epoch_mpc_data_ready_signal canonicalized the persisted
validated_peers using a weight closure that consulted the local
validator_mpc_data_announcements table: a next-epoch joiner (zero
current-committee weight) was KEPT only if present in that table,
otherwise DROPPED. The table is populated for relayed joiner
announcements only after verify_joiner_announcement passes against the
installed JoinerPubkeyProvider — a ~5s Sui-poll product installed on
wall-clock time (buffered until then). So two honest validators
processing the SAME sequenced ready signal persisted DIFFERENT canonical
validated_peers depending on their provider-install timing: one froze a
joiner, the other excluded it → divergent next-epoch mpc_data / forked
reconfiguration participant set. This is the exact taint
freeze_mpc_data_if_first documents it avoids, laundered one step earlier.

canonicalize_ready_signal_peers is now a pure function of the signal
bytes:
- KEEP every deduped (peer, hash) pair — including zero-weight joiners —
  instead of dropping non-committee peers via the announced-table lookup.
  A joiner is frozen only when a stake-quorum of signers attest it in the
  tally (compute_freeze_partition); kept non-committee names are inert
  downstream (never reach a stake-quorum → land in `excluded`, consulted
  only by committee-member key).
- Measure quorum COVERAGE on current-committee weight only, so a sparse
  signal still can't push the freeze trigger.
- Add a deterministic length cap (K × current committee size) against a
  byzantine signer padding garbage into durable storage; the deduped
  length is identical across honest validators, so the cap decision is
  itself consensus-deterministic.
The call site no longer reads any local table on the consensus path.

Diagnostics renamed non_committee_dropped → non_committee_kept (they are
kept now, surfaced only for the byzantine warn). New OverCap outcome.

Tests: joiner retained in Accept while coverage stays committee-only; a
signal of only zero-weight peers → BelowQuorumCoverage; over-cap dropped
(pure function of input length); existing dedup/coverage tests updated.

Spec: validator-mpc-data-announcements.md — receive-time canonicalization
added to the ready-signal semantics and to invariant 1 (pure function of
the consensus sequence covers canonicalization, not just the freeze).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…; document the non-inert residue; chain-test the pipeline

Review follow-ups on the ready-signal canonicalization:

1. The "likely byzantine signer" warn fired for EVERY honest signal in
   a churn epoch: honest emitters deliberately attest announced
   next-epoch joiners (zero current-committee weight), so
   non_committee_kept is the expected shape of an honest signal, not a
   byzantine tell. Split the anomaly logging: duplicates still warn
   (honest emitters dedup before broadcast); zero-weight names log at
   debug unless the kept set exceeds committee size (no honest joiner
   population looks like that).

2. The spec's "kept non-committee names are inert downstream" claim
   corrected: a garbage name cannot reach the frozen set and assembly
   reads by committee key only, but it DOES land durably in
   epoch_excluded_validators, inflates the excluded-validators gauge,
   and holds the full-coverage fast path open so the freeze fires via
   the grace path — one byzantine signer buys the grace latency for
   free, deterministically. Also documents the enlarged re-emit budget
   (up to cap - initial accepted strict-superset REPLACEs).

3. New unit test chains canonicalize -> compute_freeze_partition ->
   carry_forward_stable_mpc_data in one scenario: a quorum-attested
   zero-weight joiner freezes at its announced hash, a silent
   continuing member freezes at its prior-cert digest, a fresh
   attestation beats the prior digest, and a kept-but-never-quorum
   garbage name is excluded — the exact population the determinism
   claim covers, previously tested only per-stage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omersadika

Copy link
Copy Markdown
Contributor Author

CI green at the main-merged head 04ddf6459e (includes the merged #1809/#1820/#1821/#1822):

  • Test Cluster: ✅ 29321828453 — 22/22 first-try (re-confirms the earlier green 29197874510 on the pre-merge head)

@omersadika omersadika merged commit 5b8a0a4 into main Jul 14, 2026
12 checks passed
@omersadika omersadika deleted the fix/v4-ready-signal-determinism branch July 14, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant