diff --git a/kzg_prover/prints/range-check-layout.png b/kzg_prover/prints/range-check-layout.png index bb1ae4e6..736e963f 100644 Binary files a/kzg_prover/prints/range-check-layout.png and b/kzg_prover/prints/range-check-layout.png differ diff --git a/kzg_prover/prints/univariate-grand-sum-layout.png b/kzg_prover/prints/univariate-grand-sum-layout.png index 1308203d..e9541d1e 100644 Binary files a/kzg_prover/prints/univariate-grand-sum-layout.png and b/kzg_prover/prints/univariate-grand-sum-layout.png differ diff --git a/kzg_prover/src/chips/range/range_check.rs b/kzg_prover/src/chips/range/range_check.rs index 96c9b0a8..0418249e 100644 --- a/kzg_prover/src/chips/range/range_check.rs +++ b/kzg_prover/src/chips/range/range_check.rs @@ -1,113 +1,112 @@ +use crate::chips::range::utils::decompose_fp_to_byte_pairs; use halo2_proofs::arithmetic::Field; use halo2_proofs::circuit::{AssignedCell, Region, Value}; use halo2_proofs::halo2curves::bn256::Fr as Fp; use halo2_proofs::plonk::{Advice, Column, ConstraintSystem, Error, Expression, Fixed}; use halo2_proofs::poly::Rotation; - use std::fmt::Debug; -use crate::chips::range::utils::decompose_fp_to_bytes; - -/// Configuration for the Range Check Chip +/// Configuration for the Range Check u64 Chip +/// Used to verify that an element lies in the u64 range. /// -/// # Type Parameters +/// # Fields /// -/// * `N_BYTES`: Number of bytes in which the element to be checked should lie +/// * `zs`: Four advice columns - contain the truncated right-shifted values of the element to be checked /// -/// # Fields +/// # Assumptions /// -/// * `zs`: Advice columns - contain the truncated right-shifted values of the element to be checked +/// * The lookup table `range_u16` is by default loaded with values from 0 to 2^16 - 1. /// /// Patterned after [halo2_gadgets](https://github.com/privacy-scaling-explorations/halo2/blob/main/halo2_gadgets/src/utilities/decompose_running_sum.rs) #[derive(Debug, Copy, Clone)] -pub struct RangeCheckConfig { - zs: [Column; N_BYTES], +pub struct RangeCheckU64Config { + zs: [Column; 4], } -/// Helper chip that verfiies that the element witnessed in a given cell lies within a given range defined by N_BYTES. -/// For example, Let's say we want to constraint 0x1f2f3f4f to be within the range N_BYTES=4. +/// Helper chip that verfiies that the element witnessed in a given cell lies within the u64 range. +/// For example, Let's say we want to constraint 0x1f2f3f4f5f6f7f8f to be a u64. +/// Note that the lookup table `range` is by default loaded with values from 0 to 2^16 - 1. /// `z` is the advice column that contains the element to be checked. /// -/// `z = 0x1f2f3f4f` -/// `zs[0] = (0x1f2f3f4f - 0x4f) / 2^8 = 0x1f2f3f` -/// `zs[1] = (0x1f2f3f - 0x3f) / 2^8 = 0x1f2f` -/// `zs[2] = (0x1f2f - 0x2f) / 2^8 = 0x1f` -/// `zs[3] = (0x1f - 0x1f) / 2^8 = 0x00` +/// `z = 0x1f2f3f4f5f6f7f8f` +/// `zs[0] = (0x1f2f3f4f5f6f7f8f - 0x7f8f) / 2^16 = 0x1f2f3f4f5f6f` +/// `zs[1] = (0x1f2f3f4f5f6f - 0xf5f6f) / 2^16 = 0x1f2f3f4f` +/// `zs[2] = (0x1f2f3f4f - 0x3f4f) / 2^16 = 0x1f2f` +/// `zs[3] = (0x1f2f - 0x1f2f) / 2^16 = 0x00` /// -/// z | zs[0] | zs[1] | zs[2] | zs[3] | -/// --------- | ---------- | ---------- | ---------- | ---------- | -/// 0x1f2f3f4f | 0x1f2f3f | 0x1f2f | 0x1f | 0x00 | +/// z | zs[0] | zs[1] | zs[2] | zs[3] | +/// --------- | ---------- | ---------- | ---------- | ---------- | +/// 0x1f2f3f4f5f6f7f8f | 0x1f2f3f4f5f6f | 0x1f2f3f4f | 0x1f2f | 0x00 | /// -/// Column zs[0], at offset 0, contains the truncated right-shifted value z - ks[0] / 2^8 (shift right by 8 bits) where ks[0] is the 0-th decomposition big-endian of the element to be checked -/// Column zs[1], at offset 0, contains the truncated right-shifted value zs[0] - ks[1] / 2^8 (shift right by 8 bits) where ks[1] is the 1-th decomposition big-endian of the element to be checked -/// Column zs[2], at offset 0, contains the truncated right-shifted value zs[1] - ks[2] / 2^8 (shift right by 8 bits) where ks[2] is the 2-th decomposition big-endian of the element to be checked -/// Column zs[3], at offset 0, contains the truncated right-shifted value zs[2] - ks[3] / 2^8 (shift right by 8 bits) where ks[3] is the 3-th decomposition big-endian of the element to be checked +/// Column zs[0], at offset 0, contains the truncated right-shifted value z - ks[0] / 2^16 (shift right by 16 bits) where ks[0] is the 0-th decomposition big-endian of the element to be checked +/// Column zs[1], at offset 0, contains the truncated right-shifted value zs[0] - ks[1] / 2^16 (shift right by 16 bits) where ks[1] is the 1-th decomposition big-endian of the element to be checked +/// Column zs[2], at offset 0, contains the truncated right-shifted value zs[1] - ks[2] / 2^16 (shift right by 16 bits) where ks[2] is the 2-th decomposition big-endian of the element to be checked +/// Column zs[3], at offset 0, contains the truncated right-shifted value zs[2] - ks[3] / 2^16 (shift right by 16 bits) where ks[3] is the 3-th decomposition big-endian of the element to be checked /// /// The contraints that are enforced are: /// 1. -/// z - 2^8⋅zs[0] = ks[0] ∈ lookup_u8 +/// z - 2^16⋅zs[0] = ks[0] ∈ range_u16 /// /// 2. -/// for i = 0..=N_BYTES - 2: -/// zs[i] - 2^8⋅zs[i+1] = ks[i] ∈ lookup_u8 +/// for i = 0..=2: +/// zs[i] - 2^16⋅zs[i+1] = ks[i] ∈ range_u16 /// /// 3. -/// zs[N_BYTES - 1] == 0 +/// zs[3] == 0 #[derive(Debug, Clone)] -pub struct RangeCheckChip { - config: RangeCheckConfig, +pub struct RangeCheckU64Chip { + config: RangeCheckU64Config, } -impl RangeCheckChip { - pub fn construct(config: RangeCheckConfig) -> Self { +impl RangeCheckU64Chip { + pub fn construct(config: RangeCheckU64Config) -> Self { Self { config } } /// Configures the Range Chip + /// Note: the lookup table should be loaded with values from `0` to `2^16 - 1` otherwise the range check will fail. pub fn configure( meta: &mut ConstraintSystem, z: Column, - zs: [Column; N_BYTES], - range: Column, - ) -> RangeCheckConfig { - meta.annotate_lookup_any_column(range, || "LOOKUP_MAXBITS_RANGE"); - + zs: [Column; 4], + range_u16: Column, + ) -> RangeCheckU64Config { // Constraint that the difference between the element to be checked and the 0-th truncated right-shifted value of the element to be within the range. - // z - 2^8⋅zs[0] = ks[0] ∈ lookup_u8 + // z - 2^16⋅zs[0] = ks[0] ∈ range_u16 meta.lookup_any( - "range u8 check for difference between the element to be checked and the 0-th truncated right-shifted value of the element", + "range check in u16 for difference between the element to be checked and the 0-th truncated right-shifted value of the element", |meta| { let element = meta.query_advice(z, Rotation::cur()); let zero_truncation = meta.query_advice(zs[0], Rotation::cur()); - let u8_range = meta.query_fixed(range, Rotation::cur()); + let range_u16 = meta.query_fixed(range_u16, Rotation::cur()); - let diff = element - zero_truncation * Expression::Constant(Fp::from(1 << 8)); + let diff = element - zero_truncation * Expression::Constant(Fp::from(1 << 16)); - vec![(diff, u8_range)] + vec![(diff, range_u16)] }, ); - // For i = 0..=N_BYTES - 2: Constraint that the difference between the i-th truncated right-shifted value and the (i+1)-th truncated right-shifted value to be within the range. - // zs[i] - 2^8⋅zs[i+1] = ks[i] ∈ lookup_u8 - for i in 0..=N_BYTES - 2 { + // For i = 0..=2: Constraint that the difference between the i-th truncated right-shifted value and the (i+1)-th truncated right-shifted value to be within the range. + // zs[i] - 2^16⋅zs[i+1] = ks[i] ∈ range_u16 + for i in 0..=2 { meta.lookup_any( - format!("range u8 check for difference between the {}-th truncated right-shifted value and the {}-th truncated right-shifted value", i, i+1).as_str(), + format!("range check in u16 for difference between the {}-th truncated right-shifted value and the {}-th truncated right-shifted value", i, i+1).as_str(), |meta| { let i_truncation = meta.query_advice(zs[i], Rotation::cur()); let i_plus_one_truncation = meta.query_advice(zs[i + 1], Rotation::cur()); - let u8_range = meta.query_fixed(range, Rotation::cur()); + let range_u16 = meta.query_fixed(range_u16, Rotation::cur()); - let diff = i_truncation - i_plus_one_truncation * Expression::Constant(Fp::from(1 << 8)); + let diff = i_truncation - i_plus_one_truncation * Expression::Constant(Fp::from(1 << 16)); - vec![(diff, u8_range)] + vec![(diff, range_u16)] }, ); } - RangeCheckConfig { zs } + RangeCheckU64Config { zs } } /// Assign the truncated right-shifted values of the element to be checked to the corresponding columns zs at offset 0 starting from the element to be checked. @@ -116,25 +115,25 @@ impl RangeCheckChip { region: &mut Region<'_, Fp>, element: &AssignedCell, ) -> Result<(), Error> { - // Decompose the element in #N_BYTES bytes + // Decompose the element in 4 byte pairs. let ks = element .value() .copied() - .map(|x| decompose_fp_to_bytes(x, N_BYTES)) - .transpose_vec(N_BYTES); + .map(|x| decompose_fp_to_byte_pairs(x, 4)) + .transpose_vec(4); // Initalize an empty vector of cells for the truncated right-shifted values of the element to be checked. - let mut zs = Vec::with_capacity(N_BYTES); + let mut zs = Vec::with_capacity(4); let mut z = element.clone(); - // Calculate 1 / 2^8 - let two_pow_eight_inv = Value::known(Fp::from(1 << 8).invert().unwrap()); + // Calculate 1 / 2^16 + let two_pow_sixteen_inv = Value::known(Fp::from(1 << 16).invert().unwrap()); // Perform the assignment of the truncated right-shifted values to zs columns. for (i, k) in ks.iter().enumerate() { let zs_next = { let k = k.map(|byte| Fp::from(byte as u64)); - let zs_next_val = (z.value().copied() - k) * two_pow_eight_inv; + let zs_next_val = (z.value().copied() - k) * two_pow_sixteen_inv; region.assign_advice( || format!("zs_{:?}", i), self.config.zs[i], @@ -148,7 +147,7 @@ impl RangeCheckChip { } // Constrain the final running sum output to be zero. - region.constrain_constant(zs[N_BYTES - 1].cell(), Fp::from(0))?; + region.constrain_constant(zs[3].cell(), Fp::from(0))?; Ok(()) } diff --git a/kzg_prover/src/chips/range/tests.rs b/kzg_prover/src/chips/range/tests.rs index a1ceeca7..970f0f52 100644 --- a/kzg_prover/src/chips/range/tests.rs +++ b/kzg_prover/src/chips/range/tests.rs @@ -1,4 +1,4 @@ -use crate::chips::range::range_check::{RangeCheckChip, RangeCheckConfig}; +use crate::chips::range::range_check::{RangeCheckU64Chip, RangeCheckU64Config}; use halo2_proofs::{ circuit::{AssignedCell, Layouter, SimpleFloorPlanner, Value}, halo2curves::bn256::Fr as Fp, @@ -85,23 +85,23 @@ impl AddChip { } #[derive(Debug, Clone)] -pub struct TestConfig { +pub struct TestConfig { pub addchip_config: AddConfig, - pub range_check_config: RangeCheckConfig, - pub range: Column, + pub range_check_config: RangeCheckU64Config, + pub range_u16: Column, } // The test circuit takes two inputs a and b. // It adds them together by using the add chip to produce c = a + b. -// Performs a range check on c that should lie in N_BYTES. +// Performs a range check on c that should lie in [0, 2^64 - 1] range. #[derive(Default, Clone, Debug)] -struct TestCircuit { +struct TestCircuit { pub a: Fp, pub b: Fp, } -impl Circuit for TestCircuit { - type Config = TestConfig; +impl Circuit for TestCircuit { + type Config = TestConfig; type FloorPlanner = SimpleFloorPlanner; fn without_witnesses(&self) -> Self { @@ -109,7 +109,7 @@ impl Circuit for TestCircuit { } fn configure(meta: &mut ConstraintSystem) -> Self::Config { - let range = meta.fixed_column(); + let range_u16 = meta.fixed_column(); let a = meta.advice_column(); let b = meta.advice_column(); @@ -122,7 +122,7 @@ impl Circuit for TestCircuit { let constants = meta.fixed_column(); meta.enable_constant(constants); - let zs = [(); N_BYTES].map(|_| meta.advice_column()); + let zs = [(); 4].map(|_| meta.advice_column()); for column in zs.iter() { meta.enable_equality(*column); @@ -130,7 +130,7 @@ impl Circuit for TestCircuit { let add_selector = meta.selector(); - let range_check_config = RangeCheckChip::::configure(meta, c, zs, range); + let range_check_config = RangeCheckU64Chip::configure(meta, c, zs, range_u16); let addchip_config = AddChip::configure(meta, a, b, c, add_selector); @@ -138,7 +138,7 @@ impl Circuit for TestCircuit { TestConfig { addchip_config, range_check_config, - range, + range_u16, } } } @@ -149,18 +149,18 @@ impl Circuit for TestCircuit { mut layouter: impl Layouter, ) -> Result<(), Error> { // Initiate the range check chip - let range_chip = RangeCheckChip::construct(config.range_check_config); + let range_chip = RangeCheckU64Chip::construct(config.range_check_config); // Load the lookup table - let range = 1 << 8; + let range = 1 << 16; layouter.assign_region( - || format!("load range check table of {} bits", 8 * N_BYTES), + || format!("load range check table of 64 bits"), |mut region| { for i in 0..range { region.assign_fixed( || "assign cell in fixed column", - config.range, + config.range_u16, i, || Value::known(Fp::from(i as u64)), )?; @@ -189,77 +189,49 @@ impl Circuit for TestCircuit { #[cfg(test)] mod testing { + use crate::utils::big_uint_to_fp; + use super::TestCircuit; use halo2_proofs::{ dev::{FailureLocation, MockProver, VerifyFailure}, halo2curves::bn256::Fr as Fp, plonk::Any, }; + use num_bigint::BigUint; - // a = (1 << 16) - 2 = 0xfffe + // a = (1 << 64) - 2 // b = 1 - // c = a + b = 0xffff - // All the values are within 2 bytes range. + // c = a + b + // c is within 8 bytes range. #[test] - fn test_none_overflow_16bits() { - let k = 9; + fn test_none_overflow_64bits() { + let k = 17; - // a: new value - let a = Fp::from((1 << 16) - 2); + let a = BigUint::from(1_u64) << 64; + let a = a - 2_u64; + let a = big_uint_to_fp(&a); let b = Fp::from(1); - let circuit = TestCircuit::<2> { a, b }; + let circuit = TestCircuit { a, b }; let prover = MockProver::run(k, &circuit, vec![]).unwrap(); prover.assert_satisfied(); } - // a = (1 << 16) - 2 = 0xfffe + // a = (1 << 64) - 2 // b = 2 - // c = a + b = 0x10000 - // a and b are within 2 bytes range. - // c overflows 2 bytes so the circuit should fail. + // c = a + b + // c overflows 8 bytes range. #[test] - fn test_overflow_16bits() { - let k = 9; + fn test_overflow_64bits() { + let k = 17; - let a = Fp::from((1 << 16) - 2); + let a = BigUint::from(1_u64) << 64; + let a = a - 2_u64; + let a = big_uint_to_fp(&a); let b = Fp::from(2); - let circuit = TestCircuit::<2> { a, b }; + let circuit = TestCircuit { a, b }; let invalid_prover = MockProver::run(k, &circuit, vec![]).unwrap(); - assert_eq!( - invalid_prover.verify(), - Err(vec![ - VerifyFailure::Permutation { - column: (Any::Fixed, 1).into(), - location: FailureLocation::OutsideRegion { row: 0 } - }, - VerifyFailure::Permutation { - column: (Any::advice(), 4).into(), - location: FailureLocation::InRegion { - region: (2, "Perform range check on c").into(), - offset: 0 - } - }, - ]) - ); - } - - // a is the max value within the range (32 bits / 4 bytes) - // a = 0x-ff-ff-ff-ff - // b = 1 - // a and b are within 4 bytes range. - // c overflows 4 bytes so the circuit should fail. - #[test] - fn test_overflow_32bits() { - let k = 9; - - let a = Fp::from(0xffffffff); - let b = Fp::from(1); - - let circuit = TestCircuit::<4> { a, b }; - let invalid_prover = MockProver::run(k, &circuit, vec![]).unwrap(); - assert_eq!( invalid_prover.verify(), Err(vec![ @@ -290,7 +262,7 @@ mod testing { .titled("Range Check Layout", ("sans-serif", 60)) .unwrap(); - let circuit = TestCircuit::<4> { + let circuit = TestCircuit { a: Fp::from(0x1f2f3f4f), b: Fp::from(1), }; diff --git a/kzg_prover/src/chips/range/utils.rs b/kzg_prover/src/chips/range/utils.rs index 14d1d248..5ace9d4d 100644 --- a/kzg_prover/src/chips/range/utils.rs +++ b/kzg_prover/src/chips/range/utils.rs @@ -29,6 +29,41 @@ pub fn decompose_fp_to_bytes(value: Fp, n: usize) -> Vec { bytes } +/// Converts value Fp to array of n byte pairs in little endian order. +/// If value is decomposed in #byte pairs which are less than n, then the returned byte pairs are padded with 0s at the most significant byte pairs. +/// If value is decomposed in #byte pairs which are greater than n, then the most significant byte pairs are truncated. A warning is printed. +pub fn decompose_fp_to_byte_pairs(value: Fp, n: usize) -> Vec { + let value_biguint = fp_to_big_uint(value); + let mut bytes = value_biguint.to_bytes_le(); + + // Ensure the bytes vector has an even length for pairs of bytes. + if bytes.len() % 2 != 0 { + bytes.push(0); + } + + let mut byte_pairs = Vec::new(); + + // Iterate over the bytes two at a time. + for chunk in bytes.chunks(2) { + // Combine two adjacent bytes into a u16 (2 bytes). + let pair = (chunk[0] as u16) | ((chunk[1] as u16) << 8); + byte_pairs.push(pair); + } + + // Pad with 0s if the number of byte pairs is less than n. + while byte_pairs.len() < n { + byte_pairs.push(0); + } + + // If the byte pairs length exceeds n, print a warning and truncate. + if byte_pairs.len() > n { + println!("Warning: `decompose_fp_to_bytes` value is decomposed in #byte pairs which are greater than n. Truncating the output to fit the specified length."); + byte_pairs.truncate(n); + } + + byte_pairs +} + pub fn pow_of_two(by: usize) -> Fp { let res = BigUint::from(1u8) << by; big_uint_to_fp(&res) @@ -57,7 +92,15 @@ mod testing { assert_eq!(bytes, vec![0x4f, 0x3f, 0x2f, 0x1f]); } - // convert a 32 bit number in 6 bytes. Should correctly convert to 6 bytes in which the first 2 bytes are 0 padded. + // convert a 32 bit number in 2 byte pairs. Should correctly convert to 2 byte pairs + #[test] + fn test_decompose_fp_byte_pairs_no_padding() { + let f = Fp::from(0x1f2f3f4f); + let bytes = decompose_fp_to_byte_pairs(f, 2); + assert_eq!(bytes, vec![0x3f4f, 0x1f2f]); + } + + // convert a 32 bit number in 6 bytes. Should correctly convert to 6 bytes in which the last 2 bytes are 0 padded. #[test] fn test_decompose_fp_to_bytes_padding() { let f = Fp::from(0x1f2f3f4f); @@ -65,6 +108,14 @@ mod testing { assert_eq!(bytes, vec![0x4f, 0x3f, 0x2f, 0x1f, 0x00, 0x00]); } + // convert a 32 bit number in 3 byte pairs. Should correctly convert to 3 byte pairs in which the last pair is 0 padded. + #[test] + fn test_decompose_fp_to_byte_pairs_padding() { + let f = Fp::from(0x1f2f3f4f); + let bytes = decompose_fp_to_byte_pairs(f, 3); + assert_eq!(bytes, vec![0x3f4f, 0x1f2f, 0x00]); + } + // convert a 32 bit number in 2 bytes. Should convert to 2 bytes and truncate the most significant bytes and emit a warning #[test] fn test_decompose_fp_to_bytes_overflow() { @@ -73,6 +124,14 @@ mod testing { assert_eq!(bytes, vec![0x4f, 0x3f]); } + // convert a 32 bit number in 1 byte pair. Should convert to a byte pair and truncate the most significant byte pair and emit a warning + #[test] + fn test_decompose_fp_to_byte_pairs_overflow() { + let f = Fp::from(0x1f2f3f4f); + let bytes = decompose_fp_to_byte_pairs(f, 1); + assert_eq!(bytes, vec![0x3f4f]); + } + // convert a 40 bit number in 2 bytes. Should convert to 2 most significant bytes and truncate the least significant byte #[test] fn test_decompose_fp_to_bytes_overflow_2() { diff --git a/kzg_prover/src/circuits/tests.rs b/kzg_prover/src/circuits/tests.rs index 8d89c395..3cd04f4d 100644 --- a/kzg_prover/src/circuits/tests.rs +++ b/kzg_prover/src/circuits/tests.rs @@ -10,13 +10,12 @@ mod test { use crate::entry::Entry; use crate::utils::parse_csv_to_entries; use halo2_proofs::dev::{FailureLocation, MockProver, VerifyFailure}; - use halo2_proofs::halo2curves::bn256::Fr as Fp; - use halo2_proofs::plonk::Any; + use halo2_proofs::halo2curves::bn256::{Bn256, Fr as Fp, G1Affine}; + use halo2_proofs::plonk::{Any, ProvingKey, VerifyingKey}; + use halo2_proofs::poly::kzg::commitment::ParamsKZG; use num_bigint::BigUint; - const K: u32 = 9; - - const N_BYTES: usize = 8; + const K: u32 = 17; const N_CURRENCIES: usize = 2; const N_USERS: usize = 16; @@ -26,10 +25,9 @@ mod test { let mut entries: Vec> = vec![Entry::init_empty(); N_USERS]; let mut cryptos = vec![Cryptocurrency::init_empty(); N_CURRENCIES]; - parse_csv_to_entries::<&str, N_CURRENCIES, N_BYTES>(path, &mut entries, &mut cryptos) - .unwrap(); + parse_csv_to_entries::<&str, N_CURRENCIES>(path, &mut entries, &mut cryptos).unwrap(); - let circuit = UnivariateGrandSum::::init(entries.to_vec()); + let circuit = UnivariateGrandSum::::init(entries.to_vec()); let valid_prover = MockProver::run(K, &circuit, vec![vec![]]).unwrap(); @@ -38,28 +36,9 @@ mod test { #[test] fn test_valid_univariate_grand_sum_full_prover() { - const N_USERS: usize = 16; - - // Initialize an empty circuit - let circuit = UnivariateGrandSum::::init_empty(); - - // Generate a universal trusted setup for testing purposes. - // - // The verification key (vk) and the proving key (pk) are then generated. - // An empty circuit is used here to emphasize that the circuit inputs are not relevant when generating the keys. - // Important: The dimensions of the circuit used to generate the keys must match those of the circuit used to generate the proof. - // In this case, the dimensions are represented by the number fo users. - let (params, pk, vk) = generate_setup_artifacts(K, None, circuit).unwrap(); - - // Only now we can instantiate the circuit with the actual inputs let path = "../csv/entry_16.csv"; - let mut entries: Vec> = vec![Entry::init_empty(); N_USERS]; - let mut cryptos = vec![Cryptocurrency::init_empty(); N_CURRENCIES]; - - let _ = - parse_csv_to_entries::<&str, N_CURRENCIES, N_BYTES>(path, &mut entries, &mut cryptos) - .unwrap(); + let (entries, circuit, pk, vk, params) = set_up::(path); // Calculate total for all entry columns let mut csv_total: Vec = vec![BigUint::from(0u32); N_CURRENCIES]; @@ -70,12 +49,6 @@ mod test { } } - let circuit = UnivariateGrandSum::::init(entries.to_vec()); - - let valid_prover = MockProver::run(K, &circuit, vec![vec![]]).unwrap(); - - valid_prover.assert_satisfied(); - // 1. Proving phase // The Custodian generates the ZK-SNARK Halo2 proof that commits to the user entry values in advice polynomials // and also range-checks the user balance values @@ -164,44 +137,12 @@ mod test { } } + // The prover communicates an invalid omega to the verifier, therefore the opening proof of user inclusion should fail #[test] - fn test_invalid_univariate_grand_sum_proof() { - const N_USERS: usize = 16; - - // Initialize an empty circuit - let circuit = UnivariateGrandSum::::init_empty(); - - // Generate a universal trusted setup for testing purposes. - // - // The verification key (vk) and the proving key (pk) are then generated. - // An empty circuit is used here to emphasize that the circuit inputs are not relevant when generating the keys. - // Important: The dimensions of the circuit used to generate the keys must match those of the circuit used to generate the proof. - // In this case, the dimensions are represented by the number fo users. - let (params, pk, vk) = generate_setup_artifacts(K, None, circuit).unwrap(); - - // Only now we can instantiate the circuit with the actual inputs + fn test_invalid_omega_univariate_grand_sum_proof() { let path = "../csv/entry_16.csv"; - let mut entries: Vec> = vec![Entry::init_empty(); N_USERS]; - let mut cryptos = vec![Cryptocurrency::init_empty(); N_CURRENCIES]; - - parse_csv_to_entries::<&str, N_CURRENCIES, N_BYTES>(path, &mut entries, &mut cryptos) - .unwrap(); - - // Calculate total for all entry columns - let mut csv_total: Vec = vec![BigUint::from(0u32); N_CURRENCIES]; - - for entry in &entries { - for (i, balance) in entry.balances().iter().enumerate() { - csv_total[i] += balance; - } - } - - let circuit = UnivariateGrandSum::::init(entries.to_vec()); - - let valid_prover = MockProver::run(K, &circuit, vec![vec![]]).unwrap(); - - valid_prover.assert_satisfied(); + let (_, circuit, pk, vk, params) = set_up::(path); // 1. Proving phase // The Custodian generates the ZK proof @@ -244,21 +185,79 @@ mod test { ); //The verification should fail assert!(!balances_verified); + } + + // The prover communicates an invalid polynomial degree to the verifier (smaller than the actual degree). This will result in an understated grand sum + #[test] + fn test_invalid_poly_degree_univariate_grand_sum_full_prover() { + let path = "../csv/entry_16.csv"; - // TODO add more negative tests + let (entries, circuit, pk, vk, params) = set_up::(path); + + // Calculate total for all entry columns + let mut csv_total: Vec = vec![BigUint::from(0u32); N_CURRENCIES]; + + for entry in &entries { + for (i, balance) in entry.balances().iter().enumerate() { + csv_total[i] += balance; + } + } + + // 1. Proving phase + // The Custodian generates the ZK-SNARK Halo2 proof that commits to the user entry values in advice polynomials + // and also range-checks the user balance values + let (zk_snark_proof, advice_polys, _) = + full_prover(¶ms, &pk, circuit.clone(), vec![vec![]]); + + // Both the Custodian and the Verifier know what column range are the balance columns + // (The first column is the user IDs) + let balance_column_range = 1..N_CURRENCIES + 1; + + // The Custodian makes a batch opening proof of all user balance polynomials at x = 0 for the Verifier + let grand_sums_batch_proof = open_grand_sums::( + &advice_polys.advice_polys, + &advice_polys.advice_blinds, + ¶ms, + balance_column_range, + ); + + // 2. Verification phase + // The Verifier verifies the ZK proof + assert!(full_verifier(¶ms, &vk, &zk_snark_proof, vec![vec![]])); + + // The Custodian communicates the (invalid) polynomial degree to the Verifier + let invalid_poly_degree = u64::try_from(advice_polys.advice_polys[0].len()).unwrap() - 1; + + // Both the Custodian and the Verifier know what column range are the balance columns + let balance_column_range = 1..N_CURRENCIES + 1; + + // The Custodian communicates the KZG batch opening transcript to the Verifier + // The Verifier verifies the KZG batch opening and calculates the grand sums + let (verified, grand_sum) = verify_grand_sum_openings::( + ¶ms, + &zk_snark_proof, + grand_sums_batch_proof, + invalid_poly_degree, + balance_column_range, + ); + + // The opened grand sum is smaller than the actual sum of balances extracted from the csv file + assert!(verified); + for i in 0..N_CURRENCIES { + assert_ne!(csv_total[i], grand_sum[i]); + } } - // Building a proof using as input a csv file with an entry that is not in range [0, 2^N_BYTES*8 - 1] should fail the range check constraint on the leaf balance + // Building a proof using as input a csv file with an entry that is not in range [0, 2^64 - 1] should fail the range check constraint on the leaf balance #[test] fn test_balance_not_in_range() { let path = "../csv/entry_16_overflow.csv"; let mut entries: Vec> = vec![Entry::init_empty(); N_USERS]; let mut cryptos = vec![Cryptocurrency::init_empty(); N_CURRENCIES]; - parse_csv_to_entries::<&str, N_CURRENCIES, N_BYTES>(path, &mut entries, &mut cryptos) - .unwrap(); + parse_csv_to_entries::<&str, N_CURRENCIES>(path, &mut entries, &mut cryptos).unwrap(); - let circuit = UnivariateGrandSum::::init(entries.to_vec()); + let circuit = UnivariateGrandSum::::init(entries.to_vec()); let invalid_prover = MockProver::run(K, &circuit, vec![vec![]]).unwrap(); @@ -267,21 +266,21 @@ mod test { Err(vec![ VerifyFailure::Permutation { column: (Any::Fixed, 0).into(), - location: FailureLocation::OutsideRegion { row: 256 } + location: FailureLocation::OutsideRegion { row: 65536 } }, VerifyFailure::Permutation { column: (Any::Fixed, 0).into(), - location: FailureLocation::OutsideRegion { row: 259 } + location: FailureLocation::OutsideRegion { row: 65539 } }, VerifyFailure::Permutation { - column: (Any::advice(), 10).into(), + column: (Any::advice(), 6).into(), location: FailureLocation::InRegion { region: (2, "Perform range check on balance 0 of user 0").into(), offset: 0 } }, VerifyFailure::Permutation { - column: (Any::advice(), 18).into(), + column: (Any::advice(), 10).into(), location: FailureLocation::InRegion { region: (5, "Perform range check on balance 1 of user 1").into(), offset: 0 @@ -301,10 +300,9 @@ mod test { let mut entries: Vec> = vec![Entry::init_empty(); N_USERS]; let mut cryptos = vec![Cryptocurrency::init_empty(); N_CURRENCIES]; let _ = - parse_csv_to_entries::<&str, N_CURRENCIES, N_BYTES>(path, &mut entries, &mut cryptos) - .unwrap(); + parse_csv_to_entries::<&str, N_CURRENCIES>(path, &mut entries, &mut cryptos).unwrap(); - let circuit = UnivariateGrandSum::::init(entries); + let circuit = UnivariateGrandSum::::init(entries); let root = BitMapBackend::new("prints/univariate-grand-sum-layout.png", (2048, 32768)) .into_drawing_area(); @@ -317,4 +315,38 @@ mod test { .render(K, &circuit, &root) .unwrap(); } + + fn set_up( + path: &str, + ) -> ( + Vec>, + UnivariateGrandSum, + ProvingKey, + VerifyingKey, + ParamsKZG, + ) + where + [(); N_CURRENCIES + 1]:, + { + // Initialize an empty circuit + let circuit = UnivariateGrandSum::::init_empty(); + + // Generate a universal trusted setup for testing purposes. + // + // The verification key (vk) and the proving key (pk) are then generated. + // An empty circuit is used here to emphasize that the circuit inputs are not relevant when generating the keys. + // Important: The dimensions of the circuit used to generate the keys must match those of the circuit used to generate the proof. + // In this case, the dimensions are represented by the number fo users. + let (params, pk, vk) = generate_setup_artifacts(K, None, circuit).unwrap(); + + // Only now we can instantiate the circuit with the actual inputs + let mut entries: Vec> = vec![Entry::init_empty(); N_USERS]; + let mut cryptos = vec![Cryptocurrency::init_empty(); N_CURRENCIES]; + + parse_csv_to_entries::<&str, N_CURRENCIES>(path, &mut entries, &mut cryptos).unwrap(); + + let circuit = UnivariateGrandSum::::init(entries.to_vec()); + + (entries, circuit, pk, vk, params) + } } diff --git a/kzg_prover/src/circuits/univariate_grand_sum.rs b/kzg_prover/src/circuits/univariate_grand_sum.rs index eb5ff7b7..d23fc82c 100644 --- a/kzg_prover/src/circuits/univariate_grand_sum.rs +++ b/kzg_prover/src/circuits/univariate_grand_sum.rs @@ -1,4 +1,4 @@ -use crate::chips::range::range_check::{RangeCheckChip, RangeCheckConfig}; +use crate::chips::range::range_check::{RangeCheckU64Chip, RangeCheckU64Config}; use crate::entry::Entry; use crate::utils::big_uint_to_fp; use halo2_proofs::circuit::{AssignedCell, Layouter, SimpleFloorPlanner, Value}; @@ -6,14 +6,11 @@ use halo2_proofs::halo2curves::bn256::Fr as Fp; use halo2_proofs::plonk::{Advice, Circuit, Column, ConstraintSystem, Error, Fixed}; #[derive(Clone)] -pub struct UnivariateGrandSum -{ +pub struct UnivariateGrandSum { pub entries: Vec>, } -impl - UnivariateGrandSum -{ +impl UnivariateGrandSum { pub fn init_empty() -> Self { Self { entries: vec![Entry::init_empty(); N_USERS], @@ -31,28 +28,28 @@ impl /// Configuration for the Mst Inclusion circuit /// # Type Parameters /// -/// * `N_BYTES`: The number of bytes in which the balances should lie /// * `N_CURRENCIES`: The number of currencies for which the solvency is verified. +/// * `N_USERS`: The number of users for which the solvency is verified. /// /// # Fields /// /// * `username`: Advice column used to store the usernames of the users /// * `balances`: Advice columns used to store the balances of the users /// * `range_check_configs`: Configurations for the range check chip -/// * `range`: Fixed column used to store the lookup table for the range check chip +/// * `range_u16`: Fixed column used to store the lookup table [0, 2^16 - 1] for the range check chip #[derive(Debug, Clone)] -pub struct UnivariateGrandSumConfig +pub struct UnivariateGrandSumConfig where [(); N_CURRENCIES + 1]:, { username: Column, balances: [Column; N_CURRENCIES], - range_check_configs: [RangeCheckConfig; N_CURRENCIES], - range: Column, + range_check_configs: [RangeCheckU64Config; N_CURRENCIES], + range_u16: Column, } -impl - UnivariateGrandSumConfig +impl + UnivariateGrandSumConfig where [(); N_CURRENCIES + 1]:, { @@ -61,25 +58,25 @@ where let balances = [(); N_CURRENCIES].map(|_| meta.unblinded_advice_column()); - let range = meta.fixed_column(); + let range_u16 = meta.fixed_column(); - meta.enable_constant(range); + meta.enable_constant(range_u16); - meta.annotate_lookup_any_column(range, || "LOOKUP_MAXBITS_RANGE"); + meta.annotate_lookup_any_column(range_u16, || "LOOKUP_MAXBITS_RANGE"); // Create an empty array of range check configs let mut range_check_configs = Vec::with_capacity(N_CURRENCIES); for i in 0..N_CURRENCIES { let z = balances[i]; - // Create N_BYTES advice columns for each range check chip - let zs = [(); N_BYTES].map(|_| meta.advice_column()); + // Create 4 advice columns for each range check chip + let zs = [(); 4].map(|_| meta.advice_column()); for column in zs.iter() { meta.enable_equality(*column); } - let range_check_config = RangeCheckChip::::configure(meta, z, zs, range); + let range_check_config = RangeCheckU64Chip::configure(meta, z, zs, range_u16); range_check_configs.push(range_check_config); } @@ -91,7 +88,7 @@ where username, balances, range_check_configs: range_check_configs.try_into().unwrap(), - range, + range_u16, } } /// Assigns the entries to the circuit @@ -108,17 +105,17 @@ where // create a bidimensional vector to store the assigned balances. The first dimension is N_USERS, the second dimension is N_CURRENCIES let mut assigned_balances = vec![]; - for (i, entry) in entries.iter().enumerate() { + for i in 0..N_USERS { region.assign_advice( || "username", self.username, i, - || Value::known(big_uint_to_fp(entry.username_as_big_uint())), + || Value::known(big_uint_to_fp(entries[i].username_as_big_uint())), )?; let mut assigned_balances_row = vec![]; - for (j, balance) in entry.balances().iter().enumerate() { + for (j, balance) in entries[i].balances().iter().enumerate() { let assigned_balance = region.assign_advice( || format!("balance {}", j), self.balances[j], @@ -138,21 +135,20 @@ where } } -impl Circuit - for UnivariateGrandSum +impl Circuit + for UnivariateGrandSum where [(); N_CURRENCIES + 1]:, { - type Config = UnivariateGrandSumConfig; + type Config = UnivariateGrandSumConfig; type FloorPlanner = SimpleFloorPlanner; fn without_witnesses(&self) -> Self { Self::init_empty() } - /// Configures the circuit fn configure(meta: &mut ConstraintSystem) -> Self::Config { - UnivariateGrandSumConfig::::configure(meta) + UnivariateGrandSumConfig::::configure(meta) } fn synthesize( @@ -164,19 +160,19 @@ where let range_check_chips = config .range_check_configs .iter() - .map(|config| RangeCheckChip::construct(*config)) + .map(|config| RangeCheckU64Chip::construct(*config)) .collect::>(); - // Load lookup table to perform range check on individual balances -> Each balance should be in the range [0, 2^8 - 1] - let range = 1 << 8; + // Load lookup table for range check u64 chip + let range = 1 << 16; layouter.assign_region( - || format!("load range check table of {} bits", 8 * N_BYTES), + || format!("load range check table of 16 bits"), |mut region| { for i in 0..range { region.assign_fixed( || "assign cell in fixed column", - config.range, + config.range_u16, i, || Value::known(Fp::from(i as u64)), )?; diff --git a/kzg_prover/src/utils/csv_parser.rs b/kzg_prover/src/utils/csv_parser.rs index 4f2403b0..9f3abe89 100644 --- a/kzg_prover/src/utils/csv_parser.rs +++ b/kzg_prover/src/utils/csv_parser.rs @@ -7,7 +7,7 @@ use std::path::Path; use crate::cryptocurrency::Cryptocurrency; use crate::entry::Entry; -pub fn parse_csv_to_entries, const N_ASSETS: usize, const N_BYTES: usize>( +pub fn parse_csv_to_entries, const N_ASSETS: usize>( path: P, entries: &mut [Entry], cryptocurrencies: &mut [Cryptocurrency], diff --git a/zk_prover/src/chips/range/utils.rs b/zk_prover/src/chips/range/utils.rs index c246816a..1b20a29a 100644 --- a/zk_prover/src/chips/range/utils.rs +++ b/zk_prover/src/chips/range/utils.rs @@ -2,7 +2,7 @@ use crate::merkle_sum_tree::utils::{big_uint_to_fp, fp_to_big_uint}; use halo2_proofs::halo2curves::bn256::Fr as Fp; use num_bigint::BigUint; -/// Converts value Fp to n bytes of bytes in little endian order. +/// Converts value Fp to array of n bytes in little endian order. /// If value is decomposed in #bytes which are less than n, then the returned bytes are padded with 0s at the most significant bytes. /// Example: /// decompose_fp_to_bytes(0x1f2f3f, 4) -> [0x3f, 0x2f, 0x1f, 0x00]