diff --git a/Cargo.lock b/Cargo.lock index a5bc8e35e5..b25d89b1ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3914,6 +3914,7 @@ dependencies = [ "nargo", "noirc_abi", "num-bigint", + "num-traits", "paste", "reqwest", "serde", diff --git a/circuits/bin/dkg/share_decryption/src/main.nr b/circuits/bin/dkg/share_decryption/src/main.nr index dc09eb6365..1204951a90 100644 --- a/circuits/bin/dkg/share_decryption/src/main.nr +++ b/circuits/bin/dkg/share_decryption/src/main.nr @@ -4,7 +4,9 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use lib::configs::default::dkg::{L_THRESHOLD, N, SHARE_DECRYPTION_BIT_MSG}; +use lib::configs::default::dkg::{ + L_THRESHOLD, N, SHARE_DECRYPTION_BIT_AGG, SHARE_DECRYPTION_BIT_MSG, +}; use lib::configs::default::H; use lib::core::dkg::share_decryption::ShareDecryption; use lib::math::polynomial::Polynomial; @@ -13,7 +15,7 @@ fn main( expected_commitments: pub [[Field; L_THRESHOLD]; H], decrypted_shares: [[Polynomial; L_THRESHOLD]; H], ) -> pub Field { - let share_decryption: ShareDecryption = + let share_decryption: ShareDecryption = ShareDecryption::new(expected_commitments, decrypted_shares); share_decryption.execute() diff --git a/circuits/lib/src/configs/insecure/dkg.nr b/circuits/lib/src/configs/insecure/dkg.nr index dea183b776..c590041740 100644 --- a/circuits/lib/src/configs/insecure/dkg.nr +++ b/circuits/lib/src/configs/insecure/dkg.nr @@ -5,7 +5,10 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::configs::default::{N_PARTIES, T}; -pub use crate::configs::insecure::threshold::{L as L_THRESHOLD, QIS as QIS_THRESHOLD}; +pub use crate::configs::insecure::threshold::{ + L as L_THRESHOLD, QIS as QIS_THRESHOLD, + THRESHOLD_SHARE_DECRYPTION_BIT_SK as SHARE_DECRYPTION_BIT_AGG, +}; use crate::core::dkg::share_computation::chunk::Configs as ShareComputationChunkConfigs; use crate::core::dkg::share_encryption::Configs as ShareEncryptionConfigs; @@ -131,3 +134,4 @@ share_decryption_e_sm (CIRCUIT 4b - BFV DECRYPTION E_SM) ************************************/ pub global SHARE_DECRYPTION_BIT_MSG: u32 = 36; +// SHARE_DECRYPTION_BIT_AGG: see `pub use` of `THRESHOLD_SHARE_DECRYPTION_BIT_SK` (C6 `BIT_SK`). diff --git a/circuits/lib/src/configs/insecure/threshold.nr b/circuits/lib/src/configs/insecure/threshold.nr index 36e5c6a4df..d6b72e22c3 100644 --- a/circuits/lib/src/configs/insecure/threshold.nr +++ b/circuits/lib/src/configs/insecure/threshold.nr @@ -1088,7 +1088,7 @@ pk_aggregation (CIRCUIT 5) ------------------------------------- ************************************/ -pub global PK_AGGREGATION_BIT_PK: u32 = 36; +pub global PK_AGGREGATION_BIT_PK: u32 = 35; pub global PK_AGGREGATION_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new(QIS); @@ -1098,8 +1098,8 @@ user_data_encryption (USED FOR DATA ENCRYPTION) ------------------------------------- ************************************/ -pub global USER_DATA_ENCRYPTION_BIT_PK: u32 = 36; -pub global USER_DATA_ENCRYPTION_BIT_CT: u32 = 36; +pub global USER_DATA_ENCRYPTION_BIT_PK: u32 = 35; +pub global USER_DATA_ENCRYPTION_BIT_CT: u32 = 35; pub global USER_DATA_ENCRYPTION_BIT_U: u32 = 1; pub global USER_DATA_ENCRYPTION_BIT_E0: u32 = 5; pub global USER_DATA_ENCRYPTION_BIT_E1: u32 = 5; diff --git a/circuits/lib/src/configs/secure/dkg.nr b/circuits/lib/src/configs/secure/dkg.nr index 8ee815a07b..6bd811d619 100644 --- a/circuits/lib/src/configs/secure/dkg.nr +++ b/circuits/lib/src/configs/secure/dkg.nr @@ -5,7 +5,10 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::configs::default::{N_PARTIES, T}; -pub use crate::configs::secure::threshold::{L as L_THRESHOLD, QIS as QIS_THRESHOLD}; +pub use crate::configs::secure::threshold::{ + L as L_THRESHOLD, QIS as QIS_THRESHOLD, + THRESHOLD_SHARE_DECRYPTION_BIT_SK as SHARE_DECRYPTION_BIT_AGG, +}; use crate::core::dkg::share_computation::chunk::Configs as ShareComputationChunkConfigs; use crate::core::dkg::share_encryption::Configs as ShareEncryptionConfigs; @@ -136,4 +139,5 @@ share_decryption_e_sm (CIRCUIT 4b - BFV DECRYPTION E_SM) ------------------------------------- ************************************/ -pub global SHARE_DECRYPTION_BIT_MSG: u32 = 55; +pub global SHARE_DECRYPTION_BIT_MSG: u32 = 54; +// SHARE_DECRYPTION_BIT_AGG: see `pub use` of `THRESHOLD_SHARE_DECRYPTION_BIT_SK` (C6 `BIT_SK`). diff --git a/circuits/lib/src/configs/secure/threshold.nr b/circuits/lib/src/configs/secure/threshold.nr index 91c45600ee..19e34215ef 100644 --- a/circuits/lib/src/configs/secure/threshold.nr +++ b/circuits/lib/src/configs/secure/threshold.nr @@ -32845,7 +32845,7 @@ pk_aggregation (CIRCUIT 5) ------------------------------------- ************************************/ -pub global PK_AGGREGATION_BIT_PK: u32 = 53; +pub global PK_AGGREGATION_BIT_PK: u32 = 52; pub global PK_AGGREGATION_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new(QIS); /************************************ @@ -32854,8 +32854,8 @@ user_data_encryption (USED FOR DATA ENCRYPTION) ------------------------------------- ************************************/ -pub global USER_DATA_ENCRYPTION_BIT_PK: u32 = 53; -pub global USER_DATA_ENCRYPTION_BIT_CT: u32 = 53; +pub global USER_DATA_ENCRYPTION_BIT_PK: u32 = 52; +pub global USER_DATA_ENCRYPTION_BIT_CT: u32 = 52; pub global USER_DATA_ENCRYPTION_BIT_U: u32 = 1; pub global USER_DATA_ENCRYPTION_BIT_E0: u32 = 105; pub global USER_DATA_ENCRYPTION_BIT_E1: u32 = 5; diff --git a/circuits/lib/src/core/dkg/share_decryption.nr b/circuits/lib/src/core/dkg/share_decryption.nr index 803cadb805..a7cb886309 100644 --- a/circuits/lib/src/core/dkg/share_decryption.nr +++ b/circuits/lib/src/core/dkg/share_decryption.nr @@ -19,7 +19,10 @@ use crate::math::polynomial::Polynomial; /// **Produces:** /// - C4a: `commit(agg_sk)` -> C6 (threshold share decryption). /// - C4b: `commit(agg_e_sm)` -> C6 (threshold share decryption). -pub struct ShareDecryption { +/// +/// `BIT_MSG` hashes per-share plaintexts (aligned with C2). `BIT_AGG` hashes the CRT aggregate +/// (`compute_aggregated_shares_commitment`) and must match threshold C6 `BIT_SK` / `BIT_E_SM`. +pub struct ShareDecryption { /// Expected commitments to the share polynomials, produced in C2a (for C4a) or C2b (for C4b) /// via commit_to_party_shares. Organised as [party_idx][mod_idx], covering all H honest /// parties and L CRT moduli. @@ -32,7 +35,7 @@ pub struct ShareDecryption decrypted_shares: [[Polynomial; L]; H], } -impl ShareDecryption { +impl ShareDecryption { pub fn new( expected_commitments: [[Field; L]; H], decrypted_shares: [[Polynomial; L]; H], @@ -104,6 +107,6 @@ impl ShareDecryption(aggregated) + compute_aggregated_shares_commitment::(aggregated) } } diff --git a/crates/polynomial/src/crt_polynomial.rs b/crates/polynomial/src/crt_polynomial.rs index 2da4f21f5f..ab9fa037ea 100644 --- a/crates/polynomial/src/crt_polynomial.rs +++ b/crates/polynomial/src/crt_polynomial.rs @@ -6,10 +6,15 @@ //! CRT (Chinese Remainder Theorem) polynomial representation. +use std::sync::Arc; + use crate::polynomial::Polynomial; use crate::utils::reduce; -use fhe_math::rq::{Poly, Representation}; +use fhe_math::rq::traits::TryConvertFrom; +use fhe_math::rq::{Context, Poly, Representation}; +use ndarray::Array2; use num_bigint::BigInt; +use num_traits::{ToPrimitive, Zero}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -94,6 +99,57 @@ impl CrtPolynomial { Self { limbs } } + /// Builds an fhe-math `Poly` in PowerBasis representation (inverse of [`Self::from_fhe_polynomial`]). + /// + /// Coefficients are reduced to `[0, q_i)` per limb using `moduli[i]` before packing the RNS buffer. + /// `ctx` must match the TRBFV/BFV context: `ctx.q.len()` equals limb count, and each limb has + /// `ctx.degree` coefficients. + /// + /// # Errors + /// + /// Returns [`fhe_math::Error`] if limb counts or coefficient lengths disagree with `ctx`, ndarray + /// layout fails, a coefficient does not fit `u64`, or `Poly::try_convert_from` fails. + pub fn to_fhe_polynomial( + &self, + ctx: &Arc, + moduli: &[u64], + ) -> Result { + let degree = ctx.degree; + let l = ctx.q.len(); + if self.limbs.len() != l { + return Err(fhe_math::Error::Default(format!( + "CrtPolynomial::to_fhe_polynomial: {} limbs != ctx.q.len() {}", + self.limbs.len(), + l + ))); + } + if moduli.len() != l { + return Err(fhe_math::Error::Default(format!( + "CrtPolynomial::to_fhe_polynomial: {} moduli != ctx.q.len() {}", + moduli.len(), + l + ))); + } + let mut data = Vec::with_capacity(l * degree); + for i in 0..l { + let coeffs = self.limb(i).coefficients(); + if coeffs.len() != degree { + return Err(fhe_math::Error::Default(format!( + "CrtPolynomial::to_fhe_polynomial: limb {i} len {} != ctx.degree {degree}", + coeffs.len() + ))); + } + for j in 0..degree { + let u = bigint_to_u64_mod(&coeffs[j], moduli[i])?; + data.push(u); + } + } + let arr = Array2::from_shape_vec((l, degree), data).map_err(|e| { + fhe_math::Error::Default(format!("CrtPolynomial::to_fhe_polynomial: ndarray {e}")) + })?; + Poly::try_convert_from(arr, ctx, false, Representation::PowerBasis) + } + /// Reverses the coefficient order of every limb in-place. /// /// For each limb, converts between descending degree (a_n, …, a_0) and ascending @@ -205,3 +261,16 @@ impl CrtPolynomial { &self.limbs[i] } } + +fn bigint_to_u64_mod(c: &BigInt, m: u64) -> Result { + let bm = BigInt::from(m); + let mut r = c % &bm; + if r < BigInt::zero() { + r += bm; + } + r.to_u64().ok_or_else(|| { + fhe_math::Error::Default(format!( + "CrtPolynomial::to_fhe_polynomial: coefficient does not fit u64 after mod {m}: {r}" + )) + }) +} diff --git a/crates/zk-helpers/src/circuits/dkg/share_decryption/codegen.rs b/crates/zk-helpers/src/circuits/dkg/share_decryption/codegen.rs index 408855cebf..46cbf43997 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_decryption/codegen.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_decryption/codegen.rs @@ -61,11 +61,14 @@ share_decryption_e_sm (CIRCUIT 4b - BFV DECRYPTION E_SM) ************************************/ pub global {}_BIT_MSG: u32 = {}; +pub global {}_BIT_AGG: u32 = {}; "#, preset.dkg_counterpart().unwrap().metadata().degree, preset.dkg_counterpart().unwrap().metadata().num_moduli, prefix, configs.bits.msg_bit, + prefix, + configs.bits.agg_bit, ) } @@ -117,5 +120,8 @@ mod tests { assert!(artifacts .configs .contains(format!("{}_BIT_MSG: u32 = {}", prefix, configs.bits.msg_bit).as_str())); + assert!(artifacts + .configs + .contains(format!("{}_BIT_AGG: u32 = {}", prefix, configs.bits.agg_bit).as_str())); } } diff --git a/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs b/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs index fc1eae02a7..ca2a8a7226 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs @@ -9,12 +9,22 @@ //! [`Configs`], [`Bounds`], [`Bits`], and [`Inputs`] are produced from BFV parameters //! and (for input) honest ciphertexts and secret key. Input values are normalized for the ZKP //! field so the Noir circuit's range checks and commitment checks succeed. +//! +//! Bit widths: +//! - **`msg_bit`** — [`crate::compute_msg_bit`] on the **DKG** BFV params: coefficients in +//! `[0, t)` so the bound is `t − 1`. Matches C2 share-encryption +//! `compute_share_encryption_commitment_from_message` on per-share plaintexts. Emitted as +//! `SHARE_DECRYPTION_BIT_MSG` in codegen. +//! - **`agg_bit`** — [`crate::compute_modulus_bit`] on the **threshold** BFV params: same as C6 +//! aggregate hashing. Emitted as `SHARE_DECRYPTION_BIT_AGG`; the Noir C4 circuit uses it for +//! `compute_aggregated_shares_commitment` on the sum (per-share verification still uses `BIT_MSG`). use crate::circuits::commitments::compute_share_encryption_commitment_from_message; use crate::dkg::share_decryption::ShareDecryptionCircuit; use crate::dkg::share_decryption::ShareDecryptionCircuitData; use crate::CircuitsErrors; -use crate::{bigint_2d_to_json_values, calculate_bit_width, poly_coefficients_to_toml_json}; +use crate::{bigint_2d_to_json_values, poly_coefficients_to_toml_json}; +use crate::{compute_modulus_bit, compute_msg_bit}; use crate::{CircuitComputation, Computation}; use e3_fhe_params::build_pair_for_preset; use e3_fhe_params::BfvPreset; @@ -68,11 +78,15 @@ pub struct Configs { pub bounds: Bounds, } -/// Bit widths used by the Noir prover (e.g. for packing message coefficients). +/// Bit widths used by the Noir prover and witness recomputation. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Bits { - /// Bit width for plaintext/message coefficients (in [0, t)). + /// Per-share message coefficients in `[0, t)`; matches C2 share encryption + /// (`compute_msg_bit` on DKG params). pub msg_bit: u32, + /// CRT aggregate polynomials (same ring semantics as C6 `sk` / `e_sm`); + /// matches [`crate::compute_modulus_bit`] on threshold params. + pub agg_bit: u32, } /// Coefficient bounds for the share-decryption circuit (currently empty; bounds are derived from plaintext modulus). @@ -123,11 +137,12 @@ impl Computation for Bits { type Error = crate::utils::ZkHelpersUtilsError; fn compute(preset: Self::Preset, _: &Self::Data) -> Result { - let (_, dkg_params) = build_pair_for_preset(preset) + let (threshold_params, dkg_params) = build_pair_for_preset(preset) .map_err(|e| crate::utils::ZkHelpersUtilsError::ParseBound(e.to_string()))?; Ok(Bits { - msg_bit: calculate_bit_width(BigInt::from(dkg_params.plaintext())), + msg_bit: compute_msg_bit(&dkg_params), + agg_bit: compute_modulus_bit(&threshold_params), }) } } @@ -155,7 +170,7 @@ impl Computation for Inputs { let mut expected_commitments: Vec> = Vec::new(); let mut decrypted_shares: Vec>> = Vec::new(); - let msg_bit = calculate_bit_width(BigInt::from(dkg_params.plaintext())); + let msg_bit = compute_msg_bit(&dkg_params); // Decrypt each ciphertext and compute its commitment for party_cts in data.honest_ciphertexts.iter() { @@ -240,8 +255,9 @@ mod tests { let bits = Bits::compute(BfvPreset::InsecureThreshold512, &bounds).unwrap(); let (_, dkg_params) = build_pair_for_preset(BfvPreset::InsecureThreshold512).unwrap(); - let expected_msg_bit = calculate_bit_width(BigInt::from(dkg_params.plaintext())); - assert_eq!(bits.msg_bit, expected_msg_bit); + let (threshold_params, _) = build_pair_for_preset(BfvPreset::InsecureThreshold512).unwrap(); + assert_eq!(bits.msg_bit, compute_msg_bit(&dkg_params)); + assert_eq!(bits.agg_bit, compute_modulus_bit(&threshold_params)); } #[test] @@ -299,7 +315,7 @@ mod tests { let (threshold_params, dkg_params) = build_pair_for_preset(preset).unwrap(); let threshold_l = threshold_params.moduli().len(); - let msg_bit = calculate_bit_width(BigInt::from(dkg_params.plaintext())); + let msg_bit = compute_msg_bit(&dkg_params); let inputs = Inputs::compute(preset, &sample).unwrap(); assert_eq!( diff --git a/crates/zk-prover/Cargo.toml b/crates/zk-prover/Cargo.toml index 0e7771ea4b..f78f687240 100644 --- a/crates/zk-prover/Cargo.toml +++ b/crates/zk-prover/Cargo.toml @@ -50,6 +50,7 @@ walkdir = "2.5" e3-test-helpers = { workspace = true } ark-bn254 = { workspace = true } ark-ff = { workspace = true } +num-traits = { workspace = true } paste = "1" tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/zk-prover/tests/local_e2e_tests.rs b/crates/zk-prover/tests/local_e2e_tests.rs index 017fdfbdf8..bfd8ede407 100644 --- a/crates/zk-prover/tests/local_e2e_tests.rs +++ b/crates/zk-prover/tests/local_e2e_tests.rs @@ -16,17 +16,24 @@ mod common; +use std::sync::Arc; + use ark_bn254::Fr; -use ark_ff::{PrimeField, Zero}; +use ark_ff::{BigInteger, PrimeField, Zero}; use common::{ extract_field, extract_field_from_end, find_bb, setup_compiled_circuit, setup_test_prover, }; +use e3_events::CircuitName; use e3_fhe_params::{build_pair_for_preset, BfvPreset}; +use e3_polynomial::CrtPolynomial; use e3_zk_helpers::circuits::dkg::pk::circuit::PkCircuit; use e3_zk_helpers::circuits::dkg::pk::circuit::PkCircuitData; use e3_zk_helpers::circuits::threshold::pk_generation::utils::deterministic_crp_crt_polynomial; use e3_zk_helpers::circuits::{ - commitments::{compute_dkg_pk_commitment, compute_threshold_decryption_share_commitment}, + commitments::{ + compute_aggregated_shares_commitment, compute_dkg_pk_commitment, + compute_threshold_decryption_share_commitment, + }, threshold::decrypted_shares_aggregation::MAX_MSG_NON_ZERO_COEFFS, CircuitComputation, }; @@ -35,6 +42,10 @@ use e3_zk_helpers::dkg::share_computation::{ Configs, ShareComputationBaseCircuit, ShareComputationChunkCircuit, ShareComputationChunkCircuitData, ShareComputationCircuit, ShareComputationCircuitData, }; +use e3_zk_helpers::dkg::share_decryption::{ + ShareDecryptionCircuit as DkgShareDecryptionCircuit, + ShareDecryptionCircuitData as DkgShareDecryptionCircuitData, +}; use e3_zk_helpers::dkg::share_encryption::{ShareEncryptionCircuit, ShareEncryptionCircuitData}; use e3_zk_helpers::threshold::pk_generation::{PkGenerationCircuit, PkGenerationCircuitData}; use e3_zk_helpers::threshold::{ @@ -57,6 +68,44 @@ use e3_zk_prover::{ generate_chunk_batch_proof, generate_share_computation_final_proof, CircuitVariant, Provable, ZkBackend, ZkProver, }; +use fhe::trbfv::TRBFV; + +/// Sum per-modulus decrypted shares across honest parties (matches C4 `compute_aggregated_shares`). +/// Coefficients are summed in the BN254 field, not reduced mod each CRT modulus (see `share_decryption.nr`). +fn aggregate_dkg_decrypted_shares_to_crt( + decrypted_shares: &[Vec>], +) -> CrtPolynomial { + let h = decrypted_shares.len(); + let l = decrypted_shares[0].len(); + let n = decrypted_shares[0][0].len(); + let mut limb_vecs: Vec> = Vec::with_capacity(l); + for mod_idx in 0..l { + let mut coeffs = vec![num_bigint::BigInt::from(0u64); n]; + for coeff_idx in 0..n { + let mut sum = Fr::zero(); + for party in 0..h { + let bytes = bigint_to_field_bytes32(&decrypted_shares[party][mod_idx][coeff_idx]); + sum += Fr::from_be_bytes_mod_order(&bytes); + } + coeffs[coeff_idx] = fr_to_bigint(sum); + } + limb_vecs.push(coeffs); + } + CrtPolynomial::from_bigint_vectors(limb_vecs) +} + +fn bigint_to_field_bytes32(b: &num_bigint::BigInt) -> [u8; 32] { + let (_, bytes) = b.to_bytes_be(); + let mut out = [0u8; 32]; + let take = bytes.len().min(32); + out[32 - take..].copy_from_slice(&bytes[bytes.len() - take..]); + out +} + +fn fr_to_bigint(f: Fr) -> num_bigint::BigInt { + let le = f.into_bigint().to_bytes_le(); + num_bigint::BigInt::from_bytes_le(num_bigint::Sign::Plus, &le) +} /// Convert raw public signals bytes (32-byte big-endian chunks) to ark_bn254::Fr field elements. fn public_signals_to_fields(signals: &[u8]) -> Vec { @@ -283,6 +332,35 @@ async fn setup_share_decryption_test() -> Option<( )) } +/// Loads C4 (dkg/share_decryption) and C6 (threshold/share_decryption) artifacts for link tests. +async fn setup_c4_c6_e2e_test() -> Option<( + ZkBackend, + tempfile::TempDir, + ZkProver, + DkgShareDecryptionCircuitData, + ThresholdShareDecryptionCircuitData, + BfvPreset, +)> { + let committee = CiphernodesCommitteeSize::Micro.values(); + let preset = BfvPreset::InsecureThreshold512; + let bb = find_bb().await?; + let (backend, temp) = setup_test_prover(&bb).await; + + setup_compiled_circuit(&backend, "dkg", "share_decryption").await; + setup_compiled_circuit(&backend, "threshold", "share_decryption").await; + + let dkg_sample = DkgShareDecryptionCircuitData::generate_sample( + preset, + committee.clone(), + DkgInputType::SecretKey, + ) + .ok()?; + let c6_sample = ThresholdShareDecryptionCircuitData::generate_sample(preset, committee).ok()?; + let prover = ZkProver::new(&backend); + + Some((backend, temp, prover, dkg_sample, c6_sample, preset)) +} + async fn setup_pk_aggregation_test() -> Option<( ZkBackend, tempfile::TempDir, @@ -718,6 +796,171 @@ async fn test_threshold_share_decryption_commitment_consistency() { prover.cleanup(e3_id).unwrap(); } +/// C4a publishes `commitment` (aggregated sk); C6 consumes `expected_sk_commitment` as its first +/// public input — see `commitment_links/c4a_to_c6.rs`. This test checks both proofs expose those +/// values consistently with witness recomputation (same hash as the cross-circuit link). +#[tokio::test] +async fn test_c4_sk_commitment_is_c6_expected_sk_input_e2e() { + let Some((_backend, _temp, prover, dkg_sample, c6_sample, preset)) = + setup_c4_c6_e2e_test().await + else { + println!("skipping: bb not found"); + return; + }; + + let e3_id_c4 = "c4-e2e"; + let e3_id_c6 = "c6-e2e"; + + let c4_proof = DkgShareDecryptionCircuit + .prove_with_variant( + &prover, + &preset, + &dkg_sample, + e3_id_c4, + CircuitVariant::Recursive, + ) + .expect("C4 proof generation should succeed"); + + let c6_proof = ThresholdShareDecryptionCircuit + .prove_with_variant( + &prover, + &preset, + &c6_sample, + e3_id_c6, + CircuitVariant::Recursive, + ) + .expect("C6 proof generation should succeed"); + + let c4_commitment_bytes = CircuitName::DkgShareDecryption + .output_layout() + .extract_field(&c4_proof.public_signals, "commitment") + .expect("C4 proof must expose commitment output"); + + let c6_expected_sk_bytes = CircuitName::ThresholdShareDecryption + .input_layout() + .extract_field(&c6_proof.public_signals, "expected_sk_commitment") + .expect("C6 proof must expose expected_sk_commitment at public inputs"); + + let dkg_out = DkgShareDecryptionCircuit::compute(preset, &dkg_sample).unwrap(); + let aggregated = aggregate_dkg_decrypted_shares_to_crt(&dkg_out.inputs.decrypted_shares); + let expected_c4 = compute_aggregated_shares_commitment(&aggregated, dkg_out.bits.agg_bit); + let c4_from_proof = + num_bigint::BigInt::from_bytes_be(num_bigint::Sign::Plus, c4_commitment_bytes); + assert_eq!( + c4_from_proof, expected_c4, + "C4 commitment output must match compute_aggregated_shares_commitment on aggregated DKG shares" + ); + + let c6_out = ThresholdShareDecryptionCircuit::compute(preset, &c6_sample).unwrap(); + let expected_c6_sk = c6_out.inputs.expected_sk_commitment.clone(); + let c6_expected_sk_from_proof = + num_bigint::BigInt::from_bytes_be(num_bigint::Sign::Plus, c6_expected_sk_bytes); + assert_eq!( + c6_expected_sk_from_proof, expected_c6_sk, + "C6 expected_sk_commitment public input must match witness computation" + ); + + prover.cleanup(e3_id_c4).unwrap(); + prover.cleanup(e3_id_c6).unwrap(); +} + +/// Wires the same aggregated SK (`agg_sk`) into C6: DKG aggregate → `s` + TRBFV `decryption_share` for new `d_share`. +/// +/// C4 hashes the aggregate with `SHARE_DECRYPTION_BIT_AGG` (Rust `dkg_out.bits.agg_bit`); C6 uses the same width for `sk`. +/// Per-share C4 checks still use `SHARE_DECRYPTION_BIT_MSG` (`msg_bit`) vs C2. +#[tokio::test] +async fn test_c4_c6_sk_commitment_aligned_transcript_e2e() { + let Some((_backend, _temp, prover, dkg_sample, mut c6_sample, preset)) = + setup_c4_c6_e2e_test().await + else { + println!("skipping: bb not found"); + return; + }; + + let committee = CiphernodesCommitteeSize::Micro.values(); + let (threshold_params, _) = build_pair_for_preset(preset).unwrap(); + let ctx = threshold_params.ctx_at_level(0).unwrap(); + let moduli = threshold_params.moduli(); + + let dkg_out = DkgShareDecryptionCircuit::compute(preset, &dkg_sample).unwrap(); + let agg_sk = aggregate_dkg_decrypted_shares_to_crt(&dkg_out.inputs.decrypted_shares); + + let sk_poly = agg_sk + .to_fhe_polynomial(&ctx, moduli) + .expect("agg_sk -> Poly"); + let es_poly = c6_sample + .e + .to_fhe_polynomial(&ctx, moduli) + .expect("e -> Poly"); + + let trbfv = + TRBFV::new(committee.n, committee.threshold, threshold_params.clone()).expect("TRBFV::new"); + + let d_share_rns = trbfv + .decryption_share(Arc::new(c6_sample.ciphertext.clone()), sk_poly, es_poly) + .expect("decryption_share"); + + c6_sample.s = agg_sk.clone(); + c6_sample.d_share = CrtPolynomial::from_fhe_polynomial(&d_share_rns); + + let e3_id_c4 = "c4-align"; + let e3_id_c6 = "c6-align"; + + let c4_proof = DkgShareDecryptionCircuit + .prove_with_variant( + &prover, + &preset, + &dkg_sample, + e3_id_c4, + CircuitVariant::Recursive, + ) + .expect("C4 proof generation should succeed"); + + let c6_proof = ThresholdShareDecryptionCircuit + .prove_with_variant( + &prover, + &preset, + &c6_sample, + e3_id_c6, + CircuitVariant::Recursive, + ) + .expect("C6 proof generation should succeed"); + + let c4_commitment = CircuitName::DkgShareDecryption + .output_layout() + .extract_field(&c4_proof.public_signals, "commitment") + .expect("C4 commitment"); + + let c6_expected_sk = CircuitName::ThresholdShareDecryption + .input_layout() + .extract_field(&c6_proof.public_signals, "expected_sk_commitment") + .expect("C6 expected_sk_commitment"); + + let c4_big = num_bigint::BigInt::from_bytes_be(num_bigint::Sign::Plus, c4_commitment); + let c6_big = num_bigint::BigInt::from_bytes_be(num_bigint::Sign::Plus, c6_expected_sk); + + let expected_c4_hash = compute_aggregated_shares_commitment(&agg_sk, dkg_out.bits.agg_bit); + assert_eq!( + c4_big, expected_c4_hash, + "C4 commitment must match hash(agg_sk) with DKG agg_bit (BIT_AGG)" + ); + + let c6_out = ThresholdShareDecryptionCircuit::compute(preset, &c6_sample).unwrap(); + assert_eq!( + c6_big, c6_out.inputs.expected_sk_commitment, + "C6 expected_sk_commitment must match witness after wiring agg_sk" + ); + + assert_eq!( + dkg_out.bits.agg_bit, + c6_out.bits.sk_bit, + "DKG bits.agg_bit (compute_modulus_bit on threshold) must match C6 bits.sk_bit for aggregate hashing" + ); + + prover.cleanup(e3_id_c4).unwrap(); + prover.cleanup(e3_id_c6).unwrap(); +} + #[tokio::test] async fn test_decrypted_shares_aggregation_commitment_consistency() { let Some((_backend, _temp, prover, circuit, sample, preset, e3_id)) =