From 7fe9b6dee69b1eb6819922a8396d6e72e4b2589d Mon Sep 17 00:00:00 2001 From: Alan Szepieniec Date: Tue, 23 Apr 2024 13:48:17 +0200 Subject: [PATCH 1/4] feat: Add barycentric evaluation formula Credit to https://github.com/0xPolygonMiden/miden-vm/issues/568 --- triton-vm/src/fri.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/triton-vm/src/fri.rs b/triton-vm/src/fri.rs index 4794cb3cf..18297add8 100644 --- a/triton-vm/src/fri.rs +++ b/triton-vm/src/fri.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use itertools::Itertools; use rayon::iter::*; use twenty_first::math::traits::FiniteField; +use twenty_first::math::traits::PrimitiveRootOfUnity; use twenty_first::prelude::*; use crate::arithmetic_domain::ArithmeticDomain; @@ -650,6 +651,46 @@ fn codeword_as_digests(codeword: &[XFieldElement]) -> Vec { codeword.par_iter().map(|&xfe| xfe.into()).collect() } +/// Use the barycentric Lagrange evaluation formula to extrapolate the codeword +/// to an out-of-domain location. +/// +/// [Credit] for (re)discovering this formula and especially its application to +/// FRI goes to Al-Kindi. +/// +/// # Panics +/// +/// Panics if the codeword is some length that is not a power of 2 or greater than (1 << 32). +/// +/// [Credit]: https://github.com/0xPolygonMiden/miden-vm/issues/568 +pub fn barycentric_evaluate( + codeword: &[XFieldElement], + indeterminate: XFieldElement, +) -> XFieldElement { + let root_order = codeword.len().try_into().unwrap(); + let generator = BFieldElement::primitive_root_of_unity(root_order).unwrap(); + let domain_iter = (0..root_order) + .scan(bfe!(1), |acc, _| { + let to_yield = Some(*acc); + *acc *= generator; + to_yield + }) + .collect_vec(); + + let domain_shift = domain_iter.iter().map(|&d| indeterminate - d).collect(); + let domain_shift_inverses = XFieldElement::batch_inversion(domain_shift); + let domain_over_domain_shift = domain_iter + .into_iter() + .zip(domain_shift_inverses) + .map(|(d, inv)| d * inv); + let numerator = domain_over_domain_shift + .clone() + .zip(codeword) + .map(|(dsi, &abscis)| dsi * abscis) + .sum::(); + let denominator = domain_over_domain_shift.sum::(); + numerator / denominator +} + #[cfg(test)] mod tests { use std::cmp::max; @@ -658,6 +699,7 @@ mod tests { use assert2::assert; use assert2::let_assert; use itertools::Itertools; + use proptest::collection::vec; use proptest::prelude::*; use proptest_arbitrary_interop::arb; use rand::prelude::*; @@ -1022,4 +1064,21 @@ mod tests { ) { let _ = fri.verify(&mut proof_stream, &mut None); } + + #[proptest] + fn test_barycentric_evaluation( + #[strategy(1usize..13)] _log_num_coefficients: usize, + #[strategy(1usize..6)] log_expansion_factor: usize, + #[strategy(vec(arb(), 1 << #_log_num_coefficients))] coefficients: Vec, + #[strategy(arb())] indeterminate: XFieldElement, + ) { + let domain_len = coefficients.len() * (1 << log_expansion_factor); + let domain = ArithmeticDomain::of_length(domain_len).unwrap(); + let polynomial = Polynomial::from(&coefficients); + let codeword = domain.evaluate(&polynomial); + prop_assert_eq!( + polynomial.evaluate(indeterminate), + barycentric_evaluate(&codeword, indeterminate) + ); + } } From cff63b267dea0e2ba61b38c005ba835c83dc18b8 Mon Sep 17 00:00:00 2001 From: Alan Szepieniec Date: Tue, 23 Apr 2024 13:53:06 +0200 Subject: [PATCH 2/4] refactor: Use barycentric formula in verifier --- triton-vm/src/error.rs | 3 +++ triton-vm/src/fri.rs | 23 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/triton-vm/src/error.rs b/triton-vm/src/error.rs index 559c0b2db..de70c5eda 100644 --- a/triton-vm/src/error.rs +++ b/triton-vm/src/error.rs @@ -167,6 +167,9 @@ pub enum FriValidationError { #[error("computed and received codeword of last round do not match")] LastCodewordMismatch, + #[error("evaluations of last round's polynomial and last round codeword do not match")] + LastRoundPolynomialEvaluationMismatch, + #[error("last round's polynomial has too high degree")] LastRoundPolynomialHasTooHighDegree, diff --git a/triton-vm/src/fri.rs b/triton-vm/src/fri.rs index 18297add8..68fab7a06 100644 --- a/triton-vm/src/fri.rs +++ b/triton-vm/src/fri.rs @@ -451,7 +451,7 @@ impl<'stream, H: AlgebraicHasher> FriVerifier<'stream, H> { .collect() } - fn authenticate_last_round_codeword(&self) -> VerifierResult<()> { + fn authenticate_last_round_codeword(&mut self) -> VerifierResult<()> { self.assert_last_round_codeword_matches_last_round_commitment()?; self.assert_last_round_codeword_agrees_with_last_round_folded_codeword()?; self.assert_last_round_codeword_corresponds_to_low_degree_polynomial() @@ -501,9 +501,16 @@ impl<'stream, H: AlgebraicHasher> FriVerifier<'stream, H> { } fn assert_last_round_codeword_corresponds_to_low_degree_polynomial( - &self, + &mut self, ) -> VerifierResult<()> { - if self.last_round_polynomial().degree() > self.last_round_max_degree as isize { + let indeterminate = self.proof_stream.sample_scalars(1)[0]; + let last_round_polynomial = self.last_round_polynomial(); + let horner_evaluation = last_round_polynomial.evaluate(indeterminate); + let barycentric_evaluation = barycentric_evaluate(&self.last_round_codeword, indeterminate); + if horner_evaluation != barycentric_evaluation { + return Err(LastRoundPolynomialEvaluationMismatch); + } + if last_round_polynomial.degree() > self.last_round_max_degree.try_into().unwrap() { return Err(LastRoundPolynomialHasTooHighDegree); } Ok(()) @@ -511,7 +518,9 @@ impl<'stream, H: AlgebraicHasher> FriVerifier<'stream, H> { fn last_round_polynomial(&self) -> Polynomial { let domain = self.rounds.last().unwrap().domain; - domain.interpolate(&self.last_round_codeword) + domain + .with_offset(bfe!(1)) + .interpolate(&self.last_round_codeword) } fn first_round_partially_revealed_codeword(&self) -> Vec<(usize, XFieldElement)> { @@ -572,6 +581,12 @@ impl Fri { prover.commit(codeword)?; prover.query()?; + // Sample one XFieldElement from Fiat-Shamir and then throw it away. This + // scalar is the indeterminate for the low degree test using the barycentric + // evaluation formula. This indeterminate is used only by the verifier, but + // it is important to modify the sponge state the same way. + prover.proof_stream.sample_scalars(1); + let indices = prover.all_top_level_collinearity_check_indices(); Ok(indices) } From f8a59c5eddc755fd37146f46155a2926891ff84c Mon Sep 17 00:00:00 2001 From: Alan Szepieniec Date: Mon, 22 Apr 2024 16:34:48 +0200 Subject: [PATCH 3/4] perf!: Include last FRI polynomial into proof No performance change was observed on my laptop using benchmark `prove_verify_halt` (11ms in both cases) but the main selling point comes from the smaller anticipated clock cycle count in the recursive verifier. BREAKING CHANGE: Now the prover sends the last polynomial in addition to the last codeword in FRI. The verifier verifies that the polynomial is of low degree directly (without iNTTs!) and checks that it matches with the codeword using the barycentric evaluation function and randomness sampled from the proof stream's sponge state. Closes #156 --- triton-vm/src/fri.rs | 35 ++++++++++++++++++++++++----------- triton-vm/src/proof_item.rs | 2 ++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/triton-vm/src/fri.rs b/triton-vm/src/fri.rs index 68fab7a06..b842ad6cf 100644 --- a/triton-vm/src/fri.rs +++ b/triton-vm/src/fri.rs @@ -1,6 +1,7 @@ use std::marker::PhantomData; use itertools::Itertools; +use num_traits::Zero; use rayon::iter::*; use twenty_first::math::traits::FiniteField; use twenty_first::math::traits::PrimitiveRootOfUnity; @@ -54,6 +55,7 @@ impl<'stream, H: AlgebraicHasher> FriProver<'stream, H> { self.commit_to_next_round()?; } self.send_last_codeword(); + self.send_last_polynomial(); Ok(()) } @@ -95,6 +97,15 @@ impl<'stream, H: AlgebraicHasher> FriProver<'stream, H> { self.proof_stream.enqueue(proof_item); } + fn send_last_polynomial(&mut self) { + let last_codeword = &self.rounds.last().unwrap().codeword; + let last_polynomial = ArithmeticDomain::of_length(last_codeword.len()) + .unwrap() + .interpolate(last_codeword); + let proof_item = ProofItem::FriPolynomial(last_polynomial.coefficients); + self.proof_stream.enqueue(proof_item); + } + fn query(&mut self) -> ProverResult<()> { self.sample_first_round_collinearity_check_indices(); @@ -194,6 +205,7 @@ struct FriVerifier<'stream, H: AlgebraicHasher> { rounds: Vec, first_round_domain: ArithmeticDomain, last_round_codeword: Vec, + last_round_polynomial: Polynomial, last_round_max_degree: usize, num_rounds: usize, num_collinearity_checks: usize, @@ -212,7 +224,8 @@ struct VerifierRound { impl<'stream, H: AlgebraicHasher> FriVerifier<'stream, H> { fn initialize(&mut self) -> VerifierResult<()> { self.initialize_verification_rounds()?; - self.receive_last_round_codeword() + self.receive_last_round_codeword()?; + self.receive_last_round_polynomial() } fn initialize_verification_rounds(&mut self) -> VerifierResult<()> { @@ -289,6 +302,12 @@ impl<'stream, H: AlgebraicHasher> FriVerifier<'stream, H> { Ok(()) } + fn receive_last_round_polynomial(&mut self) -> VerifierResult<()> { + let coefficients = self.proof_stream.dequeue()?.try_into_fri_polynomial()?; + self.last_round_polynomial = Polynomial::new(coefficients); + Ok(()) + } + fn compute_last_round_folded_partial_codeword(&mut self) -> VerifierResult<()> { self.sample_first_round_collinearity_check_indices(); self.receive_authentic_partially_revealed_codewords()?; @@ -504,25 +523,17 @@ impl<'stream, H: AlgebraicHasher> FriVerifier<'stream, H> { &mut self, ) -> VerifierResult<()> { let indeterminate = self.proof_stream.sample_scalars(1)[0]; - let last_round_polynomial = self.last_round_polynomial(); - let horner_evaluation = last_round_polynomial.evaluate(indeterminate); + let horner_evaluation = self.last_round_polynomial.evaluate(indeterminate); let barycentric_evaluation = barycentric_evaluate(&self.last_round_codeword, indeterminate); if horner_evaluation != barycentric_evaluation { return Err(LastRoundPolynomialEvaluationMismatch); } - if last_round_polynomial.degree() > self.last_round_max_degree.try_into().unwrap() { + if self.last_round_polynomial.degree() > self.last_round_max_degree.try_into().unwrap() { return Err(LastRoundPolynomialHasTooHighDegree); } Ok(()) } - fn last_round_polynomial(&self) -> Polynomial { - let domain = self.rounds.last().unwrap().domain; - domain - .with_offset(bfe!(1)) - .interpolate(&self.last_round_codeword) - } - fn first_round_partially_revealed_codeword(&self) -> Vec<(usize, XFieldElement)> { let partial_codeword_a = self.rounds[0].partial_codeword_a.clone(); let partial_codeword_b = self.rounds[0].partial_codeword_b.clone(); @@ -631,6 +642,7 @@ impl Fri { rounds: vec![], first_round_domain: self.domain, last_round_codeword: vec![], + last_round_polynomial: Polynomial::zero(), last_round_max_degree: self.last_round_max_degree(), num_rounds: self.num_rounds(), num_collinearity_checks: self.num_collinearity_checks, @@ -913,6 +925,7 @@ mod tests { (MerkleRoot(p), MerkleRoot(v)) => prop_assert_eq!(p, v), (FriResponse(p), FriResponse(v)) => prop_assert_eq!(p, v), (FriCodeword(p), FriCodeword(v)) => prop_assert_eq!(p, v), + (FriPolynomial(p), FriPolynomial(v)) => prop_assert_eq!(p, v), _ => panic!("Unknown items.\nProver: {prover_item:?}\nVerifier: {verifier_item:?}"), } } diff --git a/triton-vm/src/proof_item.rs b/triton-vm/src/proof_item.rs index 6af51f8c6..0c0d8c06b 100644 --- a/triton-vm/src/proof_item.rs +++ b/triton-vm/src/proof_item.rs @@ -110,6 +110,7 @@ proof_items!( Log2PaddedHeight(u32) => false, try_into_log2_padded_height, QuotientSegmentsElements(Vec) => false, try_into_quot_segments_elements, FriCodeword(Vec) => false, try_into_fri_codeword, + FriPolynomial(Vec) => false, try_into_fri_polynomial, FriResponse(FriResponse) => false, try_into_fri_response, ); @@ -189,6 +190,7 @@ pub(crate) mod tests { assert!(let Err(UnexpectedItem{..}) = item.clone().try_into_log2_padded_height()); assert!(let Err(UnexpectedItem{..}) = item.clone().try_into_quot_segments_elements()); assert!(let Err(UnexpectedItem{..}) = item.clone().try_into_fri_codeword()); + assert!(let Err(UnexpectedItem{..}) = item.clone().try_into_fri_polynomial()); assert!(let Err(UnexpectedItem{..}) = item.try_into_fri_response()); } From 0fc7b7f577197384f7a9964b693ee68acd0e6d8f Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 23 Apr 2024 14:25:32 +0200 Subject: [PATCH 4/4] test: test failure of incorrect last ronud poly --- triton-vm/src/fri.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/triton-vm/src/fri.rs b/triton-vm/src/fri.rs index b842ad6cf..00054a61c 100644 --- a/triton-vm/src/fri.rs +++ b/triton-vm/src/fri.rs @@ -1085,6 +1085,28 @@ mod tests { } } + #[proptest] + fn incorrect_last_round_polynomial_results_in_verification_failure( + #[strategy(arbitrary_fri())] fri: Fri, + #[strategy(arbitrary_polynomial())] polynomial: Polynomial, + #[strategy(arb())] incorrect_coefficients: Vec, + ) { + let codeword = fri.domain.evaluate(&polynomial); + let mut proof_stream = ProofStream::new(); + fri.prove(&codeword, &mut proof_stream).unwrap(); + + let mut proof_stream = prepare_proof_stream_for_verification(proof_stream); + proof_stream.items.iter_mut().for_each(|item| { + if let ProofItem::FriPolynomial(coefficients) = item { + *coefficients = incorrect_coefficients.clone(); + } + }); + + let verdict = fri.verify(&mut proof_stream, &mut None); + let_assert!(Err(err) = verdict); + assert!(let LastRoundPolynomialEvaluationMismatch = err); + } + #[proptest] fn verifying_arbitrary_proof_does_not_panic( #[strategy(arbitrary_fri())] fri: Fri, @@ -1094,7 +1116,7 @@ mod tests { } #[proptest] - fn test_barycentric_evaluation( + fn polynomial_evaluation_and_barycentric_evaluation_are_equivalent( #[strategy(1usize..13)] _log_num_coefficients: usize, #[strategy(1usize..6)] log_expansion_factor: usize, #[strategy(vec(arb(), 1 << #_log_num_coefficients))] coefficients: Vec,