From 5bf3fcdfcab55ca130ab2745cbfd2fb6792ed403 Mon Sep 17 00:00:00 2001 From: Enrico Bottazzi <85900164+enricobottazzi@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:50:20 +0100 Subject: [PATCH] feat: init `PolyBigIntChip` --- Cargo.toml | 1 + data/bfv_3.in | 1 + examples/bfv_big_int.rs | 150 +++++++++++++++++++++++++++++++++ src/chips/mod.rs | 1 + src/chips/poly_big_int_chip.rs | 111 ++++++++++++++++++++++++ src/chips/utils.rs | 14 ++- src/lib.rs | 3 + 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 data/bfv_3.in create mode 100644 examples/bfv_big_int.rs create mode 100644 src/chips/poly_big_int_chip.rs diff --git a/Cargo.toml b/Cargo.toml index 17f848b..fb6c925 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] axiom-eth = { git = "https://github.com/axiom-crypto/axiom-eth.git", branch = "community-edition", default-features = false, features = ["halo2-axiom", "aggregation", "evm", "clap"] } halo2-base = { git = "https://github.com/axiom-crypto/halo2-lib", tag = "v0.3.0-ce" } +halo2-ecc= { git = "https://github.com/axiom-crypto/halo2-lib", tag = "v0.3.0-ce" } halo2-scaffold = {git = "https://github.com/axiom-crypto/halo2-scaffold" } clap = { version = "=4.0.13", features = ["derive"] } serde = { version = "=1.0", default-features = false, features = ["derive"] } diff --git a/data/bfv_3.in b/data/bfv_3.in new file mode 100644 index 0000000..694a947 --- /dev/null +++ b/data/bfv_3.in @@ -0,0 +1 @@ +{"pk0": ["87197925731567865694008055652698229895727514564428006309583693574229359739201", "19190766948543274512762788408136683100715506013451428053162009430965820063744", "49732022857003891127871974123877182371332200775439738984442859821999400681472", "93845056375314212159844419329497873252817176690407931192682839470754290352449"], "pk1": ["44689075054445631825648092709790757425794070942581020989429609731857412259840", "112927348221829992876779681044452555853103287707501671920568287742990046609729", "91556356895963307551224408797510383461262836030581047677359223026591047500097", "66744583018475108885202185082162792525811433141040021847776073694956379062593"], "m": ["67", "115792089237316195423570985008687907852837564279074904382605163141518161494285", "112", "100"], "u": ["115792089237316195423570985008687907852837564279074904382605163141518161494336", "1", "0", "115792089237316195423570985008687907852837564279074904382605163141518161494336"], "e0": ["0", "0", "0", "115792089237316195423570985008687907852837564279074904382605163141518161494334"], "e1": ["2", "5", "115792089237316195423570985008687907852837564279074904382605163141518161494335", "0"], "c0": ["14668172706893977421695047719446558455067268060659527200196580746473666248704", "22631376347713500549214576959623409940890823147946823990349164854825860464640", "48514829755184265905959624099315157442392158940059188911802816501387781734400", "97543576411441658443499539054564055591551653302475465955951454247385214370113"], "c1": ["95914788060358762263945116014244741362494896226034909222758702741295417672001", "114298399088406943257641581756188901951339780655194275299242558825341906207041", "92474005508737248923478164545839322818883945013414507636384618126059748344129", "27676514892974401212813527679582942935185679461114258291620024730162783322112"], "cyclo": ["1", "0", "0", "0", "1"]} \ No newline at end of file diff --git a/examples/bfv_big_int.rs b/examples/bfv_big_int.rs new file mode 100644 index 0000000..b8c3e4d --- /dev/null +++ b/examples/bfv_big_int.rs @@ -0,0 +1,150 @@ +#![feature(adt_const_params)] +use axiom_eth::Field; +use clap::Parser; +use halo2_base::halo2_proofs::halo2curves::secp256k1::Fq as Q; +use halo2_base::safe_types::RangeChip; +use halo2_base::{AssignedValue, Context}; +use halo2_scaffold::scaffold::cmd::Cli; +use halo2_scaffold::scaffold::run; +use serde::{Deserialize, Serialize}; +use std::env::var; +use zk_fhe::chips::poly_big_int_chip::PolyBigIntChip; +use zk_fhe::chips::utils::vec_string_to_vec_biguint; + +// TO DO: +// - [] Fix the prime field to not be something that implements PrimeField +// - [] Update assumptions at the top of the file +// - [] input generated from the python script `python3 cli.py -n 4 -q 115792089237316195423570985008687907852837564279074904382605163141518161494337 -t 257 --output input.json` +// - [] run the circuit `LOOKUP_BITS=8 cargo run --example bfv_big_int -- --name bfv_3 -k 9 mock` + +/// Circuit inputs for BFV encryption operations +/// +/// # Type Parameters +/// +/// * `DEG`: Degree of the cyclotomic polynomial `cyclo` of the polynomial ring R_q. +/// * `T`: Modulus of the plaintext field +/// * `B`: Upper bound of the Gaussian distribution Chi Error. It is defined as 6 * 𝜎 +/// +/// # Fields +/// +/// * `pk0`: Public key 0 - polynomial of degree DEG-1 living in ciphertext space R_q +/// * `pk1`: Public key 1 - polynomial of degree DEG-1 living in ciphertext space R_q +/// * `m`: Plaintext message to be encrypted - polynomial of degree DEG-1 living in plaintext space R_t +/// * `u`: Ephemeral key - polynomial of degree DEG-1 living in ciphertext space R_q - its coefficients are sampled from the distribution ChiKey +/// * `e0`: Error - polynomial of degree DEG-1 living in ciphertext space R_q - its coefficients are sampled from the distribution ChiError +/// * `e1`: Error - polynomial of degree DEG-1 living in ciphertext space R_q - its coefficients are sampled from the distribution ChiError +/// * `c0`: First ciphertext component - polynomial of degree DEG-1 living in ciphertext space R_q +/// * `c1`: Second ciphertext component - polynomial of degree DEG-1 living in ciphertext space R_q +/// * `cyclo`: Cyclotomic polynomial of degree DEG in the form x^DEG + 1 +/// +/// Note: all the polynomials are expressed by their coefficients in the form [a_DEG-1, a_DEG-2, ..., a_1, a_0] where a_0 is the constant term +/// +/// # Assumptions (to be checked on the public inputs outside the circuit) +/// +/// * `DEG` must be a power of 2 +/// * `Q` must be a prime number and be greater than 1. +/// * `T` must be a prime number and must be greater than 1 and less than `Q` +/// * `B` must be a positive integer and must be less than `Q` +/// * `cyclo` must be the cyclotomic polynomial of degree `DEG` in the form x^DEG + 1 +/// * `pk0` and `pk1` must be polynomials in the R_q ring. The ring R_q is defined as R_q = Z_q[x]/(x^DEG + 1) +/// * Q and DEG must be chosen such that (Q-1) * (Q-1) * (DEG+1) + (Q-1) < p, where p is the modulus of the circuit field to avoid overflow during polynomial addition inside the circuit +/// * Q, T must be chosen such that (Q-1) * (Q-T) + (Q-1) + (Q-1) < p, where p is the modulus of the circuit field.. This is required to avoid overflow during polynomial scalar multiplication inside the circuit +/// * Q must be chosen such that 2Q - 2 < p, where p is the modulus of the circuit field. This is required to avoid overflow during polynomial addition inside the circuit + +// For real world applications, the parameters should be chosen according to the security level required. +// DEG and Q Parameters of the BFV encryption scheme should be chosen according to TABLES of RECOMMENDED PARAMETERS for 128-bits security level +// https://homomorphicencryption.org/wp-content/uploads/2018/11/HomomorphicEncryptionStandardv1.1.pdf +// B is the upper bound of the distribution Chi Error. Pick standard deviation 𝜎 β‰ˆ 3.2 according to the HomomorphicEncryptionStandardv1 paper. +// T is picked according to Lattigo (https://github.com/tuneinsight/lattigo/blob/master/schemes/bfv/example_parameters.go) implementation +// As suggest by https://eprint.iacr.org/2021/204.pdf (paragraph 2) B = 6Οƒerr +// These are just parameters used for fast testing purpose - to match with input file `data/bfv.in` +const DEG: usize = 4; +const T: u64 = 7; +const B: u64 = 19; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInput { + pub pk0: Vec, // PUBLIC INPUT. Should live in R_q according to assumption + pub pk1: Vec, // PUBLIC INPUT. Should live in R_q according to assumption + pub m: Vec, // PRIVATE INPUT. Should in R_t (enforced inside the circuit) + pub u: Vec, // PRIVATE INPUT. Should live in R_q and be sampled from the distribution ChiKey (enforced inside the circuit) + pub e0: Vec, // PRIVATE INPUT. Should live in R_q and be sampled from the distribution ChiError (enforced inside the circuit) + pub e1: Vec, // PRIVATE INPUT. Should live in R_q and be sampled from the distribution ChiError (enforced inside the circuit) + pub c0: Vec, // PUBLIC INPUT. Should live in R_q. We constraint equality between c0 and computed_c0 namely the ciphertext computed inside the circuit + pub c1: Vec, // PUBLIC INPUT. Should live in R_q. We constraint equality between c1 and computed_c1 namely the ciphertext computed inside the circuit + pub cyclo: Vec, // PUBLIC INPUT. Should be the cyclotomic polynomial of degree DEG in the form x^DEG + 1 according to assumption +} + +fn bfv_encryption_circuit( + ctx: &mut Context, + input: CircuitInput, + make_public: &mut Vec>, +) { + // Note: this is not a constraint enforced inside the circuit, just a sanity check + assert_eq!(input.pk0.len() - 1, DEG - 1); + assert_eq!(input.pk1.len() - 1, DEG - 1); + assert_eq!(input.m.len() - 1, DEG - 1); + assert_eq!(input.u.len() - 1, DEG - 1); + assert_eq!(input.e0.len() - 1, DEG - 1); + assert_eq!(input.e1.len() - 1, DEG - 1); + assert_eq!(input.c0.len() - 1, DEG - 1); + assert_eq!(input.c1.len() - 1, DEG - 1); + assert_eq!(input.cyclo.len() - 1, DEG); + + // Transform the input polynomials from strings to BigUints + let pk0_big_int = vec_string_to_vec_biguint(&input.pk0); + let pk1_big_int = vec_string_to_vec_biguint(&input.pk1); + let m_big_int = vec_string_to_vec_biguint(&input.m); + let u_big_int = vec_string_to_vec_biguint(&input.u); + let e0_big_int = vec_string_to_vec_biguint(&input.e0); + let e1_big_int = vec_string_to_vec_biguint(&input.e1); + let c0_big_int = vec_string_to_vec_biguint(&input.c0); + let c1_big_int = vec_string_to_vec_biguint(&input.c1); + let cyclo_big_int = vec_string_to_vec_biguint(&input.cyclo); + + // lookup bits must agree with the size of the lookup table, which is specified by an environmental variable + let lookup_bits = var("LOOKUP_BITS") + .unwrap_or_else(|_| panic!("LOOKUP_BITS not set")) + .parse() + .unwrap(); + + // create a Range chip that contains methods for basic arithmetic operations + let range = RangeChip::default(lookup_bits); + + // The prime, in this case, is 256 bits. Therefore, we use 4 limbs of 64 bits each to represent the prime in the circuit + let limb_bits = 64; + let num_limbs = 4; + + // Create Field Chip + let poly_big_int_chip = PolyBigIntChip::::new(&range, limb_bits, num_limbs); + + // Assign the polynomials to the circuit + let pk0 = poly_big_int_chip.assign_poly_in_ring(ctx, &pk0_big_int); + let pk1 = poly_big_int_chip.assign_poly_in_ring(ctx, &pk1_big_int); + let m = poly_big_int_chip.assign_poly_in_ring(ctx, &m_big_int); + let u = poly_big_int_chip.assign_poly_in_ring(ctx, &u_big_int); + let e0 = poly_big_int_chip.assign_poly_in_ring(ctx, &e0_big_int); + let e1 = poly_big_int_chip.assign_poly_in_ring(ctx, &e1_big_int); + let c0 = poly_big_int_chip.assign_poly_in_ring(ctx, &c0_big_int); + let c1 = poly_big_int_chip.assign_poly_in_ring(ctx, &c1_big_int); + let cyclo = poly_big_int_chip.assing_cyclotomic_poly(ctx, &cyclo_big_int); + + /* constraint on u + - u must be a polynomial in the R_q ring => Coefficients must be in the [0, Q-1] range and the degree of u must be DEG - 1 + - u must be sampled from the distribution ChiKey, namely the coefficients of u must be either 0, 1 or Q-1 + + Approach: + - `check_poly_from_distribution_chi_key` chip guarantees that the coefficients of u are either 0, 1 or Q-1 + - As this range is a subset of the [0, Q-1] range, the coefficients of u are guaranteed to be in the [0, Q-1] range + - The assignment for loop above guarantees that the degree of u is DEG - 1 + */ + poly_big_int_chip.check_poly_from_distribution_chi_key(ctx, &u); +} + +fn main() { + env_logger::init(); + + let args = Cli::parse(); + + run(bfv_encryption_circuit, args); +} diff --git a/src/chips/mod.rs b/src/chips/mod.rs index 86662cc..7b1cbfd 100644 --- a/src/chips/mod.rs +++ b/src/chips/mod.rs @@ -1,3 +1,4 @@ +pub mod poly_big_int_chip; pub mod poly_distribution; pub mod poly_operations; pub mod utils; diff --git a/src/chips/poly_big_int_chip.rs b/src/chips/poly_big_int_chip.rs new file mode 100644 index 0000000..7121796 --- /dev/null +++ b/src/chips/poly_big_int_chip.rs @@ -0,0 +1,111 @@ +use halo2_base::safe_types::GateInstructions; +use halo2_base::utils::biguint_to_fe; +use halo2_base::{safe_types::RangeChip, utils::ScalarField, Context}; +use halo2_ecc::fields::fp::FpChip; +use halo2_ecc::fields::FieldChip; +use halo2_ecc::{bigint::ProperCrtUint, fields::PrimeField}; +use num_bigint::BigUint; +use num_traits::Num; + +// PolyBigIntChip supports operations on Polynomials where each coefficient is defined in the Wrong Field. +// The polynomial is defined in the cyclotomic ring R_q = Z_q[X]/(X^N + 1) where q is the "wrong" field modulus of the ring R_q (ciphertext space) +#[derive(Clone, Debug)] +pub struct PolyBigIntChip<'range, F: ScalarField, const N: usize, Q: PrimeField> { + fp_chip: FpChip<'range, F, Q>, +} + +impl<'range, F: ScalarField, const N: usize, Q: PrimeField> PolyBigIntChip<'range, F, N, Q> { + pub fn new(range: &'range RangeChip, limb_bits: usize, num_limbs: usize) -> Self { + let fp_chip = FpChip::new(range, limb_bits, num_limbs); + Self { fp_chip } + } + + pub fn assign_poly_in_ring( + &self, + ctx: &mut Context, + poly: &[BigUint], + ) -> Vec> { + let mut output = vec![]; + + for coeff in poly.iter().take(N) { + let coeff = biguint_to_fe::(coeff); + let loaded = self.fp_chip.load_private(ctx, coeff); + output.push(loaded); + } + + output + } + + pub fn assing_cyclotomic_poly( + &self, + ctx: &mut Context, + poly: &[BigUint], + ) -> Vec> { + let mut output = vec![]; + + for coeff in poly.iter().take(N + 1) { + let coeff = biguint_to_fe::(coeff); + let loaded = self.fp_chip.load_private(ctx, coeff); + output.push(loaded); + } + + output + } + + /// Enforce that polynomial a of degree N is sampled from the distribution chi key + /// + /// * Namely, that the coefficients are in the range [0, 1, Q-1]. + /// + /// Assumption: `a` is of degree N + pub fn check_poly_from_distribution_chi_key( + &self, + ctx: &mut Context, + a: &Vec>, + ) { + // assert that the degree of the polynomial a is equal to N - 1 + assert_eq!(a.len() - 1, N - 1); + + // In order to check that coeff is equal to either 0, 1 or q-1 + // The constraint that we want to enforce is: + // (coeff - 0) * (coeff - 1) * (coeff - (q-1)) = 0 + + // load constants + let zero_loaded_const = self.fp_chip.load_constant_uint(ctx, BigUint::from(0_u64)); + + let one_loaded_cons = self.fp_chip.load_constant_uint(ctx, BigUint::from(1_u64)); + + let q_minus_one = BigUint::from_str_radix(&Q::MODULUS[2..], 16).unwrap() - 1_u64; + let q_minus_one_loaded_const = self.fp_chip.load_constant_uint(ctx, q_minus_one); + + // loop over all the coefficients of the polynomial + for coeff in a { + // constrain (a - 0) + let factor_1 = self + .fp_chip + .sub_no_carry(ctx, coeff.clone(), zero_loaded_const.clone()); + + // constrain (a - 1) + let factor_2 = self + .fp_chip + .sub_no_carry(ctx, coeff.clone(), one_loaded_cons.clone()); + + // constrain (a - (q-1)) + let factor_3 = + self.fp_chip + .sub_no_carry(ctx, coeff.clone(), q_minus_one_loaded_const.clone()); + + // constrain (a - 0) * (a - 1) + let factor_1_2 = self.fp_chip.mul_no_carry(ctx, factor_1, factor_2); + + // constrain (a - 0) * (a - 1) * (a - (q-1)) + let factor_1_2_3 = self.fp_chip.mul_no_carry(ctx, factor_1_2, factor_3); + + // constrain (a - 0) * (a - 1) * (a - (q-1)) = 0 + // `is_zero` requires to use a `ProperCrtUint` as input + let factor_1_2_3_proper = self.fp_chip.carry_mod(ctx, factor_1_2_3); + let bool = self.fp_chip.is_zero(ctx, factor_1_2_3_proper); + + self.fp_chip.gate().assert_is_const(ctx, &bool, &F::from(1)); + } + } +} diff --git a/src/chips/utils.rs b/src/chips/utils.rs index 4cfe0be..b587998 100644 --- a/src/chips/utils.rs +++ b/src/chips/utils.rs @@ -1,5 +1,5 @@ use halo2_base::utils::ScalarField; -use num_bigint::BigInt; +use num_bigint::{BigInt, BigUint}; use num_integer::Integer; use num_traits::identities::Zero; use num_traits::Num; @@ -116,3 +116,15 @@ pub fn vec_string_to_vec_bigint(vec: &Vec) -> Vec { vec_bigint } + +/// Transfor Vec to Vec +pub fn vec_string_to_vec_biguint(vec: &Vec) -> Vec { + let mut vec_bigint = Vec::new(); + + for item in vec { + let bigint = BigUint::from_str_radix(item, 10).unwrap(); + vec_bigint.push(bigint); + } + + vec_bigint +} diff --git a/src/lib.rs b/src/lib.rs index e279075..8aa5e23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,4 @@ +#![feature(inherent_associated_types)] +#![feature(adt_const_params)] + pub mod chips;