Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1044,45 +1044,51 @@ mod network_key_id_derivation_tool {
}
}

/// Reconstructs the full-shape (V3) network DKG output in memory from a V2
/// (backward-compatible) DKG output and a full V3 reconfiguration output.
/// Reconstructs the full-shape (V3) network DKG output in memory from a V1 or
/// V2 (backward-compatible) DKG output and a full V3 reconfiguration output.
///
/// The V2 DKG output is a `decentralized_party::dkg::PublicOutputCore` and
/// lacks the trailing `threshold_encryption_to_sharing_output` that the full V3
/// Neither pre-V3 anchor carries the trailing
/// `threshold_encryption_to_sharing_output` that the full V3
/// `decentralized_party::dkg::PublicOutput` carries; that field is produced
/// only by the threshold-encryption-to-sharing sub-protocol, which the
/// backward-compatible reconfiguration predates. Once a full V3 reconfiguration
/// output is available it supplies that field, and the full V3 DKG output is
/// reconstructed by combining the V2 output's reconfiguration-invariant
/// class-group DKG output (`PublicOutputCore::class_group_dkg_output`) with the
/// V3 reconfiguration output (`PublicOutput::new_from_reconfiguration_output`,
/// the inverse of `class_group_dkg_output`).
/// reconstructed by combining the anchor's reconfiguration-invariant
/// class-group DKG output with the V3 reconfiguration output
/// (`PublicOutput::new_from_reconfiguration_output`). A V2 anchor is a
/// `decentralized_party::dkg::PublicOutputCore`, whose class-group DKG output
/// `PublicOutputCore::class_group_dkg_output` projects out; a V1 anchor (the
/// deployed mainnet/testnet shape, written by a pre-1.1.8 binary) IS the raw
/// `class_groups::dkg::PublicOutput` and decodes directly.
///
/// Returns `Some(V3)` only when `network_dkg_output` is V2 AND a V3
/// Returns `Some(V3)` only when `network_dkg_output` is V1 or V2 AND a V3
/// reconfiguration output is available; `None` otherwise. The reconstruction is
/// a pure (RNG-free) function of its inputs, so every validator holding the same
/// V2 DKG output and the same quorum-agreed V3 reconfiguration output derives
/// anchor and the same quorum-agreed V3 reconfiguration output derives
/// byte-identical V3 bytes.
fn reconstruct_full_network_dkg_output(
network_dkg_output: &VersionedNetworkDkgOutput,
latest_network_reconfiguration_public_output: Option<
&VersionedDecryptionKeyReconfigurationOutput,
>,
) -> DwalletMPCResult<Option<VersionedNetworkDkgOutput>> {
let (
VersionedNetworkDkgOutput::V2(dkg_public_output_core_bytes),
Some(VersionedDecryptionKeyReconfigurationOutput::V3(reconfiguration_output_bytes)),
) = (
network_dkg_output,
latest_network_reconfiguration_public_output,
)
let Some(VersionedDecryptionKeyReconfigurationOutput::V3(reconfiguration_output_bytes)) =
latest_network_reconfiguration_public_output
else {
return Ok(None);
};

let dkg_public_output_core: dkg::PublicOutputCore =
bcs::from_bytes(dkg_public_output_core_bytes)?;
let class_group_dkg_output = dkg_public_output_core.class_group_dkg_output();
let class_group_dkg_output = match network_dkg_output {
VersionedNetworkDkgOutput::V1(class_group_dkg_output_bytes) => {
bcs::from_bytes(class_group_dkg_output_bytes)?
}
VersionedNetworkDkgOutput::V2(dkg_public_output_core_bytes) => {
let dkg_public_output_core: dkg::PublicOutputCore =
bcs::from_bytes(dkg_public_output_core_bytes)?;
dkg_public_output_core.class_group_dkg_output()
}
VersionedNetworkDkgOutput::V3(_) => return Ok(None),
};

let reconfiguration_output: twopc_mpc::decentralized_party::reconfiguration::PublicOutput =
bcs::from_bytes(reconfiguration_output_bytes)?;
Expand Down Expand Up @@ -1306,11 +1312,13 @@ mod tests {
VersionedDecryptionKeyReconfigurationOutput, VersionedNetworkDkgOutput,
};

/// The reconstruction must fire ONLY for a V2 DKG output paired with a full
/// V3 reconfiguration output. Every other combination returns `None` without
/// touching the crypto decoders (so dummy bytes are fine here). The `Some`
/// path needs a real V2 DKG output + V3 reconfiguration output and is
/// exercised end-to-end by the v4 reconfiguration integration path.
/// The reconstruction must fire ONLY for a V1 or V2 DKG anchor paired with
/// a full V3 reconfiguration output. Every other combination returns `None`
/// without touching the crypto decoders (so dummy bytes are fine there).
/// The `Some` paths need real anchor + V3 reconfiguration bytes and are
/// exercised end-to-end by the v4 reconfiguration integration tests
/// (`test_v2_to_v3_reconfiguration_migration`,
/// `test_v1_anchor_main_reconfiguration_and_anchor_migration`).
#[test]
fn reconstruct_full_network_dkg_output_gating() {
use VersionedDecryptionKeyReconfigurationOutput as Reconfiguration;
Expand Down Expand Up @@ -1345,14 +1353,28 @@ mod tests {
.is_none()
);

// V1 is never produced anymore.
// V1 anchor (the deployed shape) with only a V2 reconfiguration
// output: no threshold-encryption-to-sharing field yet, no
// reconstruction.
assert!(
reconstruct_full_network_dkg_output(
&Dkg::V1(vec![]),
Some(&Reconfiguration::V3(vec![])),
Some(&Reconfiguration::V2(vec![])),
)
.unwrap()
.is_none()
);

// V1 anchor + V3 reconfiguration output DOES engage the
// reconstruction — with garbage bytes it must fail decoding rather
// than return `None` (a `None` here would silently skip the deployed
// keys' one-time anchor migration).
assert!(
reconstruct_full_network_dkg_output(
&Dkg::V1(vec![]),
Some(&Reconfiguration::V3(vec![])),
)
.is_err()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,19 +147,58 @@ impl ReconfigurationPartyPublicInputGenerator for ReconfigurationParty {
}

match network_dkg_public_output {
// Deployed keys still carry a V1 anchor on chain. The main (v4)
// reconfiguration party does not yet reconstruct its input from a
// V1 anchor — that support (and the V2->V3 anchor migration for
// these keys) is a tracked follow-up gated behind v4 activation.
// Fail the session with a clear error instead of aborting the
// process; v4 is not active on any deployed network, so this arm
// is unreachable in production until that follow-up lands.
VersionedNetworkDkgOutput::V1(_) => Err(DwalletMPCError::InternalError(
"The main (v4) reconfiguration path does not yet support a V1 network \
DKG anchor (deployed mainnet/testnet keys); this must be implemented \
before protocol v4 activates on those networks."
.to_string(),
)),
// The deployed mainnet/testnet keys carry a V1 anchor: the raw
// `class_groups::dkg::PublicOutput`, written once by the original
// (pre-1.1.8) DKG and never rewritten. Under the main party the
// anchor stays V1 until the one-time canonical-anchor migration
// flips it to V3 (`reconstruct_full_network_dkg_output` + the
// canonical mirror at instantiation), so the first main
// reconfiguration of a deployed key runs from (V1 anchor, V2 prior
// output) and, during the flip epoch, (V1 anchor, V3 prior output).
// The V1 bytes decode directly to the `class_groups::dkg::PublicOutput`
// the constructor takes — the same value the V2/V3 arm reaches via
// `dkg_public_output_core.into()`.
VersionedNetworkDkgOutput::V1(network_dkg_public_output_bytes) => {
match latest_reconfiguration_public_output {
// A V1 anchor with no prior reconfiguration output cannot
// bootstrap: `new_from_dkg_output` needs the multi-curve
// `PublicOutputCore`, which a V1 anchor does not carry. No
// deployed key is in this state (they have all
// reconfigured).
None => Err(DwalletMPCError::InternalError(
"Main Reconfig with a V1 network DKG anchor requires a prior V2 or \
V3 reconfiguration output; a V1 anchor with no reconfiguration \
output is unsupported."
.to_string(),
)),
Some(prior) => {
let prior_reconfig_core = decode_prior_reconfiguration_output_core(&prior)?;

debug_variable_chunks(
"Instantiating public input for reconfiguration v3 [prior_reconfig_core]",
"prior_reconfig_core",
&bcs::to_bytes(&prior_reconfig_core)?,
);

let public_input: <ReconfigurationParty as Party>::PublicInput =
<twopc_mpc::decentralized_party::reconfiguration::Party as Party>::PublicInput::new_from_reconfiguration_output(
&current_access_structure,
upcoming_access_structure,
current_encryption_keys_per_crt_prime_and_proofs.clone(),
upcoming_encryption_keys_per_crt_prime_and_proofs.clone(),
current_tangible_party_id_to_upcoming,
bcs::from_bytes(&network_dkg_public_output_bytes)?,
prior_reconfig_core,
upcoming_validators_pvss_hpke_keys_by_party_id.secp256k1_pvss.clone(),
upcoming_validators_pvss_hpke_keys_by_party_id.ristretto_pvss.clone(),
upcoming_validators_pvss_hpke_keys_by_party_id.secp256r1_pvss.clone(),
)
.map_err(DwalletMPCError::from)?;

Ok(public_input)
}
}
}
// V2 and V3 DKG outputs differ only in whether the trailing Protocol-0.1
// `threshold_encryption_to_sharing_output` is present. Decode either shape to a
// `dkg::PublicOutputCore` and feed it into the same main constructor — covers
Expand Down Expand Up @@ -201,20 +240,8 @@ impl ReconfigurationPartyPublicInputGenerator for ReconfigurationParty {

Ok(public_input)
}
Some(prior @ (VersionedDecryptionKeyReconfigurationOutput::V2(_)
| VersionedDecryptionKeyReconfigurationOutput::V3(_))) => {
let prior_reconfig_core: twopc_mpc::decentralized_party::reconfiguration::PublicOutputCore =
match &prior {
VersionedDecryptionKeyReconfigurationOutput::V2(bytes) => {
bcs::from_bytes(bytes)?
}
VersionedDecryptionKeyReconfigurationOutput::V3(bytes) => {
let full: twopc_mpc::decentralized_party::reconfiguration::PublicOutput =
bcs::from_bytes(bytes)?;
full.core
}
VersionedDecryptionKeyReconfigurationOutput::V1(_) => unreachable!(),
};
Some(prior) => {
let prior_reconfig_core = decode_prior_reconfiguration_output_core(&prior)?;

debug_variable_chunks(
"Instantiating public input for reconfiguration v3 [prior_reconfig_core]",
Expand All @@ -239,18 +266,34 @@ impl ReconfigurationPartyPublicInputGenerator for ReconfigurationParty {

Ok(public_input)
}
Some(VersionedDecryptionKeyReconfigurationOutput::V1(_)) => Err(
DwalletMPCError::InternalError(
"Main Reconfig expects V2 or V3 prior reconfig output; V1 is unsupported."
.to_string(),
),
),
}
}
}
}
}

/// Decodes a prior (V2 or V3) reconfiguration output to its
/// `reconfiguration::PublicOutputCore` for the main reconfiguration
/// constructors. V2 bytes are the core itself; V3 bytes are the full output,
/// whose trailing threshold-encryption-to-sharing field the constructor
/// re-derives. V1 outputs predate the core shape and are unsupported.
fn decode_prior_reconfiguration_output_core(
prior: &VersionedDecryptionKeyReconfigurationOutput,
) -> DwalletMPCResult<twopc_mpc::decentralized_party::reconfiguration::PublicOutputCore> {
match prior {
VersionedDecryptionKeyReconfigurationOutput::V2(bytes) => Ok(bcs::from_bytes(bytes)?),
VersionedDecryptionKeyReconfigurationOutput::V3(bytes) => {
let full: twopc_mpc::decentralized_party::reconfiguration::PublicOutput =
bcs::from_bytes(bytes)?;
Ok(full.core)
}
VersionedDecryptionKeyReconfigurationOutput::V1(_) => Err(DwalletMPCError::InternalError(
"Main Reconfig expects a V2 or V3 prior reconfig output; V1 is unsupported."
.to_string(),
)),
}
}

/// Builds the bwd-compat reconfiguration public input via
/// `cryptography-private @ 7795eb45`'s new
/// `decentralized_party_backward_compatible::reconfiguration::PublicInput::new_from_*`
Expand Down
Loading
Loading