Skip to content

Commit 5205da6

Browse files
committed
fix: correct commitment checks
1 parent dd64b48 commit 5205da6

4 files changed

Lines changed: 104 additions & 70 deletions

File tree

crates/aggregator/src/threshold_plaintext_aggregator.rs

Lines changed: 18 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ use actix::prelude::*;
1111
use anyhow::{anyhow, bail, ensure, Result};
1212
use e3_data::Persistable;
1313
use e3_events::{
14-
AggregationProofPending, AggregationProofSigned, BusHandle, CircuitName, CommitteeMemberExpelled, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, DecryptedSharesAggregationProofRequest, DecryptionshareCreated, Die, E3id, EType, EnclaveEvent, EnclaveEventData, EventContext, PartyProofsToVerify, PlaintextAggregated, Proof, ProofType, Seed, Sequenced, ShareVerificationComplete, ShareVerificationDispatched, SignedProofFailed, SignedProofPayload, TypedEvent, VerificationKind, ZkResponse, prelude::*, trap
14+
prelude::*, trap, AggregationProofPending, AggregationProofSigned, BusHandle, CircuitName,
15+
CommitteeMemberExpelled, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId,
16+
DecryptedSharesAggregationProofRequest, DecryptionshareCreated, Die, E3id, EType, EnclaveEvent,
17+
EnclaveEventData, EventContext, PartyProofsToVerify, PlaintextAggregated, Proof, ProofType,
18+
Seed, Sequenced, ShareVerificationComplete, ShareVerificationDispatched, SignedProofFailed,
19+
SignedProofPayload, TypedEvent, VerificationKind, ZkResponse,
1520
};
1621
use e3_fhe_params::BfvPreset;
1722
use e3_sortition::{E3CommitteeContainsRequest, E3CommitteeContainsResponse, Sortition};
@@ -408,53 +413,6 @@ impl ThresholdPlaintextAggregator {
408413
share_mismatch_parties,
409414
);
410415

