Skip to content

fix(mpc): fail chain reads loud on a missing active-member mpc_data record - #1850

Merged
omersadika merged 1 commit into
mainfrom
claude/keen-noether-27453f
Jul 17, 2026
Merged

fix(mpc): fail chain reads loud on a missing active-member mpc_data record#1850
omersadika merged 1 commit into
mainfrom
claude/keen-noether-27453f

Conversation

@omersadika

@omersadika omersadika commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Problem

Both chain-view committee builders tolerated a missing/undecodable on-chain mpc_data record for an active committee member as a silent member skip:

  • EpochStartSystem::get_ika_committee (epoch_start_system.rs) — a silent filter_map skip building Committee.class_groups_public_keys_and_proofs, which seeds the MPC manager's validator keys pre-v4 (class_groups_keys_by_party_id, live on every deployed network today since network_encryption_key_version = 3 only arrives with protocol v4);
  • the sui_syncer legacy chain fallback (new_committee) — the same shape building the next committee that feeds the reconfiguration MPC every epoch under deployed protocol versions, where the missing-record case was dropped with no log at all.

A record cannot legitimately be absent: it is written at candidate registration (validator_info.move fills mpc_data_bytes at creation) and rotation never empties it — under v4 chain writes remain. So every gap is a read defect (fullnode lag, table-walk race, decode failure). Because each validator reads through its own fullnode, the tolerated gap became an unagreed party-set exclusion: a locally-shrunken class-groups map producing divergent MPC public inputs across honest validators (the crypto layer deliberately accepts partial key maps and deals only to present parties, so nothing fails locally). Exclusion decisions belong exclusively to the consensus-agreed freeze — this was the follow-up flagged in #1846's investigation (spec invariant 3), and the transient chain-read gap was the mechanism behind the issue-#1772 epoch-boundary flake.

Fix

Fail the read, not just the log — retry infrastructure already existed:

  • get_epoch_start_system (ika-sui-client): an active member absent from the fetched mpc_data map fails the whole read with InvalidCommittee, a should_never_happen error log, and a dedicated sui_rpc_errors{epoch_start_missing_mpc_data} label. All callers go through must_get_epoch_start_system, which retries until the chain view is complete — and since EpochStartSystem is persisted in the epoch-start configuration, a degraded snapshot can no longer be persisted and rebuilt on every restart. The gate sits above both SuiClientInner transports.
  • new_committee chain fallback (sui_syncer): a missing or undecodable record errors the committee build (new DwalletMPCError::MissingOnChainMpcData, wired into the kind() metric-label match); the sync loop retries on the next tick instead of shipping a shrunken reconfiguration party set.
  • Builders stay defensive: the None arm in get_ika_committee (reachable only from a snapshot persisted before the fetch gate) now logs at error!; get_ika_committee_with_network_metadata no longer swallows decode errors with .ok().
  • Deliberately NOT enforced on Committee::new: a post-freeze assembled committee legitimately omits excluded members, so a type-level "map covers all members" invariant would be wrong — the completeness check belongs at the chain-read boundary, where no exclusion concept exists.

Spec dev-docs/specs/validator-mpc-data-announcements.md invariant 3 updated in the same PR: it now binds every builder of the map, documents the read-boundary gates and the unagreed-exclusion rationale, and the code anchors point at both gates.

Trade-off, considered and accepted: a persistently inconsistent fullnode now stalls epoch entry visibly (warn + metric per retry) instead of letting the validator enter with divergent MPC input. Stuck-loudly is diagnosable in minutes; divergent-input failures historically surfaced as false-malicious convictions and took days.

Validation

🤖 Generated with Claude Code

…ecord

An active committee member's on-chain mpc_data record is written at
candidate registration and never emptied, so a missing or undecodable
record at read time is always a read defect (fullnode lag, table-walk
race, decode failure) — never a legitimate state. Both chain-view
committee builders tolerated it as a silent member skip, and since each
validator reads through its own fullnode, the tolerated gap became an
UNAGREED party-set exclusion: a locally-shrunken class-groups map
feeding divergent MPC public inputs across honest validators (the
manager's validator-key seed pre-v4, and the reconfiguration MPC input
via the legacy next-committee fallback). This was the mechanism behind
the issue-#1772 epoch-boundary flake.

- get_epoch_start_system: an active member absent from the fetched
  mpc_data map now fails the whole read (InvalidCommittee) with a
  should_never_happen error log and a dedicated sui_rpc_errors label;
  must_get_epoch_start_system retries until the chain view is complete,
  and the degraded EpochStartSystem can no longer be persisted.
- sui_syncer new_committee chain fallback: a missing/undecodable record
  errors the committee build (new MissingOnChainMpcData variant) so the
  sync loop retries next tick, instead of dropping the member from the
  reconfiguration MPC party set.
- get_ika_committee: the silent filter_map None arm (reachable only
  from a pre-gate persisted EpochStartSystem) now logs at error;
  get_ika_committee_with_network_metadata no longer swallows decode
  errors with .ok().
- Exclusion decisions belong exclusively to the consensus-agreed
  freeze; the completeness check lives at the read boundary, NOT on
  Committee construction (post-freeze assembled committees legitimately
  omit excluded members). Spec invariant 3 updated to bind every
  builder of the map and document the read-boundary gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omersadika
omersadika merged commit f6eb191 into main Jul 17, 2026
14 checks passed
@omersadika
omersadika deleted the claude/keen-noether-27453f branch July 17, 2026 11:34
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