From 32e1a7d0a20dfe1b7150e75e0d3ad2029a13f6ee Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 26 Mar 2026 16:48:50 +0100 Subject: [PATCH 1/2] feat: add decryption share commitments to c6 and c7 --- agent/flow-trace/00_INDEX.md | 2 +- agent/flow-trace/04_DKG_AND_COMPUTATION.md | 25 +++-- .../flow-trace/05_FAILURE_REFUND_SLASHING.md | 2 +- .../decrypted_shares_aggregation/src/main.nr | 5 +- .../threshold/share_decryption/src/main.nr | 2 +- .../decrypted_shares_aggregation/src/main.nr | 9 +- .../threshold/share_decryption/src/main.nr | 5 +- .../lib/src/configs/insecure/threshold.nr | 1 + circuits/lib/src/configs/secure/threshold.nr | 1 + .../threshold/decrypted_shares_aggregation.nr | 22 ++++- .../src/core/threshold/share_decryption.nr | 53 +++++++--- circuits/lib/src/math/commitments.nr | 17 ++++ .../src/enclave_event/compute_request/zk.rs | 2 +- crates/events/src/enclave_event/proof.rs | 12 ++- crates/zk-helpers/src/circuits/commitments.rs | 44 +++++++++ .../zk-helpers/src/circuits/output_layout.rs | 3 + .../decrypted_shares_aggregation/codegen.rs | 9 ++ .../computation.rs | 41 +++++++- .../decrypted_shares_aggregation/mod.rs | 6 +- crates/zk-prover/tests/local_e2e_tests.rs | 68 ++++++++++++- ...holdDecryptedSharesAggregationVerifier.sol | 98 +++++++++---------- 21 files changed, 326 insertions(+), 101 deletions(-) diff --git a/agent/flow-trace/00_INDEX.md b/agent/flow-trace/00_INDEX.md index 4d2c9eb5f3..ef24c5429b 100644 --- a/agent/flow-trace/00_INDEX.md +++ b/agent/flow-trace/00_INDEX.md @@ -63,7 +63,7 @@ → Broadcast to aggregator 13. AGGREGATE Aggregator combines M+1 shares → plaintext - → C7a/C7b proofs (proves reconstruction correct) + → C7 proof (proves reconstruction correct) 14. COMPLETE publishPlaintextOutput() → rewards distributed → Each active committee member gets fee / N diff --git a/agent/flow-trace/04_DKG_AND_COMPUTATION.md b/agent/flow-trace/04_DKG_AND_COMPUTATION.md index c55cba459e..9bd2f5c485 100644 --- a/agent/flow-trace/04_DKG_AND_COMPUTATION.md +++ b/agent/flow-trace/04_DKG_AND_COMPUTATION.md @@ -663,14 +663,13 @@ ThresholdPlaintextAggregator receives DecryptionshareCreated events │ │ ├─ Dispatches ComputeRequest::zk( │ │ │ ZkRequest::DecryptedSharesAggregation {...} │ │ │ ) -│ │ │ → Circuit: DecryptedSharesAggregation (C7a/b) -│ │ │ → Two variants: BN (bn254 field) and Mod (modular arithmetic) +│ │ │ → Circuit: DecryptedSharesAggregation (C7) │ │ │ → Proves plaintext was correctly reconstructed from M+1 shares │ │ ├─ ZkActor generates proof(s) via bb binary -│ │ ├─ Signs each proof variant +│ │ ├─ Signs each C7 proof (one per ciphertext index) │ │ └─ Publishes AggregationProofSigned { │ │ e3_id, party_id, signed_proof(C7) -│ │ } × (per variant) +│ │ } │ │ │ └─ Publish PlaintextAggregated { e3_id, decrypted_output } │ @@ -748,13 +747,19 @@ ThresholdPlaintextAggregator receives DecryptionshareCreated events │ │ │ │ computed from all pk_shares │ ├──────┼────────────────────────────┼───────────────────┼──────────────────────────────┤ │ C6 │ Threshold Share Decryption │ Decryption │ Decryption share correctly │ -│ │ (T5) │ │ derived from sk + ciphertext │ +│ │ (T5) │ │ derived from sk + ciphertext;│ +│ │ │ │ public output: commitment to │ +│ │ │ │ first MAX_MSG_NON_ZERO_COEFFS│ +│ │ │ │ coeffs of d per CRT limb │ ├──────┼────────────────────────────┼───────────────────┼──────────────────────────────┤ -│ C7a │ Decrypted Shares Agg. (BN) │ Final Aggregation │ Plaintext correctly │ -│ │ │ │ reconstructed (bn254 field) │ -├──────┼────────────────────────────┼───────────────────┼──────────────────────────────┤ -│ C7b │ Decrypted Shares Agg. (Mod)│ Final Aggregation │ Plaintext correctly │ -│ │ │ │ reconstructed (modular) │ +│ C7 │ Decrypted Shares Agg. │ Final Aggregation │ Plaintext correctly │ +│ │ │ │ reconstructed from shares │ +│ │ │ │ (modular decode over t); │ +│ │ │ │ public inputs: C6 `d` │ +│ │ │ │ commitments + party IDs + msg;│ +│ │ │ │ in-circuit equality vs │ +│ │ │ │ commitments from witness │ +│ │ │ │ decryption shares │ └──────┴────────────────────────────┴───────────────────┴──────────────────────────────┘ Slash Reasons by Proof Type: diff --git a/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md b/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md index 021235ea69..7e7ecc3834 100644 --- a/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md +++ b/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md @@ -835,7 +835,7 @@ Slash Reasons (derived from ProofType for Lane A): │ C0, C1-C4 │ E3_BAD_DKG_PROOF │ │ C5 │ E3_BAD_PK_AGGREGATION │ │ C6 │ E3_BAD_DECRYPTION_PROOF │ - │ C7a, C7b │ E3_BAD_AGGREGATION_PROOF │ + │ C7 │ E3_BAD_AGGREGATION_PROOF │ └─────────────────┴──────────────────────────┘ ``` diff --git a/circuits/bin/recursive_aggregation/wrapper/threshold/decrypted_shares_aggregation/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/threshold/decrypted_shares_aggregation/src/main.nr index 6d13064071..9119f31a29 100644 --- a/circuits/bin/recursive_aggregation/wrapper/threshold/decrypted_shares_aggregation/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/threshold/decrypted_shares_aggregation/src/main.nr @@ -6,15 +6,14 @@ use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; use lib::{ - configs::default::{MAX_MSG_NON_ZERO_COEFFS, T, threshold::L}, + configs::default::{MAX_MSG_NON_ZERO_COEFFS, T}, math::commitments::compute_recursive_aggregation_commitment, }; // Number of proofs. pub global N_PROOFS: u32 = 1; /// Number of public inputs/outputs per proof. -pub global N_PUBLIC_INPUTS: u32 = - ((T + 1) * L * MAX_MSG_NON_ZERO_COEFFS) + (T + 1 + MAX_MSG_NON_ZERO_COEFFS); +pub global N_PUBLIC_INPUTS: u32 = (T + 1) + MAX_MSG_NON_ZERO_COEFFS + (T + 1); fn main( verification_key: UltraHonkVerificationKey, diff --git a/circuits/bin/recursive_aggregation/wrapper/threshold/share_decryption/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/threshold/share_decryption/src/main.nr index 02deffe03f..e83aafab3e 100644 --- a/circuits/bin/recursive_aggregation/wrapper/threshold/share_decryption/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/threshold/share_decryption/src/main.nr @@ -11,7 +11,7 @@ use lib::math::commitments::compute_recursive_aggregation_commitment; // Number of proofs. pub global N_PROOFS: u32 = 1; /// Number of public inputs/outputs per proof. -pub global N_PUBLIC_INPUTS: u32 = 2 + 2 * L * N; +pub global N_PUBLIC_INPUTS: u32 = 2 + 2 * L * N + 1; fn main( verification_key: UltraHonkVerificationKey, diff --git a/circuits/bin/threshold/decrypted_shares_aggregation/src/main.nr b/circuits/bin/threshold/decrypted_shares_aggregation/src/main.nr index 7b2885a4c7..f77cb65ff8 100644 --- a/circuits/bin/threshold/decrypted_shares_aggregation/src/main.nr +++ b/circuits/bin/threshold/decrypted_shares_aggregation/src/main.nr @@ -6,20 +6,23 @@ use lib::configs::default::{MAX_MSG_NON_ZERO_COEFFS, T}; use lib::configs::default::threshold::{ - DECRYPTED_SHARES_AGGREGATION_BIT_NOISE, DECRYPTED_SHARES_AGGREGATION_CONFIGS, L, + DECRYPTED_SHARES_AGGREGATION_BIT_D, DECRYPTED_SHARES_AGGREGATION_BIT_NOISE, + DECRYPTED_SHARES_AGGREGATION_CONFIGS, L, }; use lib::core::threshold::decrypted_shares_aggregation::DecryptedSharesAggregation; use lib::math::polynomial::Polynomial; fn main( - decryption_shares: pub [[Polynomial; L]; T + 1], + expected_d_commitments: pub [Field; T + 1], party_ids: pub [Field; T + 1], message: pub Polynomial, + decryption_shares: [[Polynomial; L]; T + 1], u_global: Polynomial, crt_quotients: [Polynomial; L], ) { - let decrypted_shares_aggregation: DecryptedSharesAggregation = DecryptedSharesAggregation::new( + let decrypted_shares_aggregation: DecryptedSharesAggregation = DecryptedSharesAggregation::new( DECRYPTED_SHARES_AGGREGATION_CONFIGS, + expected_d_commitments, decryption_shares, party_ids, message, diff --git a/circuits/bin/threshold/share_decryption/src/main.nr b/circuits/bin/threshold/share_decryption/src/main.nr index e64fc687ae..a8c18e015d 100644 --- a/circuits/bin/threshold/share_decryption/src/main.nr +++ b/circuits/bin/threshold/share_decryption/src/main.nr @@ -4,6 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +use lib::configs::default::MAX_MSG_NON_ZERO_COEFFS; use lib::configs::default::threshold::{ L, N, THRESHOLD_SHARE_DECRYPTION_BIT_CT, THRESHOLD_SHARE_DECRYPTION_BIT_D, THRESHOLD_SHARE_DECRYPTION_BIT_E_SM, THRESHOLD_SHARE_DECRYPTION_BIT_R1, @@ -23,8 +24,8 @@ fn main( r1: [Polynomial<(2 * N) - 1>; L], r2: [Polynomial; L], d: [Polynomial; L], -) { - let share_decryption: ShareDecryption = ShareDecryption::new( +) -> pub Field { + let share_decryption: ShareDecryption = ShareDecryption::new( THRESHOLD_SHARE_DECRYPTION_CONFIGS, expected_sk_commitment, expected_e_sm_commitment, diff --git a/circuits/lib/src/configs/insecure/threshold.nr b/circuits/lib/src/configs/insecure/threshold.nr index 0ee509ae5a..67a646f9ad 100644 --- a/circuits/lib/src/configs/insecure/threshold.nr +++ b/circuits/lib/src/configs/insecure/threshold.nr @@ -1183,6 +1183,7 @@ decrypted_shares_aggregation (CIRCUIT 7) ************************************/ pub global DECRYPTED_SHARES_AGGREGATION_BIT_NOISE: u32 = 65; +pub global DECRYPTED_SHARES_AGGREGATION_BIT_D: u32 = 35; pub global DECRYPTED_SHARES_AGGREGATION_CONFIGS: DecryptedSharesAggregationConfigs = DecryptedSharesAggregationConfigs::new(QIS, PLAINTEXT_MODULUS, Q_INVERSE_MOD_T); diff --git a/circuits/lib/src/configs/secure/threshold.nr b/circuits/lib/src/configs/secure/threshold.nr index ddb0d08bdd..9e16c89d0d 100644 --- a/circuits/lib/src/configs/secure/threshold.nr +++ b/circuits/lib/src/configs/secure/threshold.nr @@ -32933,6 +32933,7 @@ decrypted_shares_aggregation (CIRCUIT 7) ************************************/ pub global DECRYPTED_SHARES_AGGREGATION_BIT_NOISE: u32 = 200; +pub global DECRYPTED_SHARES_AGGREGATION_BIT_D: u32 = 52; pub global DECRYPTED_SHARES_AGGREGATION_CONFIGS: DecryptedSharesAggregationConfigs = DecryptedSharesAggregationConfigs::new(QIS, PLAINTEXT_MODULUS, Q_INVERSE_MOD_T); diff --git a/circuits/lib/src/core/threshold/decrypted_shares_aggregation.nr b/circuits/lib/src/core/threshold/decrypted_shares_aggregation.nr index c6deaff177..abdd450d36 100644 --- a/circuits/lib/src/core/threshold/decrypted_shares_aggregation.nr +++ b/circuits/lib/src/core/threshold/decrypted_shares_aggregation.nr @@ -4,6 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +use crate::math::commitments::compute_threshold_decryption_share_commitment; use crate::math::modulo::U128::ModU128; use crate::math::polynomial::Polynomial; use dep::bignum::BigNum; @@ -30,14 +31,18 @@ impl Configs { /// Uses BigNum for Q values (works for both insecure and secure parameter sets). /// /// Verifies: +/// 0. Each party's decryption share matches the corresponding C6 public `d` commitment /// 1. Lagrange interpolation to compute u^{(l)} for each CRT basis /// 2. CRT reconstruction: u^{(l)} + r^{(l)} * q_l = u_global /// 3. Decoding verification: message = -Q^{-1} * (t * u_global)_Q mod t -pub struct DecryptedSharesAggregation { +pub struct DecryptedSharesAggregation { /// Circuit parameters including crypto constants configs: Configs, - /// Decryption shares from t+1 parties (public witnesses) + /// Public `d` commitments from circuit 6 (one per reconstructing party), same order as `decryption_shares` + expected_d_commitments: [Field; T + 1], + + /// Decryption shares from t+1 parties (secret witnesses) decryption_shares: [[Polynomial; L]; T + 1], /// Party IDs (x-coordinates) for interpolation (public witnesses) @@ -54,9 +59,10 @@ pub struct DecryptedSharesAggregation; L], } -impl DecryptedSharesAggregation { +impl DecryptedSharesAggregation { pub fn new( configs: Configs, + expected_d_commitments: [Field; T + 1], decryption_shares: [[Polynomial; L]; T + 1], party_ids: [Field; T + 1], message: Polynomial, @@ -65,6 +71,7 @@ impl Self { DecryptedSharesAggregation { configs, + expected_d_commitments, decryption_shares, party_ids, message, @@ -73,8 +80,15 @@ impl( + self.decryption_shares[party_idx], + ); + assert(computed == self.expected_d_commitments[party_idx], "d commitment mismatch"); + } + // Step 1: Compute Lagrange coefficients in-circuit let lagrange_coeffs = compute_all_lagrange_coeffs::(self.configs.qis, self.party_ids); diff --git a/circuits/lib/src/core/threshold/share_decryption.nr b/circuits/lib/src/core/threshold/share_decryption.nr index 74c579f672..e33ced6807 100644 --- a/circuits/lib/src/core/threshold/share_decryption.nr +++ b/circuits/lib/src/core/threshold/share_decryption.nr @@ -5,7 +5,8 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::math::commitments::{ - compute_aggregated_shares_commitment, compute_threshold_share_decryption_challenge, + compute_aggregated_shares_commitment, compute_threshold_decryption_share_commitment, + compute_threshold_share_decryption_challenge, }; use crate::math::helpers::flatten; use crate::math::polynomial::Polynomial; @@ -32,7 +33,7 @@ impl Configs { /// 1. Commitment to sk matches expected (from DKG decryption circuit) /// 2. Commitment to e_sm matches expected (from DKG decryption circuit) /// 3. Correct computation: d_i = c_0i + c_1i * s_i + e_i + r_2i * (X^N + 1) + r_1i * q_i -pub struct ShareDecryption { +pub struct ShareDecryption { /// Circuit parameters including bounds and cryptographic constants configs: Configs, @@ -61,12 +62,11 @@ pub struct ShareDecryption; L], r2: [Polynomial; L], - /// Party's computed decryption share - /// (public witnesses) + /// Party's computed decryption share (secret witness) d: [Polynomial; L], } -impl ShareDecryption { +impl ShareDecryption { pub fn new( configs: Configs, expected_sk_commitment: Field, @@ -79,6 +79,7 @@ impl; L], d: [Polynomial; L], ) -> Self { + assert(MAX_MSG_NON_ZERO_COEFFS <= N); ShareDecryption { configs, expected_sk_commitment, @@ -111,6 +112,20 @@ impl [Polynomial; L] { + let mut out: [Polynomial; L] = + [Polynomial::new([0; MAX_MSG_NON_ZERO_COEFFS]); L]; + for i in 0..L { + let mut coeffs = [0; MAX_MSG_NON_ZERO_COEFFS]; + for j in 0..MAX_MSG_NON_ZERO_COEFFS { + coeffs[j] = self.d[i].coefficients[j]; + } + out[i] = Polynomial::new(coeffs); + } + out + } + /// Flattens all witness data into a single array for Fiat-Shamir challenge generation. /// /// This function serializes all polynomial coefficients (both public inputs and @@ -124,7 +139,7 @@ impl Vec { + fn payload(self, d_commitment: Field) -> Vec { let mut inputs = Vec::new(); // Use commitments instead of full polynomials (saves constraints) @@ -148,14 +163,15 @@ impl(inputs, self.r1); inputs = flatten::<_, _, BIT_R2>(inputs, self.r2); - // Flatten decryption shares (public outputs) - inputs = flatten::<_, _, BIT_D>(inputs, self.d); + // Bind decryption share via prefix commitment (matches circuit 7) + inputs.push(d_commitment); inputs } - /// Main verification function - pub fn execute(self) { + /// Main verification function. Returns the public commitment to the first + /// `MAX_MSG_NON_ZERO_COEFFS` coefficients of `d` per CRT limb. + pub fn execute(self) -> Field { // Step 1: Verify sk commitment matches expected self.verify_agg_sk_commitment(); @@ -167,13 +183,20 @@ impl( + d_truncated, + ); + // Step 4: Generate Fiat-Shamir challenge from the transcript - let gamma = self.generate_challenge(); + let gamma = self.generate_challenge(d_commitment); // Step 5: Verify decryption share computation for each CRT basis for i in 0..L { self.verify_decryption_share_computation(i, gamma); } + + d_commitment } /// Performs range checks on quotient polynomial witnesses. @@ -210,7 +233,7 @@ impl Field { - let inputs = self.payload(); + fn generate_challenge(self, d_commitment: Field) -> Field { + let inputs = self.payload(d_commitment); compute_threshold_share_decryption_challenge::(inputs) } diff --git a/circuits/lib/src/math/commitments.nr b/circuits/lib/src/math/commitments.nr index 4e47af542f..909009a64d 100644 --- a/circuits/lib/src/math/commitments.nr +++ b/circuits/lib/src/math/commitments.nr @@ -101,6 +101,14 @@ pub global DS_CLG_SHARE_DECRYPTION: [u8; 64] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; +// Domain separator - "THRESHOLD_DECRYPTION_SHARE" +pub global DS_THRESHOLD_DECRYPTION_SHARE: [u8; 64] = [ + 0x54, 0x48, 0x52, 0x45, 0x53, 0x48, 0x4f, 0x4c, 0x44, 0x5f, 0x44, 0x45, 0x43, 0x52, 0x59, 0x50, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x48, 0x41, 0x52, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + // Domain separator - "USER_DATA_ENCRYPTION_COMMITMENT" pub global DS_USER_DATA_ENCRYPTION_COMMITMENT: [u8; 64] = [ 0x55, 0x53, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, @@ -217,6 +225,15 @@ pub fn compute_aggregated_shares_commitment` in decrypted_shares_aggregation). +pub fn compute_threshold_decryption_share_commitment( + d_share_limbs: [Polynomial; L], +) -> Field { + let mut payload = multiple_polynomial_payload::(Vec::new(), d_share_limbs); + compute_commitment(payload, DS_THRESHOLD_DECRYPTION_SHARE) +} + pub fn compute_pk_aggregation_commitment( pk0: [Polynomial; L], pk1: [Polynomial; L], diff --git a/crates/events/src/enclave_event/compute_request/zk.rs b/crates/events/src/enclave_event/compute_request/zk.rs index d274bfbcc2..d20e70923f 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -416,7 +416,7 @@ pub struct DecryptedSharesAggregationProofRequest { pub d_share_polys: Vec<(u64, Vec)>, /// Decoded plaintext per ciphertext index. pub plaintext: Vec, - /// BFV preset (selects BN vs Mod circuit variant). + /// BFV preset (parameters for witness / circuit config). pub params_preset: BfvPreset, /// Threshold required for decryption. pub threshold_m: u64, diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index 536f436c09..22300ebc62 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -5,6 +5,7 @@ use e3_utils::utility_types::ArcBytes; use e3_zk_helpers::{ CircuitOutputLayout, DKG_SHARE_DECRYPTION_OUTPUTS, PK_AGGREGATION_OUTPUTS, PK_BFV_OUTPUTS, PK_GENERATION_OUTPUTS, SHARE_COMPUTATION_CHUNK_BATCH_OUTPUTS, SHARE_COMPUTATION_OUTPUTS, + THRESHOLD_SHARE_DECRYPTION_OUTPUTS, }; use serde::{Deserialize, Serialize}; use std::fmt; @@ -191,10 +192,13 @@ impl CircuitName { CircuitName::PkAggregation => CircuitOutputLayout::Fixed { fields: PK_AGGREGATION_OUTPUTS, }, - CircuitName::ShareComputationChunk - | CircuitName::ShareEncryption - | CircuitName::ThresholdShareDecryption - | CircuitName::DecryptedSharesAggregation => CircuitOutputLayout::None, + CircuitName::ThresholdShareDecryption => CircuitOutputLayout::Fixed { + fields: THRESHOLD_SHARE_DECRYPTION_OUTPUTS, + }, + CircuitName::ShareComputationChunk | CircuitName::ShareEncryption => { + CircuitOutputLayout::None + } + CircuitName::DecryptedSharesAggregation => CircuitOutputLayout::None, CircuitName::Fold => CircuitOutputLayout::None, } } diff --git a/crates/zk-helpers/src/circuits/commitments.rs b/crates/zk-helpers/src/circuits/commitments.rs index 0b2a8e2006..b76644b971 100644 --- a/crates/zk-helpers/src/circuits/commitments.rs +++ b/crates/zk-helpers/src/circuits/commitments.rs @@ -80,6 +80,14 @@ const DS_AGGREGATED_SHARES: [u8; 64] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; +/// String: "THRESHOLD_DECRYPTION_SHARE" +const DS_THRESHOLD_DECRYPTION_SHARE: [u8; 64] = [ + 0x54, 0x48, 0x52, 0x45, 0x53, 0x48, 0x4f, 0x4c, 0x44, 0x5f, 0x44, 0x45, 0x43, 0x52, 0x59, 0x50, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x48, 0x41, 0x52, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + /// String: "VK_HASH" const DS_VK_HASH: [u8; 64] = [ 0x56, 0x4b, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -471,6 +479,42 @@ pub fn compute_aggregated_shares_commitment(agg_shares: &CrtPolynomial, bit_msg: BigInt::from_bytes_le(num_bigint::Sign::Plus, &commitment_bytes) } +// ============================================================================ +// THRESHOLD DECRYPTION SHARES (C6 / C7) +// ============================================================================ + +fn truncate_crt_polynomial_to_max_coeffs(crt: &CrtPolynomial, max_len: usize) -> CrtPolynomial { + let limbs = crt + .limbs + .iter() + .map(|limb| { + let v: Vec<_> = limb.coefficients().iter().take(max_len).cloned().collect(); + Polynomial::new(v) + }) + .collect(); + CrtPolynomial::new(limbs) +} + +/// Commitment to a threshold decryption share: all CRT limbs, first `max_k` coefficients +/// per limb (matches Noir `compute_threshold_decryption_share_commitment`). +pub fn compute_threshold_decryption_share_commitment( + d_share: &CrtPolynomial, + bit_d: u32, + max_k: usize, +) -> BigInt { + let truncated = truncate_crt_polynomial_to_max_coeffs(d_share, max_k); + let mut payload = Vec::new(); + payload = flatten(payload, &truncated.limbs, bit_d); + + let input_size = payload.len() as u32; + let io_pattern = [0x80000000 | input_size, 1]; + + let commitment_field = + compute_commitments(payload, DS_THRESHOLD_DECRYPTION_SHARE, io_pattern)[0]; + let commitment_bytes = commitment_field.into_bigint().to_bytes_le(); + BigInt::from_bytes_le(num_bigint::Sign::Plus, &commitment_bytes) +} + // ============================================================================ // COMMITMENTS FOR CHALLENGES // ============================================================================ diff --git a/crates/zk-helpers/src/circuits/output_layout.rs b/crates/zk-helpers/src/circuits/output_layout.rs index 63bcb89f54..58118dd4ee 100644 --- a/crates/zk-helpers/src/circuits/output_layout.rs +++ b/crates/zk-helpers/src/circuits/output_layout.rs @@ -135,6 +135,9 @@ pub const DKG_SHARE_DECRYPTION_OUTPUTS: &[OutputField] = &[f("commitment")]; /// C5 — Public key aggregation. pub const PK_AGGREGATION_OUTPUTS: &[OutputField] = &[f("commitment")]; +/// C6 — Threshold share decryption (prefix commitment to `d`, per CRT limb). +pub const THRESHOLD_SHARE_DECRYPTION_OUTPUTS: &[OutputField] = &[f("d_commitment")]; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/codegen.rs b/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/codegen.rs index dc390eb0a6..b689d9862d 100644 --- a/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/codegen.rs +++ b/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/codegen.rs @@ -72,6 +72,7 @@ decrypted_shares_aggregation (CIRCUIT 7) ************************************/ pub global {}_BIT_NOISE: u32 = {}; +pub global {}_BIT_D: u32 = {}; pub global {}_CONFIGS: DecryptedSharesAggregationConfigs = DecryptedSharesAggregationConfigs::new(QIS, PLAINTEXT_MODULUS, Q_INVERSE_MOD_T); @@ -85,6 +86,8 @@ pub global {}_CONFIGS: DecryptedSharesAggregationConfigs = prefix, configs.bits.noise_bit, prefix, + configs.bits.d_bit, + prefix, ) } @@ -106,6 +109,9 @@ mod tests { "{}_BIT_NOISE: u32 = {}", prefix, configs.bits.noise_bit ))); + assert!( + codegen_configs.contains(&format!("{}_BIT_D: u32 = {}", prefix, configs.bits.d_bit)) + ); assert!(codegen_configs.contains(&format!("{}_CONFIGS:", prefix))); assert!(codegen_configs.contains( "DecryptedSharesAggregationConfigs::new(QIS, PLAINTEXT_MODULUS, Q_INVERSE_MOD_T)" @@ -126,6 +132,9 @@ mod tests { assert!(artifacts .configs .contains("DECRYPTED_SHARES_AGGREGATION_BIT_NOISE")); + assert!(artifacts + .configs + .contains("DECRYPTED_SHARES_AGGREGATION_BIT_D")); assert!(artifacts .configs .contains("DECRYPTED_SHARES_AGGREGATION_CONFIGS")); diff --git a/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/computation.rs b/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/computation.rs index ed619236d0..eb33a6bf7e 100644 --- a/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/computation.rs +++ b/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/computation.rs @@ -15,6 +15,7 @@ pub const MAX_MSG_NON_ZERO_COEFFS: usize = 100; use crate::calculate_bit_width; +use crate::circuits::commitments::compute_threshold_decryption_share_commitment; use crate::compute_q_mod_t; use crate::compute_q_mod_t_centered; use crate::get_zkp_modulus; @@ -69,6 +70,8 @@ pub struct Bounds { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Bits { pub noise_bit: u32, + /// Coefficient bit width for decryption-share commitments (aligned with threshold share_decryption BIT_D). + pub d_bit: u32, } /// Circuit config: moduli count, plaintext modulus, q_inverse_mod_t, bits, bounds, and message polynomial length. @@ -92,7 +95,9 @@ pub struct Configs { /// coefficients are reduced to [0, zkp_modulus) by compute. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Inputs { - /// One CrtPolynomial per party (public witnesses); circuit: `[[Polynomial; L]; T+1]` + /// Public `d` commitments (one per party), same as circuit 6 outputs; checked in-circuit. + pub expected_d_commitments: Vec, + /// One CrtPolynomial per party (secret witness); circuit: `[[Polynomial; L]; T+1]` pub decryption_shares: Vec, /// Party IDs (1-based: 1, 2, ..., T+1) pub party_ids: Vec, @@ -126,9 +131,19 @@ impl Computation for Bits { type Data = Bounds; type Error = CircuitsErrors; - fn compute(_: Self::Preset, data: &Self::Data) -> Result { + fn compute(preset: Self::Preset, data: &Self::Data) -> Result { let noise_bit = calculate_bit_width(BigInt::from(data.delta_half.clone())); - Ok(Bits { noise_bit }) + let (threshold_params, _) = + build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Other(e.to_string()))?; + let ctx = threshold_params + .ctx_at_level(0) + .map_err(|e| CircuitsErrors::Other(format!("ctx_at_level: {:?}", e)))?; + let mut d_bit = 0u32; + for qi in ctx.moduli_operators() { + let qi_bound = (BigInt::from(qi.modulus()) - 1) / 2; + d_bit = d_bit.max(calculate_bit_width(qi_bound)); + } + Ok(Bits { noise_bit, d_bit }) } } @@ -310,7 +325,19 @@ impl Computation for Inputs { .collect(), ); + let expected_d_commitments: Vec = decryption_shares + .iter() + .map(|share| { + compute_threshold_decryption_share_commitment( + share, + configs.bits.d_bit, + max_msg_non_zero_coeffs, + ) + }) + .collect(); + Ok(Inputs { + expected_d_commitments, decryption_shares, party_ids, message, @@ -333,8 +360,10 @@ impl Computation for Inputs { let message_json = polynomial_to_toml_json(&self.message); let u_global_json = polynomial_to_toml_json(&self.u_global); let crt_quotients_json = crt_polynomial_to_toml_json(&self.crt_quotients); + let expected_d_commitments_json = bigint_1d_to_json_values(&self.expected_d_commitments); let json = serde_json::json!({ + "expected_d_commitments": expected_d_commitments_json, "decryption_shares": decryption_shares_json, "party_ids": party_ids_json, "message": message_json, @@ -362,6 +391,7 @@ mod tests { assert!(!bounds.delta_half.is_zero()); assert!(bounds.delta_half < bounds.delta); assert!(bits.noise_bit > 0); + assert!(bits.d_bit > 0); } #[test] @@ -385,6 +415,10 @@ mod tests { let configs = Configs::compute(preset, &()).unwrap(); assert_eq!(out.inputs.decryption_shares.len(), committee.threshold + 1); + assert_eq!( + out.inputs.expected_d_commitments.len(), + committee.threshold + 1 + ); assert_eq!(out.inputs.party_ids.len(), committee.threshold + 1); assert_eq!( out.inputs.message.coefficients().len(), @@ -400,5 +434,6 @@ mod tests { configs.max_msg_non_zero_coeffs ); assert!(out.bits.noise_bit > 0); + assert!(out.bits.d_bit > 0); } } diff --git a/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/mod.rs b/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/mod.rs index 15d3a571d2..02f1aa171b 100644 --- a/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/mod.rs +++ b/crates/zk-helpers/src/circuits/threshold/decrypted_shares_aggregation/mod.rs @@ -7,9 +7,9 @@ //! Decrypted shares aggregation circuit for threshold BFV. //! //! Proves correct aggregation of T+1 decryption shares (Lagrange interpolation at 0 per modulus, -//! CRT reconstruction to u_global, and CRT quotients). Input: decryption share polynomials, -//! 1-based party IDs, and the decoded message. Output: input (decryption_shares, party_ids, -//! message, u_global, crt_quotients) in standard form for the Noir circuit. +//! CRT reconstruction to u_global, and CRT quotients). Public inputs include C6 `d` commitments +//! (checked in-circuit against recomputed commitments from witness shares), party IDs, and message; +//! secret witnesses: decryption shares, u_global, crt_quotients. pub mod circuit; pub mod codegen; diff --git a/crates/zk-prover/tests/local_e2e_tests.rs b/crates/zk-prover/tests/local_e2e_tests.rs index f30f3deb81..017fdfbdf8 100644 --- a/crates/zk-prover/tests/local_e2e_tests.rs +++ b/crates/zk-prover/tests/local_e2e_tests.rs @@ -25,7 +25,11 @@ use e3_fhe_params::{build_pair_for_preset, BfvPreset}; 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, CircuitComputation}; +use e3_zk_helpers::circuits::{ + commitments::{compute_dkg_pk_commitment, compute_threshold_decryption_share_commitment}, + threshold::decrypted_shares_aggregation::MAX_MSG_NON_ZERO_COEFFS, + CircuitComputation, +}; use e3_zk_helpers::computation::DkgInputType; use e3_zk_helpers::dkg::share_computation::{ Configs, ShareComputationBaseCircuit, ShareComputationChunkCircuit, @@ -683,3 +687,65 @@ async fn test_pk_aggregation_commitment_consistency() { prover.cleanup(e3_id).unwrap(); } + +#[tokio::test] +async fn test_threshold_share_decryption_commitment_consistency() { + let Some((_backend, _temp, prover, circuit, sample, preset, e3_id)) = + setup_share_decryption_test().await + else { + println!("skipping: bb not found"); + return; + }; + + let proof = circuit + .prove_with_variant(&prover, &preset, &sample, e3_id, CircuitVariant::Recursive) + .expect("proof generation should succeed"); + + let computation_output = ThresholdShareDecryptionCircuit::compute(preset, &sample).unwrap(); + + let expected_d_commitment = compute_threshold_decryption_share_commitment( + &computation_output.inputs.d, + computation_output.bits.d_bit, + MAX_MSG_NON_ZERO_COEFFS, + ); + + let d_commitment_from_proof = extract_field_from_end(&proof.public_signals, 0); + assert_eq!( + d_commitment_from_proof, expected_d_commitment, + "C6 d_commitment must match compute_threshold_decryption_share_commitment on witness d" + ); + + prover.cleanup(e3_id).unwrap(); +} + +#[tokio::test] +async fn test_decrypted_shares_aggregation_commitment_consistency() { + let Some((_backend, _temp, prover, circuit, sample, preset, e3_id)) = + setup_decrypted_shares_aggregation_test().await + else { + println!("skipping: bb not found"); + return; + }; + + let proof = circuit + .prove_with_variant(&prover, &preset, &sample, e3_id, CircuitVariant::Recursive) + .expect("proof generation should succeed"); + + let computation_output = DecryptedSharesAggregationCircuit::compute(preset, &sample).unwrap(); + + for (i, expected) in computation_output + .inputs + .expected_d_commitments + .iter() + .enumerate() + { + let from_proof = extract_field(&proof.public_signals, i); + assert_eq!( + from_proof, *expected, + "C7 public expected_d_commitments[{}] must match witness-derived commitment", + i + ); + } + + prover.cleanup(e3_id).unwrap(); +} diff --git a/packages/enclave-contracts/contracts/verifiers/bfv/honk/ThresholdDecryptedSharesAggregationVerifier.sol b/packages/enclave-contracts/contracts/verifiers/bfv/honk/ThresholdDecryptedSharesAggregationVerifier.sol index 754a1dcc1d..b47b3c44f2 100644 --- a/packages/enclave-contracts/contracts/verifiers/bfv/honk/ThresholdDecryptedSharesAggregationVerifier.sol +++ b/packages/enclave-contracts/contracts/verifiers/bfv/honk/ThresholdDecryptedSharesAggregationVerifier.sol @@ -7,8 +7,8 @@ pragma solidity >=0.8.21; uint256 constant N = 131072; uint256 constant LOG_N = 17; -uint256 constant NUMBER_OF_PUBLIC_INPUTS = 518; -uint256 constant VK_HASH = 0x17c0d03849a78d01533e2a8fb8e58513263cdf932e9a24f803b1c271162e6d4b; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 120; +uint256 constant VK_HASH = 0x006492da92f08a975091a25b2388c15d5e31d556e1e736f68f049c17f508a0f7; library HonkVerificationKey { function loadVerificationKey() internal @@ -18,149 +18,149 @@ library HonkVerificationKey { Honk.VerificationKey memory vk = Honk.VerificationKey({ circuitSize: uint256(131072), logCircuitSize: uint256(17), - publicInputsSize: uint256(518), + publicInputsSize: uint256(120), ql: Honk.G1Point({ x: uint256( - 0x1c5eec061e8ccbba65d9c019fb9ff76fab3e6b200d7e35bc9268c6ff4ca2cc22 + 0x02a044fd06cd7b24942c2abaa59729eedc0a19a1bbccbd665b861997f4d8bccd ), y: uint256( - 0x18ecc8ca4b2975ebb28f70cacc7ce7ec46461d6bdd4b39a602c5a954a5b5eb03 + 0x0f787ca6a2c1ea64fee756dc4dc8f01ee1231b9cc77469b9174d3d8a7a363b25 ) }), qr: Honk.G1Point({ x: uint256( - 0x1270406e66ef465ffe418e49074b2bb901cd119b01982e3c87ebc765ddd04d57 + 0x01b4b5146af758dcac2d33e6a7217c2df88a87c027c2bcdaebc70f12e3a4e833 ), y: uint256( - 0x2aca9fab8720f1f26e71ba77b00a6b1afc6f66e1530ababb5dc4bf518621ab52 + 0x075508943d6dde0268af6e8651139633111abea94eba052ed41273a37035fa11 ) }), qo: Honk.G1Point({ x: uint256( - 0x2314586f65b2c17adbdd9715fd7bdec3d4cf1c4a05c6f5df02c5f21e7b572ae5 + 0x09b9b45ef104dfa3f39c0ea6ef5dd3a489940d62d366bcccf810cc954bff1796 ), y: uint256( - 0x0b96c5a5dfad267fa9961ffe764d9ca2f6a6dcff554fbc46267d399345be8587 + 0x2fd142546ffcd1dcf46c11d4ed597d15cc640981bffcb1dee838ff4df3a663f4 ) }), q4: Honk.G1Point({ x: uint256( - 0x29c969eb2b0313da87248fccfbc94d064983017dfebd09e0929cd9cc7d0bc59f + 0x1f41dd9a00f89c9acc02b51b114f22fe45a2ba2f27d2f922c082561b4beb8d09 ), y: uint256( - 0x00d85d3f2d967049e5efe8f9df54a88bb5e1e921e54de538b79a6a5021cb8e50 + 0x2e93ef39c032fa2ca35199696adbb4131b8e8e107f298ffa674205b94605c7eb ) }), qm: Honk.G1Point({ x: uint256( - 0x14c5ed8dcb3960b8b73adc8ebe3ccbda98985df91b02d3e9aff095c335df97c5 + 0x0d4bcc607e37417bd723c4c7946da59b343a473fedef579a5c031cb96f3a53cd ), y: uint256( - 0x0125d4033d5311d9df6dfb622e11bebb9c9edd392f3f63711caffb7db0000569 + 0x1c46afee30ceb85906fb1fc2106fc39c70ba36b4bb8bf1016a43db26c45909b8 ) }), qc: Honk.G1Point({ x: uint256( - 0x1b3c74408e31ef24ca0c0236bb5eaa5df988c9edd1a595ff047f4b33b4059bbf + 0x1582855842b2e599198915afbcb2b11a30a1d9eed53d756e9da7e59f2c8a13cd ), y: uint256( - 0x03dc64a8c50e8a52c54a0b6a48e45580310c5f826ddc19a596b93ef1722164e2 + 0x1056201ea7f9daa086c4ab1598653c25194e221f41ae1b6914c64be017add2b9 ) }), qLookup: Honk.G1Point({ x: uint256( - 0x1bd27fd6f58eb624997370475afcc056da598e0820038b787506755ceafbcf54 + 0x0cbf626466e29547e383e6e44295524d22ce02930eac81322c8e95c5c83a5187 ), y: uint256( - 0x0bd4a8ea155885bd7913ef1bd8e05d0545502a2b2d015c79930237fc2d4f8ca9 + 0x0080c602403a502c36662581d00edb43fbe5145a66c5c3d90f12fa31840f6c3e ) }), qArith: Honk.G1Point({ x: uint256( - 0x28e49bbdcb72aad0dd7fcadde38f5b27ebb48cdd9454ac2984e279ae85184f60 + 0x1b67f4c10591e39a73153cf155c6b841a9e9ffd4e4b13b97895a4c3bac95b17f ), y: uint256( - 0x04c7e1670aa4d8c4654c58725c6f57336bd7f8b084027e5550fa8c8dbe1166dc + 0x008960215829d1bfdc8947cfff6facfc8f0f356f85ebeae103a15b7e81812b6f ) }), qDeltaRange: Honk.G1Point({ x: uint256( - 0x14eb2324d4a0246f60eaac9f9e85a9846ac9db9a15db012b691c6273191048da + 0x26c4901698a32b5ed849b748094e82ad5c73c5599ce6567107f831144ba79dd7 ), y: uint256( - 0x2046ec4792d93e08c622e3b3e0de6582e8f949956077f8a64a12bcf3c00850e4 + 0x0b5750e772b022309585b82465a544871eb110b68d8138f1630c941f450ecf4d ) }), qElliptic: Honk.G1Point({ x: uint256( - 0x12038609283c4e120be0ad659cc03f93254e6a5f7731215bdb24d748b6a55d93 + 0x1684112019eec88d7085bc49d21271174564d223e1adf1fbcddebbb46e547e60 ), y: uint256( - 0x258276855e8b3a9e47c1c1b24f9c95093f792156556bc1474dd4c4ac6104a6cc + 0x086fb3fafec7b58139c4243a99beb7d77fa75694a0d893a8d33008deec7ac5e5 ) }), qMemory: Honk.G1Point({ x: uint256( - 0x1af65d70ef0271dc377448971776618e6388f5327a5a9c586cb5f0d70aa6a3d5 + 0x1617d3d9a582ebbdfab4436b6069011eb9f38e6856010ed6ae6969339b0af029 ), y: uint256( - 0x25e67b5515fff332e1236f7497b35fdc100611406cd5bf34d5cc76f56e8480a7 + 0x02a9d60cc9b6b7893239d040e344223b00d35e6f6a380f532b3846493c524d4d ) }), qNnf: Honk.G1Point({ x: uint256( - 0x29c8fa2cde8ef61f6b610cdede7dca060459b504d85a6a1c85566fac26754f78 + 0x1096c9ba549299d85900f014ad8c29bf26e0c9c679cb679488ece57142810946 ), y: uint256( - 0x05b3eca951fa3773fec849603c3ee3e6cb8a47cc1455293834869fba6e3d2e6a + 0x0f466ba638fd0e1353c042f313282342cd337758a47ced02185c5f9e3bbf1ee7 ) }), qPoseidon2External: Honk.G1Point({ x: uint256( - 0x19946811d5b6434a0457d4da8d1ef29ed59fd1b849cfc45445bcb86a29ad7176 + 0x2e3aaefa6e2db144789c614228f1c8dad2d58a5e7a4113ac60e756fb2d2a0ecf ), y: uint256( - 0x05ada75b207892d619089b7ed64ce3b12cb1e4145d18542fdb1ae184e14ff209 + 0x1c4fa0ae44599c41aacbe7ebe0ffe537a09f5d77de4265e36b80ddca1d1cbde2 ) }), qPoseidon2Internal: Honk.G1Point({ x: uint256( - 0x195382b968c3c55502ea2dcf1a532bd6f1d6ff51f413670d1af094c4a118f0d1 + 0x0e5ab15ae428b51a74b427beea70fbd92ed7a6fff51d86d682c31bf84a3a29ab ), y: uint256( - 0x1b6a8c6abc95f815305b57b3492a769b1bde325d2543e49f5de98cae173c4246 + 0x269276e429daaac7a0b971dfb5713a672686d25d0eac044582430053729a8b7f ) }), s1: Honk.G1Point({ x: uint256( - 0x28e19dbb402bf0e14dbcea1070d07418d93d0032a1bd68b8629ae8bcaae9020d + 0x011552d2dd0839f3d111063c39d9c027f96c1feec00a289e2c531370f096f5e6 ), y: uint256( - 0x1c0a1f364d0bc7ed731f7bff87a6a2947d1553e078ea1917f18842aacc8bd457 + 0x0d006c5f5efecda241a2b6ad0905464e3b737e4b7864d705580ad1a2fb1a951d ) }), s2: Honk.G1Point({ x: uint256( - 0x08d54ef2216dbfda753b7f1caf7c0c0d44747116a93187d28922de6f34fd6335 + 0x0fc787c7e8b1db8a4de620d118b27f9b2446edb6a19f2c191cb6ca7c7566a69f ), y: uint256( - 0x1ef9725767480bf9db24e62eb59eaa22533926b9403abb55cf0df36a4d95cdd6 + 0x221994346dd3d27023d4d6def6ee88cca468d528b899dc5f68d9765e7afb5025 ) }), s3: Honk.G1Point({ x: uint256( - 0x18d69d27e2deeb8ae49b8149cb492962580eb6aeb61b082846838b6829fb0fc0 + 0x209aedd2d2221460809d33ed32f75b7684a2a99bdc74f0a870097ed425113fbd ), y: uint256( - 0x2d89f3ddbc6d16c73f8a0bc468d6d25188d041b8a91a2046f4848e25342d88b0 + 0x1d467cd1d4467cdf573dfa470446e80a532ae7cbf683acf5237c8e1dd2bbe285 ) }), s4: Honk.G1Point({ x: uint256( - 0x2554076112ac0e3c1c74af827d9f847d51d83e44637a9666fadb5a9031c588f1 + 0x23cc02e2498302509707a7a966bf00fa2b8f3d98285b86aac3bfe432e25413c8 ), y: uint256( - 0x040eb620b9342954735f62f87f03a382d2d148c41b20da31c7f376188b128056 + 0x0a4d516c9472521470e62312e708ac52b9cebc659c7ddcddcb5df4aef76c6c2a ) }), t1: Honk.G1Point({ @@ -197,34 +197,34 @@ library HonkVerificationKey { }), id1: Honk.G1Point({ x: uint256( - 0x1f9372b8700477f1e06a89403cbfa5f758ccbeb1d47938ac70d9b2cedcdcb96b + 0x0a330b234cf6f85c0049c6dae60a5180feeb36b38ea2c03fb041d35b0ff36638 ), y: uint256( - 0x17d5d443c2a251abcf6709776aa889bf60feca62d54b53e669fbfb86c7cc50cb + 0x2bfbe90e706022c4a237201b5900132d59ac44fbf596de26a9f3526469dbefe3 ) }), id2: Honk.G1Point({ x: uint256( - 0x1d034fb2e75259ba6352755ec37d0abd7183fd3aadb106d5e4af41f7ddbe18e4 + 0x1254120586683a7e4e1a2cb9868c95571a9c47c45cf48a312af87c266195afa6 ), y: uint256( - 0x0f6ffeec518c6c80263e76fa5e2894c4c0f7188f847e552ab25a6d9093055f7c + 0x138ae89c8588f106e91e276535a2a982e4bd9e12c400d3c934196d6a5e2e7940 ) }), id3: Honk.G1Point({ x: uint256( - 0x1a4302c045b14065f8ef323cfe5d60a467511784b0f7bdfafe626ff1099e9299 + 0x10b4638497ffe24f8faccee7a2dc2642c20d38c17ccbce308e180b199454d037 ), y: uint256( - 0x2918c1621a9f9d747d6c9e815d3dbf1d7fabffbcdc2bc4eff5146e716497f504 + 0x1742fd41969249f0c6636d55ac31d89e35b35f35dcaa5836104925c6cadfddda ) }), id4: Honk.G1Point({ x: uint256( - 0x0bc100ced8a41835760df48e1c546ce8826216dc34e0f85a038e379bdc0c277c + 0x049b651cbe75950126545589c99b8410ed210a79292f36cb99169b044acfb944 ), y: uint256( - 0x06f6de62d43820dbed337c43b7beb8d7a9548358d74ad136e90f0b2e094d266a + 0x11b185db4578b47ac98385f614ceb4d6a0ea46d07455fd6e788ebd1d19120851 ) }), lagrangeFirst: Honk.G1Point({ @@ -237,10 +237,10 @@ library HonkVerificationKey { }), lagrangeLast: Honk.G1Point({ x: uint256( - 0x273f4e8fcf6e1f733b2dfbb4d864930efc08446205cfc7fbbcb9ad3b9db7d36b + 0x1b0cd706de7e1b2455f6ae8f4ca69747e4a4b9362260fbad8c623ad8f9a4d5dc ), y: uint256( - 0x20ff6ed75290c4564a623adc786d2c8abe7f259992076092ac1f68f6ee2207ce + 0x20446017b99d830cf390875d57dff60403843e423f9f573268049e34c4f90741 ) }) }); From 82f27df76606df07a9ae429a4bf31777652d5c76 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 26 Mar 2026 17:20:25 +0100 Subject: [PATCH 2/2] refactor: use entire decryption share witness for c6 challenge --- agent/flow-trace/04_DKG_AND_COMPUTATION.md | 1 + .../src/core/threshold/share_decryption.nr | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/agent/flow-trace/04_DKG_AND_COMPUTATION.md b/agent/flow-trace/04_DKG_AND_COMPUTATION.md index 9bd2f5c485..79fba8bfdb 100644 --- a/agent/flow-trace/04_DKG_AND_COMPUTATION.md +++ b/agent/flow-trace/04_DKG_AND_COMPUTATION.md @@ -596,6 +596,7 @@ EnclaveSolReader decodes CiphertextOutputPublished event │ │ → Circuit: ThresholdShareDecryption (C6) │ │ → Proves decryption share was correctly computed from │ │ sk_poly_sum, es_poly_sum, and ciphertext + │ │ → Fiat-Shamir transcript absorbs full `d` (all coefficients per CRT limb) │ ├─ ZkActor generates proof via bb binary │ ├─ Signs proof │ └─ Publishes signed C6 proof diff --git a/circuits/lib/src/core/threshold/share_decryption.nr b/circuits/lib/src/core/threshold/share_decryption.nr index e33ced6807..5771f8196c 100644 --- a/circuits/lib/src/core/threshold/share_decryption.nr +++ b/circuits/lib/src/core/threshold/share_decryption.nr @@ -112,8 +112,10 @@ impl [Polynomial; L] { + /// Truncates each CRT limb of `d` to `MAX_MSG_NON_ZERO_COEFFS` coefficients. + /// Used only to build the C7 public commitment (`compute_threshold_decryption_share_commitment`); + /// the Fiat-Shamir transcript separately absorbs the full `d` witness via [`Self::payload`]. + fn get_truncated_decryption_share(self) -> [Polynomial; L] { let mut out: [Polynomial; L] = [Polynomial::new([0; MAX_MSG_NON_ZERO_COEFFS]); L]; for i in 0..L { @@ -139,7 +141,7 @@ impl Vec { + fn payload(self) -> Vec { let mut inputs = Vec::new(); // Use commitments instead of full polynomials (saves constraints) @@ -163,8 +165,8 @@ impl(inputs, self.r1); inputs = flatten::<_, _, BIT_R2>(inputs, self.r2); - // Bind decryption share via prefix commitment (matches circuit 7) - inputs.push(d_commitment); + // Full `d` fixes the C7 prefix commitment (computed separately as the public output) + inputs = flatten::(inputs, self.d); inputs } @@ -183,13 +185,13 @@ impl( d_truncated, ); // Step 4: Generate Fiat-Shamir challenge from the transcript - let gamma = self.generate_challenge(d_commitment); + let gamma = self.generate_challenge(); // Step 5: Verify decryption share computation for each CRT basis for i in 0..L { @@ -233,7 +235,7 @@ impl Field { - let inputs = self.payload(d_commitment); + fn generate_challenge(self) -> Field { + let inputs = self.payload(); compute_threshold_share_decryption_challenge::(inputs) }