From f8a59c5eddc755fd37146f46155a2926891ff84c Mon Sep 17 00:00:00 2001 From: Alan Szepieniec Date: Mon, 22 Apr 2024 16:34:48 +0200 Subject: [PATCH] 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 68fab7a0..b842ad6c 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 6af51f8c..0c0d8c06 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()); }