-
Notifications
You must be signed in to change notification settings - Fork 305
Bound proof deserialization without format changes #1383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,8 @@ | ||
| use crate::field::{ChallengeFieldOps, FieldChallengeOps, JoltField}; | ||
| use crate::utils::serialization::{ | ||
| deserialize_bounded_vec, serialize_vec_with_len, serialized_vec_with_len_size, | ||
| MAX_UNIPOLY_COEFFS, | ||
| }; | ||
| use std::cmp::Ordering; | ||
| use std::iter::zip; | ||
| use std::ops::{Add, AddAssign, Index, IndexMut, Mul, MulAssign, Sub}; | ||
|
|
@@ -14,21 +18,106 @@ use crate::utils::small_scalar::SmallScalar; | |
|
|
||
| // ax^2 + bx + c stored as vec![c,b,a] | ||
| // ax^3 + bx^2 + cx + d stored as vec![d,c,b,a] | ||
| #[derive(CanonicalSerialize, CanonicalDeserialize, Debug, Clone, PartialEq, Allocative)] | ||
| #[derive(Debug, Clone, PartialEq, Allocative)] | ||
| pub struct UniPoly<F: CanonicalSerialize + CanonicalDeserialize> { | ||
| pub coeffs: Vec<F>, | ||
| } | ||
|
|
||
| // ax^2 + bx + c stored as vec![c,a] | ||
| // ax^3 + bx^2 + cx + d stored as vec![d,b,a] | ||
| #[derive(CanonicalSerialize, CanonicalDeserialize, Debug, Clone)] | ||
| #[derive(Debug, Clone)] | ||
| pub struct CompressedUniPoly<F: JoltField> { | ||
| pub coeffs_except_linear_term: Vec<F>, | ||
| } | ||
|
|
||
| impl<F: CanonicalSerialize + CanonicalDeserialize> CanonicalSerialize for UniPoly<F> { | ||
| fn serialize_with_mode<W: std::io::Write>( | ||
| &self, | ||
| writer: W, | ||
| compress: Compress, | ||
| ) -> Result<(), SerializationError> { | ||
| serialize_vec_with_len(&self.coeffs, writer, compress) | ||
| } | ||
|
|
||
| fn serialized_size(&self, compress: Compress) -> usize { | ||
| serialized_vec_with_len_size(&self.coeffs, compress) | ||
| } | ||
| } | ||
|
|
||
| impl<F: CanonicalSerialize + CanonicalDeserialize> Valid for UniPoly<F> { | ||
| fn check(&self) -> Result<(), SerializationError> { | ||
| if self.coeffs.is_empty() { | ||
| return Err(SerializationError::InvalidData); | ||
| } | ||
| self.coeffs.check() | ||
| } | ||
| } | ||
|
|
||
| impl<F: CanonicalSerialize + CanonicalDeserialize> CanonicalDeserialize for UniPoly<F> { | ||
| fn deserialize_with_mode<R: std::io::Read>( | ||
| reader: R, | ||
| compress: Compress, | ||
| validate: Validate, | ||
| ) -> Result<Self, SerializationError> { | ||
| let coeffs = deserialize_bounded_vec(reader, compress, validate, MAX_UNIPOLY_COEFFS)?; | ||
| let poly = Self { coeffs }; | ||
| if validate == Validate::Yes { | ||
| poly.check()?; | ||
| } | ||
| Ok(poly) | ||
| } | ||
| } | ||
|
|
||
| impl<F: JoltField> CanonicalSerialize for CompressedUniPoly<F> { | ||
| fn serialize_with_mode<W: std::io::Write>( | ||
| &self, | ||
| writer: W, | ||
| compress: Compress, | ||
| ) -> Result<(), SerializationError> { | ||
| serialize_vec_with_len(&self.coeffs_except_linear_term, writer, compress) | ||
| } | ||
|
|
||
| fn serialized_size(&self, compress: Compress) -> usize { | ||
| serialized_vec_with_len_size(&self.coeffs_except_linear_term, compress) | ||
| } | ||
| } | ||
|
|
||
| impl<F: JoltField> Valid for CompressedUniPoly<F> { | ||
| fn check(&self) -> Result<(), SerializationError> { | ||
| if self.coeffs_except_linear_term.is_empty() { | ||
| return Err(SerializationError::InvalidData); | ||
| } | ||
| self.coeffs_except_linear_term.check() | ||
| } | ||
| } | ||
|
|
||
| impl<F: JoltField> CanonicalDeserialize for CompressedUniPoly<F> { | ||
| fn deserialize_with_mode<R: std::io::Read>( | ||
| reader: R, | ||
| compress: Compress, | ||
| validate: Validate, | ||
| ) -> Result<Self, SerializationError> { | ||
| let coeffs_except_linear_term = | ||
| deserialize_bounded_vec(reader, compress, validate, MAX_UNIPOLY_COEFFS)?; | ||
| let poly = Self { | ||
| coeffs_except_linear_term, | ||
| }; | ||
| if validate == Validate::Yes { | ||
| poly.check()?; | ||
| } | ||
| Ok(poly) | ||
| } | ||
| } | ||
|
|
||
| impl<F: JoltField> UniPoly<F> { | ||
| pub fn from_coeff(coeffs: Vec<F>) -> Self { | ||
| UniPoly { coeffs } | ||
| if coeffs.is_empty() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Score: 85] This Some((Self::from_coeff(quotient), remainder))The pop loop at line 263 can empty Also: Fix at line 267: Some((Self::from_coeff(quotient), Self::from_coeff(remainder.coeffs))) |
||
| UniPoly { | ||
| coeffs: vec![F::zero()], | ||
| } | ||
| } else { | ||
| UniPoly { coeffs } | ||
| } | ||
| } | ||
|
|
||
| /// Interpolate a polynomial from its evaluations at the points 0, 1, 2, ..., n-1. | ||
|
|
@@ -188,10 +277,11 @@ impl<F: JoltField> UniPoly<F> { | |
| } | ||
|
|
||
| pub fn zero() -> Self { | ||
| Self::from_coeff(Vec::new()) | ||
| Self::from_coeff(vec![F::zero()]) | ||
| } | ||
|
|
||
| pub fn degree(&self) -> usize { | ||
| debug_assert!(!self.coeffs.is_empty()); | ||
| self.coeffs.len() - 1 | ||
| } | ||
|
|
||
|
|
@@ -297,10 +387,14 @@ impl<F: JoltField> UniPoly<F> { | |
| } | ||
|
|
||
| pub fn compress(&self) -> CompressedUniPoly<F> { | ||
| let mut coeffs_except_linear_term = Vec::with_capacity(self.coeffs.len() - 1); | ||
| debug_assert!(!self.coeffs.is_empty()); | ||
| let mut coeffs_except_linear_term = | ||
| Vec::with_capacity(self.coeffs.len().saturating_sub(1).max(1)); | ||
| coeffs_except_linear_term.push(self.coeffs[0]); | ||
| coeffs_except_linear_term.extend_from_slice(&self.coeffs[2..]); | ||
| debug_assert_eq!(coeffs_except_linear_term.len() + 1, self.coeffs.len()); | ||
| if self.coeffs.len() > 2 { | ||
| coeffs_except_linear_term.extend_from_slice(&self.coeffs[2..]); | ||
| debug_assert_eq!(coeffs_except_linear_term.len() + 1, self.coeffs.len()); | ||
| } | ||
| CompressedUniPoly { | ||
| coeffs_except_linear_term, | ||
| } | ||
|
|
@@ -481,13 +575,22 @@ impl<F: JoltField> MulAssign<&F> for UniPoly<F> { | |
| } | ||
|
|
||
| impl<F: JoltField> CompressedUniPoly<F> { | ||
| fn recover_linear_term(&self, hint: &F) -> F { | ||
| let constant_term = self.coeffs_except_linear_term[0]; | ||
| let mut linear_term = *hint - constant_term - constant_term; | ||
| for coeff in &self.coeffs_except_linear_term[1..] { | ||
| linear_term -= *coeff; | ||
| } | ||
| linear_term | ||
| } | ||
|
|
||
| // we require eval(0) + eval(1) = hint, so we can solve for the linear term as: | ||
| // linear_term = hint - 2 * constant_term - deg2 term - deg3 term | ||
| pub fn decompress(&self, hint: &F) -> UniPoly<F> { | ||
| let mut linear_term = | ||
| *hint - self.coeffs_except_linear_term[0] - self.coeffs_except_linear_term[0]; | ||
| for i in 1..self.coeffs_except_linear_term.len() { | ||
| linear_term -= self.coeffs_except_linear_term[i]; | ||
| debug_assert!(!self.coeffs_except_linear_term.is_empty()); | ||
| let linear_term = self.recover_linear_term(hint); | ||
| if self.coeffs_except_linear_term.len() == 1 && linear_term.is_zero() { | ||
| return UniPoly::from_coeff(vec![self.coeffs_except_linear_term[0]]); | ||
| } | ||
|
|
||
| let mut coeffs = vec![self.coeffs_except_linear_term[0], linear_term]; | ||
|
|
@@ -499,11 +602,8 @@ impl<F: JoltField> CompressedUniPoly<F> { | |
| // In the verifier we do not have to check that f(0) + f(1) = hint as we can just | ||
| // recover the linear term assuming the prover did it right, then eval the poly | ||
| pub fn eval_from_hint(&self, hint: &F, x: &F::Challenge) -> F { | ||
| let mut linear_term = | ||
| *hint - self.coeffs_except_linear_term[0] - self.coeffs_except_linear_term[0]; | ||
| for i in 1..self.coeffs_except_linear_term.len() { | ||
| linear_term -= self.coeffs_except_linear_term[i]; | ||
| } | ||
| debug_assert!(!self.coeffs_except_linear_term.is_empty()); | ||
| let linear_term = self.recover_linear_term(hint); | ||
|
|
||
| let mut running_point: F = (*x).into(); | ||
| let mut running_sum = self.coeffs_except_linear_term[0] + *x * linear_term; | ||
|
|
@@ -515,14 +615,20 @@ impl<F: JoltField> CompressedUniPoly<F> { | |
| } | ||
|
|
||
| pub fn degree(&self) -> usize { | ||
| self.coeffs_except_linear_term.len() | ||
| debug_assert!(!self.coeffs_except_linear_term.is_empty()); | ||
| if self.coeffs_except_linear_term.len() == 1 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Score: 25] When a degree-1 poly In practice sumcheck rounds always have degree >= 2, so this can't trigger from an honest prover. But it weakens the defense-in-depth this PR is adding. The old behavior ( |
||
| 0 | ||
| } else { | ||
| self.coeffs_except_linear_term.len() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use ark_bn254::Fr; | ||
| use ark_serialize::CanonicalSerialize; | ||
| use rand_chacha::ChaCha20Rng; | ||
| use rand_core::SeedableRng; | ||
|
|
||
|
|
@@ -577,6 +683,20 @@ mod tests { | |
| fn test_from_evals_cubic() { | ||
| test_from_evals_cubic_helper::<Fr>() | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_compressed_linear_round_trip() { | ||
| let poly = UniPoly::<Fr>::from_coeff(vec![Fr::from_u64(5), Fr::from_u64(7)]); | ||
| let hint = poly.eval_at_zero() + poly.eval_at_one(); | ||
| let compressed_poly = poly.compress(); | ||
| let decompressed_poly = compressed_poly.decompress(&hint); | ||
|
|
||
| assert_eq!(decompressed_poly.coeffs, poly.coeffs); | ||
|
|
||
| let x = <Fr as JoltField>::Challenge::from(9u128); | ||
| assert_eq!(compressed_poly.eval_from_hint(&hint, &x), poly.evaluate(&x)); | ||
| } | ||
|
|
||
| fn test_from_evals_cubic_helper<F: JoltField>() { | ||
| // polynomial is x^3 + 2x^2 + 3x + 1 | ||
| let e0 = F::one(); | ||
|
|
@@ -661,4 +781,18 @@ mod tests { | |
| ); | ||
| assert_eq!(poly.coeffs, true_poly.coeffs); | ||
| } | ||
|
|
||
| #[test] | ||
| fn rejects_empty_unipoly_deserialization() { | ||
| let mut bytes = Vec::new(); | ||
| 0usize.serialize_compressed(&mut bytes).unwrap(); | ||
| assert!(UniPoly::<Fr>::deserialize_compressed(&bytes[..]).is_err()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn rejects_empty_compressed_unipoly_deserialization() { | ||
| let mut bytes = Vec::new(); | ||
| 0usize.serialize_compressed(&mut bytes).unwrap(); | ||
| assert!(CompressedUniPoly::<Fr>::deserialize_compressed(&bytes[..]).is_err()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Score: 25]
>> 61check and>> 3mask express the same constraint differentlyValidation:
(self.high >> 61) != 0Construction:
u64::MAX >> 3(masks top 3 bits)Both enforce the top 3 bits of
highare zero (61 usable bits), but expressed two different ways. A shared constant would make the equivalence explicit:Nit — the validation is correct for the 125-bit challenge width (64-bit low + 61-bit high).