From 3df25b4abd3d1d4416ce507f4b3b7b5fa2213256 Mon Sep 17 00:00:00 2001 From: jaehunkim Date: Mon, 23 Sep 2024 05:25:58 -0700 Subject: [PATCH] feat: implement commitment phase of fri verifier (#13) * bugfix: fix bug caused by not handling mont form of stone * feat: Add helper function for LDE calculation * feat: implement fri parameters and foler function for verifying fri * feat: implement commitment phase of fri verifier --- Cargo.toml | 4 +- felt/src/lib.rs | 21 ++- fri/Cargo.toml | 4 + fri/src/folder.rs | 29 ++++ fri/src/lde.rs | 4 +- fri/src/lib.rs | 3 + fri/src/parameters.rs | 87 ++++++++++ fri/src/verifier.rs | 388 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 536 insertions(+), 4 deletions(-) create mode 100644 fri/src/folder.rs create mode 100644 fri/src/parameters.rs create mode 100644 fri/src/verifier.rs diff --git a/Cargo.toml b/Cargo.toml index 38b02a9..5543e56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ sha3 = "0.10.8" blake2 = "0.10.6" hex-literal = "0.4.1" num-bigint = "0.4.3" +serde_json = "1.0.122" +serde = { version = "1.0.205", features = ["derive"] } paste = "1.0" hex = "0.4" -rand = "0.8" \ No newline at end of file +rand = "0.8" diff --git a/felt/src/lib.rs b/felt/src/lib.rs index 747b60f..a74cd35 100644 --- a/felt/src/lib.rs +++ b/felt/src/lib.rs @@ -1,7 +1,8 @@ use ark_ff::{ fields::{MontBackend, MontConfig}, - Fp256, Zero, + BigInteger, Fp256, PrimeField, Zero, }; +use std::fmt::Write; #[derive(MontConfig)] #[modulus = "3618502788666131213697322783095070105623107215331596699973092056135872020481"] @@ -27,6 +28,24 @@ pub fn hex(hex: &str) -> Felt252 { res } +/// This method is used for testing / debugging purposes. +pub fn felt_252_to_hex(felt: &F) -> String { + let bigint = felt.into_bigint().to_bytes_be(); + let hex_string = bigint.iter().fold(String::new(), |mut output, b| { + let _ = write!(output, "{:02x}", b); + output + }); + + // remove leading 0 + let hex_string = hex_string.trim_start_matches('0').to_string(); + // add leading 0x + format!("0x{}", hex_string) +} + +pub fn byte_size() -> usize { + (F::MODULUS_BIT_SIZE.div_ceil(8) * 8) as usize +} + #[cfg(test)] mod tests { use super::*; diff --git a/fri/Cargo.toml b/fri/Cargo.toml index 7a0eabf..0839fc3 100644 --- a/fri/Cargo.toml +++ b/fri/Cargo.toml @@ -11,3 +11,7 @@ felt = { path = "../felt" } randomness = { path = "../randomness" } channel = { path = "../channel" } sha3.workspace = true +serde_json.workspace = true +serde.workspace = true +commitment_scheme = { path = "../commitment_scheme" } +anyhow.workspace = true diff --git a/fri/src/folder.rs b/fri/src/folder.rs new file mode 100644 index 0000000..275fb0f --- /dev/null +++ b/fri/src/folder.rs @@ -0,0 +1,29 @@ +use ark_ff::FftField; + +pub struct MultiplicativeFriFolder; + +#[allow(dead_code)] +impl MultiplicativeFriFolder { + pub fn next_layer_element_from_two_previous_layer_elements( + f_x: F, + f_minus_x: F, + eval_point: F, + x_inv: F, + ) -> F { + Self::fold(f_x, f_minus_x, eval_point, x_inv) + } + + /// Interpolating a line through (x, f(x)) and (-x, f(-x)) + /// then evaluating it at "eval_point" + /// Multiplicative case folding formula: + /// f(x) = g(x^2) + xh(x^2) + /// f(-x) = g((-x)^2) - xh((-x)^2) = g(x^2) - xh(x^2) + /// => + /// 2g(x^2) = f(x) + f(-x) + /// 2h(x^2) = (f(x) - f(-x))/x + /// => + /// 2g(x^2) + 2ah(x^2) = f(x) + f(-x) + a(f(x) - f(-x))/x. + fn fold(f_x: F, f_minus_x: F, eval_point: F, x_inv: F) -> F { + f_x + f_minus_x + eval_point * (f_x - f_minus_x) * x_inv + } +} diff --git a/fri/src/lde.rs b/fri/src/lde.rs index 1391640..0c5a812 100644 --- a/fri/src/lde.rs +++ b/fri/src/lde.rs @@ -53,7 +53,7 @@ impl MultiplicativeLDE { // Evaluates the low degree extension of the evaluation that were previously added on a given coset. // The results are ordered according to the order that the LDEs were added. - pub fn eval(&self, offset: F) -> Vec> { + pub fn batch_eval(&self, offset: F) -> Vec> { let eval_domain = self.base.get_coset(offset).unwrap(); let mut evals: Vec> = vec![]; for lde_poly in self.ldes.iter() { @@ -143,7 +143,7 @@ mod tests { let mut lde = MultiplicativeLDE::new(domain, false); lde.add_eval(&src); - let evals = lde.eval(eval_domain_offset); + let evals = lde.batch_eval(eval_domain_offset); let eval_domain = domain.get_coset(eval_domain_offset).unwrap(); for (x, &result) in eval_domain.elements().zip(evals[0].iter()) { diff --git a/fri/src/lib.rs b/fri/src/lib.rs index 16f848f..6f27843 100644 --- a/fri/src/lib.rs +++ b/fri/src/lib.rs @@ -1,2 +1,5 @@ +mod folder; mod lde; +mod parameters; mod stone_domain; +mod verifier; diff --git a/fri/src/parameters.rs b/fri/src/parameters.rs new file mode 100644 index 0000000..01e492c --- /dev/null +++ b/fri/src/parameters.rs @@ -0,0 +1,87 @@ +use std::marker::PhantomData; + +use ark_ff::FftField; +use ark_poly::EvaluationDomain; +use serde::Deserialize; + +#[derive(Deserialize)] +#[allow(dead_code)] +pub struct FriParameters> { + #[serde(skip)] + pub ph: PhantomData, + + /// A list of fri_step_i (one per FRI layer). FRI reduction in the i-th layer will be 2^fri_step_i + /// and the total reduction factor will be $2^{\sum_i \fri_step_i}$. The size of fri_step_list is + /// the number of FRI layers. + /// + /// For example, if fri_step_0 = 3, the second layer will be of size N/8 (where N is the size of the + /// first layer). It means that the two merkle trees for layers of sizes N/2 and N/4 will be + /// skipped. On the other hand, it means that each coset in the first layer is of size 8 instead + /// of 2. Also note that in the fri_step_0=1 case we send 2 additional field elements per query (one + /// for each of the two layers that we skipped). So, while we send more field elements in the + /// fri_step_0=3 case (8 rather than 4), we refrain from sending the authentication paths for the + /// two skipped layers. + /// + /// For a simple FRI usage, take fri_step_list = {1, 1, ..., 1}. + pub fri_step_list: Vec, + + /// In the original FRI protocol, one has to reduce the degree from N to 1 by using a total of + /// log2(N) fri steps (sum of fri_step_list = log2(N)). This has two disadvantages: + /// 1. The last layers are small but still require Merkle authentication paths which are + /// non-negligible. + /// 2. It requires N to be of the form 2^n. + /// + /// In our implementation, we reduce the degree from N to R (last_layer_degree_bound) for a + /// relatively small R using log2(N/R) fri steps. To do it we send the R coefficients of the + /// last FRI layer instead of continuing with additional FRI layers. + /// + /// To reduce proof-length, it is always better to pick last_layer_degree_bound > 1. + pub last_layer_degree_bound: usize, + pub n_queries: usize, + #[serde(skip)] + pub fft_domains: Vec, + + /// If greater than 0, used to apply proof of work right before randomizing the FRI queries. Since + /// the probability to draw bad queries is relatively high (~rho for each query), while the + /// probability to draw bad x^(0) values is ~1/|F|, the queries are more vulnerable to enumeration. + pub proof_of_work_bits: usize, +} + +#[allow(dead_code)] +impl> FriParameters { + pub fn new( + fri_step_list: Vec, + last_layer_degree_bound: usize, + n_queries: usize, + fft_domains: Vec, + proof_of_work_bits: usize, + ) -> Self { + FriParameters { + ph: PhantomData, + fri_step_list, + last_layer_degree_bound, + n_queries, + fft_domains, + proof_of_work_bits, + } + } + + pub fn with_fft_domains(mut self, fft_domains: Vec) -> Self { + self.fft_domains = fft_domains; + self + } +} + +#[allow(dead_code)] +pub struct FriProverConfig { + pub max_non_chunked_layer_size: u64, + pub n_chunks_between_layers: usize, + pub log_n_max_in_memory_fri_layer_elements: usize, +} + +#[allow(dead_code)] +impl FriProverConfig { + pub const DEFAULT_MAX_NON_CHUNKED_LAYER_SIZE: u64 = 32768; + pub const DEFAULT_NUMBER_OF_CHUNKS_BETWEEN_LAYERS: usize = 32; + pub const ALL_IN_MEMORY_LAYERS: usize = 63; +} diff --git a/fri/src/verifier.rs b/fri/src/verifier.rs new file mode 100644 index 0000000..1751fb5 --- /dev/null +++ b/fri/src/verifier.rs @@ -0,0 +1,388 @@ +use ark_ff::{FftField, PrimeField}; +use ark_poly::{domain::EvaluationDomain, Radix2EvaluationDomain}; +use felt::byte_size; +use randomness::Prng; +use sha3::Digest; + +use crate::{lde::MultiplicativeLDE, parameters::FriParameters}; +use channel::{fs_verifier_channel::FSVerifierChannel, Channel, VerifierChannel}; +use commitment_scheme::{ + make_commitment_scheme_verifier, table_verifier::TableVerifier, CommitmentHashes, +}; + +#[allow(dead_code)] +pub trait FirstLayerQueriesCallback { + fn query(&self, indices: &[u64]) -> Vec; +} + +#[allow(dead_code)] +pub trait FriVerifierTrait< + F: FftField + PrimeField, + P: Prng + Clone + 'static, + W: Digest + Clone + 'static, + FQ: FirstLayerQueriesCallback, +> +{ + fn verify_fri(&mut self) -> Result<(), anyhow::Error> { + self.read_eval_points()?; + self.read_commitments()?; + self.read_last_layer_coefficients()?; + Ok(()) + } + + fn read_eval_points(&mut self) -> Result<(), anyhow::Error>; + fn read_commitments(&mut self) -> Result<(), anyhow::Error>; + fn read_last_layer_coefficients(&mut self) -> Result<(), anyhow::Error>; +} + +#[allow(dead_code)] +pub struct FriVerifier< + F: FftField + PrimeField, + P: Prng + Clone + 'static, + W: Digest + Clone + 'static, + FQ: FirstLayerQueriesCallback, +> { + channel: FSVerifierChannel, + params: FriParameters>, + commitment_hashes: CommitmentHashes, + first_layer_callback: FQ, + n_layers: usize, + first_eval_point: F, + eval_points: Vec, + table_verifiers: Vec>, + query_indices: Vec, + query_results: Vec, + expected_last_layer: Vec, +} + +impl< + F: FftField + PrimeField, + P: Prng + Clone + 'static, + W: Digest + Clone + 'static, + FQ: FirstLayerQueriesCallback, + > FriVerifierTrait for FriVerifier +{ + fn read_eval_points(&mut self) -> Result<(), anyhow::Error> { + let mut _basis_index = 0; + for i in 0..self.n_layers { + let cur_fri_step = self.params.fri_step_list[i]; + _basis_index += cur_fri_step; + + if i == 0 { + if self.params.fri_step_list[0] != 0 { + self.first_eval_point = self.channel.draw_felem(); + } + } else { + self.eval_points.push(self.channel.draw_felem()); + } + } + Ok(()) + } + + fn read_commitments(&mut self) -> Result<(), anyhow::Error> { + let mut basis_index = 0; + for i in 0..self.n_layers { + let cur_fri_step = self.params.fri_step_list[i]; + basis_index += cur_fri_step; + + if i < self.n_layers - 1 { + let coset_size = 1 << self.params.fri_step_list[i + 1]; + let n_rows = self.params.fft_domains[basis_index].size() / coset_size; + let n_columns = coset_size; + let size_of_row = byte_size::() * n_columns; + + let commitment_scheme = make_commitment_scheme_verifier( + size_of_row, + n_rows, + 0, + self.commitment_hashes.clone(), + n_columns, + ); + + let mut table_verifier = TableVerifier::new(n_columns, commitment_scheme); + let _ = table_verifier.read_commitment(&mut self.channel); + self.table_verifiers.push(table_verifier); + } + } + Ok(()) + } + + fn read_last_layer_coefficients(&mut self) -> Result<(), anyhow::Error> { + let mut last_layer_coefficients_vector = self + .channel + .recv_felts(self.params.last_layer_degree_bound)?; + + let fri_step_sum: usize = self.params.fri_step_list.iter().sum(); + let last_layer_size = self.params.fft_domains[fri_step_sum].size(); + + // pad last_layer_coefficients_vector with zeros to the size of last_layer_size + while last_layer_coefficients_vector.len() < last_layer_size { + last_layer_coefficients_vector.push(F::zero()); + } + + assert!( + self.params.last_layer_degree_bound <= last_layer_size, + "last_layer_degree_bound ({}) must be <= last_layer_size ({})", + self.params.last_layer_degree_bound, + last_layer_size + ); + + let last_layer_basis_index = fri_step_sum; + let lde_domain = self.params.fft_domains[last_layer_basis_index]; + let mut lde = MultiplicativeLDE::new(lde_domain, true); + + lde.add_coeff(&last_layer_coefficients_vector); + let evals = lde.batch_eval(lde_domain.element(0)); + self.expected_last_layer.clone_from(&evals[0]); + + Ok(()) + } +} + +#[allow(dead_code)] +impl< + F: FftField + PrimeField, + P: Prng + Clone + 'static, + W: Digest + Clone + 'static, + FQ: FirstLayerQueriesCallback, + > FriVerifier +{ + pub fn new( + channel: FSVerifierChannel, + params: FriParameters>, + commitment_hashes: CommitmentHashes, + first_layer_callback: FQ, + ) -> Self { + let n_layers = params.fri_step_list.len(); + Self { + channel, + params, + commitment_hashes, + first_layer_callback, + n_layers, + first_eval_point: F::zero(), + eval_points: vec![], + table_verifiers: vec![], + query_indices: vec![], + query_results: vec![], + expected_last_layer: vec![], + } + } +} + +#[cfg(test)] +mod fri_tests { + use ark_poly::{ + domain::EvaluationDomain, univariate::DensePolynomial, DenseUVPolynomial, Polynomial, + }; + use channel::fs_prover_channel::FSProverChannel; + use commitment_scheme::SupportedHashes; + use felt::{hex, Felt252}; + use randomness::{keccak256::PrngKeccak256, Prng}; + use sha3::Sha3_256; + + use crate::stone_domain::make_fft_domains; + + use super::*; + + type TestProverChannel = FSProverChannel; + + struct TestFirstLayerQueriesCallback; + + impl FirstLayerQueriesCallback for TestFirstLayerQueriesCallback { + fn query(&self, _indices: &[u64]) -> Vec { + vec![] + } + } + + pub struct TestFriVerifier< + F: FftField + PrimeField, + P: Prng + Clone + 'static, + W: Digest + Clone + 'static, + FQ: FirstLayerQueriesCallback, + > { + params: FriParameters>, + commitment_hashes: CommitmentHashes, + first_layer_callback: FQ, + n_layers: usize, + first_eval_point: F, + eval_points: Vec, + table_verifiers: Vec>, + query_indices: Vec, + query_results: Vec, + expected_last_layer: Vec, + mock_last_layer_coefs: Vec, + } + + impl< + F: FftField + PrimeField, + P: Prng + Clone + 'static, + W: Digest + Clone + 'static, + FQ: FirstLayerQueriesCallback, + > TestFriVerifier + { + pub fn new( + params: FriParameters>, + commitment_hashes: CommitmentHashes, + first_layer_callback: FQ, + mock_eval_points: Vec, + mock_last_layer_coefs: Vec, + ) -> Self { + let n_layers = params.fri_step_list.len(); + Self { + params, + commitment_hashes, + first_layer_callback, + n_layers, + first_eval_point: mock_eval_points[0], + eval_points: mock_eval_points[1..].to_vec(), + table_verifiers: vec![], + query_indices: vec![], + query_results: vec![], + expected_last_layer: vec![], + mock_last_layer_coefs, + } + } + } + + impl< + F: FftField + PrimeField, + P: Prng + Clone + 'static, + W: Digest + Clone + 'static, + FQ: FirstLayerQueriesCallback, + > FriVerifierTrait for TestFriVerifier + { + fn read_eval_points(&mut self) -> Result<(), anyhow::Error> { + // Eval points are set in constructor + Ok(()) + } + + fn read_commitments(&mut self) -> Result<(), anyhow::Error> { + // Table verifier is tightly coupled with the channel, so we exclude this part in the mock test. + // If we could manually change the 'comm' variable of MerkleCommitmentSchemeVerifier, we might be able to test this part. + Ok(()) + } + + fn read_last_layer_coefficients(&mut self) -> Result<(), anyhow::Error> { + let mut last_layer_coefficients_vector = self.mock_last_layer_coefs.clone(); + + let fri_step_sum: usize = self.params.fri_step_list.iter().sum(); + let last_layer_size = self.params.fft_domains[fri_step_sum].size(); + + // pad last_layer_coefficients_vector with zeros to the size of last_layer_size + while last_layer_coefficients_vector.len() < last_layer_size { + last_layer_coefficients_vector.push(F::zero()); + } + + assert!( + self.params.last_layer_degree_bound <= last_layer_size, + "last_layer_degree_bound ({}) must be <= last_layer_size ({})", + self.params.last_layer_degree_bound, + last_layer_size + ); + + let last_layer_basis_index = fri_step_sum; + let lde_domain = self.params.fft_domains[last_layer_basis_index]; + let mut lde = MultiplicativeLDE::new(lde_domain, true); + + lde.add_coeff(&last_layer_coefficients_vector); + let evals = lde.batch_eval(lde_domain.element(0)); + self.expected_last_layer.clone_from(&evals[0]); + + Ok(()) + } + } + + fn generate_prover_channel() -> TestProverChannel { + let prng: PrngKeccak256 = PrngKeccak256::new_with_seed(&[0u8; 4]); + TestProverChannel::new(prng) + } + + fn gen_random_field_element(prover_channel: &mut TestProverChannel) -> Felt252 { + prover_channel.draw_felem() + } + + #[test] + fn mock_commitment_phase() { + let mut rng = generate_prover_channel(); + + let last_layer_degree_bound = 5; + let proof_of_work_bits = 15; + let domain_size_log = 10; + + let offset = Felt252::from(777u64); + let domains = make_fft_domains::(domain_size_log, offset); + // check domains size is domain_size_log + 1 + assert_eq!(domains.len(), domain_size_log + 1); + + let params = FriParameters::new( + vec![2, 3, 1], + last_layer_degree_bound, + 2, + domains.clone(), + proof_of_work_bits, + ); + + let poly_coeffs: Vec = (0..64 * last_layer_degree_bound) + .map(|_| gen_random_field_element(&mut rng)) + .collect(); + + let test_layer = DensePolynomial::from_coefficients_vec(poly_coeffs); + // i will use this later + let _witness: Vec = domains[0] + .elements() + .map(|x| test_layer.evaluate(&x)) + .collect(); + + let fourth_layer_evaluations = vec![ + hex("0x663aa85d164d449a2a7c04698fb3f24f1d049984c1539c7ac3b71839ce485fd"), + hex("0x16eae1252002c24abc155c65f65cdc0df65ab85219324923be3773d1c8e99ae"), + hex("0x413a20799e9aafcbec1564442875e2b61df6ba4fc645de1764b5d60122b80fa"), + hex("0x9421b1a9cc663ae06caf9d2b6d2fc0838ed9953bb3ff01ad5d423a3e023833"), + hex("0x62a27bc1c62f4c9eefe90ef9e72923f684a46cf231d915fb7a5bc8341210462"), + hex("0xe341cd1674e7b9c4b0b21873e161df37be5a112b8c8b2f884238e39aea25cd"), + hex("0x392e66a7a39a56f907f16a32ce9a9ca7c59ad31b9da6b1b30594075b398add9"), + hex("0x19bd01106bd63a97c491540601007b21e42afd41bcb99644842ee61ea5a2876"), + hex("0x529c386668d5486b6342a92a69c6dd87269d956a51f0b189e86f6127132a637"), + hex("0x4922f64192cbb0a0c390d984fdfe586d80c90378a62184d2129af6731c7c6a3"), + hex("0x3a47a881517129e0f95bf700f820ff7c2077efce82aaa38b1782556f14b8b98"), + hex("0x4c1a36f5f5a3a84d7a014d37bd1ccb66cc6bd3aaa8d2da67dcbe41c6e4df1d4"), + hex("0x39ad62ac6c2b639601d3cb28537a10be0558c3795ac45e6201c9cd79950dda8"), + hex("0x51769b8cd304f464988512d43add79c0f24b796daa9ea912d80d851db31b2e0"), + hex("0x2c55afe71ba57d40cf2c1e16c23c1a1022087d85a99820b5044cb01da6822c4"), + hex("0x31c909221113d4c4dd32eaa4ec9d6168669c6d13ce82e843774bd8132203bc5"), + ]; + + let mut fourth_layer_lde = MultiplicativeLDE::new(domains[6], true); + fourth_layer_lde.add_eval(&fourth_layer_evaluations); + let fourth_layer_coefs = fourth_layer_lde.coeffs(0); + + // check from 6th elements of fourth_layer_coefs are all zero + for i in 6..fourth_layer_coefs.len() { + assert_eq!(fourth_layer_coefs[i], Felt252::from(0u64)); + } + + // Choose evaluation points for the three layers + let _eval_points = vec![ + hex("0x7f097aaa40a3109067011986ae40f1ce97a01f4f1a72d80a52821f317504992"), + hex("0x18bcafdd60fc70e5e8a9a18687135d0bf1a355d9882969a6b3619e56bf2d49d"), + hex("0x2f06b17e08bc409b945b951de8102653dc48a143b87d09b6c95587679816d02"), + ]; + + let commitment_hashes = CommitmentHashes::from_single_hash(SupportedHashes::Blake2s256); + + let mut fri_verifier = TestFriVerifier::< + Felt252, + PrngKeccak256, + Sha3_256, + TestFirstLayerQueriesCallback, + >::new( + params, + commitment_hashes, + TestFirstLayerQueriesCallback, + _eval_points, + fourth_layer_coefs.to_vec(), + ); + fri_verifier.verify_fri().unwrap(); + } +}