feat(dwallet-mpc): support the deployed V1 network-DKG anchor on the main (v4) reconfiguration path + one-time V1->V3 anchor migration#1799
Conversation
omersadika
left a comment
There was a problem hiding this comment.
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_outputrestructured to check the reconfig output (must be V3) first, then the anchor — (V1|V2, Some(V3)) engages, V3-anchor and non-V3-reconfig returnNone. The one-time V1→V3 anchor migration now rides the exact sameis_some()mirror trigger as V2→V3, with nompc_managerchange. Nice that it needed no new plumbing.decode_prior_reconfiguration_output_corededups the prior-output decode across the V1 and V2/V3 anchor arms and turns the last guardedV1 => unreachable!()into an honest error — good cleanup.- The unit-gating assertion is the right shape: (V1, Some(V3)) with garbage bytes must
is_err(), notNone— aNonewould 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:
- 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). - 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
…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>
e5b5e7a to
4bb4fbe
Compare
…main (v4) reconfiguration path + one-time V1->V3 anchor migration (#1799) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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 rawclass_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:
reconstruct_full_network_dkg_output(which feeds the handoff's one-time canonical V2→V3 anchor flip) returnedNonefor 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 theclass_groups::dkg::PublicOutputthe constructor takes (the V2/V3 arm reaches the same value viaPublicOutputCore::into()) and feedsnew_from_reconfiguration_outputwith the prior output's core.(V1, None)remains an error —new_from_dkg_outputneeds the multi-curvePublicOutputCorea V1 anchor doesn't carry, and no deployed key is in that state. The prior-output core decoding is extracted intodecode_prior_reconfiguration_output_core, shared with the V2/V3 arm (whose leftoverV1 => 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 projectsPublicOutputCore::class_group_dkg_output(). Since the handoff's canonical-mirror flip triggers purely onreconstructed_full_network_dkg_output().is_some(), the deployed keys' one-time V1→V3 anchor migration now rides the exact same mechanism as V2→V3, nompc_managerchange.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 intonew_from_reconfiguration_output— this PR is that same dispatch, ported onto the v4 constructor signature (PVSS HPKE keys,PublicOutputCoreprior).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!rejectedand V3-tagged; then re-injects (V1 anchor, V3 reconfiguration output) and asserts every validator's instantiation reconstructsSome(V3)— the value the canonical mirror persists.reconstruct_full_network_dkg_output_gating(unit):(V1, Some(V2))staysNone;(V1, Some(V3))now ENGAGES the reconstruction (asserted via decode failure on dummy bytes — aNonewould silently skip the deployed keys' migration).Docs
dev-docs/specs/handoff.mdanddev-docs/plans/network-dkg-anchor-v2-to-v3-migration.mdupdated: 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