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
38 changes: 25 additions & 13 deletions agent/flow-trace/04_DKG_AND_COMPUTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,16 @@ ShareVerificationActor receives ShareVerificationDispatched(kind=ShareProofs)
│ ├─ CommitmentConsistencyChecker (per-E3 actor) receives this:
│ │ ├─ Caches each party's (address, proof_type) → {public_signals, data_hash}
│ │ ├─ Evaluates all registered CommitmentLinks:
│ │ │ C0→C3 (SourceMustExistInTargets): C3's expected_pk_commitment ∈ any C0 pk_commitment
│ │ │ C1→C5 (CrossParty): C1's pk_commitment ∈ C5 expected pk inputs
│ │ │ C2→C3 (SameParty): C3's expected_message_commitment ∈ C2's share commitments
│ │ │ C2→C4 (SourceMustExistInTargets): C2's L share commitments for recipient R exactly
│ │ │ match C4_R's expected_commitments row for sender X
│ │ │ C6→C7 (CrossParty): C6's d_commitment matches C7's expected_d_commitment
│ │ │ C0→C3 (SourceMustExistInTargets): C3's expected_pk_commitment ∈ any C0 pk_commitment
│ │ │ C1→C2a (SameParty): C1's sk_commitment == C2a's expected_secret_commitment
│ │ │ C1→C2b (SameParty): C1's e_sm_commitment == C2b's expected_secret_commitment
│ │ │ C1→C5 (CrossParty): C1's pk_commitment ∈ C5 expected pk inputs
│ │ │ C2→C3 (SameParty): C3's expected_message_commitment ∈ C2's share commitments
│ │ │ C2→C4 (SourceMustExistInTargets): C2's L share commitments for recipient R exactly
│ │ │ match C4_R's expected_commitments row for sender X
│ │ │ C4a→C6 (SameParty): C4a's commitment == C6's expected_sk_commitment
│ │ │ C4b→C6 (SameParty): C4b's commitment == C6's expected_e_sm_commitment
│ │ │ C6→C7 (CrossParty): C6's d_commitment matches C7's expected_d_commitment
│ │ │
│ │ ├─ On mismatch: publishes CommitmentConsistencyViolation
│ │ │ → AccusationManager initiates accusation quorum (see Part 5)
Expand Down Expand Up @@ -458,8 +462,9 @@ ThresholdKeyshare receives AllThresholdSharesCollected
│ party_proofs: [C4a + C4b proofs per party]
│ }
│ → ShareVerificationActor performs same 2-phase verification:
│ Phase 1: ECDSA signature recovery + consistency check
│ Phase 2: ZK proof verification via bb binary
│ Phase 1: ECDSA signature recovery
│ Phase 2: Commitment consistency check (C2→C4, C4a→C6, C4b→C6)
│ Phase 3: ZK proof verification via bb binary
Comment thread
0xjei marked this conversation as resolved.
│ → On failure: SignedProofFailed → accusation pipeline
│ → On pass: ProofVerificationPassed (cached)
Expand Down Expand Up @@ -773,7 +778,9 @@ EnclaveSolReader decodes CiphertextOutputPublished event
│ │ │ │ correctly │
├──────┼────────────────────────────┼───────────────────┼──────────────────────────────┤
│ C1 │ TrBFV PK Generation │ DKG: Share Gen │ Threshold pk_share derived │
│ │ │ │ correctly from sk │
│ │ │ │ correctly from sk; outputs │
│ │ │ │ sk_commitment, pk_commitment,│
│ │ │ │ e_sm_commitment │
├──────┼────────────────────────────┼───────────────────┼──────────────────────────────┤
│ C2a │ SK Share Computation │ DKG: Share Gen │ Shamir shares of sk computed │
│ │ │ │ correctly │
Expand All @@ -787,11 +794,16 @@ EnclaveSolReader decodes CiphertextOutputPublished event
│ C3b │ ESM Share Encryption │ DKG: Share Gen │ esi_sss encrypted correctly │
│ │ │ │ under recipient's BFV key │
├──────┼────────────────────────────┼───────────────────┼──────────────────────────────┤
│ C4a │ SK Decryption Share (T2) │ DKG: Key Calc │ SK share decryption done │
│ │ │ │ correctly │
│ C4a │ SK Decryption Share (T2) │ DKG: Key Calc │ Verifies H decrypted shares │
│ │ │ │ match C2a commitments; sums │
│ │ │ │ and normalises (reduce mod │
│ │ │ │ q, reverse, center) before │
│ │ │ │ hashing; output commitment │
│ │ │ │ consumed by C6 │
├──────┼────────────────────────────┼───────────────────┼──────────────────────────────┤
│ C4b │ ESM Decryption Share (T2) │ DKG: Key Calc │ ESM share decryption done │
│ │ │ │ correctly │
│ C4b │ ESM Decryption Share (T2) │ DKG: Key Calc │ Same as C4a for e_sm branch; │
│ │ │ │ output commitment consumed │
│ │ │ │ by C6 │
├──────┼────────────────────────────┼───────────────────┼──────────────────────────────┤
│ C5 │ PK Aggregation │ Aggregation │ Aggregate PK correctly │
│ │ │ │ computed from all pk_shares │
Expand Down
4 changes: 2 additions & 2 deletions circuits/bin/dkg/share_decryption/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// or FITNESS FOR A PARTICULAR PURPOSE.

