Skip to content

fix(dwallet-mpc): gate v3 wire behavior for the 1.1.8 rolling upgrade (G2+G3)#1797

Merged
omersadika merged 1 commit into
devfrom
fix/v3-interop-gates
Jul 4, 2026
Merged

fix(dwallet-mpc): gate v3 wire behavior for the 1.1.8 rolling upgrade (G2+G3)#1797
omersadika merged 1 commit into
devfrom
fix/v3-interop-gates

Conversation

@omersadika

Copy link
Copy Markdown
Contributor

Two protocol-v3 interop fixes for the 1.1.8 → 1.2.0 rolling upgrade (findings G2 and G3 in the release review). Both share one root cause: a value/message that must not exist on the v3 wire is live at v3.

G2 — gate the IdleStatusUpdate consensus emission

send_status_update_to_consensus emitted IdleStatusUpdate whenever the idle flag changed, with no protocol gate. last_sent_idle_status starts None, so the first service-loop iteration after every restart submits the transaction — at v3, unconditionally. A 1.1.8 peer cannot decode that consensus-transaction kind: its verify_batch rejects the whole block containing one, and its replay path panics on an undecodable sequenced transaction. During the rolling restart that is a consensus-layer interop break, regardless of the feature being unused at v3.

Fix: gate the emission on internal_presign_sessions_enabled() — the same flag that already gates this feature's DB write and read streams (authority_per_epoch_store.rs, dwallet_mpc_service.rs); this one wire emission was the single missed site.

G3 — move the Schnorr presign round delay out of the base config

schnorr_presign_second_round_delay: Some(8) lived in the base protocol config, visible at v3. It is applied by signature algorithm, not session type — so it hit plain Taproot / Schnorrkel / EdDSA external presigns, which are user-requestable and served at v3 today. 1.1.8 has no such parameter (zero delay). During the mixed window an upgraded validator would advance round 2 of the same presign session 8 consensus rounds later than a 1.1.8 peer → divergent message sets → byte-divergent outputs → output-quorum split (this codebase's false-malicious / stall failure class). It is also a v3 latency change even post-swap.

Fix: None in the base config for both the second- and third-round delays; Some(8) in the 4 => arm (the pool — the delays' actual purpose — is v4-only). Consumers read *_as_option().unwrap_or(0). The delay map is now built by a round_delays() helper that omits zero-valued entries, so a v3 presign passes HashMap::new() to the crypto layer — byte-identical to 1.1.8, not an explicit (round, 0).

Snapshots

Mainnet/Testnet/version _3 snapshots drop both delay lines; _4 snapshots keep 8. This is the intended, reviewable effect of the gating — v3's protocol-config shape moves back toward 1.1.8.

Testing

  • New test_no_idle_status_update_emitted_below_activation (idle_status_voting.rs): with internal_presign_sessions=false, runs 10 service-loop iterations across 4 validators and asserts no IdleStatusUpdate is ever submitted to consensus nor reaches any epoch store. Fault-injected per the test-testing rule: reverting the G2 gate makes it fail on the first iteration (ungated emission fires immediately because last_sent_idle_status starts None), confirming it guards the real property; reverted.
  • ika-protocol-config snapshot tests updated and green; existing idle-status suite (3 tests) still green; clippy clean.

Gates the 1.2.0 release together with G1 (the V1-anchor fix, separate PR).

🤖 Generated with Claude Code

… (G2+G3)

Two protocol-v3 interop fixes sharing one root cause — a message/value
that must not exist on the v3 wire was live at v3:

G2: send_status_update_to_consensus emitted IdleStatusUpdate with no
protocol gate. last_sent_idle_status starts None, so the first service
iteration after every restart submits it at v3. A 1.1.8 peer can't
decode that consensus-transaction kind — verify_batch rejects the whole
block, and the replay path panics on the undecodable sequenced tx.
Gate the emission on internal_presign_sessions_enabled() (the same flag
that gates this feature's DB write/read streams).

G3: schnorr_presign_second_round_delay=Some(8) lived in the BASE config,
applied by signature algorithm to external Taproot/Schnorrkel/EdDSA
presigns served at v3 — where 1.1.8 uses no delay. Mixed-window round-2
timing skew on the same session diverges outputs. Move both delays to
the v4 arm (None in base); consumers use *_as_option().unwrap_or(0), and
a round_delays() helper omits zero entries so v3 passes HashMap::new()
to the crypto layer, byte-identical to 1.1.8.

v3 protocol-config snapshots drop the delay lines; v4 keeps 8. New
test_no_idle_status_update_emitted_below_activation asserts no
IdleStatusUpdate is emitted with the feature off; fault-injected
(gate reverted -> fails on iteration 1) then reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omersadika omersadika requested a review from ycscaly as a code owner July 2, 2026 20:22
@omersadika omersadika merged commit 7fee773 into dev Jul 4, 2026
10 checks passed
@omersadika omersadika deleted the fix/v3-interop-gates branch July 4, 2026 18:34
omersadika added a commit that referenced this pull request Jul 9, 2026
… (G2+G3) (#1797)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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