From 28d8369e951783f063e7a08c0830054312dab425 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 29 Jan 2026 15:19:04 +0100 Subject: [PATCH 01/20] docs(polynomial): explain descending coefficient order for circuit Horner eval - Document that descending order matches circuit Horner evaluation - Single forward pass, no reindexing, keeps constraints efficient --- crates/polynomial/src/polynomial.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/polynomial/src/polynomial.rs b/crates/polynomial/src/polynomial.rs index b148193586..a0c36fb6d6 100644 --- a/crates/polynomial/src/polynomial.rs +++ b/crates/polynomial/src/polynomial.rs @@ -57,6 +57,11 @@ pub enum PolynomialError { /// required for cryptographic operations. The polynomial is represented as: /// `a_n * x^n + a_{n-1} * x^{n-1} + ... + a_1 * x + a_0` /// +/// Coefficients are in descending order so that evaluation in the circuit matches +/// Horner's method in a single forward pass: P(x) = ((...((a_n * x + a_{n-1}) * x + ...) * x + a_0). +/// The circuit can then iterate `result = result * x + coefficients[i]` from i = 0 without +/// reversing or reindexing, keeping the constraint system simple and efficient. +/// #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Polynomial { From 75dbb59c4bb882b505d330dc2818047cbaa7fd75 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 29 Jan 2026 20:19:11 +0100 Subject: [PATCH 02/20] refactor: add CRT polynomial --- Cargo.lock | 2 + crates/polynomial/Cargo.toml | 2 + crates/polynomial/src/crt_polynomial.rs | 184 ++++++++++++++++++++++++ crates/polynomial/src/lib.rs | 2 + crates/polynomial/src/polynomial.rs | 5 - 5 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 crates/polynomial/src/crt_polynomial.rs diff --git a/Cargo.lock b/Cargo.lock index 9d98ddba1a..ac1f012b94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3310,6 +3310,8 @@ version = "0.1.8" dependencies = [ "bincode", "criterion", + "fhe-math", + "ndarray", "num-bigint", "num-traits", "serde", diff --git a/crates/polynomial/Cargo.toml b/crates/polynomial/Cargo.toml index 47739f9672..75e388c900 100644 --- a/crates/polynomial/Cargo.toml +++ b/crates/polynomial/Cargo.toml @@ -12,6 +12,8 @@ num-traits = { workspace = true } serde = { workspace = true, optional = true } bincode = { workspace = true, optional = true } thiserror = { workspace = true } +fhe-math = { workspace = true } +ndarray = { workspace = true } [dev-dependencies] criterion = "0.5" diff --git a/crates/polynomial/src/crt_polynomial.rs b/crates/polynomial/src/crt_polynomial.rs new file mode 100644 index 0000000000..93266ffaba --- /dev/null +++ b/crates/polynomial/src/crt_polynomial.rs @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! CRT (Chinese Remainder Theorem) polynomial representation. + +use crate::polynomial::Polynomial; +use crate::reduce_and_center_coefficients_mut; +use std::sync::Arc; +use thiserror::Error; + +use fhe_math::rq::{Poly, Representation}; +use num_bigint::BigInt; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Errors that can occur during CRT polynomial operations. +#[derive(Debug, Error)] +pub enum CrtPolynomialError { + /// Moduli list must not be empty. + #[error("moduli must be non-empty")] + EmptyModuli, + + /// Ring degree `n` must be non-zero. + #[error("n must be > 0")] + InvalidN, + + /// Number of limbs must match number of moduli. + #[error("limbs.len() ({actual}) must match ctx.moduli.len() ({expected})")] + LimbCountMismatch { expected: usize, actual: usize }, + + /// Each limb must have exactly `n` coefficients. + #[error("limb {limb_index} length ({actual}) must match ctx.n ({expected})")] + LimbLengthMismatch { + limb_index: usize, + expected: usize, + actual: usize, + }, + + /// fhe-math Poly must be in PowerBasis representation for conversion. + #[error("fhe Poly must be in PowerBasis representation")] + UnsupportedRepresentation, +} + +/// CRT context for a family of polynomials sharing the same ring degree `n` and CRT moduli. +/// +/// Notes: +/// - `n` is the ring degree (also the number of coefficients / NTT size). Each limb has exactly `n` coefficients. +/// - The maximum exponent is `n - 1`. +/// - `moduli` are the CRT factors q_0..q_{L-1} (u64 primes). +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CrtContext { + pub n: usize, + pub moduli: Vec, +} + +impl CrtContext { + /// Creates a new CRT context. + /// + /// # Errors + /// Returns `CrtPolynomialError::InvalidN` if `n` is zero. + /// Returns `CrtPolynomialError::EmptyModuli` if `moduli` is empty. + pub fn new(n: usize, moduli: Vec) -> Result { + if n == 0 { + return Err(CrtPolynomialError::InvalidN); + } + if moduli.is_empty() { + return Err(CrtPolynomialError::EmptyModuli); + } + Ok(Self { n, moduli }) + } + + pub fn num_moduli(&self) -> usize { + self.moduli.len() + } +} + +/// A polynomial in CRT form: one limb polynomial per modulus. +/// +/// Each limb is a `Polynomial` whose coefficients are expected to be reduced/centered +/// modulo the corresponding `ctx.moduli[i]` as required by the caller. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CrtPolynomial { + pub ctx: Arc, + pub limbs: Vec, +} + +impl CrtPolynomial { + /// Creates a new CRT polynomial and validates limb dimensions. + /// + /// # Errors + /// Returns `CrtPolynomialError::LimbCountMismatch` if the number of limbs does not match `ctx.moduli.len()`. + /// Returns `CrtPolynomialError::LimbLengthMismatch` if any limb has a coefficient vector length different from `ctx.n`. + pub fn new(ctx: Arc, limbs: Vec) -> Result { + let expected_limbs = ctx.moduli.len(); + + if limbs.len() != expected_limbs { + return Err(CrtPolynomialError::LimbCountMismatch { + expected: expected_limbs, + actual: limbs.len(), + }); + } + + let expected_len = ctx.n; + + for (i, limb) in limbs.iter().enumerate() { + let actual_len = limb.coefficients().len(); + if actual_len != expected_len { + return Err(CrtPolynomialError::LimbLengthMismatch { + limb_index: i, + expected: expected_len, + actual: actual_len, + }); + } + } + + Ok(Self { ctx, limbs }) + } + + /// Ring degree (number of coefficients). + pub fn n(&self) -> usize { + self.ctx.n + } + + /// Maximum exponent (at most `n - 1`). + pub fn max_exponent(&self) -> usize { + self.ctx.n.saturating_sub(1) + } + + pub fn modulus(&self, i: usize) -> u64 { + self.ctx.moduli[i] + } + + pub fn limb(&self, i: usize) -> &Polynomial { + &self.limbs[i] + } + + /// Builds a `CrtPolynomial` from an fhe-math `Poly` in PowerBasis representation. + /// + /// Main use: preparing inputs for ZK circuits by converting from FHE BFV ciphertext + /// polynomials to a CRT limb format compatible with the circuits. + /// + /// Coefficient layout: fhe-math rows are ascending degree (c_0 + c_1·x + …). + /// We convert to descending order so that evaluation in the circuit matches + /// Horner's method in a single forward pass: P(x) = ((...((a_n * x + a_{n-1}) * x + ...) * x + a_0). + /// The circuit can then iterate `result = result * x + coefficients[i]` from i = 0 without + /// reversing or reindexing, keeping the constraint system simple and efficient. + /// + /// # Errors + /// Returns `CrtPolynomialError::UnsupportedRepresentation` if the poly is not in PowerBasis. + pub fn from_fhe_poly(p: &Poly) -> Result { + let mut p = p.clone(); + + if *p.representation() == Representation::Ntt { + p.change_representation(Representation::PowerBasis); + } + + let ctx = p.ctx.as_ref(); + let n = ctx.degree; + let moduli = ctx.moduli().to_vec(); + let crt_ctx = Arc::new(CrtContext::new(n, moduli)?); + + let coeffs = p.coefficients(); + + let limbs: Vec = coeffs + .outer_iter() + .zip(crt_ctx.moduli.iter()) + .map(|(row, qi)| { + let mut coeffs: Vec = row.iter().rev().map(|&c| BigInt::from(c)).collect(); + let qi_bigint = BigInt::from(*qi); + + reduce_and_center_coefficients_mut(&mut coeffs, &qi_bigint); + + Polynomial::new(coeffs) + }) + .collect(); + + CrtPolynomial::new(crt_ctx, limbs) + } +} diff --git a/crates/polynomial/src/lib.rs b/crates/polynomial/src/lib.rs index 489fb1ec56..b905ea8694 100644 --- a/crates/polynomial/src/lib.rs +++ b/crates/polynomial/src/lib.rs @@ -25,8 +25,10 @@ //! - Homomorphic encryption: BFV, BGV, and CKKS schemes. //! - Zero-knowledge proofs: Polynomial commitment schemes. +pub mod crt_polynomial; pub mod polynomial; pub mod utils; +pub use crt_polynomial::{CrtContext, CrtPolynomial, CrtPolynomialError}; pub use polynomial::{Polynomial, PolynomialError}; pub use utils::*; diff --git a/crates/polynomial/src/polynomial.rs b/crates/polynomial/src/polynomial.rs index a0c36fb6d6..b148193586 100644 --- a/crates/polynomial/src/polynomial.rs +++ b/crates/polynomial/src/polynomial.rs @@ -57,11 +57,6 @@ pub enum PolynomialError { /// required for cryptographic operations. The polynomial is represented as: /// `a_n * x^n + a_{n-1} * x^{n-1} + ... + a_1 * x + a_0` /// -/// Coefficients are in descending order so that evaluation in the circuit matches -/// Horner's method in a single forward pass: P(x) = ((...((a_n * x + a_{n-1}) * x + ...) * x + a_0). -/// The circuit can then iterate `result = result * x + coefficients[i]` from i = 0 without -/// reversing or reindexing, keeping the constraint system simple and efficient. -/// #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Polynomial { From 2ec04843da70e74b9a52cf6065d11c348a8684ff Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 30 Jan 2026 10:21:10 +0100 Subject: [PATCH 03/20] chore: update crisp lockfile --- examples/CRISP/Cargo.lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/CRISP/Cargo.lock b/examples/CRISP/Cargo.lock index f5f50ab79a..5c03a4a60e 100644 --- a/examples/CRISP/Cargo.lock +++ b/examples/CRISP/Cargo.lock @@ -2459,6 +2459,8 @@ dependencies = [ name = "e3-polynomial" version = "0.1.8" dependencies = [ + "fhe-math", + "ndarray", "num-bigint", "num-traits", "serde", @@ -3946,6 +3948,7 @@ dependencies = [ "num-integer", "num-traits", "rawpointer", + "serde", ] [[package]] From 2e164a56656f5186b19d26dd808474f5608c86ab Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 30 Jan 2026 10:43:33 +0100 Subject: [PATCH 04/20] fix(polynomial): enable serde "rc" for Arc - Add serde rc feature to polynomial crate serde dependency - Allows derived Serialize/Deserialize on CrtPolynomial (ctx: Arc) --- crates/polynomial/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/polynomial/Cargo.toml b/crates/polynomial/Cargo.toml index 75e388c900..60a5be6995 100644 --- a/crates/polynomial/Cargo.toml +++ b/crates/polynomial/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/gnosisguild/enclave/crates/polynomial" [dependencies] num-bigint = { workspace = true, features = ["serde"] } num-traits = { workspace = true } -serde = { workspace = true, optional = true } +serde = { workspace = true, optional = true, features = ["rc"] } bincode = { workspace = true, optional = true } thiserror = { workspace = true } fhe-math = { workspace = true } From f1d805bcd5bd5752e9bfead298fae911a26d3c4f Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 30 Jan 2026 17:39:43 +0100 Subject: [PATCH 05/20] refactor: update ciphertext addition logic with new poly struct --- Cargo.lock | 1 + crates/bfv-client/Cargo.toml | 1 + crates/bfv-client/src/client.rs | 13 +- crates/polynomial/benches/polynomial.rs | 6 +- crates/polynomial/src/crt_polynomial.rs | 226 +++++----- crates/polynomial/src/lib.rs | 2 +- crates/polynomial/src/polynomial.rs | 27 +- crates/zk-helpers/src/commitments.rs | 91 +++-- crates/zk-helpers/src/packing.rs | 37 +- examples/CRISP/Cargo.lock | 7 +- .../CRISP/crates/zk-inputs-wasm/Cargo.toml | 1 + .../CRISP/crates/zk-inputs-wasm/src/lib.rs | 14 +- examples/CRISP/crates/zk-inputs/Cargo.toml | 1 - .../zk-inputs/src/ciphertext_addition.rs | 385 +++++++----------- examples/CRISP/crates/zk-inputs/src/lib.rs | 38 +- .../crates/zk-inputs/src/serialization.rs | 74 ++-- examples/CRISP/packages/crisp-sdk/src/vote.ts | 4 +- .../packages/crisp-sdk/tests/vote.test.ts | 8 +- 18 files changed, 447 insertions(+), 489 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac1f012b94..186650d78f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2837,6 +2837,7 @@ dependencies = [ "anyhow", "e3-fhe-params", "e3-greco-helpers", + "e3-polynomial 0.1.8", "e3-zk-helpers", "fhe", "fhe-traits", diff --git a/crates/bfv-client/Cargo.toml b/crates/bfv-client/Cargo.toml index 82491bc0c2..3278d1b3d3 100644 --- a/crates/bfv-client/Cargo.toml +++ b/crates/bfv-client/Cargo.toml @@ -15,4 +15,5 @@ fhe-traits.workspace = true greco = { package = "zkfhe-greco", git = "https://github.com/gnosisguild/zkfhe-generator" } rand.workspace = true thiserror = { workspace = true } +e3-polynomial = { workspace = true } e3-zk-helpers = { workspace = true } diff --git a/crates/bfv-client/src/client.rs b/crates/bfv-client/src/client.rs index e8c5ab404f..a32764a878 100644 --- a/crates/bfv-client/src/client.rs +++ b/crates/bfv-client/src/client.rs @@ -7,6 +7,7 @@ use anyhow::{anyhow, Result}; use e3_fhe_params::build_bfv_params_arc; use e3_greco_helpers::{bfv_ciphertext_to_greco, bfv_public_key_to_greco}; +use e3_polynomial::CrtPolynomial; use e3_zk_helpers::commitments::{ compute_ciphertext_commitment, compute_pk_aggregation_commitment, }; @@ -17,6 +18,7 @@ use fhe_traits::{DeserializeParametrized, FheEncoder, FheEncrypter, Serialize}; use greco::bounds::GrecoBounds; use greco::vectors::GrecoVectors; use rand::thread_rng; +use std::sync::Arc; /// Encrypt some data using BFV homomorphic encryption /// @@ -149,7 +151,11 @@ pub fn compute_pk_commitment( let bit_pk = calculate_bit_width(&bounds.pk_bounds[0].to_string())?; let (pk0is, pk1is) = bfv_public_key_to_greco(&public_key, ¶ms); - let commitment_bigint = compute_pk_aggregation_commitment(&pk0is, &pk1is, bit_pk); + + let pk0 = CrtPolynomial::from_bigint_vectors(pk0is); + let pk1 = CrtPolynomial::from_bigint_vectors(pk1is); + + let commitment_bigint = compute_pk_aggregation_commitment(&pk0, &pk1, bit_pk); let bytes = commitment_bigint.to_bytes_be().1; @@ -187,7 +193,10 @@ pub fn compute_ct_commitment( let (_, bounds) = GrecoBounds::compute(¶ms, 0)?; let bit_ct = calculate_bit_width(&bounds.pk_bounds[0].to_string())?; - let commitment_bigint = compute_ciphertext_commitment(&ct0is, &ct1is, bit_ct); + let ct0 = CrtPolynomial::from_bigint_vectors(ct0is); + let ct1 = CrtPolynomial::from_bigint_vectors(ct1is); + + let commitment_bigint = compute_ciphertext_commitment(&ct0, &ct1, bit_ct); let bytes = commitment_bigint.to_bytes_be().1; diff --git a/crates/polynomial/benches/polynomial.rs b/crates/polynomial/benches/polynomial.rs index ce738a188c..4249cf1f06 100644 --- a/crates/polynomial/benches/polynomial.rs +++ b/crates/polynomial/benches/polynomial.rs @@ -86,7 +86,11 @@ fn benchmark_modular_reduction(c: &mut Criterion) { let modulus = BigInt::from(1000000007); // Large prime group.bench_function(&format!("degree_{}", degree), |b| { - b.iter(|| black_box(poly1.reduce_and_center(&modulus))) + b.iter(|| { + let mut p = poly1.clone(); + p.reduce_and_center(&modulus); + black_box(p) + }) }); } diff --git a/crates/polynomial/src/crt_polynomial.rs b/crates/polynomial/src/crt_polynomial.rs index 93266ffaba..4d09dc87ae 100644 --- a/crates/polynomial/src/crt_polynomial.rs +++ b/crates/polynomial/src/crt_polynomial.rs @@ -7,75 +7,18 @@ //! CRT (Chinese Remainder Theorem) polynomial representation. use crate::polynomial::Polynomial; -use crate::reduce_and_center_coefficients_mut; -use std::sync::Arc; -use thiserror::Error; - use fhe_math::rq::{Poly, Representation}; use num_bigint::BigInt; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use thiserror::Error; /// Errors that can occur during CRT polynomial operations. #[derive(Debug, Error)] pub enum CrtPolynomialError { - /// Moduli list must not be empty. - #[error("moduli must be non-empty")] - EmptyModuli, - - /// Ring degree `n` must be non-zero. - #[error("n must be > 0")] - InvalidN, - - /// Number of limbs must match number of moduli. - #[error("limbs.len() ({actual}) must match ctx.moduli.len() ({expected})")] - LimbCountMismatch { expected: usize, actual: usize }, - - /// Each limb must have exactly `n` coefficients. - #[error("limb {limb_index} length ({actual}) must match ctx.n ({expected})")] - LimbLengthMismatch { - limb_index: usize, - expected: usize, - actual: usize, - }, - - /// fhe-math Poly must be in PowerBasis representation for conversion. - #[error("fhe Poly must be in PowerBasis representation")] - UnsupportedRepresentation, -} - -/// CRT context for a family of polynomials sharing the same ring degree `n` and CRT moduli. -/// -/// Notes: -/// - `n` is the ring degree (also the number of coefficients / NTT size). Each limb has exactly `n` coefficients. -/// - The maximum exponent is `n - 1`. -/// - `moduli` are the CRT factors q_0..q_{L-1} (u64 primes). -#[derive(Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct CrtContext { - pub n: usize, - pub moduli: Vec, -} - -impl CrtContext { - /// Creates a new CRT context. - /// - /// # Errors - /// Returns `CrtPolynomialError::InvalidN` if `n` is zero. - /// Returns `CrtPolynomialError::EmptyModuli` if `moduli` is empty. - pub fn new(n: usize, moduli: Vec) -> Result { - if n == 0 { - return Err(CrtPolynomialError::InvalidN); - } - if moduli.is_empty() { - return Err(CrtPolynomialError::EmptyModuli); - } - Ok(Self { n, moduli }) - } - - pub fn num_moduli(&self) -> usize { - self.moduli.len() - } + /// Moduli slice length does not match number of limbs. + #[error("moduli length ({moduli_len}) must match limbs length ({limbs_len})")] + ModuliLengthMismatch { limbs_len: usize, moduli_len: usize }, } /// A polynomial in CRT form: one limb polynomial per modulus. @@ -85,100 +28,121 @@ impl CrtContext { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct CrtPolynomial { - pub ctx: Arc, pub limbs: Vec, } impl CrtPolynomial { - /// Creates a new CRT polynomial and validates limb dimensions. + /// Builds a `CrtPolynomial` from a vector of polynomials. /// - /// # Errors - /// Returns `CrtPolynomialError::LimbCountMismatch` if the number of limbs does not match `ctx.moduli.len()`. - /// Returns `CrtPolynomialError::LimbLengthMismatch` if any limb has a coefficient vector length different from `ctx.n`. - pub fn new(ctx: Arc, limbs: Vec) -> Result { - let expected_limbs = ctx.moduli.len(); - - if limbs.len() != expected_limbs { - return Err(CrtPolynomialError::LimbCountMismatch { - expected: expected_limbs, - actual: limbs.len(), - }); - } - - let expected_len = ctx.n; - - for (i, limb) in limbs.iter().enumerate() { - let actual_len = limb.coefficients().len(); - if actual_len != expected_len { - return Err(CrtPolynomialError::LimbLengthMismatch { - limb_index: i, - expected: expected_len, - actual: actual_len, - }); - } - } - - Ok(Self { ctx, limbs }) - } - - /// Ring degree (number of coefficients). - pub fn n(&self) -> usize { - self.ctx.n - } - - /// Maximum exponent (at most `n - 1`). - pub fn max_exponent(&self) -> usize { - self.ctx.n.saturating_sub(1) + /// # Arguments + /// + /// * `limbs` - Vector of polynomials. + pub fn new(limbs: Vec) -> Self { + Self { limbs } } - pub fn modulus(&self, i: usize) -> u64 { - self.ctx.moduli[i] - } + /// Builds a `CrtPolynomial` from coefficient vectors (one `Vec` per modulus). + /// + /// # Arguments + /// + /// * `limbs` - Vector of coefficient vectors. + pub fn from_bigint_vectors(limbs: Vec>) -> Self { + let limbs = limbs.into_iter().map(Polynomial::new).collect::>(); - pub fn limb(&self, i: usize) -> &Polynomial { - &self.limbs[i] + Self { limbs } } /// Builds a `CrtPolynomial` from an fhe-math `Poly` in PowerBasis representation. /// - /// Main use: preparing inputs for ZK circuits by converting from FHE BFV ciphertext - /// polynomials to a CRT limb format compatible with the circuits. + /// Used to prepare inputs for ZK circuits by converting FHE BFV ciphertext polynomials + /// into CRT limb format. If `p` is in NTT form, it is converted to PowerBasis first. /// - /// Coefficient layout: fhe-math rows are ascending degree (c_0 + c_1·x + …). - /// We convert to descending order so that evaluation in the circuit matches - /// Horner's method in a single forward pass: P(x) = ((...((a_n * x + a_{n-1}) * x + ...) * x + a_0). - /// The circuit can then iterate `result = result * x + coefficients[i]` from i = 0 without - /// reversing or reindexing, keeping the constraint system simple and efficient. + /// # Arguments /// - /// # Errors - /// Returns `CrtPolynomialError::UnsupportedRepresentation` if the poly is not in PowerBasis. - pub fn from_fhe_poly(p: &Poly) -> Result { + /// * `p` - An fhe-math polynomial (PowerBasis or Ntt). + /// + /// # Coefficient order + /// + + pub fn from_fhe_polynomial(p: &Poly) -> Self { let mut p = p.clone(); if *p.representation() == Representation::Ntt { p.change_representation(Representation::PowerBasis); } - let ctx = p.ctx.as_ref(); - let n = ctx.degree; - let moduli = ctx.moduli().to_vec(); - let crt_ctx = Arc::new(CrtContext::new(n, moduli)?); + let limbs = p + .coefficients() + .outer_iter() + .map(|row| Polynomial::from_u64_vector(row.to_vec())) + .collect(); - let coeffs = p.coefficients(); + Self { limbs } + } - let limbs: Vec = coeffs - .outer_iter() - .zip(crt_ctx.moduli.iter()) - .map(|(row, qi)| { - let mut coeffs: Vec = row.iter().rev().map(|&c| BigInt::from(c)).collect(); - let qi_bigint = BigInt::from(*qi); + /// Reverses the coefficient order of every limb in-place. + /// + /// For each limb, converts between descending degree (a_n, …, a_0) and ascending + /// degree (a_0, …, a_n). Calling this twice restores the original order. + pub fn reverse(&mut self) { + for limb in &mut self.limbs { + limb.reverse(); + } + } - reduce_and_center_coefficients_mut(&mut coeffs, &qi_bigint); + /// Reduces and centers each limb's coefficients modulo the corresponding modulus in-place. + /// + /// Each limb `self.limbs[i]` is reduced modulo `moduli[i]`, with coefficients centered + /// in the symmetric range `(-q/2, q/2]`. + /// + /// # Arguments + /// + /// * `moduli` - One modulus per limb; `moduli[i]` is used for `self.limbs[i]`. + /// + /// # Errors + /// + /// Returns [`CrtPolynomialError::ModuliLengthMismatch`] if `moduli.len() != self.limbs.len()`. + pub fn reduce_and_center(&mut self, moduli: &[u64]) -> Result<(), CrtPolynomialError> { + if self.limbs.len() != moduli.len() { + return Err(CrtPolynomialError::ModuliLengthMismatch { + limbs_len: self.limbs.len(), + moduli_len: moduli.len(), + }); + } - Polynomial::new(coeffs) - }) - .collect(); + for (limb, qi) in self.limbs.iter_mut().zip(moduli.iter()) { + limb.reduce_and_center(&BigInt::from(*qi)); + } + + Ok(()) + } + + /// Returns a reference to the limb polynomial at the given index. + /// + /// # Arguments + /// + /// * `i` - Limb index; must be in range `0..self.limbs.len()`. + /// + /// # Returns + /// + /// A reference to the polynomial for modulus `i`. Coefficients are expected to be + /// reduced/centered modulo the corresponding modulus as required by the caller. + /// + /// # Panics + /// + /// Panics if `i >= self.limbs.len()`. + pub fn limb(&self, i: usize) -> &Polynomial { + &self.limbs[i] + } - CrtPolynomial::new(crt_ctx, limbs) + /// Returns limb coefficient vectors (one `Vec` per modulus). + /// + /// Use when you need a raw CRT representation for serialization, hashing, + /// or APIs that expect `&[Vec]`. The inverse of [`from_limb_coefficients`](Self::from_limb_coefficients). + pub fn to_limb_coefficients(&self) -> Vec> { + self.limbs + .iter() + .map(|l| l.coefficients().to_vec()) + .collect() } } diff --git a/crates/polynomial/src/lib.rs b/crates/polynomial/src/lib.rs index b905ea8694..6faee034ca 100644 --- a/crates/polynomial/src/lib.rs +++ b/crates/polynomial/src/lib.rs @@ -29,6 +29,6 @@ pub mod crt_polynomial; pub mod polynomial; pub mod utils; -pub use crt_polynomial::{CrtContext, CrtPolynomial, CrtPolynomialError}; +pub use crt_polynomial::{CrtPolynomial, CrtPolynomialError}; pub use polynomial::{Polynomial, PolynomialError}; pub use utils::*; diff --git a/crates/polynomial/src/polynomial.rs b/crates/polynomial/src/polynomial.rs index b148193586..830465149c 100644 --- a/crates/polynomial/src/polynomial.rs +++ b/crates/polynomial/src/polynomial.rs @@ -127,6 +127,13 @@ impl Polynomial { Self { coefficients } } + /// Creates a new polynomial from a vector of u64 coefficients. + pub fn from_u64_vector(coefficients: Vec) -> Self { + let coefficients = coefficients.iter().map(|&c| BigInt::from(c)).collect(); + + Self { coefficients } + } + /// Creates a polynomial from coefficients in ascending order format. /// /// This method converts from ascending order coefficient ordering (lowest degree first) @@ -155,6 +162,14 @@ impl Polynomial { coefficients } + /// Reverses the coefficient order in-place. + /// + /// Converts between descending order (highest degree first) and ascending order + /// (lowest degree first). Calling `reverse()` twice restores the original order. + pub fn reverse(&mut self) { + self.coefficients.reverse() + } + /// Creates a zero polynomial of specified degree. /// /// # Arguments @@ -459,14 +474,12 @@ impl Polynomial { /// # Returns /// /// A new polynomial with coefficients reduced and centered. - pub fn reduce_and_center(&self, modulus: &BigInt) -> Self { + pub fn reduce_and_center(&mut self, modulus: &BigInt) { let half_modulus = modulus / 2; - let reduced_coeffs = self - .coefficients - .iter() - .map(|x| reduce_and_center(x, modulus, &half_modulus)) - .collect(); - Polynomial::new(reduced_coeffs) + + self.coefficients + .iter_mut() + .for_each(|x| *x = reduce_and_center(x, modulus, &half_modulus)); } /// Evaluates the polynomial at a given point using Horner's method. diff --git a/crates/zk-helpers/src/commitments.rs b/crates/zk-helpers/src/commitments.rs index 0dd9afc88d..66d4dd0233 100644 --- a/crates/zk-helpers/src/commitments.rs +++ b/crates/zk-helpers/src/commitments.rs @@ -15,7 +15,9 @@ use crate::utils::compute_safe; use ark_bn254::Fr as Field; use ark_ff::BigInteger; use ark_ff::PrimeField; +use e3_polynomial::{CrtPolynomial, Polynomial}; use num_bigint::BigInt; +use std::slice::from_ref; // ============================================================================ // DOMAIN SEPARATORS @@ -157,10 +159,10 @@ pub fn compute_commitments( /// /// # Returns /// A `BigInt` representing the commitment hash value -pub fn compute_dkg_pk_commitment(pk0: &[Vec], pk1: &[Vec], bit_pk: u32) -> BigInt { +pub fn compute_dkg_pk_commitment(pk0: &CrtPolynomial, pk1: &CrtPolynomial, bit_pk: u32) -> BigInt { let mut payload = Vec::new(); - payload = flatten(payload, pk0, bit_pk); - payload = flatten(payload, pk1, bit_pk); + payload = flatten(payload, &pk0.limbs, bit_pk); + payload = flatten(payload, &pk1.limbs, bit_pk); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; @@ -175,20 +177,20 @@ pub fn compute_dkg_pk_commitment(pk0: &[Vec], pk1: &[Vec], bit_p /// This matches the Noir `compute_threshold_pk_commitment` function exactly. /// /// # Arguments -/// * `pk0` - First component of the threshold public key (one vector per modulus) -/// * `pk1` - Second component of the threshold public key (one vector per modulus) +/// * `pk0` - First component of the thershold public key (CRT limbs) +/// * `pk1` - Second component of the thershold public key (CRT limbs) /// * `bit_pk` - The bit width for public key coefficient bounds /// /// # Returns /// A `BigInt` representing the commitment hash value pub fn compute_threshold_pk_commitment( - pk0: &[Vec], - pk1: &[Vec], + pk0: &CrtPolynomial, + pk1: &CrtPolynomial, bit_pk: u32, ) -> BigInt { let mut payload = Vec::new(); - payload = flatten(payload, pk0, bit_pk); - payload = flatten(payload, pk1, bit_pk); + payload = flatten(payload, &pk0.limbs, bit_pk); + payload = flatten(payload, &pk1.limbs, bit_pk); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; @@ -203,14 +205,14 @@ pub fn compute_threshold_pk_commitment( /// This matches the Noir `compute_share_computation_sk_commitment` function exactly. /// /// # Arguments -/// * `sk` - Threshold secret key share coefficients +/// * `sk` - Threshold secret key polynomial /// * `bit_sk` - The bit width for threshold secret key share coefficient bounds /// /// # Returns /// A `BigInt` representing the commitment hash value -pub fn compute_share_computation_sk_commitment(sk: &[BigInt], bit_sk: u32) -> BigInt { +pub fn compute_share_computation_sk_commitment(sk: &Polynomial, bit_sk: u32) -> BigInt { let mut payload = Vec::new(); - payload = flatten(payload, &[sk.to_vec()], bit_sk); + payload = flatten(payload, from_ref(sk), bit_sk); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; @@ -225,14 +227,14 @@ pub fn compute_share_computation_sk_commitment(sk: &[BigInt], bit_sk: u32) -> Bi /// This matches the Noir `compute_share_computation_e_sm_commitment` function exactly. /// /// # Arguments -/// * `e_sm` - Threshold smudging noise share coefficients (one vector per modulus) +/// * `e_sm` - Threshold smudging noise polynomial (CRT limbs) /// * `bit_e_sm` - The bit width for threshold smudging noise share coefficient bounds /// /// # Returns /// A `BigInt` representing the commitment hash value -pub fn compute_share_computation_e_sm_commitment(e_sm: &[Vec], bit_e_sm: u32) -> BigInt { +pub fn compute_share_computation_e_sm_commitment(e_sm: &CrtPolynomial, bit_e_sm: u32) -> BigInt { let mut payload = Vec::new(); - payload = flatten(payload, e_sm, bit_e_sm); + payload = flatten(payload, &e_sm.limbs, bit_e_sm); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; @@ -247,17 +249,17 @@ pub fn compute_share_computation_e_sm_commitment(e_sm: &[Vec], bit_e_sm: /// This matches the Noir `compute_share_encryption_commitment_from_message` function exactly. /// /// # Arguments -/// * `message` - Message polynomial coefficients +/// * `message` - Message polynomial /// * `bit_msg` - The bit width for message coefficient bounds /// /// # Returns /// A `BigInt` representing the commitment hash value pub fn compute_share_encryption_commitment_from_message( - message: &[BigInt], + message: &Polynomial, bit_msg: u32, ) -> BigInt { let mut payload = Vec::new(); - payload = flatten(payload, &[message.to_vec()], bit_msg); + payload = flatten(payload, from_ref(message), bit_msg); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; @@ -312,20 +314,20 @@ pub fn compute_share_encryption_commitment_from_shares( /// This matches the Noir `compute_pk_aggregation_commitment` function exactly. /// /// # Arguments -/// * `pk0` - First component of the threshold public key (one vector per modulus) -/// * `pk1` - Second component of the threshold public key (one vector per modulus) +/// * `pk0` - First component of the threshold public key (CRT limbs) +/// * `pk1` - Second component of the threshold public key (CRT limbs) /// * `bit_pk` - The bit width for threshold public key coefficient bounds /// /// # Returns /// A `BigInt` representing the commitment hash value pub fn compute_pk_aggregation_commitment( - pk0: &[Vec], - pk1: &[Vec], + pk0: &CrtPolynomial, + pk1: &CrtPolynomial, bit_pk: u32, ) -> BigInt { let mut payload = Vec::new(); - payload = flatten(payload, pk0, bit_pk); - payload = flatten(payload, pk1, bit_pk); + payload = flatten(payload, &pk0.limbs, bit_pk); + payload = flatten(payload, &pk1.limbs, bit_pk); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; @@ -356,20 +358,20 @@ pub fn compute_recursive_aggregation_commitment(payload: Vec) -> BigInt { /// Compute CRISP ciphertext commitment. /// /// # Arguments -/// * `ct0` - First component of the ciphertext (one vector per modulus) -/// * `ct1` - Second component of the ciphertext (one vector per modulus) +/// * `ct0` - First component of the ciphertext (CRT limbs) +/// * `ct1` - Second component of the ciphertext (CRT limbs) /// * `bit_ct` - The bit width for ciphertext coefficient bounds /// /// # Returns /// A `BigInt` representing the commitment hash value pub fn compute_ciphertext_commitment( - ct0: &[Vec], - ct1: &[Vec], + ct0: &CrtPolynomial, + ct1: &CrtPolynomial, bit_ct: u32, ) -> BigInt { let mut payload = Vec::new(); - payload = flatten(payload, ct0, bit_ct); - payload = flatten(payload, ct1, bit_ct); + payload = flatten(payload, &ct0.limbs, bit_ct); + payload = flatten(payload, &ct1.limbs, bit_ct); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; @@ -384,14 +386,14 @@ pub fn compute_ciphertext_commitment( /// This matches the Noir `compute_aggregated_shares_commitment` function exactly. /// /// # Arguments -/// * `agg_shares` - Array of aggregated share polynomials (one per modulus) +/// * `agg_shares` - Aggregated share polynomial (CRT limbs) /// * `bit_msg` - The bit width for message coefficient bounds /// /// # Returns /// A `BigInt` representing the commitment hash value -pub fn compute_aggregated_shares_commitment(agg_shares: &[Vec], bit_msg: u32) -> BigInt { +pub fn compute_aggregated_shares_commitment(agg_shares: &CrtPolynomial, bit_msg: u32) -> BigInt { let mut payload = Vec::new(); - payload = flatten(payload, agg_shares, bit_msg); + payload = flatten(payload, &agg_shares.limbs, bit_msg); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; @@ -457,8 +459,8 @@ pub fn compute_share_encryption_challenge(payload: Vec, l: usize) -> Vec< /// Verifies pk_commitment using pk0is and pk1is, then generates challenges from gammas_payload. /// /// # Arguments -/// * `pk0is` - First component of public keys (one vector per modulus) -/// * `pk1is` - Second component of public keys (one vector per modulus) +/// * `pk0is` - First component of public keys (CRT limbs) +/// * `pk1is` - Second component of public keys (CRT limbs) /// * `gammas_payload` - Payload for generating challenges /// * `pk_commitment` - Expected public key commitment value /// * `bit_pk` - The bit width for public key coefficient bounds @@ -470,14 +472,13 @@ pub fn compute_share_encryption_challenge(payload: Vec, l: usize) -> Vec< /// # Panics /// Panics if the computed public key commitment doesn't match `pk_commitment` pub fn compute_user_data_encryption_challenge_commitment( - pk0is: &[Vec], - pk1is: &[Vec], + pk0is: &CrtPolynomial, + pk1is: &CrtPolynomial, gammas_payload: Vec, pk_commitment: &BigInt, bit_pk: u32, l: usize, ) -> Vec { - // Verify pk_commitment matches the commitment from pk0is and pk1is let computed_pk_commitment = compute_pk_aggregation_commitment(pk0is, pk1is, bit_pk); if computed_pk_commitment != *pk_commitment { panic!( @@ -520,6 +521,8 @@ pub fn compute_threshold_share_decryption_challenge(payload: Vec) -> BigI mod tests { use super::*; use crate::utils::bigint_to_field; + use e3_polynomial::{CrtContext, CrtPolynomial}; + use std::sync::Arc; fn field_to_bigint(value: Field) -> BigInt { let bytes = value.into_bigint().to_bytes_le(); @@ -528,13 +531,17 @@ mod tests { #[test] fn compute_ciphertext_commitment_matches_manual_payload() { - let ct0 = vec![vec![BigInt::from(1), BigInt::from(2)]]; - let ct1 = vec![vec![BigInt::from(3), BigInt::from(4)]]; let bit_ct = 4; + let ct0 = + CrtPolynomial::from_limb_coefficients(vec![vec![BigInt::from(1), BigInt::from(2)]]) + .unwrap(); + let ct1 = + CrtPolynomial::from_limb_coefficients(vec![vec![BigInt::from(3), BigInt::from(4)]]) + .unwrap(); let mut payload = Vec::new(); - payload = flatten(payload, &ct0, bit_ct); - payload = flatten(payload, &ct1, bit_ct); + payload = flatten(payload, &ct0.limbs, bit_ct); + payload = flatten(payload, &ct1.limbs, bit_ct); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; diff --git a/crates/zk-helpers/src/packing.rs b/crates/zk-helpers/src/packing.rs index 8105d77d79..5fecc55131 100644 --- a/crates/zk-helpers/src/packing.rs +++ b/crates/zk-helpers/src/packing.rs @@ -11,6 +11,7 @@ use ark_bn254::Fr as Field; use ark_ff::PrimeField; +use e3_polynomial::Polynomial; use num_bigint::BigInt; use num_traits::Zero; @@ -38,27 +39,26 @@ fn packing_layout(bit: u32) -> (u32, u32) { (nibble_bits, group) } -/// Pack values into a Vec of carriers using the shared hex-aligned layout. +/// Pack a polynomial's coefficients into a Vec of carriers using the shared hex-aligned layout. /// /// Matches the Noir `packer` function exactly. /// Packs multiple coefficients into each field element using nibble-aligned layout. /// /// # Arguments -/// * `values` - Slice of BigInt coefficients to pack +/// * `poly` - Polynomial whose coefficients to pack /// * `bit` - The bit width for coefficient bounds /// /// # Returns /// A vector of field elements containing the packed coefficients. -/// The number of field elements is `ceil(values.len() / group)` where `group` is +/// The number of field elements is `ceil(poly.coefficients().len() / group)` where `group` is /// determined by the packing layout. -fn packer(values: &[BigInt], bit: u32) -> Vec { - // Layout parameters: nibble-aligned width and limbs-per-carrier group size. +fn packer(polynomial: &Polynomial, bit: u32) -> Vec { + let values = polynomial.coefficients(); let (nibble_bits, group) = packing_layout(bit); let base = BigInt::from(2).pow(nibble_bits); let radix = BigInt::from(2).pow(nibble_bits + 4); - // Number of chunks to emit: ceil(A / group). let a = values.len() as u32; let num_chunks = a.div_ceil(group); let mut out = Vec::new(); @@ -97,29 +97,24 @@ fn packer(values: &[BigInt], bit: u32) -> Vec { out } -/// Flatten `L` polynomials into a single linear stream of packed `Field` carriers. +/// Flatten a slice of polynomials into a single linear stream of packed `Field` carriers. /// /// Matches the Noir `flatten` function exactly. -/// Packs each polynomial using the same bit width and appends them sequentially. +/// Packs each polynomial's coefficients using the same bit width and appends them sequentially. /// /// # Arguments /// * `inputs` - Initial vector of field elements to append to -/// * `polys` - Slice of polynomials (each represented as Vec) +/// * `polys` - Slice of polynomials to pack /// * `bit` - The bit width for coefficient bounds /// /// # Returns /// Extended vector with packed polynomial coefficients appended in order. /// The polynomials are packed sequentially, maintaining a stable transcript layout. -pub fn flatten(mut inputs: Vec, polys: &[Vec], bit: u32) -> Vec { - for poly in polys { - // Pack coefficients into carriers using the same BIT layout. - let packed = packer(poly, bit); - - // Append carriers in-order to `inputs` to keep a stable transcript layout. +pub fn flatten(mut inputs: Vec, polynomials: &[Polynomial], bit: u32) -> Vec { + for polynomial in polynomials { + let packed = packer(polynomial, bit); inputs.extend(packed); } - - // Return the extended input stream. inputs } @@ -142,15 +137,15 @@ mod tests { #[test] fn test_packer_single_value() { - let values = vec![BigInt::from(42)]; - let packed = packer(&values, 8); + let poly = Polynomial::new(vec![BigInt::from(42)]); + let packed = packer(&poly, 8); assert!(!packed.is_empty()); } #[test] fn test_flatten_empty() { let inputs = Vec::new(); - let polys: Vec> = vec![]; + let polys: Vec = vec![]; let result = flatten(inputs, &polys, 8); assert_eq!(result.len(), 0); } @@ -158,7 +153,7 @@ mod tests { #[test] fn test_flatten_single_poly() { let inputs = Vec::new(); - let poly = vec![BigInt::from(1), BigInt::from(2), BigInt::from(3)]; + let poly = Polynomial::new(vec![BigInt::from(1), BigInt::from(2), BigInt::from(3)]); let polys = vec![poly]; let result = flatten(inputs, &polys, 8); assert!(!result.is_empty()); diff --git a/examples/CRISP/Cargo.lock b/examples/CRISP/Cargo.lock index 5c03a4a60e..bafa64ef45 100644 --- a/examples/CRISP/Cargo.lock +++ b/examples/CRISP/Cargo.lock @@ -2099,6 +2099,7 @@ name = "crisp-zk-inputs" version = "0.1.0" dependencies = [ "e3-fhe-params", + "e3-polynomial 0.1.8", "getrandom 0.2.17", "js-sys", "num-bigint", @@ -2373,6 +2374,7 @@ dependencies = [ "anyhow", "e3-fhe-params", "e3-greco-helpers", + "e3-polynomial 0.1.8", "e3-zk-helpers", "fhe", "fhe-traits", @@ -2470,7 +2472,7 @@ dependencies = [ [[package]] name = "e3-polynomial" version = "0.1.8" -source = "git+https://github.com/gnosisguild/enclave?branch=main#ebf6f386dcefd6ab9c5060d4b8932ed1fa1132b9" +source = "git+https://github.com/gnosisguild/enclave?branch=main#e29a5a1308e2395fea85cc2f9540ab8c9d16a668" dependencies = [ "num-bigint", "num-traits", @@ -2507,7 +2509,7 @@ dependencies = [ [[package]] name = "e3-safe" version = "0.1.8" -source = "git+https://github.com/gnosisguild/enclave#ebf6f386dcefd6ab9c5060d4b8932ed1fa1132b9" +source = "git+https://github.com/gnosisguild/enclave#e29a5a1308e2395fea85cc2f9540ab8c9d16a668" dependencies = [ "ark-bn254 0.5.0", "ark-ff 0.5.0", @@ -6779,7 +6781,6 @@ dependencies = [ "hex", "itertools 0.14.0", "num-bigint", - "num-integer", "num-traits", "rand 0.8.5", "rayon", diff --git a/examples/CRISP/crates/zk-inputs-wasm/Cargo.toml b/examples/CRISP/crates/zk-inputs-wasm/Cargo.toml index f9d4553162..c82c87f3b6 100644 --- a/examples/CRISP/crates/zk-inputs-wasm/Cargo.toml +++ b/examples/CRISP/crates/zk-inputs-wasm/Cargo.toml @@ -10,6 +10,7 @@ description = "Core logic to pre-compute CRISP ZK inputs (WASM/JavaScript bindin crate-type = ["cdylib"] [dependencies] +e3-polynomial = { workspace = true } zk-inputs = { path = "../zk-inputs" } wasm-bindgen = "0.2" js-sys = "0.3" diff --git a/examples/CRISP/crates/zk-inputs-wasm/src/lib.rs b/examples/CRISP/crates/zk-inputs-wasm/src/lib.rs index 55fdc262ea..e637c3498f 100644 --- a/examples/CRISP/crates/zk-inputs-wasm/src/lib.rs +++ b/examples/CRISP/crates/zk-inputs-wasm/src/lib.rs @@ -8,6 +8,7 @@ //! //! This crate provides JavaScript bindings for the CRISP ZK inputs generator using WASM. +use e3_polynomial::CrtPolynomial; use js_sys; use num_bigint::BigInt; use wasm_bindgen::prelude::*; @@ -128,8 +129,12 @@ impl ZKInputsGenerator { } /// Compute the commitment to a set of ciphertext polynomials from JavaScript. - #[wasm_bindgen(js_name = "computeCommitment")] - pub fn compute_ct_commitment(&self, ct0is: JsValue, ct1is: JsValue) -> Result { + #[wasm_bindgen(js_name = "computeCiphertextCommitment")] + pub fn compute_ciphertext_commitment( + &self, + ct0is: JsValue, + ct1is: JsValue, + ) -> Result { // Parse nested arrays: ct0is and ct1is are arrays of arrays (one array per CRT limb) let ct0is_array: js_sys::Array = js_sys::Array::from(&ct0is); let ct1is_array: js_sys::Array = js_sys::Array::from(&ct1is); @@ -176,7 +181,10 @@ impl ZKInputsGenerator { ct1is_vec.push(coefficients); } - match self.generator.compute_commitment(&ct0is_vec, &ct1is_vec) { + let ct0 = CrtPolynomial::from_bigint_vectors(ct0is_vec); + let ct1 = CrtPolynomial::from_bigint_vectors(ct1is_vec); + + match self.generator.compute_commitment(&ct0, &ct1) { Ok(commitment) => Ok(commitment.to_string()), Err(e) => Err(JsValue::from_str(&e.to_string())), } diff --git a/examples/CRISP/crates/zk-inputs/Cargo.toml b/examples/CRISP/crates/zk-inputs/Cargo.toml index 80ea9a0c44..f3863cacd0 100644 --- a/examples/CRISP/crates/zk-inputs/Cargo.toml +++ b/examples/CRISP/crates/zk-inputs/Cargo.toml @@ -19,7 +19,6 @@ e3-polynomial = { workspace = true, features = ["serde"] } serde.workspace = true serde_json.workspace = true num-bigint = "0.4.6" -num-integer = "0.1" num-traits = "0.2" rayon = "1.10.0" rand = "0.8.5" diff --git a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs index 033b96e448..4ef78cd8dc 100644 --- a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs +++ b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs @@ -4,20 +4,13 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use e3_polynomial::{reduce_and_center_coefficients_mut, reduce_coefficients_2d}; +use e3_polynomial::{reduce_coefficients, CrtPolynomial, Polynomial}; use e3_zk_helpers::commitments::compute_ciphertext_commitment; -use e3_zk_helpers::utils::{calculate_bit_width, get_zkp_modulus}; +use e3_zk_helpers::utils::get_zkp_modulus; use eyre::{Context, Result}; use fhe::bfv::BfvParameters; use fhe::bfv::Ciphertext; -use fhe::bfv::Plaintext; -use fhe_math::rq::Representation; -use greco::bounds::GrecoBounds; -use itertools::izip; use num_bigint::BigInt; -use num_integer::Integer; -use num_traits::Zero; -use rayon::iter::{ParallelBridge, ParallelIterator}; use std::sync::Arc; /// Set of inputs for validation of a ciphertext addition. @@ -26,37 +19,19 @@ use std::sync::Arc; /// was performed correctly in the zero-knowledge proof system. #[derive(Clone, Debug)] pub struct CiphertextAdditionInputs { - pub prev_ct0is: Vec>, - pub prev_ct1is: Vec>, + pub prev_ct0is: CrtPolynomial, + pub prev_ct1is: CrtPolynomial, + pub sum_ct0is: CrtPolynomial, + pub sum_ct1is: CrtPolynomial, + pub r0is: CrtPolynomial, + pub r1is: CrtPolynomial, pub prev_ct_commitment: BigInt, - pub sum_ct0is: Vec>, - pub sum_ct1is: Vec>, - pub r0is: Vec>, - pub r1is: Vec>, } impl CiphertextAdditionInputs { - /// Creates a new CiphertextAdditionInputs with zero-initialized vectors. - /// - /// # Arguments - /// * `num_moduli` - Number of CRT moduli - /// * `degree` - Polynomial degree - pub fn new(num_moduli: usize, degree: usize) -> Self { - CiphertextAdditionInputs { - prev_ct0is: vec![vec![BigInt::zero(); degree]; num_moduli], - prev_ct1is: vec![vec![BigInt::zero(); degree]; num_moduli], - prev_ct_commitment: BigInt::zero(), - sum_ct0is: vec![vec![BigInt::zero(); degree]; num_moduli], - sum_ct1is: vec![vec![BigInt::zero(); degree]; num_moduli], - r0is: vec![vec![BigInt::zero(); degree]; num_moduli], - r1is: vec![vec![BigInt::zero(); degree]; num_moduli], - } - } - /// Computes the ciphertext addition inputs for zero-knowledge proof validation. /// /// # Arguments - /// * `pt` - The plaintext being encrypted /// * `prev_ct` - The existing ciphertext to add to /// * `ct` - The ciphertext being added (from Greco) /// * `sum_ct` - The result of the ciphertext addition @@ -66,177 +41,54 @@ impl CiphertextAdditionInputs { /// # Returns /// CiphertextAdditionInputs containing all necessary proof data pub fn compute( - pt: &Plaintext, prev_ct: &Ciphertext, ct: &Ciphertext, sum_ct: &Ciphertext, params: Arc, + bit_ct: u32, ) -> Result { - let ctx: &Arc = params - .ctx_at_level(pt.level()) - .with_context(|| "Failed to get context at level")?; - let n: u64 = params.degree() as u64; - - // Extract and convert ciphertexts to power basis representation. - let mut prev_ct0 = prev_ct.c[0].clone(); - let mut prev_ct1 = prev_ct.c[1].clone(); - prev_ct0.change_representation(Representation::PowerBasis); - prev_ct1.change_representation(Representation::PowerBasis); - - let mut ct0 = ct.c[0].clone(); - let mut ct1 = ct.c[1].clone(); - ct0.change_representation(Representation::PowerBasis); - ct1.change_representation(Representation::PowerBasis); - - let mut sum_ct0 = sum_ct.c[0].clone(); - let mut sum_ct1 = sum_ct.c[1].clone(); - sum_ct0.change_representation(Representation::PowerBasis); - sum_ct1.change_representation(Representation::PowerBasis); - - // Initialize matrices to store results. - let mut res = CiphertextAdditionInputs::new(params.moduli().len(), n as usize); - - let prev_ct0_coeffs = prev_ct0.coefficients(); - let prev_ct1_coeffs = prev_ct1.coefficients(); - let ct0_coeffs = ct0.coefficients(); - let ct1_coeffs = ct1.coefficients(); - let sum_ct0_coeffs = sum_ct0.coefficients(); - let sum_ct1_coeffs = sum_ct1.coefficients(); - - let prev_ct0_coeffs_rows = prev_ct0_coeffs.rows(); - let prev_ct1_coeffs_rows = prev_ct1_coeffs.rows(); - let ct0_coeffs_rows = ct0_coeffs.rows(); - let ct1_coeffs_rows = ct1_coeffs.rows(); - let sum_ct0_coeffs_rows = sum_ct0_coeffs.rows(); - let sum_ct1_coeffs_rows = sum_ct1_coeffs.rows(); - - // Perform the main computation logic in parallel across moduli. - let results: Vec<_> = izip!( - ctx.moduli_operators(), - prev_ct0_coeffs_rows, - prev_ct1_coeffs_rows, - ct0_coeffs_rows, - ct1_coeffs_rows, - sum_ct0_coeffs_rows, - sum_ct1_coeffs_rows, - ) - .enumerate() - .par_bridge() - .map( - |( - i, - ( - qi, - prev_ct0_coeffs, - prev_ct1_coeffs, - ct0_coeffs, - ct1_coeffs, - sum_ct0_coeffs, - sum_ct1_coeffs, - ), - )| { - // Convert to vectors of BigInt, center, and reverse order. - let mut prev_ct0i: Vec = prev_ct0_coeffs - .iter() - .rev() - .map(|&x| BigInt::from(x)) - .collect(); - let mut prev_ct1i: Vec = prev_ct1_coeffs - .iter() - .rev() - .map(|&x| BigInt::from(x)) - .collect(); - let mut ct0i: Vec = ct0_coeffs - .iter() - .rev() - .map(|&x| BigInt::from(x)) - .collect(); - let mut ct1i: Vec = ct1_coeffs - .iter() - .rev() - .map(|&x| BigInt::from(x)) - .collect(); - let mut sum_ct0i: Vec = sum_ct0_coeffs - .iter() - .rev() - .map(|&x| BigInt::from(x)) - .collect(); - let mut sum_ct1i: Vec = sum_ct1_coeffs - .iter() - .rev() - .map(|&x| BigInt::from(x)) - .collect(); - - let qi_bigint = BigInt::from(qi.modulus()); - - // Center coefficients around zero for proper modular arithmetic. - reduce_and_center_coefficients_mut(&mut prev_ct0i, &qi_bigint); - reduce_and_center_coefficients_mut(&mut prev_ct1i, &qi_bigint); - reduce_and_center_coefficients_mut(&mut ct0i, &qi_bigint); - reduce_and_center_coefficients_mut(&mut ct1i, &qi_bigint); - reduce_and_center_coefficients_mut(&mut sum_ct0i, &qi_bigint); - reduce_and_center_coefficients_mut(&mut sum_ct1i, &qi_bigint); - - // Compute quotient polynomials: r = (sum_centered - (ct_centered + prev_ct_centered)) / qi. - // For ciphertext addition: sum_centered = ct_centered + prev_ct_centered + r * qi. - // So: r = (sum_centered - (ct_centered + prev_ct_centered)) / qi. - let mut r0i = Vec::new(); - let mut r1i = Vec::new(); - - // Reserve space for the quotient polynomials. - r0i.reserve_exact(n as usize); - r1i.reserve_exact(n as usize); - - for j in 0..n as usize { - let diff0 = &sum_ct0i[j] - (&ct0i[j] + &prev_ct0i[j]); - let (q0, r0) = diff0.div_rem(&qi_bigint); - if !r0.is_zero() { - return Err(eyre::eyre!( - "Non-zero remainder in ct0 division at modulus index {}, coeff {}: remainder = {}", i, j, r0 - )); - } - if q0 < (-1).into() || q0 > 1.into() { - return Err(eyre::eyre!( - "Quotient out of range [-1, 1] for ct0 at modulus index {}, coeff {}: quotient = {}", i, j, q0 - )); - } - let diff1 = &sum_ct1i[j] - (&ct1i[j] + &prev_ct1i[j]); - let (q1, r1) = diff1.div_rem(&qi_bigint); - if !r1.is_zero() { - return Err(eyre::eyre!( - "Non-zero remainder in ct1 division at modulus index {}, coeff {}: remainder = {}", i, j, r1 - )); - } - if q1 < (-1).into() || q1 > 1.into() { - return Err(eyre::eyre!( - "Quotient out of range [-1, 1] for ct1 at modulus index {}, coeff {}: quotient = {}", i, j, q1 - )); - } - r0i.push(q0); - r1i.push(q1); - } - - Ok((i, prev_ct0i, prev_ct1i, sum_ct0i, sum_ct1i, r0i, r1i)) - }, - ) - .collect::, _>>()?; - - // Merge results into the `res` structure after parallel execution. - for (i, prev_ct0i, prev_ct1i, sum_ct0i, sum_ct1i, r0i, r1i) in results { - res.prev_ct0is[i] = prev_ct0i; - res.prev_ct1is[i] = prev_ct1i; - res.sum_ct0is[i] = sum_ct0i; - res.sum_ct1is[i] = sum_ct1i; - res.r0is[i] = r0i; - res.r1is[i] = r1i; + let n = params.degree(); + let moduli = params.moduli(); + + let mut crt_polynomials = [ + CrtPolynomial::from_fhe_polynomial(&prev_ct.c[0]), + CrtPolynomial::from_fhe_polynomial(&prev_ct.c[1]), + CrtPolynomial::from_fhe_polynomial(&ct.c[0]), + CrtPolynomial::from_fhe_polynomial(&ct.c[1]), + CrtPolynomial::from_fhe_polynomial(&sum_ct.c[0]), + CrtPolynomial::from_fhe_polynomial(&sum_ct.c[1]), + ]; + + // fhe-math stores coefficients in ascending degree (c_0, c_1, …). But here we want + // that each limb is stored in **descending** order (a_n, …, a_0) so circuit evaluation can use Horner's + // method in one forward pass: `result = result * x + coefficients[i]` from i = 0, + // i.e. P(x) = ((…((a_n·x + a_{n-1})·x + …)·x + a_0), with no extra reversing or reindexing. + for c in &mut crt_polynomials { + c.reverse(); + c.reduce_and_center(&moduli)?; } - let (_, bounds) = GrecoBounds::compute(¶ms, 0)?; - let bit = calculate_bit_width(&bounds.pk_bounds[0].to_string())?; - res.prev_ct_commitment = - compute_ciphertext_commitment(&res.prev_ct0is, &res.prev_ct1is, bit); - - Ok(res) + let [prev_ct0, prev_ct1, ct0, ct1, sum_ct0, sum_ct1] = crt_polynomials; + + // Compute quotient polynomials: r = (sum_centered - (ct_centered + prev_ct_centered)) / qi. + // For ciphertext addition: sum_centered = ct_centered + prev_ct_centered + r * qi. + // So: r = (sum_centered - (ct_centered + prev_ct_centered)) / qi. + let r0 = Self::compute_quotient(&sum_ct0, &ct0, &prev_ct0, n, &moduli) + .with_context(|| "Failed to compute r0 quotient")?; + let r1 = Self::compute_quotient(&sum_ct1, &ct1, &prev_ct1, n, &moduli) + .with_context(|| "Failed to compute r1 quotient")?; + + let prev_ct_commitment = compute_ciphertext_commitment(&prev_ct0, &prev_ct1, bit_ct); + + Ok(CiphertextAdditionInputs { + prev_ct0is: prev_ct0, + prev_ct1is: prev_ct1, + sum_ct0is: sum_ct0, + sum_ct1is: sum_ct1, + r0is: r0, + r1is: r1, + prev_ct_commitment, + }) } /// Converts the inputs to standard form by reducing coefficients modulo the ZKP modulus. @@ -246,31 +98,107 @@ impl CiphertextAdditionInputs { pub fn standard_form(&self) -> Self { let zkp_modulus = &get_zkp_modulus(); CiphertextAdditionInputs { - prev_ct0is: reduce_coefficients_2d(&self.prev_ct0is, zkp_modulus), - prev_ct1is: reduce_coefficients_2d(&self.prev_ct1is, zkp_modulus), + prev_ct0is: reduce_crt_polynomial(&self.prev_ct0is, zkp_modulus), + prev_ct1is: reduce_crt_polynomial(&self.prev_ct1is, zkp_modulus), prev_ct_commitment: self.prev_ct_commitment.clone() % zkp_modulus, - sum_ct0is: reduce_coefficients_2d(&self.sum_ct0is, zkp_modulus), - sum_ct1is: reduce_coefficients_2d(&self.sum_ct1is, zkp_modulus), - r0is: reduce_coefficients_2d(&self.r0is, zkp_modulus), - r1is: reduce_coefficients_2d(&self.r1is, zkp_modulus), + sum_ct0is: reduce_crt_polynomial(&self.sum_ct0is, zkp_modulus), + sum_ct1is: reduce_crt_polynomial(&self.sum_ct1is, zkp_modulus), + r0is: reduce_crt_polynomial(&self.r0is, zkp_modulus), + r1is: reduce_crt_polynomial(&self.r1is, zkp_modulus), } } + + /// Computes the quotient CRT polynomial `(sum - (a + b)) / q_i` per modulus. + /// + /// For each limb index `i`, divides `sum_i - (a_i + b_i)` by the modulus `q_i`. + /// Used when verifying that sum ciphertext equals a + b and recovering the + /// quotient (small integer) from the difference. + /// + /// # Arguments + /// + /// * `sum` - CRT polynomial of the sum ciphertext + /// * `a` - CRT polynomial of the first ciphertext + /// * `b` - CRT polynomial of the second ciphertext + /// * `n` - polynomial degree (number of coefficients per limb) + /// * `moduli` - moduli for each CRT limb + /// + /// # Returns + /// + /// The quotient CRT polynomial, or an error if division is not exact or the + /// quotient is not in `{-1, 0, 1}`. + fn compute_quotient( + sum: &CrtPolynomial, + a: &CrtPolynomial, + b: &CrtPolynomial, + _n: usize, + moduli: &[u64], + ) -> Result { + let num_moduli = moduli.len(); + + let mut quotient_limbs = Vec::with_capacity(num_moduli); + + for i in 0..num_moduli { + let sum_limb = sum.limb(i); + let a_limb = a.limb(i); + let b_limb = b.limb(i); + let qi = Polynomial::constant(BigInt::from(moduli[i])); + + let diff = sum_limb.sub(&a_limb.add(b_limb)); + let (q_poly, _remainder) = diff + .div(&qi) + .map_err(|e| eyre::eyre!("division by modulus q_i at index {}: {}", i, e))?; + + for (j, q) in q_poly.coefficients().iter().enumerate() { + if *q < (-1).into() || *q > 1.into() { + return Err(eyre::eyre!( + "Quotient out of range [-1, 1] at modulus index {}, coeff {}: quotient = {}", + i, + j, + q + )); + } + } + + quotient_limbs.push(q_poly); + } + + Ok(CrtPolynomial::new(quotient_limbs)) + } +} + +/// Reduces all coefficients of a CRT polynomial modulo the given modulus. +fn reduce_crt_polynomial(crt_poly: &CrtPolynomial, modulus: &BigInt) -> CrtPolynomial { + let reduced_limbs: Vec = crt_poly + .limbs + .iter() + .map(|limb| { + let reduced_coeffs = reduce_coefficients(limb.coefficients(), modulus); + Polynomial::new(reduced_coeffs) + }) + .collect(); + + // Safe to unwrap because we're preserving the structure + CrtPolynomial::new(reduced_limbs) } #[cfg(test)] mod tests { use super::*; - use fhe::bfv::{BfvParametersBuilder, Encoding, Plaintext, PublicKey, SecretKey}; + use e3_fhe_params::{BfvParamSet, BfvPreset}; + use e3_zk_helpers::utils::calculate_bit_width; + use fhe::bfv::{Encoding, Plaintext, PublicKey, SecretKey}; use fhe_traits::FheEncoder; + use greco::bounds::GrecoBounds; use rand::thread_rng; + fn test_bit_ct(params: &Arc) -> u32 { + let (_, bounds) = GrecoBounds::compute(params, 0).unwrap(); + calculate_bit_width(&bounds.pk_bounds[0].to_string()).unwrap() + } + fn create_test_generator() -> (Arc, PublicKey, SecretKey) { - let bfv_params = BfvParametersBuilder::new() - .set_degree(1024) // Smaller degree for faster tests. - .set_plaintext_modulus(1032193) - .set_moduli(&[0x3FFFFFFF000001]) - .build_arc() - .unwrap(); + let param_set: BfvParamSet = BfvPreset::InsecureThresholdBfv512.into(); + let bfv_params = param_set.build_arc(); let mut rng = thread_rng(); let sk = SecretKey::random(&bfv_params, &mut rng); @@ -285,18 +213,6 @@ mod tests { Plaintext::try_encode(&message_data, Encoding::poly(), params).unwrap() } - #[test] - fn test_new_initialization() { - let inputs = CiphertextAdditionInputs::new(2, 1024); - - assert_eq!(inputs.prev_ct0is.len(), 2); - assert_eq!(inputs.prev_ct1is.len(), 2); - assert_eq!(inputs.sum_ct0is.len(), 2); - assert_eq!(inputs.sum_ct1is.len(), 2); - assert_eq!(inputs.r0is.len(), 2); - assert_eq!(inputs.r1is.len(), 2); - } - #[test] fn test_compute_basic_functionality() { let (bfv_params, pk, _sk) = create_test_generator(); @@ -314,19 +230,20 @@ mod tests { let sum_ct = &ct1 + &ct2; // Compute ciphertext addition inputs. + let bit_ct = test_bit_ct(&bfv_params); let result = - CiphertextAdditionInputs::compute(&pt2, &ct1, &ct2, &sum_ct, bfv_params.clone()); + CiphertextAdditionInputs::compute(&ct1, &ct2, &sum_ct, bfv_params.clone(), bit_ct); assert!(result.is_ok()); let inputs = result.unwrap(); - // Verify structure. - assert_eq!(inputs.prev_ct0is.len(), 1); // One modulus - assert_eq!(inputs.prev_ct1is.len(), 1); - assert_eq!(inputs.sum_ct0is.len(), 1); - assert_eq!(inputs.sum_ct1is.len(), 1); - assert_eq!(inputs.r0is.len(), 1); - assert_eq!(inputs.r1is.len(), 1); + let num_moduli = bfv_params.moduli().len(); + assert_eq!(inputs.prev_ct0is.limbs.len(), num_moduli); + assert_eq!(inputs.prev_ct1is.limbs.len(), num_moduli); + assert_eq!(inputs.sum_ct0is.limbs.len(), num_moduli); + assert_eq!(inputs.sum_ct1is.limbs.len(), num_moduli); + assert_eq!(inputs.r0is.limbs.len(), num_moduli); + assert_eq!(inputs.r1is.limbs.len(), num_moduli); } #[test] @@ -339,12 +256,16 @@ mod tests { let (ct2, _u2, _e0_2, _e1_2) = pk.try_encrypt_extended(&pt, &mut rng).unwrap(); let sum_ct = &ct1 + &ct2; + let bit_ct = test_bit_ct(&bfv_params); let inputs = - CiphertextAdditionInputs::compute(&pt, &ct1, &ct2, &sum_ct, bfv_params.clone()) + CiphertextAdditionInputs::compute(&ct1, &ct2, &sum_ct, bfv_params.clone(), bit_ct) .unwrap(); let standard_form = inputs.standard_form(); // Verify structure is preserved. - assert_eq!(standard_form.prev_ct0is.len(), inputs.prev_ct0is.len()); + assert_eq!( + standard_form.prev_ct0is.limbs.len(), + inputs.prev_ct0is.limbs.len() + ); } } diff --git a/examples/CRISP/crates/zk-inputs/src/lib.rs b/examples/CRISP/crates/zk-inputs/src/lib.rs index bed18ae6d2..9439abde86 100644 --- a/examples/CRISP/crates/zk-inputs/src/lib.rs +++ b/examples/CRISP/crates/zk-inputs/src/lib.rs @@ -11,10 +11,12 @@ use e3_fhe_params::build_bfv_params_arc; use e3_fhe_params::default_param_set; use e3_fhe_params::BfvParamSet; +use e3_polynomial::CrtPolynomial; use e3_zk_helpers::commitments::compute_ciphertext_commitment; use e3_zk_helpers::utils::calculate_bit_width; use eyre::{Context, Result}; use fhe::bfv::BfvParameters; +use std::sync::Arc; use fhe::bfv::Ciphertext; use fhe::bfv::PublicKey; use fhe::bfv::SecretKey; @@ -25,7 +27,6 @@ use greco::vectors::GrecoVectors; use num_bigint::BigInt; use num_traits::Zero; use rand::thread_rng; -use std::sync::Arc; mod ciphertext_addition; use crate::ciphertext_addition::CiphertextAdditionInputs; mod serialization; @@ -123,9 +124,15 @@ impl ZKInputsGenerator { let sum_ct = &ct + &prev_ct; // Compute the inputs of the ciphertext addition. - let ciphertext_addition_inputs = - CiphertextAdditionInputs::compute(&pt, &prev_ct, &ct, &sum_ct, self.bfv_params.clone()) - .with_context(|| "Failed to compute ciphertext addition inputs")?; + // bit_pk + let ciphertext_addition_inputs = CiphertextAdditionInputs::compute( + &prev_ct, + &ct, + &sum_ct, + self.bfv_params.clone(), + bit_pk, + ) + .with_context(|| "Failed to compute ciphertext addition inputs")?; // Construct Inputs Section. let inputs = construct_inputs( @@ -199,9 +206,14 @@ impl ZKInputsGenerator { let sum_ct = &ct + &prev_ct; // Compute the inputs of the ciphertext addition. - let mut ciphertext_addition_inputs = - CiphertextAdditionInputs::compute(&pt, &prev_ct, &ct, &sum_ct, self.bfv_params.clone()) - .with_context(|| "Failed to compute ciphertext addition inputs")?; + let mut ciphertext_addition_inputs = CiphertextAdditionInputs::compute( + &prev_ct, + &ct, + &sum_ct, + self.bfv_params.clone(), + bit_pk, + ) + .with_context(|| "Failed to compute ciphertext addition inputs")?; // For first votes, set prev_ct_commitment to 0 since there's no previous ciphertext ciphertext_addition_inputs.prev_ct_commitment = BigInt::zero(); @@ -263,20 +275,16 @@ impl ZKInputsGenerator { /// Computes the commitment to a set of ciphertext polynomials. /// /// # Arguments - /// * `ct0is` - The first component of the ciphertext polynomials. - /// * `ct1is` - The second component of the ciphertext polynomials. + /// * `ct0` - First component of the ciphertext (CRT limbs). + /// * `ct1` - Second component of the ciphertext (CRT limbs). /// /// # Returns /// The commitment as a BigInt. - pub fn compute_commitment( - &self, - ct0is: &[Vec], - ct1is: &[Vec], - ) -> Result { + pub fn compute_commitment(&self, ct0: &CrtPolynomial, ct1: &CrtPolynomial) -> Result { let (_, bounds) = GrecoBounds::compute(&self.bfv_params, 0)?; let bit = calculate_bit_width(&bounds.pk_bounds[0].to_string())?; - Ok(compute_ciphertext_commitment(ct0is, ct1is, bit)) + Ok(compute_ciphertext_commitment(ct0, ct1, bit)) } } diff --git a/examples/CRISP/crates/zk-inputs/src/serialization.rs b/examples/CRISP/crates/zk-inputs/src/serialization.rs index 8a3c53017d..1397a6960b 100644 --- a/examples/CRISP/crates/zk-inputs/src/serialization.rs +++ b/examples/CRISP/crates/zk-inputs/src/serialization.rs @@ -93,19 +93,21 @@ pub fn construct_inputs( ZKInputs { prev_ct0is: ciphertext_addition_inputs_standard .prev_ct0is + .limbs .iter() - .map(|v| { + .map(|limb| { serde_json::json!({ - "coefficients": to_string_1d_vec(v) + "coefficients": to_string_1d_vec(limb.coefficients()) }) }) .collect(), prev_ct1is: ciphertext_addition_inputs_standard .prev_ct1is + .limbs .iter() - .map(|v| { + .map(|limb| { serde_json::json!({ - "coefficients": to_string_1d_vec(v) + "coefficients": to_string_1d_vec(limb.coefficients()) }) }) .collect(), @@ -114,37 +116,41 @@ pub fn construct_inputs( .to_string(), sum_ct0is: ciphertext_addition_inputs_standard .sum_ct0is + .limbs .iter() - .map(|v| { + .map(|limb| { serde_json::json!({ - "coefficients": to_string_1d_vec(v) + "coefficients": to_string_1d_vec(limb.coefficients()) }) }) .collect(), sum_ct1is: ciphertext_addition_inputs_standard .sum_ct1is + .limbs .iter() - .map(|v| { + .map(|limb| { serde_json::json!({ - "coefficients": to_string_1d_vec(v) + "coefficients": to_string_1d_vec(limb.coefficients()) }) }) .collect(), sum_r0is: ciphertext_addition_inputs_standard .r0is + .limbs .iter() - .map(|v| { + .map(|limb| { serde_json::json!({ - "coefficients": to_string_1d_vec(v) + "coefficients": to_string_1d_vec(limb.coefficients()) }) }) .collect(), sum_r1is: ciphertext_addition_inputs_standard .r1is + .limbs .iter() - .map(|v| { + .map(|limb| { serde_json::json!({ - "coefficients": to_string_1d_vec(v) + "coefficients": to_string_1d_vec(limb.coefficients()) }) }) .collect(), @@ -349,14 +355,34 @@ mod tests { } fn create_mock_ciphertext_addition_inputs() -> CiphertextAdditionInputs { + use e3_polynomial::{CrtPolynomial, Polynomial}; + CiphertextAdditionInputs { - prev_ct0is: vec![vec![BigInt::from(1), BigInt::from(2)]], - prev_ct1is: vec![vec![BigInt::from(3), BigInt::from(4)]], + prev_ct0is: CrtPolynomial::new(vec![ + Polynomial::new(vec![BigInt::from(1), BigInt::from(2)]), + Polynomial::new(vec![BigInt::from(1), BigInt::from(2)]), + ]), + prev_ct1is: CrtPolynomial::new(vec![ + Polynomial::new(vec![BigInt::from(3), BigInt::from(4)]), + Polynomial::new(vec![BigInt::from(3), BigInt::from(4)]), + ]), prev_ct_commitment: BigInt::from(0), - sum_ct0is: vec![vec![BigInt::from(5), BigInt::from(6)]], - sum_ct1is: vec![vec![BigInt::from(7), BigInt::from(8)]], - r0is: vec![vec![BigInt::from(9), BigInt::from(10)]], - r1is: vec![vec![BigInt::from(11), BigInt::from(12)]], + sum_ct0is: CrtPolynomial::new(vec![ + Polynomial::new(vec![BigInt::from(5), BigInt::from(6)]), + Polynomial::new(vec![BigInt::from(5), BigInt::from(6)]), + ]), + sum_ct1is: CrtPolynomial::new(vec![ + Polynomial::new(vec![BigInt::from(7), BigInt::from(8)]), + Polynomial::new(vec![BigInt::from(7), BigInt::from(8)]), + ]), + r0is: CrtPolynomial::new(vec![ + Polynomial::new(vec![BigInt::from(9), BigInt::from(10)]), + Polynomial::new(vec![BigInt::from(9), BigInt::from(10)]), + ]), + r1is: CrtPolynomial::new(vec![ + Polynomial::new(vec![BigInt::from(11), BigInt::from(12)]), + Polynomial::new(vec![BigInt::from(11), BigInt::from(12)]), + ]), } } @@ -376,12 +402,12 @@ mod tests { // Verify basic structure. assert!(inputs.params.is_object()); - assert_eq!(inputs.prev_ct0is.len(), 1); - assert_eq!(inputs.prev_ct1is.len(), 1); - assert_eq!(inputs.sum_ct0is.len(), 1); - assert_eq!(inputs.sum_ct1is.len(), 1); - assert_eq!(inputs.sum_r0is.len(), 1); - assert_eq!(inputs.sum_r1is.len(), 1); + assert_eq!(inputs.prev_ct0is.len(), 2); // 2 moduli + assert_eq!(inputs.prev_ct1is.len(), 2); // 2 moduli + assert_eq!(inputs.sum_ct0is.len(), 2); // 2 moduli + assert_eq!(inputs.sum_ct1is.len(), 2); // 2 moduli + assert_eq!(inputs.sum_r0is.len(), 2); // 2 moduli + assert_eq!(inputs.sum_r1is.len(), 2); // 2 moduli assert_eq!(inputs.ct0is.len(), 2); assert_eq!(inputs.ct1is.len(), 2); assert_eq!(inputs.pk0is.len(), 2); diff --git a/examples/CRISP/packages/crisp-sdk/src/vote.ts b/examples/CRISP/packages/crisp-sdk/src/vote.ts index 811b29c636..7e2504736a 100644 --- a/examples/CRISP/packages/crisp-sdk/src/vote.ts +++ b/examples/CRISP/packages/crisp-sdk/src/vote.ts @@ -152,8 +152,8 @@ export const generatePublicKey = (): Uint8Array => { * @param ct1is - The second component of the ciphertext polynomials. * @returns The commitment as a bigint. */ -export const computeCommitment = (ct0is: Polynomial[], ct1is: Polynomial[]): bigint => { - const commitment = zkInputsGenerator.computeCommitment( +export const computeCiphertextCommitment = (ct0is: Polynomial[], ct1is: Polynomial[]): bigint => { + const commitment = zkInputsGenerator.computeCiphertextCommitment( ct0is.map((p) => p.coefficients), ct1is.map((p) => p.coefficients), ) diff --git a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts index aa6daf66b8..0399af5f7b 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts @@ -14,7 +14,7 @@ import { encodeVote, generateCircuitInputs, executeCircuit, - computeCommitment, + computeCiphertextCommitment, encryptVote, } from '../src/vote' import { publicKeyToAddress, signMessage } from 'viem/accounts' @@ -127,7 +127,7 @@ describe('Vote', () => { }) const { returnValue } = await executeCircuit(crispInputs) - const commitment = computeCommitment(crispInputs.ct0is, crispInputs.ct1is) + const commitment = computeCiphertextCommitment(crispInputs.ct0is, crispInputs.ct1is) expect(returnValue).toEqual(commitment) }) @@ -151,7 +151,7 @@ describe('Vote', () => { }) const { returnValue } = await executeCircuit(crispInputs) - const commitment = computeCommitment(crispInputs.sum_ct0is, crispInputs.sum_ct1is) + const commitment = computeCiphertextCommitment(crispInputs.sum_ct0is, crispInputs.sum_ct1is) expect(returnValue).toEqual(commitment) }) @@ -174,7 +174,7 @@ describe('Vote', () => { }) const { returnValue } = await executeCircuit(crispInputs) - const commitment = computeCommitment(crispInputs.ct0is, crispInputs.ct1is) + const commitment = computeCiphertextCommitment(crispInputs.ct0is, crispInputs.ct1is) expect(returnValue).toEqual(commitment) }) From 240825ae3614bb383527a3505009d92c01a7f7f0 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 30 Jan 2026 17:44:44 +0100 Subject: [PATCH 06/20] chore: update lockfile --- crates/bfv-client/src/client.rs | 1 - templates/default/Cargo.lock | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bfv-client/src/client.rs b/crates/bfv-client/src/client.rs index a32764a878..c9c6829cc4 100644 --- a/crates/bfv-client/src/client.rs +++ b/crates/bfv-client/src/client.rs @@ -18,7 +18,6 @@ use fhe_traits::{DeserializeParametrized, FheEncoder, FheEncrypter, Serialize}; use greco::bounds::GrecoBounds; use greco::vectors::GrecoVectors; use rand::thread_rng; -use std::sync::Arc; /// Encrypt some data using BFV homomorphic encryption /// diff --git a/templates/default/Cargo.lock b/templates/default/Cargo.lock index 97e041d1e0..3b421f369d 100644 --- a/templates/default/Cargo.lock +++ b/templates/default/Cargo.lock @@ -1266,6 +1266,7 @@ dependencies = [ "anyhow", "e3-fhe-params", "e3-greco-helpers", + "e3-polynomial", "e3-zk-helpers", "fhe", "fhe-traits", @@ -1321,6 +1322,8 @@ dependencies = [ name = "e3-polynomial" version = "0.1.8" dependencies = [ + "fhe-math", + "ndarray", "num-bigint", "num-traits", "thiserror", @@ -2483,6 +2486,7 @@ dependencies = [ "num-integer", "num-traits", "rawpointer", + "serde", ] [[package]] From c8a75eb733c1790f8514b45259999764e125d55e Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 30 Jan 2026 18:59:16 +0100 Subject: [PATCH 07/20] chore: fix lint issues --- crates/zk-helpers/src/commitments.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/crates/zk-helpers/src/commitments.rs b/crates/zk-helpers/src/commitments.rs index 66d4dd0233..855a27da48 100644 --- a/crates/zk-helpers/src/commitments.rs +++ b/crates/zk-helpers/src/commitments.rs @@ -521,8 +521,7 @@ pub fn compute_threshold_share_decryption_challenge(payload: Vec) -> BigI mod tests { use super::*; use crate::utils::bigint_to_field; - use e3_polynomial::{CrtContext, CrtPolynomial}; - use std::sync::Arc; + use e3_polynomial::CrtPolynomial; fn field_to_bigint(value: Field) -> BigInt { let bytes = value.into_bigint().to_bytes_le(); @@ -532,12 +531,8 @@ mod tests { #[test] fn compute_ciphertext_commitment_matches_manual_payload() { let bit_ct = 4; - let ct0 = - CrtPolynomial::from_limb_coefficients(vec![vec![BigInt::from(1), BigInt::from(2)]]) - .unwrap(); - let ct1 = - CrtPolynomial::from_limb_coefficients(vec![vec![BigInt::from(3), BigInt::from(4)]]) - .unwrap(); + let ct0 = CrtPolynomial::from_bigint_vectors(vec![vec![BigInt::from(1), BigInt::from(2)]]); + let ct1 = CrtPolynomial::from_bigint_vectors(vec![vec![BigInt::from(3), BigInt::from(4)]]); let mut payload = Vec::new(); payload = flatten(payload, &ct0.limbs, bit_ct); From 82e7be86c5a9613c0c56580b024d0d58773d9e2a Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 30 Jan 2026 19:02:05 +0100 Subject: [PATCH 08/20] chore: add rand feature to num-bigint --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7db22f132e..2c320b6cbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,7 +154,7 @@ greco = { package = "e3-greco-generator", git = "https://github.com/gnosisguild/ hex = "=0.4.3" lazy_static = "=1.5.0" num = "=0.4.3" -num-bigint = "=0.4.6" +num-bigint = { version = "=0.4.6", features = ["rand"] } num-traits = "=0.2.19" ndarray = { version = "=0.15.6", features = ["serde"] } once_cell = "=1.21.3" From bc273f6dee88bcd3f4a5dd1a3618f87ef53dea3d Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 30 Jan 2026 21:16:00 +0100 Subject: [PATCH 09/20] refactor(polynomial): add reduce functions --- Cargo.lock | 8 +- crates/polynomial/src/crt_polynomial.rs | 39 ++++++ crates/polynomial/src/polynomial.rs | 9 +- crates/polynomial/src/utils.rs | 112 ++++++------------ .../zk-inputs/src/ciphertext_addition.rs | 80 +++---------- examples/CRISP/crates/zk-inputs/src/lib.rs | 11 +- 6 files changed, 110 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 186650d78f..5b95ebade4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3862,7 +3862,7 @@ dependencies = [ [[package]] name = "fhe" version = "0.1.0-beta.7" -source = "git+https://github.com/gnosisguild/fhe.rs#3824c52cb457c55551ffcdaeeaef9f3c53145a93" +source = "git+https://github.com/gnosisguild/fhe.rs.git#3824c52cb457c55551ffcdaeeaef9f3c53145a93" dependencies = [ "bincode", "doc-comment", @@ -3888,7 +3888,7 @@ dependencies = [ [[package]] name = "fhe-math" version = "0.1.0-beta.7" -source = "git+https://github.com/gnosisguild/fhe.rs#3824c52cb457c55551ffcdaeeaef9f3c53145a93" +source = "git+https://github.com/gnosisguild/fhe.rs.git#3824c52cb457c55551ffcdaeeaef9f3c53145a93" dependencies = [ "ethnum", "fhe-traits", @@ -3911,7 +3911,7 @@ dependencies = [ [[package]] name = "fhe-traits" version = "0.1.0-beta.7" -source = "git+https://github.com/gnosisguild/fhe.rs#3824c52cb457c55551ffcdaeeaef9f3c53145a93" +source = "git+https://github.com/gnosisguild/fhe.rs.git#3824c52cb457c55551ffcdaeeaef9f3c53145a93" dependencies = [ "rand 0.8.5", ] @@ -3919,7 +3919,7 @@ dependencies = [ [[package]] name = "fhe-util" version = "0.1.0-beta.7" -source = "git+https://github.com/gnosisguild/fhe.rs#3824c52cb457c55551ffcdaeeaef9f3c53145a93" +source = "git+https://github.com/gnosisguild/fhe.rs.git#3824c52cb457c55551ffcdaeeaef9f3c53145a93" dependencies = [ "itertools 0.12.1", "num-bigint-dig", diff --git a/crates/polynomial/src/crt_polynomial.rs b/crates/polynomial/src/crt_polynomial.rs index 4d09dc87ae..8d7328488c 100644 --- a/crates/polynomial/src/crt_polynomial.rs +++ b/crates/polynomial/src/crt_polynomial.rs @@ -117,6 +117,45 @@ impl CrtPolynomial { Ok(()) } + /// Reduces each limb's coefficients modulo the corresponding modulus in-place (range [0, qi)). + /// + /// # Arguments + /// + /// * `moduli` - One modulus per limb; `moduli[i]` is used for `self.limbs[i]`. + /// + /// # Errors + /// + /// Returns [`CrtPolynomialError::ModuliLengthMismatch`] if `moduli.len() != self.limbs.len()`. + pub fn reduce(&mut self, moduli: &[u64]) -> Result<(), CrtPolynomialError> { + if self.limbs.len() != moduli.len() { + return Err(CrtPolynomialError::ModuliLengthMismatch { + limbs_len: self.limbs.len(), + moduli_len: moduli.len(), + }); + } + + for (limb, qi) in self.limbs.iter_mut().zip(moduli.iter()) { + limb.reduce(&BigInt::from(*qi)); + } + + Ok(()) + } + + /// Reduces each limb's coefficients modulo the same modulus in-place. + /// + /// Every limb uses the same `modulus`; coefficients are reduced into the range `[0, modulus)`. + /// Use this when all limbs should be reduced by one common modulus (e.g. a single prime) + /// instead of per-limb moduli as in [`reduce`](Self::reduce). + /// + /// # Arguments + /// + /// * `modulus` - The modulus applied to every limb. + pub fn reduce_uniform(&mut self, modulus: &BigInt) { + for limb in &mut self.limbs { + limb.reduce(&modulus); + } + } + /// Returns a reference to the limb polynomial at the given index. /// /// # Arguments diff --git a/crates/polynomial/src/polynomial.rs b/crates/polynomial/src/polynomial.rs index 830465149c..7e6a9341c9 100644 --- a/crates/polynomial/src/polynomial.rs +++ b/crates/polynomial/src/polynomial.rs @@ -6,7 +6,7 @@ //! Polynomial arithmetic implementation. -use crate::utils::reduce_and_center; +use crate::utils::{reduce, reduce_and_center}; use num_bigint::BigInt; use num_traits::{One, Zero}; use std::fmt; @@ -482,6 +482,13 @@ impl Polynomial { .for_each(|x| *x = reduce_and_center(x, modulus, &half_modulus)); } + /// Reduces coefficients modulo a modulus (in range [0, modulus)). + pub fn reduce(&mut self, modulus: &BigInt) { + self.coefficients + .iter_mut() + .for_each(|x| *x = reduce(x, modulus)); + } + /// Evaluates the polynomial at a given point using Horner's method. /// /// # Arguments diff --git a/crates/polynomial/src/utils.rs b/crates/polynomial/src/utils.rs index 95d685ed8a..5c392d96d7 100644 --- a/crates/polynomial/src/utils.rs +++ b/crates/polynomial/src/utils.rs @@ -27,11 +27,7 @@ use num_traits::Zero; /// /// A `BigInt` representing the reduced and centered number. pub fn reduce_and_center(x: &BigInt, modulus: &BigInt, half_modulus: &BigInt) -> BigInt { - // Calculate the remainder ensuring it's non-negative. - let mut r: BigInt = x % modulus; - if r < BigInt::zero() { - r += modulus; - } + let mut r = reduce(x, modulus); // Adjust the remainder if it is greater than half_modulus. if (modulus % BigInt::from(2)) == BigInt::from(1) { @@ -45,6 +41,24 @@ pub fn reduce_and_center(x: &BigInt, modulus: &BigInt, half_modulus: &BigInt) -> r } +/// Reduces a number modulo a modulus. +/// +/// # Arguments +/// +/// * `x` - The number to reduce +/// * `modulus` - The modulus to reduce by +/// +/// # Returns +/// +/// The reduced number in the range [0, modulus) +pub fn reduce(x: &BigInt, modulus: &BigInt) -> BigInt { + let mut r = x % modulus; + if r < BigInt::zero() { + r += modulus; + } + r +} + /// Reduces and centers polynomial coefficients modulo a prime modulus. /// /// This function iterates over a mutable slice of polynomial coefficients, reducing each coefficient @@ -86,39 +100,6 @@ pub fn reduce_and_center_coefficients(coefficients: &[BigInt], modulus: &BigInt) .collect() } -/// Reduces and centers a scalar value. -/// -/// # Arguments -/// -/// * `x` - The scalar value to reduce and center -/// * `modulus` - The modulus to reduce by -/// -/// # Returns -/// -/// The reduced and centered scalar value -pub fn reduce_and_center_scalar(x: &BigInt, modulus: &BigInt) -> BigInt { - let half_modulus = modulus / 2; - reduce_and_center(x, modulus, &half_modulus) -} - -/// Reduces a scalar value modulo a modulus. -/// -/// # Arguments -/// -/// * `x` - The scalar value to reduce -/// * `modulus` - The modulus to reduce by -/// -/// # Returns -/// -/// The reduced scalar value in the range [0, modulus) -pub fn reduce_scalar(x: &BigInt, modulus: &BigInt) -> BigInt { - let mut r = x % modulus; - if r < BigInt::zero() { - r += modulus; - } - r -} - /// Reduces a polynomial's coefficients within a polynomial ring defined by a cyclotomic polynomial and a modulus. /// /// This function performs two reductions on the polynomial represented by `coefficients`: @@ -166,16 +147,7 @@ pub fn reduce_in_ring( /// /// A `Vec` where each element is reduced modulo `p`. pub fn reduce_coefficients(coefficients: &[BigInt], p: &BigInt) -> Vec { - coefficients - .iter() - .map(|coeff| { - let mut r = coeff % p; - if r < BigInt::zero() { - r += p; - } - r - }) - .collect() + coefficients.iter().map(|coeff| reduce(coeff, p)).collect() } /// Reduces coefficients in a 2D matrix. @@ -227,11 +199,7 @@ pub fn reduce_coefficients_3d( /// * `p` - A reference to a `BigInt` that represents the modulus value. pub fn reduce_coefficients_mut(coefficients: &mut [BigInt], p: &BigInt) { for coeff in coefficients.iter_mut() { - let mut r = &*coeff % p; - if r < BigInt::zero() { - r += p; - } - *coeff = r; + *coeff = reduce(coeff, p); } } @@ -447,35 +415,35 @@ mod tests { } #[test] - fn test_reduce_scalar() { + fn test_reduce() { let x = BigInt::from(-3); let modulus = BigInt::from(7); - let result = reduce_scalar(&x, &modulus); + let result = reduce(&x, &modulus); assert_eq!(result, BigInt::from(4)); } #[test] - fn test_reduce_scalar_less_than_neg_modulus() { + fn test_reduce_less_than_neg_modulus() { let modulus = BigInt::from(7); // Test value < -p (the bug fix case) - assert_eq!(reduce_scalar(&BigInt::from(-10), &modulus), BigInt::from(4)); // -10 % 7 = -3, -3 + 7 = 4 - assert_eq!(reduce_scalar(&BigInt::from(-14), &modulus), BigInt::from(0)); // -14 % 7 = 0 - assert_eq!(reduce_scalar(&BigInt::from(-15), &modulus), BigInt::from(6)); // -15 % 7 = -1, -1 + 7 = 6 - assert_eq!(reduce_scalar(&BigInt::from(-21), &modulus), BigInt::from(0)); // -21 % 7 = 0 + assert_eq!(reduce(&BigInt::from(-10), &modulus), BigInt::from(4)); // -10 % 7 = -3, -3 + 7 = 4 + assert_eq!(reduce(&BigInt::from(-14), &modulus), BigInt::from(0)); // -14 % 7 = 0 + assert_eq!(reduce(&BigInt::from(-15), &modulus), BigInt::from(6)); // -15 % 7 = -1, -1 + 7 = 6 + assert_eq!(reduce(&BigInt::from(-21), &modulus), BigInt::from(0)); // -21 % 7 = 0 // Test exactly -p - assert_eq!(reduce_scalar(&BigInt::from(-7), &modulus), BigInt::from(0)); + assert_eq!(reduce(&BigInt::from(-7), &modulus), BigInt::from(0)); // Test values in [-p, 0) - assert_eq!(reduce_scalar(&BigInt::from(-6), &modulus), BigInt::from(1)); // -6 % 7 = -6, -6 + 7 = 1 - assert_eq!(reduce_scalar(&BigInt::from(-1), &modulus), BigInt::from(6)); // -1 % 7 = -1, -1 + 7 = 6 + assert_eq!(reduce(&BigInt::from(-6), &modulus), BigInt::from(1)); // -6 % 7 = -6, -6 + 7 = 1 + assert_eq!(reduce(&BigInt::from(-1), &modulus), BigInt::from(6)); // -1 % 7 = -1, -1 + 7 = 6 // Test positive values - assert_eq!(reduce_scalar(&BigInt::from(0), &modulus), BigInt::from(0)); - assert_eq!(reduce_scalar(&BigInt::from(3), &modulus), BigInt::from(3)); - assert_eq!(reduce_scalar(&BigInt::from(7), &modulus), BigInt::from(0)); - assert_eq!(reduce_scalar(&BigInt::from(10), &modulus), BigInt::from(3)); + assert_eq!(reduce(&BigInt::from(0), &modulus), BigInt::from(0)); + assert_eq!(reduce(&BigInt::from(3), &modulus), BigInt::from(3)); + assert_eq!(reduce(&BigInt::from(7), &modulus), BigInt::from(0)); + assert_eq!(reduce(&BigInt::from(10), &modulus), BigInt::from(3)); // Verify all results are in [0, modulus) let test_values = vec![ @@ -490,7 +458,7 @@ mod tests { BigInt::from(100), ]; for val in test_values { - let result = reduce_scalar(&val, &modulus); + let result = reduce(&val, &modulus); assert!( result >= BigInt::from(0), "Result {} should be >= 0", @@ -505,14 +473,6 @@ mod tests { } } - #[test] - fn test_reduce_and_center_scalar() { - let x = BigInt::from(6); - let modulus = BigInt::from(7); - let result = reduce_and_center_scalar(&x, &modulus); - assert_eq!(result, BigInt::from(-1)); - } - #[test] fn test_reduce_in_ring() { // Test successful reduction diff --git a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs index 4ef78cd8dc..74409b578a 100644 --- a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs +++ b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use e3_polynomial::{reduce_coefficients, CrtPolynomial, Polynomial}; +use e3_polynomial::{CrtPolynomial, Polynomial}; use e3_zk_helpers::commitments::compute_ciphertext_commitment; use e3_zk_helpers::utils::get_zkp_modulus; use eyre::{Context, Result}; @@ -47,7 +47,6 @@ impl CiphertextAdditionInputs { params: Arc, bit_ct: u32, ) -> Result { - let n = params.degree(); let moduli = params.moduli(); let mut crt_polynomials = [ @@ -68,16 +67,31 @@ impl CiphertextAdditionInputs { c.reduce_and_center(&moduli)?; } - let [prev_ct0, prev_ct1, ct0, ct1, sum_ct0, sum_ct1] = crt_polynomials; + let [mut prev_ct0, mut prev_ct1, mut ct0, mut ct1, mut sum_ct0, mut sum_ct1] = + crt_polynomials; // Compute quotient polynomials: r = (sum_centered - (ct_centered + prev_ct_centered)) / qi. // For ciphertext addition: sum_centered = ct_centered + prev_ct_centered + r * qi. // So: r = (sum_centered - (ct_centered + prev_ct_centered)) / qi. - let r0 = Self::compute_quotient(&sum_ct0, &ct0, &prev_ct0, n, &moduli) + let mut r0 = Self::compute_quotient(&sum_ct0, &ct0, &prev_ct0, &moduli) .with_context(|| "Failed to compute r0 quotient")?; - let r1 = Self::compute_quotient(&sum_ct1, &ct1, &prev_ct1, n, &moduli) + let mut r1 = Self::compute_quotient(&sum_ct1, &ct1, &prev_ct1, &moduli) .with_context(|| "Failed to compute r1 quotient")?; + let zkp_modulus = &get_zkp_modulus(); + + // Reduce all coefficients modulo the ZKP modulus so they lie in the proof system's + // native field. The circuit expects witnesses in [0, zkp_modulus); unreduced values + // would break constraint satisfaction or overflow the field representation. + prev_ct0.reduce_uniform(zkp_modulus); + prev_ct1.reduce_uniform(zkp_modulus); + ct0.reduce_uniform(zkp_modulus); + ct1.reduce_uniform(zkp_modulus); + sum_ct0.reduce_uniform(zkp_modulus); + sum_ct1.reduce_uniform(zkp_modulus); + r0.reduce_uniform(zkp_modulus); + r1.reduce_uniform(zkp_modulus); + let prev_ct_commitment = compute_ciphertext_commitment(&prev_ct0, &prev_ct1, bit_ct); Ok(CiphertextAdditionInputs { @@ -91,23 +105,6 @@ impl CiphertextAdditionInputs { }) } - /// Converts the inputs to standard form by reducing coefficients modulo the ZKP modulus. - /// - /// # Returns - /// A new CiphertextAdditionInputs with coefficients reduced to the ZKP modulus - pub fn standard_form(&self) -> Self { - let zkp_modulus = &get_zkp_modulus(); - CiphertextAdditionInputs { - prev_ct0is: reduce_crt_polynomial(&self.prev_ct0is, zkp_modulus), - prev_ct1is: reduce_crt_polynomial(&self.prev_ct1is, zkp_modulus), - prev_ct_commitment: self.prev_ct_commitment.clone() % zkp_modulus, - sum_ct0is: reduce_crt_polynomial(&self.sum_ct0is, zkp_modulus), - sum_ct1is: reduce_crt_polynomial(&self.sum_ct1is, zkp_modulus), - r0is: reduce_crt_polynomial(&self.r0is, zkp_modulus), - r1is: reduce_crt_polynomial(&self.r1is, zkp_modulus), - } - } - /// Computes the quotient CRT polynomial `(sum - (a + b)) / q_i` per modulus. /// /// For each limb index `i`, divides `sum_i - (a_i + b_i)` by the modulus `q_i`. @@ -130,7 +127,6 @@ impl CiphertextAdditionInputs { sum: &CrtPolynomial, a: &CrtPolynomial, b: &CrtPolynomial, - _n: usize, moduli: &[u64], ) -> Result { let num_moduli = moduli.len(); @@ -166,21 +162,6 @@ impl CiphertextAdditionInputs { } } -/// Reduces all coefficients of a CRT polynomial modulo the given modulus. -fn reduce_crt_polynomial(crt_poly: &CrtPolynomial, modulus: &BigInt) -> CrtPolynomial { - let reduced_limbs: Vec = crt_poly - .limbs - .iter() - .map(|limb| { - let reduced_coeffs = reduce_coefficients(limb.coefficients(), modulus); - Polynomial::new(reduced_coeffs) - }) - .collect(); - - // Safe to unwrap because we're preserving the structure - CrtPolynomial::new(reduced_limbs) -} - #[cfg(test)] mod tests { use super::*; @@ -245,27 +226,4 @@ mod tests { assert_eq!(inputs.r0is.limbs.len(), num_moduli); assert_eq!(inputs.r1is.limbs.len(), num_moduli); } - - #[test] - fn test_standard_form_conversion() { - let (bfv_params, pk, _sk) = create_test_generator(); - let mut rng = thread_rng(); - - let pt = create_test_plaintext(&bfv_params, 1); - let (ct1, _u1, _e0_1, _e1_1) = pk.try_encrypt_extended(&pt, &mut rng).unwrap(); - let (ct2, _u2, _e0_2, _e1_2) = pk.try_encrypt_extended(&pt, &mut rng).unwrap(); - let sum_ct = &ct1 + &ct2; - - let bit_ct = test_bit_ct(&bfv_params); - let inputs = - CiphertextAdditionInputs::compute(&ct1, &ct2, &sum_ct, bfv_params.clone(), bit_ct) - .unwrap(); - let standard_form = inputs.standard_form(); - - // Verify structure is preserved. - assert_eq!( - standard_form.prev_ct0is.limbs.len(), - inputs.prev_ct0is.limbs.len() - ); - } } diff --git a/examples/CRISP/crates/zk-inputs/src/lib.rs b/examples/CRISP/crates/zk-inputs/src/lib.rs index 9439abde86..19d1166ab3 100644 --- a/examples/CRISP/crates/zk-inputs/src/lib.rs +++ b/examples/CRISP/crates/zk-inputs/src/lib.rs @@ -16,7 +16,6 @@ use e3_zk_helpers::commitments::compute_ciphertext_commitment; use e3_zk_helpers::utils::calculate_bit_width; use eyre::{Context, Result}; use fhe::bfv::BfvParameters; -use std::sync::Arc; use fhe::bfv::Ciphertext; use fhe::bfv::PublicKey; use fhe::bfv::SecretKey; @@ -27,6 +26,7 @@ use greco::vectors::GrecoVectors; use num_bigint::BigInt; use num_traits::Zero; use rand::thread_rng; +use std::sync::Arc; mod ciphertext_addition; use crate::ciphertext_addition::CiphertextAdditionInputs; mod serialization; @@ -139,7 +139,7 @@ impl ZKInputsGenerator { &crypto_params, &bounds, &greco_vectors.standard_form(), - &ciphertext_addition_inputs.standard_form(), + &ciphertext_addition_inputs, ); let inputs_json = serialize_inputs_to_json(&inputs)?; @@ -206,7 +206,7 @@ impl ZKInputsGenerator { let sum_ct = &ct + &prev_ct; // Compute the inputs of the ciphertext addition. - let mut ciphertext_addition_inputs = CiphertextAdditionInputs::compute( + let ciphertext_addition_inputs = CiphertextAdditionInputs::compute( &prev_ct, &ct, &sum_ct, @@ -215,15 +215,12 @@ impl ZKInputsGenerator { ) .with_context(|| "Failed to compute ciphertext addition inputs")?; - // For first votes, set prev_ct_commitment to 0 since there's no previous ciphertext - ciphertext_addition_inputs.prev_ct_commitment = BigInt::zero(); - // Construct Inputs Section. let inputs = construct_inputs( &crypto_params, &bounds, &greco_vectors.standard_form(), - &ciphertext_addition_inputs.standard_form(), + &ciphertext_addition_inputs, ); let inputs_json = serialize_inputs_to_json(&inputs)?; From 38074b4ffd1cae492c91e9b11cc8b54287cac3a2 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 11:37:02 +0100 Subject: [PATCH 10/20] refactor: remove unused function --- crates/polynomial/src/crt_polynomial.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/crates/polynomial/src/crt_polynomial.rs b/crates/polynomial/src/crt_polynomial.rs index 8d7328488c..a637b6d9a5 100644 --- a/crates/polynomial/src/crt_polynomial.rs +++ b/crates/polynomial/src/crt_polynomial.rs @@ -60,10 +60,6 @@ impl CrtPolynomial { /// # Arguments /// /// * `p` - An fhe-math polynomial (PowerBasis or Ntt). - /// - /// # Coefficient order - /// - pub fn from_fhe_polynomial(p: &Poly) -> Self { let mut p = p.clone(); @@ -173,15 +169,4 @@ impl CrtPolynomial { pub fn limb(&self, i: usize) -> &Polynomial { &self.limbs[i] } - - /// Returns limb coefficient vectors (one `Vec` per modulus). - /// - /// Use when you need a raw CRT representation for serialization, hashing, - /// or APIs that expect `&[Vec]`. The inverse of [`from_limb_coefficients`](Self::from_limb_coefficients). - pub fn to_limb_coefficients(&self) -> Vec> { - self.limbs - .iter() - .map(|l| l.coefficients().to_vec()) - .collect() - } } From 2d542412cfd21be62f809c2f833f3f4957a4f4b6 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 11:39:24 +0100 Subject: [PATCH 11/20] fix(polynomial): enable num-bigint rand feature for fhe-math - fhe-math uses RandBigInt, gated behind num-bigint "rand" feature - add "rand" to polynomial num-bigint features so dependency builds Co-authored-by: Cursor --- crates/polynomial/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/polynomial/Cargo.toml b/crates/polynomial/Cargo.toml index 60a5be6995..68f2808416 100644 --- a/crates/polynomial/Cargo.toml +++ b/crates/polynomial/Cargo.toml @@ -7,7 +7,7 @@ description = "A polynomial library with big integer coefficients for cryptograp repository = "https://github.com/gnosisguild/enclave/crates/polynomial" [dependencies] -num-bigint = { workspace = true, features = ["serde"] } +num-bigint = { workspace = true, features = ["rand", "serde"] } num-traits = { workspace = true } serde = { workspace = true, optional = true, features = ["rc"] } bincode = { workspace = true, optional = true } From 71b00a2b9765bf7c90c774c6fc1ac36470e71290 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 11:39:33 +0100 Subject: [PATCH 12/20] chore: remove unused rand feature from global num-bigint --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2c320b6cbe..5ffa80e8d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,7 +154,7 @@ greco = { package = "e3-greco-generator", git = "https://github.com/gnosisguild/ hex = "=0.4.3" lazy_static = "=1.5.0" num = "=0.4.3" -num-bigint = { version = "=0.4.6", features = ["rand"] } +num-bigint = { version = "=0.4.6" } num-traits = "=0.2.19" ndarray = { version = "=0.15.6", features = ["serde"] } once_cell = "=1.21.3" From 7521dbeae377912901722ba28bc606ccaff74faf Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 12:05:56 +0100 Subject: [PATCH 13/20] docs(zk-inputs): clarify first-in-slot prev_ct_commitment comment - Reword comment for first-in-slot votes and prev_ct_commitment - Add IMPORTANT prefix so the requirement is not missed Co-authored-by: Cursor --- examples/CRISP/crates/zk-inputs/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/CRISP/crates/zk-inputs/src/lib.rs b/examples/CRISP/crates/zk-inputs/src/lib.rs index 19d1166ab3..314dc45c64 100644 --- a/examples/CRISP/crates/zk-inputs/src/lib.rs +++ b/examples/CRISP/crates/zk-inputs/src/lib.rs @@ -206,7 +206,7 @@ impl ZKInputsGenerator { let sum_ct = &ct + &prev_ct; // Compute the inputs of the ciphertext addition. - let ciphertext_addition_inputs = CiphertextAdditionInputs::compute( + let mut ciphertext_addition_inputs = CiphertextAdditionInputs::compute( &prev_ct, &ct, &sum_ct, @@ -215,6 +215,10 @@ impl ZKInputsGenerator { ) .with_context(|| "Failed to compute ciphertext addition inputs")?; + // IMPORTANT: First-in-slot votes have no previous ciphertext; set prev_ct_commitment to 0 + // so the on-chain verifier accepts the proof. + ciphertext_addition_inputs.prev_ct_commitment = BigInt::zero(); + // Construct Inputs Section. let inputs = construct_inputs( &crypto_params, From 12414916c1941acf71a9a580838e6795f5fe391b Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 15:01:50 +0100 Subject: [PATCH 14/20] fix(zk-inputs): validate exact division remainder in ciphertext addition - Capture remainder from diff.div(&qi) instead of discarding it - Verify remainder is zero (all-zero polynomial) after division - Return eyre error including modulus index i when remainder is non-zero Co-authored-by: Cursor --- .../CRISP/crates/zk-inputs/src/ciphertext_addition.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs index 74409b578a..a16f6f5bb5 100644 --- a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs +++ b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs @@ -140,10 +140,17 @@ impl CiphertextAdditionInputs { let qi = Polynomial::constant(BigInt::from(moduli[i])); let diff = sum_limb.sub(&a_limb.add(b_limb)); - let (q_poly, _remainder) = diff + let (q_poly, remainder) = diff .div(&qi) .map_err(|e| eyre::eyre!("division by modulus q_i at index {}: {}", i, e))?; + if !remainder.is_zero() { + return Err(eyre::eyre!( + "Division by q_i at modulus index {} was not exact; non-zero remainder", + i + )); + } + for (j, q) in q_poly.coefficients().iter().enumerate() { if *q < (-1).into() || *q > 1.into() { return Err(eyre::eyre!( From a9686ac691110a86c853550a6c22cf33ebe673cf Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 15:10:16 +0100 Subject: [PATCH 15/20] refactor(polynomial): remove from_ascending_coefficients and to_ascending_coefficients - Drop from_ascending_coefficients and to_ascending_coefficients; use new() + reverse() instead - Point new() doc to reverse() for ordering - Remove tests and benchmark that relied on removed APIs - Update README to mention reverse() for ordering Co-authored-by: Cursor --- crates/polynomial/README.md | 5 +- crates/polynomial/benches/polynomial.rs | 29 +----- crates/polynomial/src/polynomial.rs | 127 ++---------------------- 3 files changed, 10 insertions(+), 151 deletions(-) diff --git a/crates/polynomial/README.md b/crates/polynomial/README.md index 6039328360..cc282b3318 100644 --- a/crates/polynomial/README.md +++ b/crates/polynomial/README.md @@ -28,9 +28,8 @@ Polynomials are represented as: a_n * x^n + a_{n-1} * x^{n-1} + ... + a_1 * x + a_0 ``` -Where coefficients are stored in descending order (highest degree first) of degree using `BigInt` -for arbitrary precision. You can rely on some methods to transform to ascending order (lowest degree -first) and viceversa. +Where coefficients are stored in descending order (highest degree first) using `BigInt` for +arbitrary precision. Use `reverse()` to convert in-place between descending and ascending order. ### Performance diff --git a/crates/polynomial/benches/polynomial.rs b/crates/polynomial/benches/polynomial.rs index 4249cf1f06..1934195959 100644 --- a/crates/polynomial/benches/polynomial.rs +++ b/crates/polynomial/benches/polynomial.rs @@ -150,32 +150,6 @@ fn benchmark_utility_functions(c: &mut Criterion) { group.finish(); } -fn benchmark_coefficient_conversion(c: &mut Criterion) { - let mut group = c.benchmark_group("coefficient_conversion"); - - for degree in [10, 50, 100, 500, 1000] { - let (poly, _) = create_test_polynomials(degree); - - // Benchmark conversion from ascending to descending order - let ascending_coeffs: Vec = (0..=degree).map(|i| BigInt::from(i)).collect(); - - group.bench_function(&format!("from_ascending_degree_{}", degree), |b| { - b.iter(|| { - black_box(Polynomial::from_ascending_coefficients( - ascending_coeffs.clone(), - )) - }) - }); - - // Benchmark conversion from descending to ascending order - group.bench_function(&format!("to_ascending_degree_{}", degree), |b| { - b.iter(|| black_box(poly.to_ascending_coefficients())) - }); - } - - group.finish(); -} - criterion_group!( benches, benchmark_polynomial_addition, @@ -184,7 +158,6 @@ criterion_group!( benchmark_polynomial_evaluation, benchmark_modular_reduction, benchmark_cyclotomic_reduction, - benchmark_utility_functions, - benchmark_coefficient_conversion + benchmark_utility_functions ); criterion_main!(benches); diff --git a/crates/polynomial/src/polynomial.rs b/crates/polynomial/src/polynomial.rs index 7e6a9341c9..67d7eefe6a 100644 --- a/crates/polynomial/src/polynomial.rs +++ b/crates/polynomial/src/polynomial.rs @@ -51,16 +51,12 @@ pub enum PolynomialError { ParseError(#[from] num_bigint::ParseBigIntError), } -/// A polynomial represented by its coefficients in descending order of degree. -/// -/// The coefficients are stored as `BigInt` to support arbitrary precision arithmetic -/// required for cryptographic operations. The polynomial is represented as: -/// `a_n * x^n + a_{n-1} * x^{n-1} + ... + a_1 * x + a_0` +/// A polynomial with coefficients stored as `BigInt` for arbitrary precision arithmetic. + /// #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Polynomial { - /// Coefficients in descending order (highest degree first). pub(crate) coefficients: Vec, } @@ -122,7 +118,7 @@ impl Polynomial { /// /// # Arguments /// - /// * `coefficients` - Vector of coefficients in descending order of degree. + /// * `coefficients` - Vector of coefficients (see [`reverse`](Self::reverse) for ordering). pub fn new(coefficients: Vec) -> Self { Self { coefficients } } @@ -134,38 +130,11 @@ impl Polynomial { Self { coefficients } } - /// Creates a polynomial from coefficients in ascending order format. + /// Reverses coefficient order in-place. /// - /// This method converts from ascending order coefficient ordering (lowest degree first) - /// to this library's ordering (highest degree first). - /// - /// # Arguments - /// - /// * `ascending_coefficients` - Vector of coefficients in ascending order. - pub fn from_ascending_coefficients(ascending_coefficients: Vec) -> Self { - let mut coefficients = ascending_coefficients; - coefficients.reverse(); - Self { coefficients } - } - - /// Converts the polynomial to ascending order coefficient format. - /// - /// This method converts from this library's ordering (highest degree first) - /// to ascending order (lowest degree first). - /// - /// # Returns - /// - /// Vector of coefficients in ascending order. - pub fn to_ascending_coefficients(&self) -> Vec { - let mut coefficients = self.coefficients.clone(); - coefficients.reverse(); - coefficients - } - - /// Reverses the coefficient order in-place. - /// - /// Converts between descending order (highest degree first) and ascending order - /// (lowest degree first). Calling `reverse()` twice restores the original order. + /// **Coefficient ordering:** Converts between **descending** (highest degree first, + /// internal storage) and **ascending** (lowest degree first). Calling `reverse()` + /// twice restores the original order. pub fn reverse(&mut self) { self.coefficients.reverse() } @@ -660,73 +629,6 @@ mod tests { assert_eq!(trimmed.coefficients(), &[BigInt::from(0)]); } - #[test] - fn test_ascending_coefficients_conversion() { - // Test conversion from ascending format to Rust format - let ascending_coeffs = vec![BigInt::from(2), BigInt::from(3), BigInt::from(1)]; // 2 + 3x + x^2 - let poly = Polynomial::from_ascending_coefficients(ascending_coeffs); - assert_eq!( - poly.coefficients(), - &[BigInt::from(1), BigInt::from(3), BigInt::from(2)] - ); // x^2 + 3x + 2 - - // Test conversion back to ascending format - let back_to_ascending = poly.to_ascending_coefficients(); - assert_eq!( - back_to_ascending, - vec![BigInt::from(2), BigInt::from(3), BigInt::from(1)] - ); - } - - #[test] - fn test_ascending_coefficients_conversion_edge_cases() { - // Test empty polynomial - let empty_ascending = vec![]; - let poly_empty = Polynomial::from_ascending_coefficients(empty_ascending); - assert_eq!(poly_empty.coefficients(), &[]); - assert_eq!(poly_empty.to_ascending_coefficients(), vec![]); - - // Test single coefficient - let single_ascending = vec![BigInt::from(5)]; - let poly_single = Polynomial::from_ascending_coefficients(single_ascending); - assert_eq!(poly_single.coefficients(), &[BigInt::from(5)]); - assert_eq!( - poly_single.to_ascending_coefficients(), - vec![BigInt::from(5)] - ); - - // Test two coefficients - let two_ascending = vec![BigInt::from(1), BigInt::from(2)]; // 1 + 2x - let poly_two = Polynomial::from_ascending_coefficients(two_ascending); - assert_eq!(poly_two.coefficients(), &[BigInt::from(2), BigInt::from(1)]); // 2x + 1 - assert_eq!( - poly_two.to_ascending_coefficients(), - vec![BigInt::from(1), BigInt::from(2)] - ); - } - - #[test] - fn test_ascending_coefficients_compatibility_example() { - // This test demonstrates the exact scenario mentioned in the issue - // Ascending: [2, 3, 1] represents 2 + 3x + x^2 - let ascending_coefficients = vec![BigInt::from(2), BigInt::from(3), BigInt::from(1)]; - let poly = Polynomial::from_ascending_coefficients(ascending_coefficients); - - // Rust: [1, 3, 2] represents x^2 + 3x + 2 - assert_eq!( - poly.coefficients(), - &[BigInt::from(1), BigInt::from(3), BigInt::from(2)] - ); - assert_eq!(poly.to_string(), "x^2 + 3x + 2"); - - // Convert back to ascending format - let back_to_ascending = poly.to_ascending_coefficients(); - assert_eq!( - back_to_ascending, - vec![BigInt::from(2), BigInt::from(3), BigInt::from(1)] - ); - } - #[cfg(feature = "serde")] mod serialization_tests { use super::*; @@ -817,21 +719,6 @@ mod tests { let reconstructed_product = reconstructed1.mul(&reconstructed2); assert_eq!(original_product, reconstructed_product); } - - #[test] - fn test_polynomial_bincode_serialization_ascending_conversion() { - // Test that ascending coefficient conversion works after serialization - let ascending_coeffs = vec![BigInt::from(2), BigInt::from(3), BigInt::from(1)]; - let poly = Polynomial::from_ascending_coefficients(ascending_coeffs.clone()); - - let bytes = bincode::serialize(&poly).expect("Failed to serialize"); - let reconstructed: Polynomial = - bincode::deserialize(&bytes).expect("Failed to deserialize"); - - // Test ascending conversion still works - let back_to_ascending = reconstructed.to_ascending_coefficients(); - assert_eq!(back_to_ascending, ascending_coeffs); - } } #[test] From ab22b317d306971a2237fba51f9bf709a803dbc7 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 15:50:27 +0100 Subject: [PATCH 16/20] refactor: remove redundant reduce and center functions --- crates/polynomial/benches/polynomial.rs | 3 +- crates/polynomial/src/crt_polynomial.rs | 9 +-- crates/polynomial/src/polynomial.rs | 18 ++--- crates/polynomial/src/utils.rs | 78 ++++++++----------- .../pvss/src/circuits/pk_bfv/computation.rs | 6 +- .../zk-inputs/src/ciphertext_addition.rs | 8 +- 6 files changed, 55 insertions(+), 67 deletions(-) diff --git a/crates/polynomial/benches/polynomial.rs b/crates/polynomial/benches/polynomial.rs index 1934195959..2e3beb019b 100644 --- a/crates/polynomial/benches/polynomial.rs +++ b/crates/polynomial/benches/polynomial.rs @@ -88,7 +88,8 @@ fn benchmark_modular_reduction(c: &mut Criterion) { group.bench_function(&format!("degree_{}", degree), |b| { b.iter(|| { let mut p = poly1.clone(); - p.reduce_and_center(&modulus); + p.reduce(&modulus); + p.center(&modulus); black_box(p) }) }); diff --git a/crates/polynomial/src/crt_polynomial.rs b/crates/polynomial/src/crt_polynomial.rs index a637b6d9a5..ddf244b31b 100644 --- a/crates/polynomial/src/crt_polynomial.rs +++ b/crates/polynomial/src/crt_polynomial.rs @@ -86,10 +86,7 @@ impl CrtPolynomial { } } - /// Reduces and centers each limb's coefficients modulo the corresponding modulus in-place. - /// - /// Each limb `self.limbs[i]` is reduced modulo `moduli[i]`, with coefficients centered - /// in the symmetric range `(-q/2, q/2]`. + /// Centers each limb's coefficients (already in [0, q_i)) into (-q_i/2, q_i/2] in-place. /// /// # Arguments /// @@ -98,7 +95,7 @@ impl CrtPolynomial { /// # Errors /// /// Returns [`CrtPolynomialError::ModuliLengthMismatch`] if `moduli.len() != self.limbs.len()`. - pub fn reduce_and_center(&mut self, moduli: &[u64]) -> Result<(), CrtPolynomialError> { + pub fn center(&mut self, moduli: &[u64]) -> Result<(), CrtPolynomialError> { if self.limbs.len() != moduli.len() { return Err(CrtPolynomialError::ModuliLengthMismatch { limbs_len: self.limbs.len(), @@ -107,7 +104,7 @@ impl CrtPolynomial { } for (limb, qi) in self.limbs.iter_mut().zip(moduli.iter()) { - limb.reduce_and_center(&BigInt::from(*qi)); + limb.center(&BigInt::from(*qi)); } Ok(()) diff --git a/crates/polynomial/src/polynomial.rs b/crates/polynomial/src/polynomial.rs index 67d7eefe6a..a497631e90 100644 --- a/crates/polynomial/src/polynomial.rs +++ b/crates/polynomial/src/polynomial.rs @@ -6,7 +6,7 @@ //! Polynomial arithmetic implementation. -use crate::utils::{reduce, reduce_and_center}; +use crate::utils::{center, reduce}; use num_bigint::BigInt; use num_traits::{One, Zero}; use std::fmt; @@ -376,7 +376,7 @@ impl Polynomial { /// /// # Returns /// - /// A new polynomial with each coefficient multiplied by the scalar. + /// Mutates the polynomial in place. pub fn scalar_mul(&self, scalar: &BigInt) -> Self { Polynomial::new(self.coefficients.iter().map(|x| x * scalar).collect()) } @@ -396,7 +396,7 @@ impl Polynomial { /// /// # Returns /// - /// A new polynomial of degree `n-1` representing the remainder after reduction. + /// Mutates the polynomial in place. /// /// # Errors /// @@ -434,21 +434,19 @@ impl Polynomial { Ok(Polynomial::new(out)) } - /// Reduces coefficients modulo a prime and centers them. + /// Centers coefficients already in [0, modulus) into (-modulus/2, modulus/2]. /// /// # Arguments /// - /// * `modulus` - The prime modulus. + /// * `modulus` - The modulus. /// /// # Returns /// - /// A new polynomial with coefficients reduced and centered. - pub fn reduce_and_center(&mut self, modulus: &BigInt) { - let half_modulus = modulus / 2; - + /// Mutates the polynomial in place. + pub fn center(&mut self, modulus: &BigInt) { self.coefficients .iter_mut() - .for_each(|x| *x = reduce_and_center(x, modulus, &half_modulus)); + .for_each(|x| *x = center(x, modulus)); } /// Reduces coefficients modulo a modulus (in range [0, modulus)). diff --git a/crates/polynomial/src/utils.rs b/crates/polynomial/src/utils.rs index 5c392d96d7..9b611c94c6 100644 --- a/crates/polynomial/src/utils.rs +++ b/crates/polynomial/src/utils.rs @@ -11,6 +11,31 @@ use crate::Polynomial; use num_bigint::BigInt; use num_traits::Zero; +/// Centers a value already in [0, modulus) into the symmetric range (-modulus/2, modulus/2]. +/// +/// Caller must ensure `x` is in [0, modulus). For odd modulus the range is (-(q-1)/2, (q-1)/2]; +/// for even modulus, values ≥ q/2 become negative. +/// +/// # Arguments +/// +/// * `x` - Value in [0, modulus). +/// * `modulus` - The modulus. +pub fn center(x: &BigInt, modulus: &BigInt) -> BigInt { + let half_modulus = modulus / 2; + + let mut r = x.clone(); + + if (modulus % BigInt::from(2)) == BigInt::from(1) { + if r > half_modulus { + r -= modulus; + } + } else if r >= half_modulus { + r -= modulus; + } + + r +} + /// Reduces a number modulo a prime modulus and centers it. /// /// This function takes an arbitrary number and reduces it modulo the specified prime modulus. @@ -59,45 +84,16 @@ pub fn reduce(x: &BigInt, modulus: &BigInt) -> BigInt { r } -/// Reduces and centers polynomial coefficients modulo a prime modulus. -/// -/// This function iterates over a mutable slice of polynomial coefficients, reducing each coefficient -/// modulo a given prime modulus and adjusting the result to be within the symmetric range -/// [−(modulus−1)/2, (modulus−1)/2]. +/// Centers polynomial coefficients that are already in [0, modulus) into (-modulus/2, modulus/2]. /// /// # Arguments /// -/// * `coefficients` - A mutable slice of `BigInt` coefficients to be reduced and centered -/// * `modulus` - A prime modulus `BigInt` used for reduction and centering -/// -/// # Panics -/// -/// Panics if `modulus` is zero due to division by zero -pub fn reduce_and_center_coefficients_mut(coefficients: &mut [BigInt], modulus: &BigInt) { - let half_modulus = modulus / 2; +/// * `coefficients` - Coefficients in [0, modulus); mutated in place. +/// * `modulus` - The modulus. +pub fn center_coefficients_mut(coefficients: &mut [BigInt], modulus: &BigInt) { coefficients .iter_mut() - .for_each(|x| *x = reduce_and_center(x, modulus, &half_modulus)); -} - -/// Reduces and centers polynomial coefficients modulo a prime modulus. -/// -/// This function creates a new vector with coefficients reduced and centered modulo the given modulus. -/// -/// # Arguments -/// -/// * `coefficients` - A slice of `BigInt` coefficients to be reduced and centered -/// * `modulus` - A prime modulus `BigInt` used for reduction and centering -/// -/// # Returns -/// -/// A new `Vec` with reduced and centered coefficients -pub fn reduce_and_center_coefficients(coefficients: &[BigInt], modulus: &BigInt) -> Vec { - let half_modulus = modulus / 2; - coefficients - .iter() - .map(|x| reduce_and_center(x, modulus, &half_modulus)) - .collect() + .for_each(|x| *x = center(x, modulus)); } /// Reduces a polynomial's coefficients within a polynomial ring defined by a cyclotomic polynomial and a modulus. @@ -128,7 +124,8 @@ pub fn reduce_in_ring( let poly = Polynomial::new(coeffs); let reduced = poly.reduce_by_cyclotomic(cyclo)?; *coefficients = reduced.coefficients; - reduce_and_center_coefficients_mut(coefficients, modulus); + reduce_coefficients_mut(coefficients, modulus); + center_coefficients_mut(coefficients, modulus); Ok(()) } @@ -403,17 +400,6 @@ mod tests { assert!(range_check_standard(&vec, &bound, &modulus)); } - #[test] - fn test_reduce_and_center_coefficients() { - let coeffs = vec![BigInt::from(10), BigInt::from(15), BigInt::from(20)]; - let modulus = BigInt::from(7); - let result = reduce_and_center_coefficients(&coeffs, &modulus); - assert_eq!( - result, - vec![BigInt::from(3), BigInt::from(1), BigInt::from(-1)] - ); - } - #[test] fn test_reduce() { let x = BigInt::from(-3); diff --git a/crates/pvss/src/circuits/pk_bfv/computation.rs b/crates/pvss/src/circuits/pk_bfv/computation.rs index 0df2c0e890..93870e5fbd 100644 --- a/crates/pvss/src/circuits/pk_bfv/computation.rs +++ b/crates/pvss/src/circuits/pk_bfv/computation.rs @@ -7,8 +7,8 @@ use crate::traits::Computation; use crate::traits::ConvertToJson; use crate::traits::ReduceToZkpModulus; +use e3_polynomial::center_coefficients_mut; use e3_polynomial::reduce_coefficients_2d; -use e3_polynomial::utils::reduce_and_center_coefficients_mut; use e3_zk_helpers::utils::calculate_bit_width; use e3_zk_helpers::utils::get_zkp_modulus; use fhe::bfv::BfvParameters; @@ -126,8 +126,8 @@ impl Computation for Witness { let mut pk1i: Vec = pk1_coeffs.iter().rev().map(|&x| BigInt::from(x)).collect(); - reduce_and_center_coefficients_mut(&mut pk0i, &BigInt::from(**qi)); - reduce_and_center_coefficients_mut(&mut pk1i, &BigInt::from(**qi)); + center_coefficients_mut(&mut pk0i, &BigInt::from(**qi)); + center_coefficients_mut(&mut pk1i, &BigInt::from(**qi)); (pk0i, pk1i) }) diff --git a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs index a16f6f5bb5..347a9f572c 100644 --- a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs +++ b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs @@ -62,9 +62,15 @@ impl CiphertextAdditionInputs { // that each limb is stored in **descending** order (a_n, …, a_0) so circuit evaluation can use Horner's // method in one forward pass: `result = result * x + coefficients[i]` from i = 0, // i.e. P(x) = ((…((a_n·x + a_{n-1})·x + …)·x + a_0), with no extra reversing or reindexing. + // + // We center so the quotient r = (sum − (prev + ct)) / q_i lies in {-1, 0, 1}. + // BFV/fhe-math already gives coefficients in [0, q_i), so reduce is redundant. We need centering + // into (-q/2, q/2]: then the difference per coefficient is small in absolute value, and for valid + // ciphertext addition that difference is a multiple of q_i, so the quotient is in {-1, 0, 1}, + // which the circuit and compute_quotient expect. for c in &mut crt_polynomials { c.reverse(); - c.reduce_and_center(&moduli)?; + c.center(&moduli)?; } let [mut prev_ct0, mut prev_ct1, mut ct0, mut ct1, mut sum_ct0, mut sum_ct1] = From b88ffaa659436dd834cec6468ca04b78c580ac64 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 16:06:12 +0100 Subject: [PATCH 17/20] chore: remove .git from fhe-math dependency --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b95ebade4..186650d78f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3862,7 +3862,7 @@ dependencies = [ [[package]] name = "fhe" version = "0.1.0-beta.7" -source = "git+https://github.com/gnosisguild/fhe.rs.git#3824c52cb457c55551ffcdaeeaef9f3c53145a93" +source = "git+https://github.com/gnosisguild/fhe.rs#3824c52cb457c55551ffcdaeeaef9f3c53145a93" dependencies = [ "bincode", "doc-comment", @@ -3888,7 +3888,7 @@ dependencies = [ [[package]] name = "fhe-math" version = "0.1.0-beta.7" -source = "git+https://github.com/gnosisguild/fhe.rs.git#3824c52cb457c55551ffcdaeeaef9f3c53145a93" +source = "git+https://github.com/gnosisguild/fhe.rs#3824c52cb457c55551ffcdaeeaef9f3c53145a93" dependencies = [ "ethnum", "fhe-traits", @@ -3911,7 +3911,7 @@ dependencies = [ [[package]] name = "fhe-traits" version = "0.1.0-beta.7" -source = "git+https://github.com/gnosisguild/fhe.rs.git#3824c52cb457c55551ffcdaeeaef9f3c53145a93" +source = "git+https://github.com/gnosisguild/fhe.rs#3824c52cb457c55551ffcdaeeaef9f3c53145a93" dependencies = [ "rand 0.8.5", ] @@ -3919,7 +3919,7 @@ dependencies = [ [[package]] name = "fhe-util" version = "0.1.0-beta.7" -source = "git+https://github.com/gnosisguild/fhe.rs.git#3824c52cb457c55551ffcdaeeaef9f3c53145a93" +source = "git+https://github.com/gnosisguild/fhe.rs#3824c52cb457c55551ffcdaeeaef9f3c53145a93" dependencies = [ "itertools 0.12.1", "num-bigint-dig", diff --git a/Cargo.toml b/Cargo.toml index 5ffa80e8d1..b2c13fd745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,7 +143,7 @@ duct = "=1.0.0" eyre = { version = "=0.6.12" } fhe = { git = "https://github.com/gnosisguild/fhe.rs" } fhe-traits = { git = "https://github.com/gnosisguild/fhe.rs" } -fhe-math = { git = "https://github.com/gnosisguild/fhe.rs.git" } +fhe-math = { git = "https://github.com/gnosisguild/fhe.rs" } fhe-util = { git = "https://github.com/gnosisguild/fhe.rs" } figment = { version = "=0.10.19", features = ["yaml", "test"] } futures = "=0.3.31" From 0dac129014f3451d78ffab1450578a1326d98530 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 16:15:06 +0100 Subject: [PATCH 18/20] chore: update scripts to lint circuits --- circuits/bin/threshold/Nargo.toml | 3 ++- scripts/lint-circuits.sh | 13 +------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/circuits/bin/threshold/Nargo.toml b/circuits/bin/threshold/Nargo.toml index 852e24d056..a42e26748e 100644 --- a/circuits/bin/threshold/Nargo.toml +++ b/circuits/bin/threshold/Nargo.toml @@ -4,5 +4,6 @@ members = [ "pk_aggregation", "user_data_encryption", "share_decryption", - "decrypted_shares_aggregation" + "decrypted_shares_aggregation_bn", + "decrypted_shares_aggregation_mod" ] \ No newline at end of file diff --git a/scripts/lint-circuits.sh b/scripts/lint-circuits.sh index 80c72e7840..61f7ee455a 100755 --- a/scripts/lint-circuits.sh +++ b/scripts/lint-circuits.sh @@ -11,7 +11,7 @@ fi cd circuits # Directories to check -DIRS=("lib" "bin/aggregation/fold" "bin/aggregation/insecure" "bin/aggregation/production" "bin/insecure" "bin/production") +DIRS=("lib" "bin/recursive_aggregation/fold" "bin/recursive_aggregation/wrapper/dkg" "bin/recursive_aggregation/wrapper/threshold" "bin/dkg" "bin/threshold") for dir in "${DIRS[@]}"; do if [ ! -d "$dir" ]; then @@ -22,17 +22,6 @@ for dir in "${DIRS[@]}"; do echo "Checking $dir..." cd "$dir" - # Find all package directories and create empty Prover.toml to prevent nargo from creating them - created_files=() - while IFS= read -r -d '' nargo_file; do - pkg_dir=$(dirname "$nargo_file") - prover_file="${pkg_dir}/Prover.toml" - if [ ! -f "$prover_file" ]; then - touch "$prover_file" - created_files+=("$prover_file") - fi - done < <(find . -name "Nargo.toml" -type f -print0 2>/dev/null || true) - # Checking circuit format. if ! (nargo fmt --check); then echo "Error: Circuit format check failed in $dir" From 36bf62c87154f5160be1451dc2adbfe1f3ae7b46 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 16:39:04 +0100 Subject: [PATCH 19/20] docs: remove polynomial representation from README --- crates/polynomial/README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/crates/polynomial/README.md b/crates/polynomial/README.md index cc282b3318..efc90cb0e4 100644 --- a/crates/polynomial/README.md +++ b/crates/polynomial/README.md @@ -20,17 +20,6 @@ reduction operations commonly used in: - **Homomorphic encryption**: BFV, BGV, and CKKS schemes - **Zero-knowledge proofs**: Polynomial commitment schemes -### Polynomial Representation - -Polynomials are represented as: - -``` -a_n * x^n + a_{n-1} * x^{n-1} + ... + a_1 * x + a_0 -``` - -Where coefficients are stored in descending order (highest degree first) using `BigInt` for -arbitrary precision. Use `reverse()` to convert in-place between descending and ascending order. - ### Performance The library is optimized for cryptographic workloads with: From 522eb82e5868ba0877b7fa92eda2027ebe041af1 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Feb 2026 16:40:20 +0100 Subject: [PATCH 20/20] fix: update param set name in ciphertext addition --- examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs index 347a9f572c..a757b2048c 100644 --- a/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs +++ b/examples/CRISP/crates/zk-inputs/src/ciphertext_addition.rs @@ -191,7 +191,7 @@ mod tests { } fn create_test_generator() -> (Arc, PublicKey, SecretKey) { - let param_set: BfvParamSet = BfvPreset::InsecureThresholdBfv512.into(); + let param_set: BfvParamSet = BfvPreset::InsecureThreshold512.into(); let bfv_params = param_set.build_arc(); let mut rng = thread_rng();