Skip to content

feat(dwallet-mpc): support the deployed V1 network-DKG anchor on the main (v4) reconfiguration path + one-time V1->V3 anchor migration#1799

Merged
omersadika merged 1 commit into
devfrom
fix/v1-anchor-v4-migration
Jul 4, 2026
Merged

feat(dwallet-mpc): support the deployed V1 network-DKG anchor on the main (v4) reconfiguration path + one-time V1->V3 anchor migration#1799
omersadika merged 1 commit into
devfrom
fix/v1-anchor-v4-migration

Conversation

@ycscaly

@ycscaly ycscaly commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1798, stacked on fix/v1-anchor-reconfiguration — the v4-scope work that PR explicitly deferred ("must land before v4 activates on the deployed networks"). With this, the deployed V1 network-DKG anchor is fully supported on the main (protocol-v4) reconfiguration path and in the one-time canonical-anchor migration the cross-epoch handoff performs, mirroring the same insight as the v3 fix: the V1 inner bytes ARE the raw class_groups::dkg::PublicOutput — decode them directly, no wrapper projection.

Why (recap of the deployed shape)

The deployed mainnet/testnet keys were DKG'd by a pre-1.1.8 binary: the on-chain anchor is V1-tagged and is never rewritten (reconfiguration writes a separate field); their reconfiguration outputs are V2. #1798 fixed the v3 (bwd-compat) reconfiguration path — the active-network wedge — but left two v4 gaps as clear errors:

  1. the main (v4) reconfiguration input builder errored on any V1 anchor, and
  2. reconstruct_full_network_dkg_output (which feeds the handoff's one-time canonical V2→V3 anchor flip) returned None for a V1 anchor, so the deployed keys' anchor migration would never fire.

Timeline for a deployed key once v4 activates: the first v4 reshare runs from (V1 anchor, V2 reconfiguration output); the next epoch's reshare runs from (V1 anchor, V3 reconfiguration output) — the flip is only observable via the handoff one epoch after the V3 output is cert-pinned; from the flip onward the anchor is V3. Both pre-flip shapes previously failed.

The fix

  • ReconfigurationParty::generate_public_input (main/v4 builder): functional V1 arm. (V1, Some(V2|V3 prior)) decodes the V1 bytes directly as the class_groups::dkg::PublicOutput the constructor takes (the V2/V3 arm reaches the same value via PublicOutputCore::into()) and feeds new_from_reconfiguration_output with the prior output's core. (V1, None) remains an error — new_from_dkg_output needs the multi-curve PublicOutputCore a V1 anchor doesn't carry, and no deployed key is in that state. The prior-output core decoding is extracted into decode_prior_reconfiguration_output_core, shared with the V2/V3 arm (whose leftover V1 => unreachable!() on the prior output also becomes an error through the helper).
  • reconstruct_full_network_dkg_output: accepts (V1 anchor, Some(V3 reconfiguration output)) — decode the V1 bytes directly where the V2 arm projects PublicOutputCore::class_group_dkg_output(). Since the handoff's canonical-mirror flip triggers purely on reconstructed_full_network_dkg_output().is_some(), the deployed keys' one-time V1→V3 anchor migration now rides the exact same mechanism as V2→V3, no mpc_manager change.

Parity with main (the production 1.1.8 line): its reconfiguration input builder handles (V1 anchor, V2 reconfiguration output) by decoding the V1 bytes directly into new_from_reconfiguration_output — this PR is that same dispatch, ported onto the v4 constructor signature (PVSS HPKE keys, PublicOutputCore prior).

Tests

  • test_v1_anchor_main_reconfiguration_and_anchor_migration (integration): fabricates the deployed shape at v2 (DKG → V2 anchor faithfully projected to V1 + one reshare → V2 reconfiguration output), then at the default (v3) protocol config drives (V1 anchor, V2 prior) through the main builder end-to-end and asserts the output is !rejected and V3-tagged; then re-injects (V1 anchor, V3 reconfiguration output) and asserts every validator's instantiation reconstructs Some(V3) — the value the canonical mirror persists.
  • reconstruct_full_network_dkg_output_gating (unit): (V1, Some(V2)) stays None; (V1, Some(V3)) now ENGAGES the reconstruction (asserted via decode failure on dummy bytes — a None would silently skip the deployed keys' migration).

Docs

dev-docs/specs/handoff.md and dev-docs/plans/network-dkg-anchor-v2-to-v3-migration.md updated: the one-time canonical-anchor migration is now stated as pre-V3 (V1|V2) → V3, and the main builder's V1 arm is documented as landed rather than deferred.

🤖 Generated with Claude Code

@omersadika omersadika left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed thoroughly — this correctly completes the exact v4 follow-up #1798 deferred, and the crypto is sound. Approving.

Verified the central claim against the pinned crypto source (cryptography-private @ 32a27aa, 2pc-mpc/src/decentralized_party/reconfiguration.rs:178): the main v4 new_from_reconfiguration_output takes dkg_output: class_groups::dkg::PublicOutput<...> directly. So decoding the V1 inner bytes straight into it is byte-equivalent to the V2/V3 arm's dkg_public_output_core.into() — the From<PublicOutputCore> projection lands on the same value the V1 bytes already are. Same reasoning validated the v3 path in #1798.

Also correct:

  • reconstruct_full_network_dkg_output restructured to check the reconfig output (must be V3) first, then the anchor — (V1|V2, Some(V3)) engages, V3-anchor and non-V3-reconfig return None. The one-time V1→V3 anchor migration now rides the exact same is_some() mirror trigger as V2→V3, with no mpc_manager change. Nice that it needed no new plumbing.
  • decode_prior_reconfiguration_output_core dedups the prior-output decode across the V1 and V2/V3 anchor arms and turns the last guarded V1 => unreachable!() into an honest error — good cleanup.
  • The unit-gating assertion is the right shape: (V1, Some(V3)) with garbage bytes must is_err(), not None — a None would silently skip the deployed keys' migration, so this discriminates the fix. The integration test reaches the full !rejected + V3-tagged + reconstruction-is-Some(V3) assertions.

CI: Cargo Test Check green (13m35s).

Two coordination notes, not blockers:

  1. Merge order: this is stacked on fix/v1-anchor-reconfiguration (#1798). Merge #1798 first; GitHub will retarget this to dev — confirm the diff stays clean (it should, it's one commit).
  2. Release scope: this is v4-gated (v4 isn't active on any deployed network), so it's not strictly 1.2.0-release-gating like #1798's v3 path is. But landing it now removes the tracked follow-up and means the deployed V1 anchor is fully handled before v4 ever activates — worth including. Your call whether it rides 1.2.0 or lands right after.

🤖 Generated with Claude Code

Base automatically changed from fix/v1-anchor-reconfiguration to dev July 4, 2026 18:30
…main (v4) reconfiguration path + one-time V1->V3 anchor migration

The deployed mainnet/testnet keys carry a V1-tagged network-DKG anchor
(the raw class_groups::dkg::PublicOutput, written by a pre-1.1.8 binary
and never rewritten). The v3 (bwd-compat) path was fixed to read it; the
main (v4) path still errored on any V1 anchor, and
reconstruct_full_network_dkg_output returned None for (V1 anchor, V3
reconfiguration output) — so once v4 activates on the deployed networks,
their first v4 reshare would fail and the handoff's one-time canonical
anchor migration would never fire.

- ReconfigurationParty::generate_public_input: functional V1 arm —
  (V1, Some(V2|V3 prior)) decodes the V1 bytes directly as the
  class_groups::dkg::PublicOutput the constructor takes (the V2/V3 arm
  reaches the same value via PublicOutputCore::into()); (V1, None)
  remains an error (new_from_dkg_output needs the multi-curve core a V1
  anchor does not carry). Prior-output core decoding extracted into
  decode_prior_reconfiguration_output_core, shared with the V2/V3 arm.
- reconstruct_full_network_dkg_output: accepts (V1, Some(V3)), so the
  deployed keys' V1->V3 anchor migration rides the existing canonical
  mirror flip unchanged (it triggers purely on the reconstruction being
  present).
- New integration test drives the deployed shape end to end at the
  default protocol config: (V1 anchor, V2 prior) through the main
  builder -> !rejected, V3-tagged output; then (V1 anchor, V3 output)
  instantiation reconstructs Some(V3) on every validator.
- handoff.md + the anchor-migration plan updated: the one-time canonical
  migration is pre-V3 (V1|V2) -> V3.

Follow-up to the v3-path fix in fix/v1-anchor-reconfiguration (stacked
on that branch); with it, no V1-anchor gap remains before v4 activation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omersadika omersadika force-pushed the fix/v1-anchor-v4-migration branch from e5b5e7a to 4bb4fbe Compare July 4, 2026 18:33
@omersadika omersadika merged commit f4edb6e into dev Jul 4, 2026
10 checks passed
@omersadika omersadika deleted the fix/v1-anchor-v4-migration branch July 4, 2026 18:35
omersadika pushed a commit that referenced this pull request Jul 9, 2026
…main (v4) reconfiguration path + one-time V1->V3 anchor migration (#1799)

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.

2 participants