use lib::configs::default::dkg::{
L_THRESHOLD, N, SHARE_DECRYPTION_BIT_AGG, SHARE_DECRYPTION_BIT_MSG,
L_THRESHOLD, N, QIS_THRESHOLD, SHARE_DECRYPTION_BIT_AGG, SHARE_DECRYPTION_BIT_MSG,
};
use lib::configs::default::H;
use lib::core::dkg::share_decryption::ShareDecryption;
Expand All @@ -18,5 +18,5 @@ fn main(
let share_decryption: ShareDecryption<N, L_THRESHOLD, H, SHARE_DECRYPTION_BIT_MSG, SHARE_DECRYPTION_BIT_AGG> =
ShareDecryption::new(expected_commitments, decrypted_shares);

share_decryption.execute()
share_decryption.execute(QIS_THRESHOLD)
}
2 changes: 1 addition & 1 deletion circuits/lib/src/configs/insecure/dkg.nr
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ share_computation_e_sm (CIRCUIT 2b)
-------------------------------------
************************************/

pub global SHARE_COMPUTATION_E_SM_BIT_SECRET: u32 = 24;
pub global SHARE_COMPUTATION_E_SM_BIT_SECRET: u32 = 28;

pub global SHARE_COMPUTATION_E_SM_CONFIGS: ShareComputationConfigs<L_THRESHOLD> =
ShareComputationConfigs::new(QIS_THRESHOLD);
Expand Down
42 changes: 36 additions & 6 deletions circuits/lib/src/core/dkg/share_computation.nr
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,19 @@ impl<let N: u32, let L: u32, let N_PARTIES: u32, let T: u32, let BIT_SECRET: u32
commit_to_party_shares::<N, L, N_PARTIES, BIT_SHARE>(self.y)
}

/// Verifies that secret hashes to expected_secret_commitment
/// Verifies that secret hashes to expected_secret_commitment.
///
/// Reverses `sk_secret` coefficients before hashing to match C1 (PkGeneration)'s
/// convention, which passes sk in highest-degree-first order. This ensures
/// `C1.sk_commitment == C2a.expected_secret_commitment` for the same sk.
fn verify_secret_commitment(self) {
let mut reversed_coeffs: [Field; N] = [0; N];
for i in 0..N {
reversed_coeffs[i] = self.sk_secret.coefficients[N - 1 - i];
}
let reversed_sk = Polynomial::new(reversed_coeffs);
assert(
compute_share_computation_sk_commitment::<N, BIT_SECRET>(self.sk_secret)
compute_share_computation_sk_commitment::<N, BIT_SECRET>(reversed_sk)
== self.expected_secret_commitment,
"SK commitment mismatch",
);
Expand Down Expand Up @@ -169,12 +178,33 @@ impl<let N: u32, let L: u32, let N_PARTIES: u32, let T: u32, let BIT_SECRET: u32
commit_to_party_shares::<N, L, N_PARTIES, BIT_SHARE>(self.y)
}

