mirror of
https://github.com/enricobottazzi/zk-fhe.git
synced 2026-01-09 05:08:04 -05:00
feat: init PolyBigIntChip
This commit is contained in:
@@ -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"] }
|
||||
|
||||
1
data/bfv_3.in
Normal file
1
data/bfv_3.in
Normal file
@@ -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"]}
|
||||
150
examples/bfv_big_int.rs
Normal file
150
examples/bfv_big_int.rs
Normal file
@@ -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<const DEG: usize, const T: u64, const B: u64> {
|
||||
pub pk0: Vec<String>, // PUBLIC INPUT. Should live in R_q according to assumption
|
||||
pub pk1: Vec<String>, // PUBLIC INPUT. Should live in R_q according to assumption
|
||||
pub m: Vec<String>, // PRIVATE INPUT. Should in R_t (enforced inside the circuit)
|
||||
pub u: Vec<String>, // PRIVATE INPUT. Should live in R_q and be sampled from the distribution ChiKey (enforced inside the circuit)
|
||||
pub e0: Vec<String>, // PRIVATE INPUT. Should live in R_q and be sampled from the distribution ChiError (enforced inside the circuit)
|
||||
pub e1: Vec<String>, // PRIVATE INPUT. Should live in R_q and be sampled from the distribution ChiError (enforced inside the circuit)
|
||||
pub c0: Vec<String>, // 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<String>, // 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<String>, // PUBLIC INPUT. Should be the cyclotomic polynomial of degree DEG in the form x^DEG + 1 according to assumption
|
||||
}
|
||||
|
||||
fn bfv_encryption_circuit<F: Field>(
|
||||
ctx: &mut Context<F>,
|
||||
input: CircuitInput<DEG, T, B>,
|
||||
make_public: &mut Vec<AssignedValue<F>>,
|
||||
) {
|
||||
// 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::<F, DEG, Q>::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);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod poly_big_int_chip;
|
||||
pub mod poly_distribution;
|
||||
pub mod poly_operations;
|
||||
pub mod utils;
|
||||
|
||||
111
src/chips/poly_big_int_chip.rs
Normal file
111
src/chips/poly_big_int_chip.rs
Normal file
@@ -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<F>, 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<F>,
|
||||
poly: &[BigUint],
|
||||
) -> Vec<ProperCrtUint<F>> {
|
||||
let mut output = vec![];
|
||||
|
||||
for coeff in poly.iter().take(N) {
|
||||
let coeff = biguint_to_fe::<Q>(coeff);
|
||||
let loaded = self.fp_chip.load_private(ctx, coeff);
|
||||
output.push(loaded);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn assing_cyclotomic_poly(
|
||||
&self,
|
||||
ctx: &mut Context<F>,
|
||||
poly: &[BigUint],
|
||||
) -> Vec<ProperCrtUint<F>> {
|
||||
let mut output = vec![];
|
||||
|
||||
for coeff in poly.iter().take(N + 1) {
|
||||
let coeff = biguint_to_fe::<Q>(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<F>,
|
||||
a: &Vec<ProperCrtUint<F>>,
|
||||
) {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>) -> Vec<BigInt> {
|
||||
|
||||
vec_bigint
|
||||
}
|
||||
|
||||
/// Transfor Vec<String> to Vec<BigUint>
|
||||
pub fn vec_string_to_vec_biguint(vec: &Vec<String>) -> Vec<BigUint> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
#![feature(inherent_associated_types)]
|
||||
#![feature(adt_const_params)]
|
||||
|
||||
pub mod chips;
|
||||
|
||||
Reference in New Issue
Block a user