411-
// Emit SignedProofFailed for each mismatched party so the
412-
// AccusationManager can initiate the slashing quorum protocol.
413-
// NOTE: These proofs have already passed ECDSA validation in
414-
// ShareVerificationActor, so recover_address() returns the
415-
// authenticated signer. We still verify_address() as defense
416-
// in depth before attributing a fault.
417-
for party_id in &share_mismatch_parties {
418-
if let Some(proofs) = state.c6_proofs.get(party_id) {
419-
if let Some(signed) = proofs.first() {
420-
let Ok(faulting_node) = signed.recover_address() else {
421-
warn!(
422-
"Could not recover address for party {} C6 proof — skipping accusation",
423-
party_id
424-
);
425-
continue;
426-
};
427-
// Defense in depth: only publish accusation if the
428-
// recovered address matches the wrapper signature.
429-
match signed.verify_address(&faulting_node) {
430-
Ok(true) => {}
431-
_ => {
432-
warn!(
433-
"Wrapper signature verification failed for party {} — \
434-
excluding share locally but not publishing accusation",
435-
party_id
436-
);
437-
continue;
438-
}
439-
}
440-
if let Err(err) = self.bus.publish(
441-
SignedProofFailed {
442-
e3_id: self.e3_id.clone(),
443-
faulting_node,
444-
proof_type: ProofType::C6ThresholdShareDecryption,
445-
signed_payload: signed.clone(),
446-
},
447-
ec.clone(),
448-
) {
449-
error!(
450-
"Failed to publish SignedProofFailed for party {}: {err}",
451-
party_id
452-
);
453-
}
454-
}
455-
}
456-
}
457-
458416
dishonest_parties.extend(&share_mismatch_parties);
459417
honest_shares.retain(|(id, _)| !share_mismatch_parties.contains(id));
460418
ensure!(
@@ -527,6 +485,7 @@ impl ThresholdPlaintextAggregator {
527485
Ok(())
528486
}
529487

488+
/// Verify that each honest party's decryption share bytes deserialize to
530489
/// Verify that each honest party's raw decryption share bytes match the
531490
/// `d_commitment` output in their verified C6 proof. Returns party IDs
532491
/// that failed the check.
@@ -537,7 +496,6 @@ impl ThresholdPlaintextAggregator {
537496
) -> BTreeSet<u64> {
538497
let mut mismatched = BTreeSet::new();
539498

540-
// Build BFV params and compute d_bit
541499
let Ok((threshold_params, _)) = e3_fhe_params::build_pair_for_preset(self.params_preset)
542500
else {
543501
warn!("Could not build BFV params for d_commitment check — skipping");
@@ -560,9 +518,9 @@ impl ThresholdPlaintextAggregator {
560518
let max_k =
561519
e3_zk_helpers::circuits::threshold::decrypted_shares_aggregation::MAX_MSG_NON_ZERO_COEFFS;
562520
let c6_output_layout = CircuitName::ThresholdShareDecryption.output_layout();
521+
let moduli: Vec<u64> = threshold_params.moduli().to_vec();
563522

564523
for (party_id, shares) in honest_shares {
565-
// Extract d_commitment from C6 proof
566524
let Some(proofs) = c6_proofs.get(party_id) else {
567525
continue;
568526
};
@@ -573,18 +531,16 @@ impl ThresholdPlaintextAggregator {
573531
.extract_field(&first_proof.payload.proof.public_signals, "d_commitment")
574532
else {
575533
warn!(
576-
"Could not extract d_commitment from C6 proof for party {} — skipping check",
534+
"Could not extract d_commitment from C6 proof for party {} — skipping",
577535
party_id
578536
);
579537
continue;
580538
};
581539

582-
// Compute d_commitment from raw share bytes (first ciphertext index)
583540
let Some(share_bytes) = shares.first() else {
584541
continue;
585542
};
586-
let Ok(mut poly) =
587-
e3_trbfv::helpers::try_poly_from_bytes(share_bytes, &threshold_params)
543+
let Ok(poly) = e3_trbfv::helpers::try_poly_from_bytes(share_bytes, &threshold_params)
588544
else {
589545
warn!(
590546
"Could not deserialize share for party {} — marking as mismatched",
@@ -593,18 +549,14 @@ impl ThresholdPlaintextAggregator {
593549
mismatched.insert(*party_id);
594550
continue;
595551
};
596-
poly.change_representation(fhe_math::rq::Representation::PowerBasis);
597552
let mut crt = e3_polynomial::CrtPolynomial::from_fhe_polynomial(&poly);
598553

599-
// Apply the same transformations C6's Inputs::compute applies before
600-
// hashing: reverse coefficient order + center each limb mod qi.
601-
// Without this, the commitment is over a different polynomial
602-
// representation and always mismatches the C6 proof output.
603-
let moduli: Vec<u64> = threshold_params.moduli().to_vec();
554+
// Apply the same transformations C6's Inputs::compute applies:
555+
// reverse coefficient order + center each limb mod qi.
604556
crt.reverse();
605557
if let Err(e) = crt.center(&moduli) {
606558
warn!(
607-
"Could not center d_share for party {} — skipping check: {e}",
559+
"Could not center d_share for party {} — skipping: {e}",
608560
party_id
609561
);
610562
continue;
@@ -615,11 +567,12 @@ impl ThresholdPlaintextAggregator {
615567
&crt, d_bit, max_k,
616568
);
617569

618-
// Convert BigInt to LE bytes for comparison
619-
let computed_bytes = computed.to_bytes_le().1;
570+
// Convert to big-endian 32-byte padded format matching
571+
// Barretenberg's public_signals encoding.
572+
let (_, be_bytes) = computed.to_bytes_be();
620573
let mut computed_padded = [0u8; 32];
621-
let len = computed_bytes.len().min(32);
622-
computed_padded[..len].copy_from_slice(&computed_bytes[..len]);
574+
let start = 32usize.saturating_sub(be_bytes.len());
575+
computed_padded[start..].copy_from_slice(&be_bytes[..be_bytes.len().min(32)]);
623576

624577
if computed_padded != c6_d_bytes {
625578
warn!(

crates/polynomial/src/crt_polynomial.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,19 @@ impl CrtPolynomial {
7575
Self::from_bigint_vectors(limbs)
7676
}
7777

78-
/// Builds a `CrtPolynomial` from an fhe-math `Poly` in PowerBasis representation.
78+
/// Builds a `CrtPolynomial` from an fhe-math `Poly` in any representation.
7979
///
8080
/// Used to prepare inputs for ZK circuits by converting FHE BFV ciphertext polynomials
81-
/// into CRT limb format. If `p` is in NTT form, it is converted to PowerBasis first.
81+
/// into CRT limb format. If `p` is not in PowerBasis form (e.g. NTT or NttShoup),
82+
/// it is converted first.
8283
///
8384
/// # Arguments
8485
///
85-
/// * `p` - An fhe-math polynomial (PowerBasis or Ntt).
86+
/// * `p` - An fhe-math polynomial (any representation).
8687
pub fn from_fhe_polynomial(p: &Poly) -> Self {
8788
let mut p = p.clone();
8889

89-
if *p.representation() == Representation::Ntt {
90+
if *p.representation() != Representation::PowerBasis {
9091
p.change_representation(Representation::PowerBasis);
9192
}
9293

crates/zk-helpers/src/circuits/threshold/share_decryption/computation.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,86 @@ mod tests {
363363
assert_eq!(bits.d_bit, expected_bit);
364364
}
365365

366+
/// Verifies that `CrtPolynomial::reverse()` + `center()` matches
367+
/// `Inputs::compute` for d_commitment, and that the Poly bytes round-trip
368+
/// is lossless.
369+
#[test]
370+
fn test_d_commitment_matches_inputs_compute() {
371+
use crate::circuits::commitments::compute_threshold_decryption_share_commitment;
372+
use crate::circuits::threshold::decrypted_shares_aggregation::MAX_MSG_NON_ZERO_COEFFS;
373+
use crate::threshold::share_decryption::ShareDecryptionCircuitData;
374+
use crate::CiphernodesCommitteeSize;
375+
use fhe_math::rq::{Poly, Representation};
376+
use fhe_traits::{DeserializeWithContext, Serialize as FheSer};
377+
use num_traits::ToPrimitive;
378+
379+
let preset = DEFAULT_BFV_PRESET;
380+
let committee = CiphernodesCommitteeSize::Small.values();
381+
let sample = ShareDecryptionCircuitData::generate_sample(preset, committee).unwrap();
382+
let (threshold_params, _) = build_pair_for_preset(preset).unwrap();
383+
let bounds = Bounds::compute(preset, &()).unwrap();
384+
let bits = Bits::compute(preset, &bounds).unwrap();
385+
let moduli: Vec<u64> = threshold_params.moduli().to_vec();
386+
387+
// Ground truth: Inputs::compute (what the Noir prover receives)
388+
let inputs = Inputs::compute(preset, &sample).unwrap();
389+
let truth = compute_threshold_decryption_share_commitment(
390+
&inputs.d,
391+
bits.d_bit,
392+
MAX_MSG_NON_ZERO_COEFFS,
393+
);
394+
395+
// Aggregator path: CrtPolynomial::reverse() + center()
396+
let mut crt = sample.d_share.clone();
397+
crt.reverse();
398+
crt.center(&moduli).unwrap();
399+
let from_api = compute_threshold_decryption_share_commitment(
400+
&crt,
401+
bits.d_bit,
402+
MAX_MSG_NON_ZERO_COEFFS,
403+
);
404+
assert_eq!(
405+
truth, from_api,
406+
"CrtPolynomial API must match Inputs::compute"
407+
);
408+
409+
// Bytes round-trip: Poly → to_bytes → from_bytes → from_fhe_polynomial
410+
let raw: Vec<Vec<u64>> = sample
411+
.d_share
412+
.limbs
413+
.iter()
414+
.map(|l| {
415+
l.coefficients()
416+
.iter()
417+
.map(|c| c.to_u64().unwrap())
418+
.collect()
419+
})
420+
.collect();
421+
let n = raw[0].len();
422+
let mut arr = ndarray::Array2::<u64>::zeros((raw.len(), n));
423+
for (i, limb) in raw.iter().enumerate() {
424+
for (j, &v) in limb.iter().enumerate() {
425+
arr[[i, j]] = v;
426+
}
427+
}
428+
let ctx = threshold_params.ctx_at_level(0).unwrap();
429+
let mut poly = Poly::zero(&ctx, Representation::PowerBasis);
430+
poly.set_coefficients(arr);
431+
let poly_rt = Poly::from_bytes(&poly.to_bytes(), &ctx).unwrap();
432+
let mut crt_rt = CrtPolynomial::from_fhe_polynomial(&poly_rt);
433+
crt_rt.reverse();
434+
crt_rt.center(&moduli).unwrap();
435+
let from_bytes = compute_threshold_decryption_share_commitment(
436+
&crt_rt,
437+
bits.d_bit,
438+
MAX_MSG_NON_ZERO_COEFFS,
439+
);
440+
assert_eq!(
441+
truth, from_bytes,
442+
"Bytes round-trip must match Inputs::compute"
443+
);
444+
}
445+
366446
#[test]
367447
fn test_constants_json_roundtrip() {
368448
let constants = Configs::compute(DEFAULT_BFV_PRESET, &()).unwrap();

templates/default/tests/integration.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ describe('Integration', () => {
189189
const { waitForEvent } = await setupEventListeners(sdk, store)
190190

191191
const committeeSize = CommitteeSize.Micro
192-
const duration = 500
192+
const duration = 300
193193
const inputWindow = await calculateInputWindow(publicClient, duration)
194194
const thresholdBfvParams = await sdk.getThresholdBfvParamsSet()
195195
const e3ProgramParams = encodeBfvParams(thresholdBfvParams)

0 commit comments

Comments
 (0)