/// Verifies that secret hashes to expected_secret_commitment
/// The commitment is computed over all L RNS polynomials (matching
/// multiple_polynomial_payload's behavior which hashes all L modulus polynomials)
/// Verifies that secret hashes to expected_secret_commitment.
///
/// Reverses and centers each `e_sm_secret` limb before hashing to match C1 (PkGeneration)'s
/// convention: C1 passes e_sm reversed+centered (highest-degree-first, coefficients in
/// `[-(q-1)/2, (q-1)/2]`). `e_sm_secret` here is in `[0, q)`, so we apply the same
/// reverse+center transform before hashing.
fn verify_secret_commitment(self) {
let mut normalized: [Polynomial<N>; L] = [Polynomial::new([0; N]); L];
for j in 0..L {
let q = self.configs.qis[j];
let half = (q - 1) / 2;
let mut coeffs: [Field; N] = [0; N];
for i in 0..N {
// Reverse: position i gets the coefficient from N-1-i.
let c = self.e_sm_secret[j].coefficients[N - 1 - i];
// Center: shift [0, q) to [-(q-1)/2, (q-1)/2].
let centered = if (c as u128) > (half as u128) {
c - q
} else {
c
};
coeffs[i] = centered;
}
normalized[j] = Polynomial::new(coeffs);
}
assert(
compute_share_computation_e_sm_commitment::<N, L, BIT_SECRET>(self.e_sm_secret)
compute_share_computation_e_sm_commitment::<N, L, BIT_SECRET>(normalized)
== self.expected_secret_commitment,
"ESM commitment mismatch",
);
Expand Down
63 changes: 56 additions & 7 deletions circuits/lib/src/core/dkg/share_decryption.nr
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use crate::math::commitments::{
compute_aggregated_shares_commitment, compute_share_encryption_commitment_from_message,
};
use crate::math::modulo::U128::ModU128;
use crate::math::polynomial::Polynomial;

/// DKG share decryption and aggregation (C4a / C4b).
Expand Down Expand Up @@ -73,12 +74,12 @@ impl<let N: u32, let L: u32, let H: u32, let BIT_MSG: u32, let BIT_AGG: u32> Sha
/// Aggregates all verified shares by summing coefficient-wise across honest parties.
///
/// For each CRT modulus and each coefficient position, computes:
/// aggregated[mod_idx][coeff] = sum(decrypted_shares[party_idx][mod_idx][coeff]) mod q_l
/// aggregated[mod_idx][coeff] = sum(decrypted_shares[party_idx][mod_idx][coeff])
/// for party_idx in 0..H
///
/// Note: explicit modular reduction is omitted here because shares are verified to be in
/// [0, q_l) by C2 range checks, and the sum fits in the circuit field. The modular
/// reduction is enforced implicitly via the quotient polynomials in the downstream C6 circuit.
/// No modular reduction is applied here each share coefficient is in [0, q_l) by C2 range
/// checks, so the sum lies in [0, H*q_l) and fits in the circuit field. Reduction to [0, q_l)
/// is performed by normalize_aggregated before committing.
///
/// This reflects the threshold key construction: sk = sum(sk_i) for i in H.
/// Each ciphernode now holds a share of this sum, used during threshold decryption in P4.
Expand Down Expand Up @@ -106,14 +107,62 @@ impl<let N: u32, let L: u32, let H: u32, let BIT_MSG: u32, let BIT_AGG: u32> Sha
/// Returns a commitment to the aggregated shares:
/// - C4a: commit(agg_sk), consumed by C6 in P4
/// - C4b: commit(agg_e_sm), consumed by C6 in P4
pub fn execute(self) -> Field {
///
/// `moduli` must be the threshold CRT moduli `[q_0, ..., q_{L-1}]`.
/// They are used to reduce, reverse, and center the aggregated polynomials so that
/// `compute_aggregated_shares_commitment` produces the same hash as C6's
/// Rust witness (which operates on sk coefficients already reduced to `[0, q_l)`
/// and then applies `.reverse()` and `.center(&qi)` before hashing).
pub fn execute(self, moduli: [Field; L]) -> Field {
// Step 1: Verify decrypted shares match expected C2 commitments.
self.verify_commitments();

// Step 2: Sum shares coefficient-wise per CRT limb.
let aggregated = self.compute_aggregated_shares();

// Step 3: Publish commitment to the aggregate (C6 input).
compute_aggregated_shares_commitment::<N, L, BIT_AGG>(aggregated)
// Step 3: Normalize to match C6's representation: reduce mod q, reverse, center.
let normalized = normalize_aggregated::<N, L>(aggregated, moduli);

// Step 4: Publish commitment to the aggregate (C6 input).
compute_aggregated_shares_commitment::<N, L, BIT_AGG>(normalized)
}
}

/// Normalize aggregated share polynomials to match C6's Rust witness representation.
///
/// C6 applies three transformations before hashing:
/// 1. Reduce: coefficients to `[0, q_l)` (FHE `sk` is already reduced; C4 sums lie in `[0, H*q_l)`).
/// 2. Reverse: coefficients are reordered highest-degree-first.
/// 3. Center: each coefficient `c` is mapped to `c - q` when `c > (q-1)/2`,
/// so values lie in `[-(q-1)/2, (q-1)/2]` instead of `[0, q)`.
///
/// Reduction uses [`ModU128::reduce_mod`] (unconstrained quotient/remainder + in-circuit checks).
fn normalize_aggregated<let N: u32, let L: u32>(
aggregated: [Polynomial<N>; L],
moduli: [Field; L],
) -> [Polynomial<N>; L] {
let mut normalized: [Polynomial<N>; L] = [Polynomial::new([0; N]); L];

for mod_idx in 0..L {
let poly = aggregated[mod_idx];
let q = moduli[mod_idx];
let half = (q - 1) / 2;

let mut coeffs: [Field; N] = [0; N];
for i in 0..N {
// Reverse: position i gets the coefficient from N-1-i.
let c = poly.coefficients[N - 1 - i];
let reduced = ModU128::new(q).reduce_mod(c);
// Center: shift to [-(q-1)/2, (q-1)/2].
let centered = if (reduced as u128) > (half as u128) {
reduced - q
} else {
reduced
};
coeffs[i] = centered;
}
normalized[mod_idx] = Polynomial::new(coeffs);
}

normalized
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ impl Computation for Bounds {
type Data = ShareComputationCircuitData;
type Error = CircuitsErrors;

fn compute(preset: Self::Preset, data: &Self::Data) -> Result<Self, Self::Error> {
fn compute(preset: Self::Preset, _data: &Self::Data) -> Result<Self, Self::Error> {
let (threshold_params, _) =
build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Sample(e.to_string()))?;
let defaults = preset
Expand All @@ -159,9 +159,12 @@ impl Computation for Bounds {
let num_ciphertexts = defaults.z;
let lambda = defaults.lambda;

// Use search_defaults.n (same as C1/PkGeneration) so the smudging bound and
// resulting bit width match C1's PK_GENERATION_BIT_E_SM. This ensures
// C1.e_sm_commitment == C2b.expected_secret_commitment for the same e_sm.
let e_sm_config = SmudgingBoundCalculatorConfig::new(
threshold_params,
data.n_parties as usize,
defaults.n as usize,
num_ciphertexts as usize,
lambda as usize,
);
Expand Down Expand Up @@ -219,12 +222,35 @@ impl Computation for Inputs {

let bounds = Bounds::compute(preset, data)?;
let bits = Bits::compute(preset, &bounds)?;
// Reverse+center before committing to match C1 (PkGeneration)'s convention:
// C1 applies reverse then center to sk/e_sm before computing the commitment.
// For SK the values are already centered ({-1,0,1}) so centering is a no-op.
// For e_sm values are in [0,q) here, so we must center to [-(q-1)/2, (q-1)/2].
let expected_secret_commitment = match data.dkg_input_type {
DkgInputType::SecretKey => {
compute_share_computation_sk_commitment(secret_crt.limb(0), bits.bit_sk_secret)
let mut reversed = secret_crt.limb(0).clone();
reversed.reverse();
compute_share_computation_sk_commitment(&reversed, bits.bit_sk_secret)
}
DkgInputType::SmudgingNoise => {
compute_share_computation_e_sm_commitment(&secret_crt, bits.bit_e_sm_secret)
let centered_reversed_crt = e3_polynomial::CrtPolynomial::new(
secret_crt
.limbs
.iter()
.zip(moduli.iter())
.map(|(l, &qi)| {
let q = num_bigint::BigInt::from(qi);
let mut r = l.clone();
r.reverse();
r.center(&q);
r
})
.collect(),
);
compute_share_computation_e_sm_commitment(
&centered_reversed_crt,
bits.bit_e_sm_secret,
)
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,12 +330,13 @@ mod tests {
for (party_idx, party_cts) in sample.honest_ciphertexts.iter().enumerate() {
for mod_idx in 0..threshold_l {
let decrypted_pt = sample.secret_key.try_decrypt(&party_cts[mod_idx]).unwrap();
let mut share_coeffs = decrypted_pt.value.deref().to_vec();
// Reverse to match Inputs::compute, which reverses before committing
// (matching C3's message witness construction).
share_coeffs.reverse();
let share_coeffs = decrypted_pt.value.deref().to_vec();
// Reverse to match Inputs::compute, which reverses before committing to align
// with C2's commit_to_party_shares (highest-degree-first convention).
let mut reversed = share_coeffs.clone();
reversed.reverse();
let direct_commitment = compute_share_encryption_commitment_from_message(
&Polynomial::from_u64_vector(share_coeffs),
&Polynomial::from_u64_vector(reversed),
msg_bit,
);
assert_eq!(
Expand Down
Loading
Loading