From 5d3ec008ee90dff0343743a46eb4d1461c716670 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Fri, 3 Jan 2025 21:59:33 -0500 Subject: [PATCH 01/22] style!: issue15 - cleaner polynomial interface Separate Polynomial into PolynomialEvalForm and PolynomialCoefForm to make library type safe when converting between these. Eg: fft can only go from coeff->eval, and ifft can only go from eval->coeff --- src/blob.rs | 16 ++-- src/kzg.rs | 69 +++++++--------- src/polynomial.rs | 206 ++++++++++++++++++++++++---------------------- 3 files changed, 148 insertions(+), 143 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index beb613c..553e8e8 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,7 +1,7 @@ use crate::{ errors::BlobError, helpers, - polynomial::{Polynomial, PolynomialFormat}, + polynomial::{PolynomialCoefForm, PolynomialEvalForm}, }; /// A blob which is Eigen DA spec aligned. @@ -56,12 +56,16 @@ impl Blob { self.blob_data.is_empty() } - /// Converts the blob data to a `Polynomial` if the data is padded. - pub fn to_polynomial(&self, form: PolynomialFormat) -> Result { + /// Convert the blob data to a `PolynomialEvalForm`. + pub fn to_polynomial_eval_form(&self) -> PolynomialEvalForm { let fr_vec = helpers::to_fr_array(&self.blob_data); - let poly = Polynomial::new(&fr_vec, self.len(), form) - .map_err(|err| BlobError::GenericError(err.to_string()))?; - Ok(poly) + PolynomialEvalForm::new(fr_vec) + } + + /// Convert the blob data to a `PolynomialCoefForm`. + pub fn to_polynomial_coeff_form(&self) -> PolynomialCoefForm { + let fr_vec = helpers::to_fr_array(&self.blob_data); + PolynomialCoefForm::new(fr_vec) } } diff --git a/src/kzg.rs b/src/kzg.rs index 22cc649..932ff39 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -3,7 +3,7 @@ use crate::{ consts::BYTES_PER_FIELD_ELEMENT, errors::KzgError, helpers, - polynomial::{Polynomial, PolynomialFormat}, + polynomial::{PolynomialCoefForm, PolynomialEvalForm}, traits::ReadPointFromBytes, }; use ark_bn254::{g1::G1Affine, Bn254, Fr, G1Projective, G2Affine}; @@ -462,55 +462,58 @@ impl Kzg { } /// commit the actual polynomial with the values setup - pub fn commit(&self, polynomial: &Polynomial) -> Result { + pub fn commit_eval_form(&self, polynomial: &PolynomialEvalForm) -> Result { if polynomial.len() > self.g1.len() { return Err(KzgError::SerializationError( "polynomial length is not correct".to_string(), )); } - let bases = match polynomial.get_form() { - PolynomialFormat::InEvaluationForm => { - // If the polynomial is in evaluation form, use the original g1 points - self.g1[..polynomial.len()].to_vec() - }, - PolynomialFormat::InCoefficientForm => { - // If the polynomial is in coefficient form, use inverse FFT - self.g1_ifft(polynomial.len())? - }, - }; + // When the polynomial is in evaluation form, use the original g1 points + let bases = self.g1[..polynomial.len()].to_vec(); - match G1Projective::msm(&bases, &polynomial.to_vec()) { + match G1Projective::msm(&bases, polynomial.evaluations()) { Ok(res) => Ok(res.into_affine()), Err(err) => Err(KzgError::CommitError(err.to_string())), } } - pub fn blob_to_kzg_commitment( - &self, - blob: &Blob, - form: PolynomialFormat, - ) -> Result { - let polynomial = blob - .to_polynomial(form) - .map_err(|err| KzgError::SerializationError(err.to_string()))?; - let commitment = self.commit(&polynomial)?; - Ok(commitment) + /// commit the actual polynomial with the values setup + pub fn commit_coef_form(&self, polynomial: &PolynomialCoefForm) -> Result { + if polynomial.len() > self.g1.len() { + return Err(KzgError::SerializationError( + "polynomial length is not correct".to_string(), + )); + } + // When the polynomial is in coefficient form, use inverse FFT on the srs points. + // TODO: is this correct? See https://github.com/Layr-Labs/rust-kzg-bn254/issues/20 + let bases = self.g1_ifft(polynomial.len())?; + + match G1Projective::msm(&bases, polynomial.coeffs()) { + Ok(res) => Ok(res.into_affine()), + Err(err) => Err(KzgError::CommitError(err.to_string())), + } + } + + pub fn blob_to_kzg_commitment(&self, blob: &Blob) -> Result { + let polynomial = blob.to_polynomial_eval_form(); + self.commit_eval_form(&polynomial) } /// helper function to work with the library and the env of the kzg instance pub fn compute_kzg_proof_with_roots_of_unity( &self, - polynomial: &Polynomial, + polynomial: &PolynomialEvalForm, index: u64, ) -> Result { self.compute_kzg_proof(polynomial, index, &self.expanded_roots_of_unity) } /// function to compute the kzg proof given the values. + /// TODO: do we want a separate function for polynomials in evaluation form? pub fn compute_kzg_proof( &self, - polynomial: &Polynomial, + polynomial: &PolynomialEvalForm, index: u64, root_of_unities: &[Fr], ) -> Result { @@ -526,7 +529,7 @@ impl Kzg { )); } - let eval_fr = polynomial.to_vec(); + let eval_fr = polynomial.evaluations(); let mut poly_shift: Vec = Vec::with_capacity(eval_fr.len()); let usized_index = if let Some(x) = index.to_usize() { x @@ -539,7 +542,7 @@ impl Kzg { let value_fr = eval_fr[usized_index]; let z_fr = root_of_unities[usized_index]; - for fr in &eval_fr { + for fr in eval_fr { poly_shift.push(*fr - value_fr); } @@ -563,16 +566,8 @@ impl Kzg { } } - let bases = match polynomial.get_form() { - PolynomialFormat::InEvaluationForm => { - // If the polynomial is in evaluation form, use the original g1 points - self.g1[..polynomial.len()].to_vec() - }, - PolynomialFormat::InCoefficientForm => { - // If the polynomial is in coefficient form, use inverse FFT - self.g1_ifft(polynomial.len())? - }, - }; + // Polynomial is in evaluation form, so use the original g1 points + let bases = self.g1[..polynomial.len()].to_vec(); match G1Projective::msm(&bases, "ient_poly) { Ok(res) => Ok(G1Affine::from(res)), diff --git a/src/polynomial.rs b/src/polynomial.rs index 0595f3a..8c0b532 100644 --- a/src/polynomial.rs +++ b/src/polynomial.rs @@ -1,137 +1,143 @@ -use crate::{errors::PolynomialError, helpers}; +use crate::{consts::BYTES_PER_FIELD_ELEMENT, errors::PolynomialError, helpers}; use ark_bn254::Fr; use ark_poly::{EvaluationDomain, GeneralEvaluationDomain}; use ark_std::Zero; -#[derive(Clone, Debug, PartialEq, Copy)] -pub enum PolynomialFormat { - InCoefficientForm, - InEvaluationForm, -} - #[derive(Clone, Debug, PartialEq)] -pub struct Polynomial { - elements: Vec, - // TODO: Remove this field, its just a duplicate of length_of_padded_blob_as_fr_vector. - // One can easily convert between them by *4 or /4. Also it should be calculated and not passed in, - // which is error prone (user might think length is in field elements). - length_of_padded_blob: usize, - length_of_padded_blob_as_fr_vector: usize, - form: PolynomialFormat, +pub struct PolynomialCoefForm { + /// coeffs contains the coefficients of the polynomial, padded with 0s to the next power of two. + /// Hence if the polynomial is created with coefficients [1, 2, 3], the internal representation + /// will be [1, 2, 3, 0]. + coeffs: Vec, + /// Number of bytes in the underlying blob, which was used to create the polynomial. + /// This is passed as is when converting between Coefficient and Evaluation forms, + /// so that the blob can be reconstructed with the same length. + /// + /// TODO: We should get rid of this: polynomial should not know about the blob. + /// This len is equivalent to the coeffs len before it gets padded. + /// Perhaps we can store the original coeffs and only pad when needed? + len_underlying_blob_bytes: usize, } -impl Polynomial { - /// Constructs a new `Polynomial` with a given vector of `Fr` elements. - pub fn new( - elements: &[Fr], - length_of_padded_blob: usize, - form: PolynomialFormat, - ) -> Result { - if elements.is_empty() { - return Err(PolynomialError::GenericError( - "elements are empty".to_string(), - )); - } - let mut padded_input_fr = vec![]; - for i in 0..elements.len().next_power_of_two() { - if i < elements.len() { - padded_input_fr.push(elements[i]); - } else { - padded_input_fr.push(Fr::zero()); - } +impl PolynomialCoefForm { + pub fn new(coeffs: Vec) -> Self { + let underlying_blob_len_in_bytes = coeffs.len() * BYTES_PER_FIELD_ELEMENT; + let next_power_of_two = coeffs.len().next_power_of_two(); + let mut padded_coeffs = coeffs; + padded_coeffs.resize(next_power_of_two, Fr::zero()); + + Self { + coeffs: padded_coeffs, + len_underlying_blob_bytes: underlying_blob_len_in_bytes, } - Ok(Polynomial { - elements: padded_input_fr, - length_of_padded_blob, - length_of_padded_blob_as_fr_vector: elements.len(), - form, - }) } - pub fn get_length_of_padded_blob_as_fr_vector(&self) -> usize { - self.length_of_padded_blob_as_fr_vector + pub fn coeffs(&self) -> &[Fr] { + &self.coeffs } - /// Returns the form of the polynomial. - pub fn get_form(&self) -> PolynomialFormat { - self.form + /// Returns the number of coefficients in the polynomial. Note that this returns the number of + /// coefficients in the padded polynomial, not the number of coefficients in the original + /// polynomial. + pub fn len(&self) -> usize { + self.coeffs.len() } - /// Returns the number of elements in the polynomial. - pub fn len(&self) -> usize { - self.elements.len() + /// TODO: we should deprecate this. See comment in the struct. + pub fn len_underlying_blob_bytes(&self) -> usize { + self.len_underlying_blob_bytes } pub fn get_at_index(&self, i: usize) -> Option<&Fr> { - self.elements.get(i) + self.coeffs.get(i) } /// Checks if the polynomial has no elements. pub fn is_empty(&self) -> bool { - self.elements.is_empty() + self.coeffs.is_empty() } /// Converts all `Fr` elements in the `Polynomial` to a single byte vector. pub fn to_bytes_be(&self) -> Vec { - helpers::to_byte_array(&self.elements, self.length_of_padded_blob) + helpers::to_byte_array(&self.coeffs, self.len_underlying_blob_bytes) } - /// Returns a clone of the elements as a `Vec`. - pub fn to_vec(&self) -> Vec { - self.elements.clone() + pub fn to_eval_form(&self) -> Result { + let evals = GeneralEvaluationDomain::::new(self.len()) + .ok_or(PolynomialError::FFTError( + "Failed to construct domain for FFT".to_string(), + ))? + .fft(&self.coeffs); + Ok(PolynomialEvalForm::new(evals)) } +} - /// Helper function to transform the polynomial to the given form. - pub fn transform_to_form(&mut self, form: PolynomialFormat) -> Result<(), PolynomialError> { - if self.form == form { - return Err(PolynomialError::IncorrectFormError( - "Polynomial is already in the given form".to_string(), - )); - } +#[derive(Clone, Debug, PartialEq)] +pub struct PolynomialEvalForm { + /// evaluations contains the evaluations of the polynomial, padded with 0s to the next power of two. + /// Hence if the polynomial is created with coefficients [1, 2, 3], the internal representation + /// will be [1, 2, 3, 0]. + evaluations: Vec, + /// Number of bytes in the underlying blob, which was used to create the polynomial. + /// This is passed as is when converting between Coefficient and Evaluation forms, + /// so that the blob can be reconstructed with the same length. + /// + /// TODO: We should get rid of this: polynomial should not know about the blob. + /// This len is equivalent to the coeffs len before it gets padded. + /// Perhaps we can store the original coeffs and only pad when needed? + len_underlying_blob_bytes: usize, +} + +impl PolynomialEvalForm { + pub fn new(evals: Vec) -> Self { + let underlying_blob_len_in_bytes = evals.len() * BYTES_PER_FIELD_ELEMENT; + let next_power_of_two = evals.len().next_power_of_two(); + let mut padded_evals = evals; + // TODO: does it make sense to pad evaluations with zeros? This changes the polynomial... + padded_evals.resize(next_power_of_two, Fr::zero()); - match form { - PolynomialFormat::InCoefficientForm => { - // Transform from evaluation form to coefficient form using FFT - self.form = form; - self.fft_on_elements(false) - }, - PolynomialFormat::InEvaluationForm => { - // Transform from coefficient form to evaluation form using IFFT - self.form = form; - self.fft_on_elements(true) - }, + Self { + evaluations: padded_evals, + len_underlying_blob_bytes: underlying_blob_len_in_bytes, } } - /// Performs an fft or ifft on the polynomial's elements - pub fn fft_on_elements(&mut self, inverse: bool) -> Result<(), PolynomialError> { - let fft_result = Self::fft(&self.to_vec(), inverse); - match fft_result { - Ok(fft_result) => { - self.elements = fft_result; - Ok(()) - }, - Err(e) => Err(e), - } + pub fn evaluations(&self) -> &[Fr] { + &self.evaluations } - /// helper function to perform fft or ifft on a vector of Fr - pub fn fft(vals: &Vec, inverse: bool) -> Result, PolynomialError> { - let length = vals.len(); - - match GeneralEvaluationDomain::::new(length) { - Some(domain) => { - if inverse { - let result = domain.ifft(vals); - Ok(result) - } else { - let result = domain.fft(vals); - Ok(result) - } - }, - None => Err(PolynomialError::FFTError( - "Failed to construct domain for FFT".to_string(), - )), - } + /// Returns the number of evaluations in the polynomial. Note that this returns the number of + /// evaluations in the padded polynomial, not the number of evaluations in the original + /// polynomial. + pub fn len(&self) -> usize { + self.evaluations.len() + } + + /// TODO: we should deprecate this. See comment in the struct. + pub fn len_underlying_blob_bytes(&self) -> usize { + self.len_underlying_blob_bytes + } + + pub fn get_at_index(&self, i: usize) -> Option<&Fr> { + self.evaluations.get(i) + } + + /// Checks if the polynomial has no elements. + pub fn is_empty(&self) -> bool { + self.evaluations.is_empty() + } + + /// Converts all `Fr` elements in the `Polynomial` to a single byte vector. + pub fn to_bytes_be(&self) -> Vec { + helpers::to_byte_array(&self.evaluations, self.len_underlying_blob_bytes) + } + + pub fn to_coef_form(&self) -> Result { + let coeffs = GeneralEvaluationDomain::::new(self.len()) + .ok_or(PolynomialError::FFTError( + "Failed to construct domain for IFFT".to_string(), + ))? + .ifft(&self.evaluations); + Ok(PolynomialCoefForm::new(coeffs)) } } From b07e0a08ac97e336000f62017ce5388bb52482b4 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Mon, 6 Jan 2025 15:07:13 -0500 Subject: [PATCH 02/22] refactor!: change kzg commit functions to take a specific form Made the internal function take a Polynomial trait (that has len() and elements() fns) to be polymorphic. But public fns are now duplicated, one for each form (foo_coeff_form, foo_eval_form) --- src/blob.rs | 6 ++--- src/kzg.rs | 58 ++++++++++++++++++++++++++++++++++++++--------- src/polynomial.rs | 43 +++++++++++++++++++++++++++++++---- 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index 553e8e8..1a67ba2 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,7 +1,7 @@ use crate::{ errors::BlobError, helpers, - polynomial::{PolynomialCoefForm, PolynomialEvalForm}, + polynomial::{PolynomialCoeffForm, PolynomialEvalForm}, }; /// A blob which is Eigen DA spec aligned. @@ -63,9 +63,9 @@ impl Blob { } /// Convert the blob data to a `PolynomialCoefForm`. - pub fn to_polynomial_coeff_form(&self) -> PolynomialCoefForm { + pub fn to_polynomial_coeff_form(&self) -> PolynomialCoeffForm { let fr_vec = helpers::to_fr_array(&self.blob_data); - PolynomialCoefForm::new(fr_vec) + PolynomialCoeffForm::new(fr_vec) } } diff --git a/src/kzg.rs b/src/kzg.rs index 932ff39..03362e7 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -3,7 +3,7 @@ use crate::{ consts::BYTES_PER_FIELD_ELEMENT, errors::KzgError, helpers, - polynomial::{PolynomialCoefForm, PolynomialEvalForm}, + polynomial::{Polynomial, PolynomialCoeffForm, PolynomialEvalForm}, traits::ReadPointFromBytes, }; use ark_bn254::{g1::G1Affine, Bn254, Fr, G1Projective, G2Affine}; @@ -479,7 +479,7 @@ impl Kzg { } /// commit the actual polynomial with the values setup - pub fn commit_coef_form(&self, polynomial: &PolynomialCoefForm) -> Result { + pub fn commit_coef_form(&self, polynomial: &PolynomialCoeffForm) -> Result { if polynomial.len() > self.g1.len() { return Err(KzgError::SerializationError( "polynomial length is not correct".to_string(), @@ -501,21 +501,60 @@ impl Kzg { } /// helper function to work with the library and the env of the kzg instance - pub fn compute_kzg_proof_with_roots_of_unity( + pub fn compute_kzg_proof_with_roots_of_unity_eval_form( &self, polynomial: &PolynomialEvalForm, index: u64, ) -> Result { - self.compute_kzg_proof(polynomial, index, &self.expanded_roots_of_unity) + self.compute_kzg_proof_eval_form(polynomial, index, &self.expanded_roots_of_unity) } - /// function to compute the kzg proof given the values. - /// TODO: do we want a separate function for polynomials in evaluation form? - pub fn compute_kzg_proof( + pub fn compute_kzg_proof_with_roots_of_unity_coef_form( + &self, + polynomial: &PolynomialCoeffForm, + index: u64, + ) -> Result { + self.compute_kzg_proof_coeff_form(polynomial, index, &self.expanded_roots_of_unity) + } + + fn compute_kzg_proof_eval_form( &self, polynomial: &PolynomialEvalForm, index: u64, root_of_unities: &[Fr], + ) -> Result { + self.compute_kzg_proof( + Box::new(polynomial), + index, + root_of_unities, + &self.g1[..polynomial.len()], + ) + } + + pub fn compute_kzg_proof_coeff_form( + &self, + polynomial: &PolynomialCoeffForm, + index: u64, + root_of_unities: &[Fr], + ) -> Result { + self.compute_kzg_proof( + Box::new(polynomial), + index, + root_of_unities, + &self.g1_ifft(polynomial.len())?, + ) + } + + /// function to compute the kzg proof given the values. + fn compute_kzg_proof( + &self, + // Boxed polynomial to allow working with eval and coeff form polynomials. + // This was done to copy the previous implementation at: https://github.com/Layr-Labs/rust-kzg-bn254/blob/85be35704cfd64e53f7e63ee7ec1601bfb4abe26/src/kzg.rs#L566 + // TODO: is this implementation correct? + polynomial: Box, + index: u64, + root_of_unities: &[Fr], + bases: &[G1Affine], ) -> Result { if !self.params.completed_setup { return Err(KzgError::GenericError( @@ -529,7 +568,7 @@ impl Kzg { )); } - let eval_fr = polynomial.evaluations(); + let eval_fr = polynomial.elements(); let mut poly_shift: Vec = Vec::with_capacity(eval_fr.len()); let usized_index = if let Some(x) = index.to_usize() { x @@ -566,9 +605,6 @@ impl Kzg { } } - // Polynomial is in evaluation form, so use the original g1 points - let bases = self.g1[..polynomial.len()].to_vec(); - match G1Projective::msm(&bases, "ient_poly) { Ok(res) => Ok(G1Affine::from(res)), Err(err) => Err(KzgError::SerializationError(err.to_string())), diff --git a/src/polynomial.rs b/src/polynomial.rs index 8c0b532..e11b170 100644 --- a/src/polynomial.rs +++ b/src/polynomial.rs @@ -3,8 +3,18 @@ use ark_bn254::Fr; use ark_poly::{EvaluationDomain, GeneralEvaluationDomain}; use ark_std::Zero; +/// Polynomial trait that defines the common methods for both Coefficient and Evaluation forms. +/// Right now this is only used in [kzg::Kzg::compute_kzg_proof_coeff_form]. +/// TODO: feels like a hack. Is there a better API design that wouldn't require this? +pub trait Polynomial { + /// Returns the number of elements in the polynomial (whether coefficients or evaluations) + fn len(&self) -> usize; + /// Returns the underlying elements, which can be either coefficients or evaluations. + fn elements(&self) -> &[Fr]; +} + #[derive(Clone, Debug, PartialEq)] -pub struct PolynomialCoefForm { +pub struct PolynomialCoeffForm { /// coeffs contains the coefficients of the polynomial, padded with 0s to the next power of two. /// Hence if the polynomial is created with coefficients [1, 2, 3], the internal representation /// will be [1, 2, 3, 0]. @@ -19,7 +29,17 @@ pub struct PolynomialCoefForm { len_underlying_blob_bytes: usize, } -impl PolynomialCoefForm { +impl Polynomial for &PolynomialCoeffForm { + fn len(&self) -> usize { + self.coeffs.len() + } + + fn elements(&self) -> &[Fr] { + self.coeffs() + } +} + +impl PolynomialCoeffForm { pub fn new(coeffs: Vec) -> Self { let underlying_blob_len_in_bytes = coeffs.len() * BYTES_PER_FIELD_ELEMENT; let next_power_of_two = coeffs.len().next_power_of_two(); @@ -48,6 +68,11 @@ impl PolynomialCoefForm { self.len_underlying_blob_bytes } + /// Similar to [Self::len_underlying_blob_bytes], but returns the number of field elements instead of bytes + pub fn len_underlying_blob_field_elements(&self) -> usize { + self.len_underlying_blob_bytes / BYTES_PER_FIELD_ELEMENT + } + pub fn get_at_index(&self, i: usize) -> Option<&Fr> { self.coeffs.get(i) } @@ -88,6 +113,16 @@ pub struct PolynomialEvalForm { len_underlying_blob_bytes: usize, } +impl Polynomial for &PolynomialEvalForm { + fn len(&self) -> usize { + self.evaluations.len() + } + + fn elements(&self) -> &[Fr] { + self.evaluations() + } +} + impl PolynomialEvalForm { pub fn new(evals: Vec) -> Self { let underlying_blob_len_in_bytes = evals.len() * BYTES_PER_FIELD_ELEMENT; @@ -132,12 +167,12 @@ impl PolynomialEvalForm { helpers::to_byte_array(&self.evaluations, self.len_underlying_blob_bytes) } - pub fn to_coef_form(&self) -> Result { + pub fn to_coef_form(&self) -> Result { let coeffs = GeneralEvaluationDomain::::new(self.len()) .ok_or(PolynomialError::FFTError( "Failed to construct domain for IFFT".to_string(), ))? .ifft(&self.evaluations); - Ok(PolynomialCoefForm::new(coeffs)) + Ok(PolynomialCoeffForm::new(coeffs)) } } From 339d3095eed552f9dca390a8f057a18cd663a0f4 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Mon, 6 Jan 2025 15:07:31 -0500 Subject: [PATCH 03/22] test: fix tests after new breaking api changes --- tests/kzg_test.rs | 59 ++++++++----------- tests/polynomial_test.rs | 122 +++++---------------------------------- 2 files changed, 39 insertions(+), 142 deletions(-) diff --git a/tests/kzg_test.rs b/tests/kzg_test.rs index 2be1a03..a281551 100644 --- a/tests/kzg_test.rs +++ b/tests/kzg_test.rs @@ -3,11 +3,7 @@ mod tests { use ark_bn254::{Fr, G1Affine, G2Affine}; use lazy_static::lazy_static; use rust_kzg_bn254::{ - blob::Blob, - errors::KzgError, - helpers, - kzg::Kzg, - polynomial::{Polynomial, PolynomialFormat}, + blob::Blob, errors::KzgError, helpers, kzg::Kzg, polynomial::PolynomialCoeffForm, }; use std::{ env, @@ -54,13 +50,13 @@ mod tests { #[test] fn test_commit_errors() { - let mut poly = vec![]; + let mut coeffs = vec![]; for _ in 0..4000 { - poly.push(Fr::one()); + coeffs.push(Fr::one()); } - let polynomial = Polynomial::new(&poly, 2, PolynomialFormat::InCoefficientForm).unwrap(); - let result = KZG_3000.commit(&polynomial); + let polynomial = PolynomialCoeffForm::new(coeffs); + let result = KZG_3000.commit_coef_form(&polynomial); assert_eq!( result, Err(KzgError::SerializationError( @@ -176,9 +172,7 @@ mod tests { .calculate_roots_of_unity(input.len().try_into().unwrap()) .unwrap(); - let polynomial_input = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let polynomial_input = input.to_polynomial_coeff_form(); let expanded_roots_of_unity_vec_1: Vec<&Fr> = (0..polynomial_input.len()) .map(|i| kzg_clone1.get_nth_root_of_unity(i).unwrap()) .collect(); @@ -195,9 +189,7 @@ mod tests { use ark_bn254::Fq; let blob = Blob::from_raw_data(GETTYSBURG_ADDRESS_BYTES); - let fn_output = KZG_3000 - .blob_to_kzg_commitment(&blob, PolynomialFormat::InCoefficientForm) - .unwrap(); + let fn_output = KZG_3000.blob_to_kzg_commitment(&blob).unwrap(); let commitment_from_da = G1Affine::new_unchecked( Fq::from_str( "2961155957874067312593973807786254905069537311739090798303675273531563528369", @@ -226,17 +218,18 @@ mod tests { println!("generating blob of length is {}", blob_length); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); - let index = rand::thread_rng() - .gen_range(0..input_poly.get_length_of_padded_blob_as_fr_vector()); - let commitment = kzg.commit(&input_poly.clone()).unwrap(); + let index = + rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); + let commitment = kzg.commit_coef_form(&input_poly.clone()).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) + .compute_kzg_proof_with_roots_of_unity_coef_form( + &input_poly, + index.try_into().unwrap(), + ) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); @@ -251,7 +244,7 @@ mod tests { proof, value_fr.clone(), kzg.get_nth_root_of_unity( - (index + 1) % input_poly.get_length_of_padded_blob_as_fr_vector() + (index + 1) % input_poly.len_underlying_blob_field_elements() ) .unwrap() .clone() @@ -268,9 +261,7 @@ mod tests { let mut kzg = KZG_INSTANCE.clone(); let input = Blob::from_raw_data(GETTYSBURG_ADDRESS_BYTES); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); for index in 0..input_poly.len() - 1 { kzg.data_setup_custom(4, input.len().try_into().unwrap()) @@ -283,9 +274,12 @@ mod tests { break; } } - let commitment = kzg.commit(&input_poly.clone()).unwrap(); + let commitment = kzg.commit_coef_form(&input_poly).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) + .compute_kzg_proof_with_roots_of_unity_coef_form( + &input_poly, + index.try_into().unwrap(), + ) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); @@ -459,16 +453,11 @@ mod tests { let hard_coded_x = Fq::from_str(the_strings_str[1]).expect("should be fine"); let hard_coded_y = Fq::from_str(the_strings_str[2]).expect("should be fine"); let gnark_proof = G1Affine::new(hard_coded_x, hard_coded_y); - let poly = Polynomial::new( - &padded_input_fr_elements, - 30, - PolynomialFormat::InCoefficientForm, - ) - .unwrap(); + let poly = PolynomialCoeffForm::new(padded_input_fr_elements.to_vec()); kzg.data_setup_custom(4, poly.len().try_into().unwrap()) .unwrap(); let result = kzg - .compute_kzg_proof(&poly, index, &roots_of_unities) + .compute_kzg_proof_coeff_form(&poly, index, &roots_of_unities) .unwrap(); assert_eq!(gnark_proof, result) } diff --git a/tests/polynomial_test.rs b/tests/polynomial_test.rs index db69e4b..3c60b3f 100644 --- a/tests/polynomial_test.rs +++ b/tests/polynomial_test.rs @@ -1,29 +1,8 @@ #[cfg(test)] mod tests { - use ark_bn254::Fr; - use ark_std::One; - use rust_kzg_bn254::{ - blob::Blob, - errors::PolynomialError, - polynomial::{Polynomial, PolynomialFormat}, - }; + use rust_kzg_bn254::blob::Blob; const GETTYSBURG_ADDRESS_BYTES: &[u8] = "Fourscore and seven years ago our fathers brought forth, on this continent, a new nation, conceived in liberty, and dedicated to the proposition that all men are created equal. Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived, and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting-place for those who here gave their lives, that that nation might live. It is altogether fitting and proper that we should do this. But, in a larger sense, we cannot dedicate, we cannot consecrate—we cannot hallow—this ground. The brave men, living and dead, who struggled here, have consecrated it far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us—that from these honored dead we take increased devotion to that cause for which they here gave the last full measure of devotion—that we here highly resolve that these dead shall not have died in vain—that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.".as_bytes(); - #[test] - fn test_errors() { - let polynomial_empty = Polynomial::new(&vec![], 2, PolynomialFormat::InCoefficientForm); - assert_eq!( - polynomial_empty, - Err(PolynomialError::GenericError( - "elements are empty".to_string() - )) - ); - - let polynomial_non_empty = - Polynomial::new(&vec![Fr::one()], 2, PolynomialFormat::InCoefficientForm); - assert!(!polynomial_non_empty.unwrap().is_empty()); - } - #[test] fn test_to_fr_array() { let blob = Blob::from_raw_data( @@ -33,9 +12,7 @@ mod tests { ] .as_slice(), ); - let poly = blob - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let poly = blob.to_polynomial_coeff_form(); assert_eq!( poly.to_bytes_be(), blob.data(), @@ -52,9 +29,7 @@ mod tests { ); let long_blob = Blob::from_raw_data(GETTYSBURG_ADDRESS_BYTES); - let long_poly = long_blob - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let long_poly = long_blob.to_polynomial_coeff_form(); // let ga_converted_fr = to_fr_array(&ga_converted); assert_eq!( long_blob.data(), @@ -72,95 +47,28 @@ mod tests { ] .as_slice(), ); - let mut poly = blob - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); - // assert form is in coefficient form - assert_eq!( - poly.get_form(), - PolynomialFormat::InCoefficientForm, - "should be in coefficient form" - ); + let poly_coeff = blob.to_polynomial_coeff_form(); - poly.transform_to_form(PolynomialFormat::InEvaluationForm) - .unwrap(); - // assert form is in evaluation form + let poly_eval = poly_coeff.to_eval_form().unwrap(); + let poly_coeff_back = poly_eval.to_coef_form().unwrap(); assert_eq!( - poly.get_form(), - PolynomialFormat::InEvaluationForm, - "should be in evaluation form" - ); - - poly.transform_to_form(PolynomialFormat::InCoefficientForm) - .unwrap(); - // assert form is in coefficient form - assert_eq!( - poly.get_form(), - PolynomialFormat::InCoefficientForm, - "should be in coefficient form" - ); - assert_eq!( - &poly.to_bytes_be(), + &poly_coeff_back.to_bytes_be(), blob.data(), "start and finish bytes should be the same" ); - - let long_blob = Blob::from_raw_data(GETTYSBURG_ADDRESS_BYTES); - let mut long_poly = long_blob - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); - // assert form is in coefficient form - assert_eq!( - long_poly.get_form(), - PolynomialFormat::InCoefficientForm, - "should be in coefficient form" - ); - long_poly - .transform_to_form(PolynomialFormat::InEvaluationForm) - .unwrap(); - // assert form is in evaluation form - assert_eq!( - long_poly.get_form(), - PolynomialFormat::InEvaluationForm, - "should be in evaluation form" - ); - long_poly - .transform_to_form(PolynomialFormat::InCoefficientForm) - .unwrap(); - // assert form is in coefficient form - assert_eq!( - long_poly.get_form(), - PolynomialFormat::InCoefficientForm, - "should be in coefficient form" - ); - - assert_eq!( - long_blob.data(), - &long_poly.to_bytes_be(), - "start and finish bytes should be the same" - ); } #[test] - fn test_transform_form_errors() { - let mut poly = - Polynomial::new(&vec![Fr::one()], 2, PolynomialFormat::InEvaluationForm).unwrap(); - assert_eq!( - poly.transform_to_form(PolynomialFormat::InEvaluationForm), - Err(PolynomialError::IncorrectFormError( - "Polynomial is already in the given form".to_string() - )), - "should throw an error" - ); + fn test_transform_form_large_blob() { + let blob = Blob::from_raw_data(GETTYSBURG_ADDRESS_BYTES); + let poly_coeff = blob.to_polynomial_coeff_form(); - let mut poly = - Polynomial::new(&vec![Fr::one()], 2, PolynomialFormat::InCoefficientForm).unwrap(); + let poly_eval = poly_coeff.to_eval_form().unwrap(); + let poly_coeff_back = poly_eval.to_coef_form().unwrap(); assert_eq!( - poly.transform_to_form(PolynomialFormat::InCoefficientForm), - Err(PolynomialError::IncorrectFormError( - "Polynomial is already in the given form".to_string() - )), - "should throw an error" + &poly_coeff_back.to_bytes_be(), + blob.data(), + "start and finish bytes should be the same" ); } } From e8ad64ef54cc8af097320b93ee6929331482f3d9 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Mon, 6 Jan 2025 15:16:37 -0500 Subject: [PATCH 04/22] style!: rename commit_coef_form -> commit_coeff_form follows renaming done in a previous commit, forgot this fn --- src/kzg.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kzg.rs b/src/kzg.rs index 03362e7..d90fdab 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -479,7 +479,7 @@ impl Kzg { } /// commit the actual polynomial with the values setup - pub fn commit_coef_form(&self, polynomial: &PolynomialCoeffForm) -> Result { + pub fn commit_coeff_form(&self, polynomial: &PolynomialCoeffForm) -> Result { if polynomial.len() > self.g1.len() { return Err(KzgError::SerializationError( "polynomial length is not correct".to_string(), @@ -509,7 +509,7 @@ impl Kzg { self.compute_kzg_proof_eval_form(polynomial, index, &self.expanded_roots_of_unity) } - pub fn compute_kzg_proof_with_roots_of_unity_coef_form( + pub fn compute_kzg_proof_with_roots_of_unity_coeff_form( &self, polynomial: &PolynomialCoeffForm, index: u64, From 98139b4e5a4aab282fb7ecadc812a6315bfbf435 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Mon, 6 Jan 2025 15:16:49 -0500 Subject: [PATCH 05/22] test: fix benchmarks following api changes --- benches/bench_kzg_commit.rs | 20 +++++------- benches/bench_kzg_commit_large_blobs.rs | 14 +++------ benches/bench_kzg_proof.rs | 41 +++++++++++++------------ benches/bench_kzg_verify.rs | 41 +++++++++++++------------ tests/kzg_test.rs | 10 +++--- 5 files changed, 61 insertions(+), 65 deletions(-) diff --git a/benches/bench_kzg_commit.rs b/benches/bench_kzg_commit.rs index 2d540a4..d8e108b 100644 --- a/benches/bench_kzg_commit.rs +++ b/benches/bench_kzg_commit.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; use rand::Rng; -use rust_kzg_bn254::{blob::Blob, kzg::Kzg, polynomial::PolynomialFormat}; +use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; use std::time::Duration; fn bench_kzg_commit(c: &mut Criterion) { @@ -17,34 +17,28 @@ fn bench_kzg_commit(c: &mut Criterion) { c.bench_function("bench_kzg_commit_10000", |b| { let random_blob: Vec = (0..10000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); - b.iter(|| kzg.commit(&input_poly).unwrap()); + b.iter(|| kzg.commit_coeff_form(&input_poly).unwrap()); }); c.bench_function("bench_kzg_commit_30000", |b| { let random_blob: Vec = (0..30000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); - b.iter(|| kzg.commit(&input_poly).unwrap()); + b.iter(|| kzg.commit_coeff_form(&input_poly).unwrap()); }); c.bench_function("bench_kzg_commit_50000", |b| { let random_blob: Vec = (0..50000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); - b.iter(|| kzg.commit(&input_poly).unwrap()); + b.iter(|| kzg.commit_coeff_form(&input_poly).unwrap()); }); } diff --git a/benches/bench_kzg_commit_large_blobs.rs b/benches/bench_kzg_commit_large_blobs.rs index 8df590a..7181413 100644 --- a/benches/bench_kzg_commit_large_blobs.rs +++ b/benches/bench_kzg_commit_large_blobs.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; use rand::Rng; -use rust_kzg_bn254::{blob::Blob, kzg::Kzg, polynomial::PolynomialFormat}; +use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; use std::time::Duration; fn bench_kzg_commit(c: &mut Criterion) { @@ -19,12 +19,10 @@ fn bench_kzg_commit(c: &mut Criterion) { .map(|_| rng.gen_range(32..=126) as u8) .collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); - b.iter(|| kzg.commit(&input_poly).unwrap()); + b.iter(|| kzg.commit_coeff_form(&input_poly).unwrap()); }); c.bench_function("bench_kzg_commit_16mb", |b| { @@ -32,12 +30,10 @@ fn bench_kzg_commit(c: &mut Criterion) { .map(|_| rng.gen_range(32..=126) as u8) .collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); - b.iter(|| kzg.commit(&input_poly).unwrap()); + b.iter(|| kzg.commit_coeff_form(&input_poly).unwrap()); }); } diff --git a/benches/bench_kzg_proof.rs b/benches/bench_kzg_proof.rs index 8ad81fe..e5f896b 100644 --- a/benches/bench_kzg_proof.rs +++ b/benches/bench_kzg_proof.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; use rand::Rng; -use rust_kzg_bn254::{blob::Blob, kzg::Kzg, polynomial::PolynomialFormat}; +use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; use std::time::Duration; fn bench_kzg_proof(c: &mut Criterion) { @@ -17,48 +17,51 @@ fn bench_kzg_proof(c: &mut Criterion) { c.bench_function("bench_kzg_proof_10000", |b| { let random_blob: Vec = (0..10000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = - rand::thread_rng().gen_range(0..input_poly.get_length_of_padded_blob_as_fr_vector()); + rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); b.iter(|| { - kzg.compute_kzg_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) - .unwrap() + kzg.compute_kzg_proof_with_roots_of_unity_coeff_form( + &input_poly, + index.try_into().unwrap(), + ) + .unwrap() }); }); c.bench_function("bench_kzg_proof_30000", |b| { let random_blob: Vec = (0..30000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = - rand::thread_rng().gen_range(0..input_poly.get_length_of_padded_blob_as_fr_vector()); + rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); b.iter(|| { - kzg.compute_kzg_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) - .unwrap() + kzg.compute_kzg_proof_with_roots_of_unity_coeff_form( + &input_poly, + index.try_into().unwrap(), + ) + .unwrap() }); }); c.bench_function("bench_kzg_proof_50000", |b| { let random_blob: Vec = (0..50000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = - rand::thread_rng().gen_range(0..input_poly.get_length_of_padded_blob_as_fr_vector()); + rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); b.iter(|| { - kzg.compute_kzg_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) - .unwrap() + kzg.compute_kzg_proof_with_roots_of_unity_coeff_form( + &input_poly, + index.try_into().unwrap(), + ) + .unwrap() }); }); } diff --git a/benches/bench_kzg_verify.rs b/benches/bench_kzg_verify.rs index 5fc69da..d6a7b5c 100644 --- a/benches/bench_kzg_verify.rs +++ b/benches/bench_kzg_verify.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; use rand::Rng; -use rust_kzg_bn254::{blob::Blob, kzg::Kzg, polynomial::PolynomialFormat}; +use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; use std::time::Duration; fn bench_kzg_verify(c: &mut Criterion) { @@ -17,16 +17,17 @@ fn bench_kzg_verify(c: &mut Criterion) { c.bench_function("bench_kzg_verify_10000", |b| { let random_blob: Vec = (0..10000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = - rand::thread_rng().gen_range(0..input_poly.get_length_of_padded_blob_as_fr_vector()); - let commitment = kzg.commit(&input_poly.clone()).unwrap(); + rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); + let commitment = kzg.commit_coeff_form(&input_poly).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) + .compute_kzg_proof_with_roots_of_unity_coeff_form( + &input_poly, + index.try_into().unwrap(), + ) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); @@ -36,16 +37,17 @@ fn bench_kzg_verify(c: &mut Criterion) { c.bench_function("bench_kzg_verify_30000", |b| { let random_blob: Vec = (0..30000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = - rand::thread_rng().gen_range(0..input_poly.get_length_of_padded_blob_as_fr_vector()); - let commitment = kzg.commit(&input_poly.clone()).unwrap(); + rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); + let commitment = kzg.commit_coeff_form(&input_poly).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) + .compute_kzg_proof_with_roots_of_unity_coeff_form( + &input_poly, + index.try_into().unwrap(), + ) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); @@ -55,16 +57,17 @@ fn bench_kzg_verify(c: &mut Criterion) { c.bench_function("bench_kzg_verify_50000", |b| { let random_blob: Vec = (0..50000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input - .to_polynomial(PolynomialFormat::InCoefficientForm) - .unwrap(); + let input_poly = input.to_polynomial_coeff_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = - rand::thread_rng().gen_range(0..input_poly.get_length_of_padded_blob_as_fr_vector()); - let commitment = kzg.commit(&input_poly.clone()).unwrap(); + rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); + let commitment = kzg.commit_coeff_form(&input_poly).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) + .compute_kzg_proof_with_roots_of_unity_coeff_form( + &input_poly, + index.try_into().unwrap(), + ) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); diff --git a/tests/kzg_test.rs b/tests/kzg_test.rs index a281551..e262fdc 100644 --- a/tests/kzg_test.rs +++ b/tests/kzg_test.rs @@ -56,7 +56,7 @@ mod tests { } let polynomial = PolynomialCoeffForm::new(coeffs); - let result = KZG_3000.commit_coef_form(&polynomial); + let result = KZG_3000.commit_coeff_form(&polynomial); assert_eq!( result, Err(KzgError::SerializationError( @@ -224,9 +224,9 @@ mod tests { let index = rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); - let commitment = kzg.commit_coef_form(&input_poly.clone()).unwrap(); + let commitment = kzg.commit_coeff_form(&input_poly.clone()).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity_coef_form( + .compute_kzg_proof_with_roots_of_unity_coeff_form( &input_poly, index.try_into().unwrap(), ) @@ -274,9 +274,9 @@ mod tests { break; } } - let commitment = kzg.commit_coef_form(&input_poly).unwrap(); + let commitment = kzg.commit_coeff_form(&input_poly).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity_coef_form( + .compute_kzg_proof_with_roots_of_unity_coeff_form( &input_poly, index.try_into().unwrap(), ) From d88faf5d455108ba6e7216fd9058374c1f943544 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Mon, 6 Jan 2025 15:27:18 -0500 Subject: [PATCH 06/22] style: fix lint issues --- src/blob.rs | 1 - src/kzg.rs | 9 ++++++--- src/polynomial.rs | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index 1a67ba2..ac4e9d3 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,5 +1,4 @@ use crate::{ - errors::BlobError, helpers, polynomial::{PolynomialCoeffForm, PolynomialEvalForm}, }; diff --git a/src/kzg.rs b/src/kzg.rs index d90fdab..a1a1d15 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -479,7 +479,10 @@ impl Kzg { } /// commit the actual polynomial with the values setup - pub fn commit_coeff_form(&self, polynomial: &PolynomialCoeffForm) -> Result { + pub fn commit_coeff_form( + &self, + polynomial: &PolynomialCoeffForm, + ) -> Result { if polynomial.len() > self.g1.len() { return Err(KzgError::SerializationError( "polynomial length is not correct".to_string(), @@ -596,7 +599,7 @@ impl Kzg { if denom_poly[i].is_zero() { quotient_poly.push(self.compute_quotient_eval_on_domain( z_fr, - &eval_fr, + eval_fr, value_fr, root_of_unities, )); @@ -605,7 +608,7 @@ impl Kzg { } } - match G1Projective::msm(&bases, "ient_poly) { + match G1Projective::msm(bases, "ient_poly) { Ok(res) => Ok(G1Affine::from(res)), Err(err) => Err(KzgError::SerializationError(err.to_string())), } diff --git a/src/polynomial.rs b/src/polynomial.rs index e11b170..299d731 100644 --- a/src/polynomial.rs +++ b/src/polynomial.rs @@ -9,6 +9,9 @@ use ark_std::Zero; pub trait Polynomial { /// Returns the number of elements in the polynomial (whether coefficients or evaluations) fn len(&self) -> usize; + fn is_empty(&self) -> bool { + self.len() == 0 + } /// Returns the underlying elements, which can be either coefficients or evaluations. fn elements(&self) -> &[Fr]; } From f61a4337433405cee1f9f813b482649671c78f12 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Mon, 6 Jan 2025 17:08:58 -0500 Subject: [PATCH 07/22] fix: removed polymorphism on commit function this fixes all the tests and benchmarks that were failing The previous code also had the implicit assumption that it only worked with polys in eval form. So just made that explicit. We can add the function to commit from coeff form (by taking ifft for eg) later if needed, but opted to keep API simple for now. --- benches/bench_kzg_proof.rs | 27 +++++--------- benches/bench_kzg_verify.rs | 33 ++++++----------- src/kzg.rs | 73 ++++++++++--------------------------- src/polynomial.rs | 38 +++---------------- tests/kzg_test.rs | 44 +++++++++++----------- tests/polynomial_test.rs | 10 +++-- 6 files changed, 74 insertions(+), 151 deletions(-) diff --git a/benches/bench_kzg_proof.rs b/benches/bench_kzg_proof.rs index e5f896b..e8813f0 100644 --- a/benches/bench_kzg_proof.rs +++ b/benches/bench_kzg_proof.rs @@ -17,51 +17,42 @@ fn bench_kzg_proof(c: &mut Criterion) { c.bench_function("bench_kzg_proof_10000", |b| { let random_blob: Vec = (0..10000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input.to_polynomial_coeff_form(); + let input_poly = input.to_polynomial_eval_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); b.iter(|| { - kzg.compute_kzg_proof_with_roots_of_unity_coeff_form( - &input_poly, - index.try_into().unwrap(), - ) - .unwrap() + kzg.compute_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) + .unwrap() }); }); c.bench_function("bench_kzg_proof_30000", |b| { let random_blob: Vec = (0..30000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input.to_polynomial_coeff_form(); + let input_poly = input.to_polynomial_eval_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); b.iter(|| { - kzg.compute_kzg_proof_with_roots_of_unity_coeff_form( - &input_poly, - index.try_into().unwrap(), - ) - .unwrap() + kzg.compute_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) + .unwrap() }); }); c.bench_function("bench_kzg_proof_50000", |b| { let random_blob: Vec = (0..50000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input.to_polynomial_coeff_form(); + let input_poly = input.to_polynomial_eval_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); b.iter(|| { - kzg.compute_kzg_proof_with_roots_of_unity_coeff_form( - &input_poly, - index.try_into().unwrap(), - ) - .unwrap() + kzg.compute_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) + .unwrap() }); }); } diff --git a/benches/bench_kzg_verify.rs b/benches/bench_kzg_verify.rs index d6a7b5c..0fe8266 100644 --- a/benches/bench_kzg_verify.rs +++ b/benches/bench_kzg_verify.rs @@ -17,61 +17,52 @@ fn bench_kzg_verify(c: &mut Criterion) { c.bench_function("bench_kzg_verify_10000", |b| { let random_blob: Vec = (0..10000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input.to_polynomial_coeff_form(); + let input_poly = input.to_polynomial_eval_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); - let commitment = kzg.commit_coeff_form(&input_poly).unwrap(); + let commitment = kzg.commit_eval_form(&input_poly).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity_coeff_form( - &input_poly, - index.try_into().unwrap(), - ) + .compute_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); - b.iter(|| kzg.verify_kzg_proof(commitment, proof, *value_fr, *z_fr)); + b.iter(|| kzg.verify_proof(commitment, proof, *value_fr, *z_fr)); }); c.bench_function("bench_kzg_verify_30000", |b| { let random_blob: Vec = (0..30000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input.to_polynomial_coeff_form(); + let input_poly = input.to_polynomial_eval_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); - let commitment = kzg.commit_coeff_form(&input_poly).unwrap(); + let commitment = kzg.commit_eval_form(&input_poly).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity_coeff_form( - &input_poly, - index.try_into().unwrap(), - ) + .compute_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); - b.iter(|| kzg.verify_kzg_proof(commitment, proof, *value_fr, *z_fr)); + b.iter(|| kzg.verify_proof(commitment, proof, *value_fr, *z_fr)); }); c.bench_function("bench_kzg_verify_50000", |b| { let random_blob: Vec = (0..50000).map(|_| rng.gen_range(32..=126) as u8).collect(); let input = Blob::from_raw_data(&random_blob); - let input_poly = input.to_polynomial_coeff_form(); + let input_poly = input.to_polynomial_eval_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); - let commitment = kzg.commit_coeff_form(&input_poly).unwrap(); + let commitment = kzg.commit_eval_form(&input_poly).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity_coeff_form( - &input_poly, - index.try_into().unwrap(), - ) + .compute_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); - b.iter(|| kzg.verify_kzg_proof(commitment, proof, *value_fr, *z_fr)); + b.iter(|| kzg.verify_proof(commitment, proof, *value_fr, *z_fr)); }); } diff --git a/src/kzg.rs b/src/kzg.rs index a1a1d15..8014194 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -3,7 +3,7 @@ use crate::{ consts::BYTES_PER_FIELD_ELEMENT, errors::KzgError, helpers, - polynomial::{Polynomial, PolynomialCoeffForm, PolynomialEvalForm}, + polynomial::{PolynomialCoeffForm, PolynomialEvalForm}, traits::ReadPointFromBytes, }; use ark_bn254::{g1::G1Affine, Bn254, Fr, G1Projective, G2Affine}; @@ -21,6 +21,9 @@ use std::{ #[derive(Debug, PartialEq, Clone)] pub struct Kzg { + // SRS points are stored in monomial form, ready to be used for commitments with polynomials + // in coefficient form. To commit against a polynomial in evaluation form, we need to transform + // the SRS points to lagrange form using IFFT. g1: Vec, g2: Vec, params: Params, @@ -469,8 +472,8 @@ impl Kzg { )); } - // When the polynomial is in evaluation form, use the original g1 points - let bases = self.g1[..polynomial.len()].to_vec(); + // When the polynomial is in evaluation form, use IFFT to transform srs points to lagrange form. + let bases = self.g1_ifft(polynomial.len())?; match G1Projective::msm(&bases, polynomial.evaluations()) { Ok(res) => Ok(res.into_affine()), @@ -488,9 +491,8 @@ impl Kzg { "polynomial length is not correct".to_string(), )); } - // When the polynomial is in coefficient form, use inverse FFT on the srs points. - // TODO: is this correct? See https://github.com/Layr-Labs/rust-kzg-bn254/issues/20 - let bases = self.g1_ifft(polynomial.len())?; + // When the polynomial is in coefficient form, use the original srs points (in monomial form). + let bases = self.g1[..polynomial.len()].to_vec(); match G1Projective::msm(&bases, polynomial.coeffs()) { Ok(res) => Ok(res.into_affine()), @@ -504,60 +506,23 @@ impl Kzg { } /// helper function to work with the library and the env of the kzg instance - pub fn compute_kzg_proof_with_roots_of_unity_eval_form( + pub fn compute_proof_with_roots_of_unity( &self, polynomial: &PolynomialEvalForm, index: u64, ) -> Result { - self.compute_kzg_proof_eval_form(polynomial, index, &self.expanded_roots_of_unity) + self.compute_proof(polynomial, index, &self.expanded_roots_of_unity) } - pub fn compute_kzg_proof_with_roots_of_unity_coeff_form( - &self, - polynomial: &PolynomialCoeffForm, - index: u64, - ) -> Result { - self.compute_kzg_proof_coeff_form(polynomial, index, &self.expanded_roots_of_unity) - } - - fn compute_kzg_proof_eval_form( + /// Compute a kzg proof from a polynomial in evaluation form. + /// We don't currently support proofs for polynomials in coefficient form, but one can + /// take the FFT of the polynomial in coefficient form to get the polynomial in evaluation form. + /// This is available via the method [PolynomialCoeffForm::to_eval_form]. + pub fn compute_proof( &self, polynomial: &PolynomialEvalForm, index: u64, root_of_unities: &[Fr], - ) -> Result { - self.compute_kzg_proof( - Box::new(polynomial), - index, - root_of_unities, - &self.g1[..polynomial.len()], - ) - } - - pub fn compute_kzg_proof_coeff_form( - &self, - polynomial: &PolynomialCoeffForm, - index: u64, - root_of_unities: &[Fr], - ) -> Result { - self.compute_kzg_proof( - Box::new(polynomial), - index, - root_of_unities, - &self.g1_ifft(polynomial.len())?, - ) - } - - /// function to compute the kzg proof given the values. - fn compute_kzg_proof( - &self, - // Boxed polynomial to allow working with eval and coeff form polynomials. - // This was done to copy the previous implementation at: https://github.com/Layr-Labs/rust-kzg-bn254/blob/85be35704cfd64e53f7e63ee7ec1601bfb4abe26/src/kzg.rs#L566 - // TODO: is this implementation correct? - polynomial: Box, - index: u64, - root_of_unities: &[Fr], - bases: &[G1Affine], ) -> Result { if !self.params.completed_setup { return Err(KzgError::GenericError( @@ -571,7 +536,7 @@ impl Kzg { )); } - let eval_fr = polynomial.elements(); + let eval_fr = polynomial.evaluations(); let mut poly_shift: Vec = Vec::with_capacity(eval_fr.len()); let usized_index = if let Some(x) = index.to_usize() { x @@ -608,7 +573,9 @@ impl Kzg { } } - match G1Projective::msm(bases, "ient_poly) { + let bases = self.g1_ifft(polynomial.len())?; + + match G1Projective::msm(&bases, "ient_poly) { Ok(res) => Ok(G1Affine::from(res)), Err(err) => Err(KzgError::SerializationError(err.to_string())), } @@ -668,7 +635,7 @@ impl Kzg { Ok(ifft_result) } - pub fn verify_kzg_proof( + pub fn verify_proof( &self, commitment: G1Affine, proof: G1Affine, diff --git a/src/polynomial.rs b/src/polynomial.rs index 299d731..0318060 100644 --- a/src/polynomial.rs +++ b/src/polynomial.rs @@ -3,19 +3,6 @@ use ark_bn254::Fr; use ark_poly::{EvaluationDomain, GeneralEvaluationDomain}; use ark_std::Zero; -/// Polynomial trait that defines the common methods for both Coefficient and Evaluation forms. -/// Right now this is only used in [kzg::Kzg::compute_kzg_proof_coeff_form]. -/// TODO: feels like a hack. Is there a better API design that wouldn't require this? -pub trait Polynomial { - /// Returns the number of elements in the polynomial (whether coefficients or evaluations) - fn len(&self) -> usize; - fn is_empty(&self) -> bool { - self.len() == 0 - } - /// Returns the underlying elements, which can be either coefficients or evaluations. - fn elements(&self) -> &[Fr]; -} - #[derive(Clone, Debug, PartialEq)] pub struct PolynomialCoeffForm { /// coeffs contains the coefficients of the polynomial, padded with 0s to the next power of two. @@ -32,16 +19,6 @@ pub struct PolynomialCoeffForm { len_underlying_blob_bytes: usize, } -impl Polynomial for &PolynomialCoeffForm { - fn len(&self) -> usize { - self.coeffs.len() - } - - fn elements(&self) -> &[Fr] { - self.coeffs() - } -} - impl PolynomialCoeffForm { pub fn new(coeffs: Vec) -> Self { let underlying_blob_len_in_bytes = coeffs.len() * BYTES_PER_FIELD_ELEMENT; @@ -116,16 +93,6 @@ pub struct PolynomialEvalForm { len_underlying_blob_bytes: usize, } -impl Polynomial for &PolynomialEvalForm { - fn len(&self) -> usize { - self.evaluations.len() - } - - fn elements(&self) -> &[Fr] { - self.evaluations() - } -} - impl PolynomialEvalForm { pub fn new(evals: Vec) -> Self { let underlying_blob_len_in_bytes = evals.len() * BYTES_PER_FIELD_ELEMENT; @@ -156,6 +123,11 @@ impl PolynomialEvalForm { self.len_underlying_blob_bytes } + /// Similar to [Self::len_underlying_blob_bytes], but returns the number of field elements instead of bytes + pub fn len_underlying_blob_field_elements(&self) -> usize { + self.len_underlying_blob_bytes / BYTES_PER_FIELD_ELEMENT + } + pub fn get_at_index(&self, i: usize) -> Option<&Fr> { self.evaluations.get(i) } diff --git a/tests/kzg_test.rs b/tests/kzg_test.rs index e262fdc..9552586 100644 --- a/tests/kzg_test.rs +++ b/tests/kzg_test.rs @@ -3,7 +3,11 @@ mod tests { use ark_bn254::{Fr, G1Affine, G2Affine}; use lazy_static::lazy_static; use rust_kzg_bn254::{ - blob::Blob, errors::KzgError, helpers, kzg::Kzg, polynomial::PolynomialCoeffForm, + blob::Blob, + errors::KzgError, + helpers, + kzg::Kzg, + polynomial::{PolynomialCoeffForm, PolynomialEvalForm}, }; use std::{ env, @@ -218,28 +222,25 @@ mod tests { println!("generating blob of length is {}", blob_length); let input = Blob::from_raw_data(&random_blob); - let input_poly = input.to_polynomial_coeff_form(); + let input_poly = input.to_polynomial_eval_form(); kzg.data_setup_custom(1, input.len().try_into().unwrap()) .unwrap(); let index = rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); - let commitment = kzg.commit_coeff_form(&input_poly.clone()).unwrap(); + let commitment = kzg.commit_eval_form(&input_poly.clone()).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity_coeff_form( - &input_poly, - index.try_into().unwrap(), - ) + .compute_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); let pairing_result = - kzg.verify_kzg_proof(commitment, proof, value_fr.clone(), z_fr.clone()); + kzg.verify_proof(commitment, proof, value_fr.clone(), z_fr.clone()); assert_eq!(pairing_result, true); // take random index, not the same index and check assert_eq!( - kzg.verify_kzg_proof( + kzg.verify_proof( commitment, proof, value_fr.clone(), @@ -255,39 +256,38 @@ mod tests { } #[test] - fn test_compute_kzg_proof() { + fn test_compute_proof() { use rand::Rng; let mut kzg = KZG_INSTANCE.clone(); let input = Blob::from_raw_data(GETTYSBURG_ADDRESS_BYTES); - let input_poly = input.to_polynomial_coeff_form(); + let input_poly = input.to_polynomial_eval_form(); for index in 0..input_poly.len() - 1 { kzg.data_setup_custom(4, input.len().try_into().unwrap()) .unwrap(); - let mut rand_index = rand::thread_rng().gen_range(0..input_poly.len()); + let mut rand_index = + rand::thread_rng().gen_range(0..input_poly.len_underlying_blob_field_elements()); loop { if index == rand_index { - rand_index = rand::thread_rng().gen_range(0..input_poly.len()); + rand_index = rand::thread_rng() + .gen_range(0..input_poly.len_underlying_blob_field_elements()); } else { break; } } - let commitment = kzg.commit_coeff_form(&input_poly).unwrap(); + let commitment = kzg.commit_eval_form(&input_poly).unwrap(); let proof = kzg - .compute_kzg_proof_with_roots_of_unity_coeff_form( - &input_poly, - index.try_into().unwrap(), - ) + .compute_proof_with_roots_of_unity(&input_poly, index.try_into().unwrap()) .unwrap(); let value_fr = input_poly.get_at_index(index).unwrap(); let z_fr = kzg.get_nth_root_of_unity(index).unwrap(); let pairing_result = - kzg.verify_kzg_proof(commitment, proof, value_fr.clone(), z_fr.clone()); + kzg.verify_proof(commitment, proof, value_fr.clone(), z_fr.clone()); assert_eq!(pairing_result, true); assert_eq!( - kzg.verify_kzg_proof( + kzg.verify_proof( commitment, proof, value_fr.clone(), @@ -453,11 +453,11 @@ mod tests { let hard_coded_x = Fq::from_str(the_strings_str[1]).expect("should be fine"); let hard_coded_y = Fq::from_str(the_strings_str[2]).expect("should be fine"); let gnark_proof = G1Affine::new(hard_coded_x, hard_coded_y); - let poly = PolynomialCoeffForm::new(padded_input_fr_elements.to_vec()); + let poly = PolynomialEvalForm::new(padded_input_fr_elements.to_vec()); kzg.data_setup_custom(4, poly.len().try_into().unwrap()) .unwrap(); let result = kzg - .compute_kzg_proof_coeff_form(&poly, index, &roots_of_unities) + .compute_proof(&poly, index, &roots_of_unities) .unwrap(); assert_eq!(gnark_proof, result) } diff --git a/tests/polynomial_test.rs b/tests/polynomial_test.rs index 3c60b3f..b3568a2 100644 --- a/tests/polynomial_test.rs +++ b/tests/polynomial_test.rs @@ -14,7 +14,7 @@ mod tests { ); let poly = blob.to_polynomial_coeff_form(); assert_eq!( - poly.to_bytes_be(), + &poly.to_bytes_be()[0..blob.data().len()], blob.data(), "should be deserialized properly" ); @@ -33,7 +33,7 @@ mod tests { // let ga_converted_fr = to_fr_array(&ga_converted); assert_eq!( long_blob.data(), - &long_poly.to_bytes_be(), + &long_poly.to_bytes_be()[0..long_blob.data().len()], "should be deserialized properly" ); } @@ -52,7 +52,7 @@ mod tests { let poly_eval = poly_coeff.to_eval_form().unwrap(); let poly_coeff_back = poly_eval.to_coef_form().unwrap(); assert_eq!( - &poly_coeff_back.to_bytes_be(), + &poly_coeff_back.to_bytes_be()[0..blob.data().len()], blob.data(), "start and finish bytes should be the same" ); @@ -66,7 +66,9 @@ mod tests { let poly_eval = poly_coeff.to_eval_form().unwrap(); let poly_coeff_back = poly_eval.to_coef_form().unwrap(); assert_eq!( - &poly_coeff_back.to_bytes_be(), + // TODO: we might want to change the API to return the underlying blob data len? + // right now this info is lost after converting between forms. + &poly_coeff_back.to_bytes_be()[0..blob.data().len()], blob.data(), "start and finish bytes should be the same" ); From 965fea855aee607e2796803df7247ae8cc242016 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Mon, 6 Jan 2025 17:19:09 -0500 Subject: [PATCH 08/22] docs: add README section about kzg commitment --- README.md | 19 ++++++++++++------- kzg_commitment_diagram.png | Bin 0 -> 71730 bytes 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 kzg_commitment_diagram.png diff --git a/README.md b/README.md index 04f9913..dec808f 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,7 @@ See the `test_compute_kzg_proof` function in [./tests/kzg_test.rs](./tests/kzg_t ## Function Reference -### `from_bytes_and_pad()` - -The `Blob` is loaded with `from_bytes_and_pad` which accepts bytes and "pads" it so that the data fits within the requirements of Eigen DA functioning. It also keeps track of the blob length after padding. - -### `to_polynomial()` +### `to_polynomial_coeff_form()` / `to_polynomial_eval_form()` From the `Blob`, a polynomial can be obtained via calling the `to_polynomial()` function. This converts the Blob to Field elements, then calculates the next power of 2 from this length of field elements and appends `zero` value elements for the remaining length. @@ -51,6 +47,15 @@ The `data_setup_custom` (for testing) or `data_setup_mins` should be used to spe The `commit` function takes in a `polynomial`. It is computed over `lagrange` basis by performing the (i)FFT depending on the `polynomial` form specified. -### `compute_kzg_proof_with_roots_of_unity()` +### `compute_proof_with_roots_of_unity()` + +The `compute_proof_with_roots_of_unity` takes in a `Polynomial` and an `index` at which it needs to be computed. + +## KZG Commitments + +Below diagram explains the difference types involved between polynomials, SRS points, and kzg commitments. +A KZG commitment can be taken by an inner product between (poly_eval, srs_monomial) or (poly_coeff, srs_lagrange). FFT and IFFT operations can be performed to convert between these forms. + +![KZG Commitments](./kzg_commitment_diagram.png) -The `compute_kzg_proof_with_roots_of_unity` takes in a `Polynomial` and an `index` at which it needs to be computed. +Our current codebase has the types PolynomialEvalForm and PolynomialCoeffForm to represent the polynomial in evaluation and coefficient form respectively. However, we do not have types to represent the two forms of srs points. They are implicitly assumed to be in monomial form when loaded, and an IFFT is performed before taking the inner product with the polynomial in evaluation form. \ No newline at end of file diff --git a/kzg_commitment_diagram.png b/kzg_commitment_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..85801a35f532960f2abe4a7a218b81fd1b603180 GIT binary patch literal 71730 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 Ay8r+H literal 0 HcmV?d00001 From 5933b0102e8d688d23e1d3e72c9c2e451431f2e1 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Mon, 6 Jan 2025 17:19:55 -0500 Subject: [PATCH 09/22] style: cargo fmt --- src/blob.rs | 15 ++++++----- src/kzg.rs | 31 ++++++++++++++--------- src/polynomial.rs | 63 +++++++++++++++++++++++++++-------------------- tests/kzg_test.rs | 4 +-- 4 files changed, 65 insertions(+), 48 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index ac4e9d3..58ae212 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -14,28 +14,31 @@ pub struct Blob { impl Blob { /// Creates a new `Blob` from the given blob_data. /// blob_data should already be padded according to DA specs, meaning - /// that it contains bn254 field elements. Otherwise, use [`Blob::from_raw_data`]. + /// that it contains bn254 field elements. Otherwise, use + /// [`Blob::from_raw_data`]. /// /// WARNING: This function does not check if the bytes are modulo bn254 /// if the data has 32 byte segments exceeding the modulo of the field /// then the bytes will be modded by the order of the field and the data /// will be transformed incorrectly. - /// TODO: we should check that the bytes are correct and return an error instead of - /// relying on the users reading this documentation. + /// TODO: we should check that the bytes are correct and return an error + /// instead of relying on the users reading this documentation. pub fn new(blob_data: &[u8]) -> Self { Blob { blob_data: blob_data.to_vec(), } } - /// Creates a new `Blob` from the provided raw_data byte slice and pads it according - /// to DA specs. If the data is already padded, use [`Blob::new`] instead. + /// Creates a new `Blob` from the provided raw_data byte slice and pads it + /// according to DA specs. If the data is already padded, use + /// [`Blob::new`] instead. pub fn from_raw_data(raw_data: &[u8]) -> Self { let blob_data = helpers::convert_by_padding_empty_byte(raw_data); Blob { blob_data } } - /// Returns the raw data of the blob, removing any padding added by [`Blob::from_raw_data`]. + /// Returns the raw data of the blob, removing any padding added by + /// [`Blob::from_raw_data`]. pub fn to_raw_data(&self) -> Vec { helpers::remove_empty_byte_from_padded_bytes_unchecked(&self.blob_data) } diff --git a/src/kzg.rs b/src/kzg.rs index 8014194..75c59d3 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -272,7 +272,8 @@ impl Kzg { } /// read files in chunks with specified length - /// TODO: chunks seems misleading here, since we read one field element at a time. + /// TODO: chunks seems misleading here, since we read one field element at a + /// time. fn read_file_chunks( file_path: &str, sender: Sender<(Vec, usize, bool)>, @@ -287,8 +288,9 @@ impl Kzg { let mut i = 0; // We are making one syscall per field element, which is super inefficient. - // FIXME: Read the entire file (or large segments) into memory and then split it into field elements. - // Entire G1 file might be ~8GiB, so might not fit in RAM. + // FIXME: Read the entire file (or large segments) into memory and then split it + // into field elements. Entire G1 file might be ~8GiB, so might not fit + // in RAM. while let Ok(bytes_read) = reader.read(&mut buffer) { if bytes_read == 0 { break; @@ -352,9 +354,10 @@ impl Kzg { Ok(all_points.iter().map(|(point, _)| *point).collect()) } - /// read G1 points in parallel, by creating one reader thread, which reads bytes from the file, - /// and fans them out to worker threads (one per cpu) which parse the bytes into G1Affine points. - /// The worker threads then fan in the parsed points to the main thread, which sorts them by + /// read G1 points in parallel, by creating one reader thread, which reads + /// bytes from the file, and fans them out to worker threads (one per + /// cpu) which parse the bytes into G1Affine points. The worker threads + /// then fan in the parsed points to the main thread, which sorts them by /// their original position in the file to maintain order. /// /// # Arguments @@ -370,7 +373,8 @@ impl Kzg { srs_points_to_load: u32, is_native: bool, ) -> Result, KzgError> { - // Channel contains (bytes, position, is_native) tuples. The position is used to reorder the points after processing them. + // Channel contains (bytes, position, is_native) tuples. The position is used to + // reorder the points after processing them. let (sender, receiver) = bounded::<(Vec, usize, bool)>(1000); // Spawning the reader thread @@ -472,7 +476,8 @@ impl Kzg { )); } - // When the polynomial is in evaluation form, use IFFT to transform srs points to lagrange form. + // When the polynomial is in evaluation form, use IFFT to transform srs points + // to lagrange form. let bases = self.g1_ifft(polynomial.len())?; match G1Projective::msm(&bases, polynomial.evaluations()) { @@ -491,7 +496,8 @@ impl Kzg { "polynomial length is not correct".to_string(), )); } - // When the polynomial is in coefficient form, use the original srs points (in monomial form). + // When the polynomial is in coefficient form, use the original srs points (in + // monomial form). let bases = self.g1[..polynomial.len()].to_vec(); match G1Projective::msm(&bases, polynomial.coeffs()) { @@ -515,9 +521,10 @@ impl Kzg { } /// Compute a kzg proof from a polynomial in evaluation form. - /// We don't currently support proofs for polynomials in coefficient form, but one can - /// take the FFT of the polynomial in coefficient form to get the polynomial in evaluation form. - /// This is available via the method [PolynomialCoeffForm::to_eval_form]. + /// We don't currently support proofs for polynomials in coefficient form, + /// but one can take the FFT of the polynomial in coefficient form to + /// get the polynomial in evaluation form. This is available via the + /// method [PolynomialCoeffForm::to_eval_form]. pub fn compute_proof( &self, polynomial: &PolynomialEvalForm, diff --git a/src/polynomial.rs b/src/polynomial.rs index 0318060..ef5320f 100644 --- a/src/polynomial.rs +++ b/src/polynomial.rs @@ -5,17 +5,20 @@ use ark_std::Zero; #[derive(Clone, Debug, PartialEq)] pub struct PolynomialCoeffForm { - /// coeffs contains the coefficients of the polynomial, padded with 0s to the next power of two. - /// Hence if the polynomial is created with coefficients [1, 2, 3], the internal representation - /// will be [1, 2, 3, 0]. + /// coeffs contains the coefficients of the polynomial, padded with 0s to + /// the next power of two. Hence if the polynomial is created with + /// coefficients [1, 2, 3], the internal representation will be [1, 2, + /// 3, 0]. coeffs: Vec, - /// Number of bytes in the underlying blob, which was used to create the polynomial. - /// This is passed as is when converting between Coefficient and Evaluation forms, - /// so that the blob can be reconstructed with the same length. + /// Number of bytes in the underlying blob, which was used to create the + /// polynomial. This is passed as is when converting between Coefficient + /// and Evaluation forms, so that the blob can be reconstructed with the + /// same length. /// - /// TODO: We should get rid of this: polynomial should not know about the blob. - /// This len is equivalent to the coeffs len before it gets padded. - /// Perhaps we can store the original coeffs and only pad when needed? + /// TODO: We should get rid of this: polynomial should not know about the + /// blob. This len is equivalent to the coeffs len before it gets + /// padded. Perhaps we can store the original coeffs and only pad + /// when needed? len_underlying_blob_bytes: usize, } @@ -36,9 +39,9 @@ impl PolynomialCoeffForm { &self.coeffs } - /// Returns the number of coefficients in the polynomial. Note that this returns the number of - /// coefficients in the padded polynomial, not the number of coefficients in the original - /// polynomial. + /// Returns the number of coefficients in the polynomial. Note that this + /// returns the number of coefficients in the padded polynomial, not the + /// number of coefficients in the original polynomial. pub fn len(&self) -> usize { self.coeffs.len() } @@ -48,7 +51,8 @@ impl PolynomialCoeffForm { self.len_underlying_blob_bytes } - /// Similar to [Self::len_underlying_blob_bytes], but returns the number of field elements instead of bytes + /// Similar to [Self::len_underlying_blob_bytes], but returns the number of + /// field elements instead of bytes pub fn len_underlying_blob_field_elements(&self) -> usize { self.len_underlying_blob_bytes / BYTES_PER_FIELD_ELEMENT } @@ -79,17 +83,20 @@ impl PolynomialCoeffForm { #[derive(Clone, Debug, PartialEq)] pub struct PolynomialEvalForm { - /// evaluations contains the evaluations of the polynomial, padded with 0s to the next power of two. - /// Hence if the polynomial is created with coefficients [1, 2, 3], the internal representation - /// will be [1, 2, 3, 0]. + /// evaluations contains the evaluations of the polynomial, padded with 0s + /// to the next power of two. Hence if the polynomial is created with + /// coefficients [1, 2, 3], the internal representation will be [1, 2, + /// 3, 0]. evaluations: Vec, - /// Number of bytes in the underlying blob, which was used to create the polynomial. - /// This is passed as is when converting between Coefficient and Evaluation forms, - /// so that the blob can be reconstructed with the same length. + /// Number of bytes in the underlying blob, which was used to create the + /// polynomial. This is passed as is when converting between Coefficient + /// and Evaluation forms, so that the blob can be reconstructed with the + /// same length. /// - /// TODO: We should get rid of this: polynomial should not know about the blob. - /// This len is equivalent to the coeffs len before it gets padded. - /// Perhaps we can store the original coeffs and only pad when needed? + /// TODO: We should get rid of this: polynomial should not know about the + /// blob. This len is equivalent to the coeffs len before it gets + /// padded. Perhaps we can store the original coeffs and only pad + /// when needed? len_underlying_blob_bytes: usize, } @@ -98,7 +105,8 @@ impl PolynomialEvalForm { let underlying_blob_len_in_bytes = evals.len() * BYTES_PER_FIELD_ELEMENT; let next_power_of_two = evals.len().next_power_of_two(); let mut padded_evals = evals; - // TODO: does it make sense to pad evaluations with zeros? This changes the polynomial... + // TODO: does it make sense to pad evaluations with zeros? This changes the + // polynomial... padded_evals.resize(next_power_of_two, Fr::zero()); Self { @@ -111,9 +119,9 @@ impl PolynomialEvalForm { &self.evaluations } - /// Returns the number of evaluations in the polynomial. Note that this returns the number of - /// evaluations in the padded polynomial, not the number of evaluations in the original - /// polynomial. + /// Returns the number of evaluations in the polynomial. Note that this + /// returns the number of evaluations in the padded polynomial, not the + /// number of evaluations in the original polynomial. pub fn len(&self) -> usize { self.evaluations.len() } @@ -123,7 +131,8 @@ impl PolynomialEvalForm { self.len_underlying_blob_bytes } - /// Similar to [Self::len_underlying_blob_bytes], but returns the number of field elements instead of bytes + /// Similar to [Self::len_underlying_blob_bytes], but returns the number of + /// field elements instead of bytes pub fn len_underlying_blob_field_elements(&self) -> usize { self.len_underlying_blob_bytes / BYTES_PER_FIELD_ELEMENT } diff --git a/tests/kzg_test.rs b/tests/kzg_test.rs index 9552586..9f13e59 100644 --- a/tests/kzg_test.rs +++ b/tests/kzg_test.rs @@ -456,9 +456,7 @@ mod tests { let poly = PolynomialEvalForm::new(padded_input_fr_elements.to_vec()); kzg.data_setup_custom(4, poly.len().try_into().unwrap()) .unwrap(); - let result = kzg - .compute_proof(&poly, index, &roots_of_unities) - .unwrap(); + let result = kzg.compute_proof(&poly, index, &roots_of_unities).unwrap(); assert_eq!(gnark_proof, result) } } From 0ed4496946b7707455d5623dc387389bde6fd0a8 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 10:53:27 -0500 Subject: [PATCH 10/22] docs: fix commitment explanation in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dec808f..88e8ab9 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ The `compute_proof_with_roots_of_unity` takes in a `Polynomial` and an `index` a ## KZG Commitments Below diagram explains the difference types involved between polynomials, SRS points, and kzg commitments. -A KZG commitment can be taken by an inner product between (poly_eval, srs_monomial) or (poly_coeff, srs_lagrange). FFT and IFFT operations can be performed to convert between these forms. +A KZG commitment can be taken by an inner product between (poly_coeff, srs_monomial) or (poly_eval, srs_lagrange). FFT and IFFT operations can be performed to convert between these forms. ![KZG Commitments](./kzg_commitment_diagram.png) From 09444ffc8e0bdd5362917e2cf4f9f32b7624fb05 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 11:13:10 -0500 Subject: [PATCH 11/22] docs: add doc comment for fn convert_by_padding_empty_byte --- src/helpers.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers.rs b/src/helpers.rs index 008a6d4..8c40d6e 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -28,6 +28,11 @@ pub fn set_bytes_canonical_manual(data: &[u8]) -> Fr { // Functions being used +/// Copied the referenced bytes array argument into a Vec, inserting an empty byte at the front of every 31 bytes. +/// The empty byte is padded at the low address, because we use big endian to interpret a field element. +/// This ensures every 32 bytes is within the valid range of a field element for the bn254 curve. +/// If the input data is not a multiple of 31 bytes, the remainder is added to the output by +/// inserting a 0 and the remainder. The output is thus not necessarily a multiple of 32. pub fn convert_by_padding_empty_byte(data: &[u8]) -> Vec { let data_size = data.len(); let parse_size = BYTES_PER_FIELD_ELEMENT - 1; From 464ac34c0f65d19928f819a43ae93b5d95a13559 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 11:15:04 -0500 Subject: [PATCH 12/22] docs: make read_file_chunks comment more precise --- src/kzg.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/kzg.rs b/src/kzg.rs index 75c59d3..0aa04ee 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -290,7 +290,8 @@ impl Kzg { // We are making one syscall per field element, which is super inefficient. // FIXME: Read the entire file (or large segments) into memory and then split it // into field elements. Entire G1 file might be ~8GiB, so might not fit - // in RAM. + // in RAM. But we can only read the subset of the file that we need. + // For eg. for fault proof usage, only need to read 32MiB if our blob size is that large. while let Ok(bytes_read) = reader.read(&mut buffer) { if bytes_read == 0 { break; From 88eb5ff37dc87ba7f8c61c041926d40396bde48a Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 11:16:06 -0500 Subject: [PATCH 13/22] docs: add "monomial" prefix to srs points in comment --- src/kzg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kzg.rs b/src/kzg.rs index 0aa04ee..635d3ff 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -477,7 +477,7 @@ impl Kzg { )); } - // When the polynomial is in evaluation form, use IFFT to transform srs points + // When the polynomial is in evaluation form, use IFFT to transform monomial srs points // to lagrange form. let bases = self.g1_ifft(polynomial.len())?; From c4c2e9134a76f686580e83616446b44eeab77c7a Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 11:18:22 -0500 Subject: [PATCH 14/22] style: rename function to_coef_form -> to_coeff_form for consistency --- src/polynomial.rs | 2 +- tests/polynomial_test.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/polynomial.rs b/src/polynomial.rs index ef5320f..5602de6 100644 --- a/src/polynomial.rs +++ b/src/polynomial.rs @@ -151,7 +151,7 @@ impl PolynomialEvalForm { helpers::to_byte_array(&self.evaluations, self.len_underlying_blob_bytes) } - pub fn to_coef_form(&self) -> Result { + pub fn to_coeff_form(&self) -> Result { let coeffs = GeneralEvaluationDomain::::new(self.len()) .ok_or(PolynomialError::FFTError( "Failed to construct domain for IFFT".to_string(), diff --git a/tests/polynomial_test.rs b/tests/polynomial_test.rs index b3568a2..f8c29bb 100644 --- a/tests/polynomial_test.rs +++ b/tests/polynomial_test.rs @@ -50,7 +50,7 @@ mod tests { let poly_coeff = blob.to_polynomial_coeff_form(); let poly_eval = poly_coeff.to_eval_form().unwrap(); - let poly_coeff_back = poly_eval.to_coef_form().unwrap(); + let poly_coeff_back = poly_eval.to_coeff_form().unwrap(); assert_eq!( &poly_coeff_back.to_bytes_be()[0..blob.data().len()], blob.data(), @@ -64,7 +64,7 @@ mod tests { let poly_coeff = blob.to_polynomial_coeff_form(); let poly_eval = poly_coeff.to_eval_form().unwrap(); - let poly_coeff_back = poly_eval.to_coef_form().unwrap(); + let poly_coeff_back = poly_eval.to_coeff_form().unwrap(); assert_eq!( // TODO: we might want to change the API to return the underlying blob data len? // right now this info is lost after converting between forms. From 123578bd9b59500813448d3fa383ff9260d088b1 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 14:05:02 -0500 Subject: [PATCH 15/22] docs: clarify some comments --- src/blob.rs | 6 +++--- src/helpers.rs | 12 +++++++----- src/kzg.rs | 7 ++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index 58ae212..9cd00ae 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -17,12 +17,12 @@ impl Blob { /// that it contains bn254 field elements. Otherwise, use /// [`Blob::from_raw_data`]. /// - /// WARNING: This function does not check if the bytes are modulo bn254 - /// if the data has 32 byte segments exceeding the modulo of the field + /// WARNING: This function does not check if the bytes are modulo bn254. + /// If the data has 32 byte segments exceeding the modulo of the field /// then the bytes will be modded by the order of the field and the data /// will be transformed incorrectly. /// TODO: we should check that the bytes are correct and return an error - /// instead of relying on the users reading this documentation. + /// instead of relying on the users reading this documentation. pub fn new(blob_data: &[u8]) -> Self { Blob { blob_data: blob_data.to_vec(), diff --git a/src/helpers.rs b/src/helpers.rs index 8c40d6e..f049240 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -28,11 +28,13 @@ pub fn set_bytes_canonical_manual(data: &[u8]) -> Fr { // Functions being used -/// Copied the referenced bytes array argument into a Vec, inserting an empty byte at the front of every 31 bytes. -/// The empty byte is padded at the low address, because we use big endian to interpret a field element. -/// This ensures every 32 bytes is within the valid range of a field element for the bn254 curve. -/// If the input data is not a multiple of 31 bytes, the remainder is added to the output by -/// inserting a 0 and the remainder. The output is thus not necessarily a multiple of 32. +/// Copied the referenced bytes array argument into a Vec, inserting an empty +/// byte at the front of every 31 bytes. The empty byte is padded at the low +/// address, because we use big endian to interpret a field element. +/// This ensures every 32 bytes is within the valid range of a field element for +/// the bn254 curve. If the input data is not a multiple of 31 bytes, the +/// remainder is added to the output by inserting a 0 and the remainder. The +/// output is thus not necessarily a multiple of 32. pub fn convert_by_padding_empty_byte(data: &[u8]) -> Vec { let data_size = data.len(); let parse_size = BYTES_PER_FIELD_ELEMENT - 1; diff --git a/src/kzg.rs b/src/kzg.rs index 635d3ff..13243d0 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -291,7 +291,8 @@ impl Kzg { // FIXME: Read the entire file (or large segments) into memory and then split it // into field elements. Entire G1 file might be ~8GiB, so might not fit // in RAM. But we can only read the subset of the file that we need. - // For eg. for fault proof usage, only need to read 32MiB if our blob size is that large. + // For eg. for fault proof usage, only need to read 32MiB if our blob size is + // that large. while let Ok(bytes_read) = reader.read(&mut buffer) { if bytes_read == 0 { break; @@ -477,8 +478,8 @@ impl Kzg { )); } - // When the polynomial is in evaluation form, use IFFT to transform monomial srs points - // to lagrange form. + // When the polynomial is in evaluation form, use IFFT to transform monomial srs + // points to lagrange form. let bases = self.g1_ifft(polynomial.len())?; match G1Projective::msm(&bases, polynomial.evaluations()) { From 5ee952ea4dd35d23d094021c845a9a1130860aa3 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 16:31:17 -0500 Subject: [PATCH 16/22] docs: clean up and make much more precise data type documentation + readme --- README.md | 42 ++++++++++++++++++++++++------------------ src/blob.rs | 4 ++-- src/kzg.rs | 18 ++++++++++++------ src/polynomial.rs | 19 ++++++++++++++++--- tests/kzg_test.rs | 2 +- 5 files changed, 55 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 88e8ab9..afb0cd3 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,6 @@ This code is unaudited and under construction. This is experimental software and 2. Specify the files in `kzg.setup()` function, leave the `g2_points` empty, and specify the `srs_order` per the guide. 3. Note that this is process will take a few minutes to load since it is a bit intensive. -## Clippy -Linting can be triggered via running `cargo clippy --all --manifest-path Cargo.toml -- -D warnings`. - ## Quick Start See the `test_compute_kzg_proof` function in [./tests/kzg_test.rs](./tests/kzg_test.rs#) for an end to end usage of the library. @@ -32,30 +29,39 @@ See the `test_compute_kzg_proof` function in [./tests/kzg_test.rs](./tests/kzg_t 2. Committing is performed in Lagrange format. The required IFFT is done within the function and is not required to be performed separately. 3. For proof generation, the data is treated as evaluation of polynomial. The required (I)FFT is performed by the compute function and is not required to be performed separately. -## Function Reference - -### `to_polynomial_coeff_form()` / `to_polynomial_eval_form()` - -From the `Blob`, a polynomial can be obtained via calling the `to_polynomial()` function. This converts the Blob to Field elements, then calculates the next power of 2 from this length of field elements and appends `zero` value elements for the remaining length. +## Library Design / Architecture -### `data_setup_custom` and `data_setup_mins` parameters +The main purpose of this library is to allow taking a piece of data, committing to it, and then generating and verifying proofs against that commitment. -The `data_setup_custom` (for testing) or `data_setup_mins` should be used to specify the number of chunks and chunk length. These parameters are used to calculate the FFT params required for FFT operations. +### Data Types -### `commit()` +The main data pipeline goes: +> user data -> Blob -> Polynomial -> KZG Commitment / Proof -The `commit` function takes in a `polynomial`. It is computed over `lagrange` basis by performing the (i)FFT depending on the `polynomial` form specified. +- User Data: bytes array + - meaningful to users (typically will be a rollup batch) +- Blob: bn254 field elements array + - meaningful to EigenDA network + - Obtained from User Data by inserting zeroes every 31 bytes to make every 32 byte an element of bn254. +- Polynomial: bn254 field elements array, interpreted as coefficients or evaluations of a polynomial + - meaningful when committing and generating/verifying proofs + - Obtained from Blob by appending zeroes to make the length a power of 2, and then interpreting the array as coefficients or evaluations of a polynomial. +- Kzg: struct storing the SRS points used to generate commitments and proofs +- SRS points: bn254 group elements + - inner producted with the polynomial to generate commitments +The [Blob](./src/blob.rs) and [Polynomial](./src/polynomial.rs) structs are mostly [Plain Old Data](https://en.wikipedia.org/wiki/Passive_data_structure) with constructor and few helper methods. The interesting stuff happens in the [KZG](./src/kzg.rs) struct, which has methods for committing to a blob, polynomial in coeff or eval form, and generating and verifying proofs. -### `compute_proof_with_roots_of_unity()` +Our current codebase has the types PolynomialEvalForm and PolynomialCoeffForm to represent the polynomial in evaluation and coefficient form respectively. However, we do not have types to represent the two forms of srs points. They are implicitly assumed to be in monomial form when loaded, and an IFFT is performed before taking the inner product with the polynomial in evaluation form. -The `compute_proof_with_roots_of_unity` takes in a `Polynomial` and an `index` at which it needs to be computed. +### KZG Commitments -## KZG Commitments - -Below diagram explains the difference types involved between polynomials, SRS points, and kzg commitments. +Below diagram explains the different types involved between polynomials, SRS points, and KZG commitments. A KZG commitment can be taken by an inner product between (poly_coeff, srs_monomial) or (poly_eval, srs_lagrange). FFT and IFFT operations can be performed to convert between these forms. ![KZG Commitments](./kzg_commitment_diagram.png) -Our current codebase has the types PolynomialEvalForm and PolynomialCoeffForm to represent the polynomial in evaluation and coefficient form respectively. However, we do not have types to represent the two forms of srs points. They are implicitly assumed to be in monomial form when loaded, and an IFFT is performed before taking the inner product with the polynomial in evaluation form. \ No newline at end of file + +### KZG Proofs + +TODO: Add diagram for KZG Proofs diff --git a/src/blob.rs b/src/blob.rs index 9cd00ae..533ef5e 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -58,13 +58,13 @@ impl Blob { self.blob_data.is_empty() } - /// Convert the blob data to a `PolynomialEvalForm`. + /// Convert the blob data to a [PolynomialEvalForm]. pub fn to_polynomial_eval_form(&self) -> PolynomialEvalForm { let fr_vec = helpers::to_fr_array(&self.blob_data); PolynomialEvalForm::new(fr_vec) } - /// Convert the blob data to a `PolynomialCoefForm`. + /// Convert the blob data to a [PolynomialCoeffForm]. pub fn to_polynomial_coeff_form(&self) -> PolynomialCoeffForm { let fr_vec = helpers::to_fr_array(&self.blob_data); PolynomialCoeffForm::new(fr_vec) diff --git a/src/kzg.rs b/src/kzg.rs index 13243d0..6c425cc 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -104,7 +104,9 @@ impl Kzg { Ok(chunks) } - /// data_setup_custom is a helper function + /// Similar to [Kzg::data_setup_mins], but mainly used for setting up Kzg for testing purposes. + /// Used to specify the number of chunks and chunk length. + /// These parameters are then used to calculate the FFT params required for FFT operations. pub fn data_setup_custom( &mut self, num_of_nodes: u64, @@ -117,7 +119,8 @@ impl Kzg { self.data_setup_mins(min_num_chunks, num_of_nodes) } - /// data_setup_mins sets up the environment per the blob data + /// Used to specify the number of chunks and chunk length. + /// These parameters are then used to calculate the FFT params required for FFT operations. pub fn data_setup_mins( &mut self, min_chunk_length: u64, @@ -470,7 +473,7 @@ impl Kzg { self.g2.to_vec() } - /// commit the actual polynomial with the values setup + /// Commit the polynomial with the srs values loaded into [Kzg]. pub fn commit_eval_form(&self, polynomial: &PolynomialEvalForm) -> Result { if polynomial.len() > self.g1.len() { return Err(KzgError::SerializationError( @@ -488,7 +491,7 @@ impl Kzg { } } - /// commit the actual polynomial with the values setup + /// Commit the polynomial with the srs values loaded into [Kzg]. pub fn commit_coeff_form( &self, polynomial: &PolynomialCoeffForm, @@ -508,12 +511,15 @@ impl Kzg { } } - pub fn blob_to_kzg_commitment(&self, blob: &Blob) -> Result { + /// commit to a [Blob], by transforming it into a [PolynomialEvalForm] and then + /// calling [Kzg::commit_eval_form]. + pub fn commit_blob(&self, blob: &Blob) -> Result { let polynomial = blob.to_polynomial_eval_form(); self.commit_eval_form(&polynomial) } - /// helper function to work with the library and the env of the kzg instance + /// Wrapper around [Kzg::compute_proof] that uses the roots of unity + /// that were expanded and stored in [Kzg::setup]. pub fn compute_proof_with_roots_of_unity( &self, polynomial: &PolynomialEvalForm, diff --git a/src/polynomial.rs b/src/polynomial.rs index 5602de6..3dda8e0 100644 --- a/src/polynomial.rs +++ b/src/polynomial.rs @@ -23,6 +23,10 @@ pub struct PolynomialCoeffForm { } impl PolynomialCoeffForm { + /// Creates a new [PolynomialCoeffForm] from the given coefficients, passed as a vector of `Fr`. + /// The coefficients are padded to the next power of two by appending zeros. + /// This typically wouldn't be used directly, but instead a [crate::blob::Blob] would be + /// converted to a [PolynomialCoeffForm] using [crate::blob::Blob::to_polynomial_coeff_form]. pub fn new(coeffs: Vec) -> Self { let underlying_blob_len_in_bytes = coeffs.len() * BYTES_PER_FIELD_ELEMENT; let next_power_of_two = coeffs.len().next_power_of_two(); @@ -71,6 +75,8 @@ impl PolynomialCoeffForm { helpers::to_byte_array(&self.coeffs, self.len_underlying_blob_bytes) } + /// Converts the polynomial to evaluation form. This is done by performing + /// an FFT on the coefficients. pub fn to_eval_form(&self) -> Result { let evals = GeneralEvaluationDomain::::new(self.len()) .ok_or(PolynomialError::FFTError( @@ -86,7 +92,10 @@ pub struct PolynomialEvalForm { /// evaluations contains the evaluations of the polynomial, padded with 0s /// to the next power of two. Hence if the polynomial is created with /// coefficients [1, 2, 3], the internal representation will be [1, 2, - /// 3, 0]. + /// 3, 0]. Note that this changes the polynomial! This is an inconsistency in our + /// current representations. Polynomials are the objects that get committed, not the + /// underlying Blobs. + /// TODO: do we also want to force blobs to be of powers-of-two length? evaluations: Vec, /// Number of bytes in the underlying blob, which was used to create the /// polynomial. This is passed as is when converting between Coefficient @@ -101,12 +110,14 @@ pub struct PolynomialEvalForm { } impl PolynomialEvalForm { + /// Creates a new [PolynomialEvalForm] from the given coefficients, passed as a vector of `Fr`. + /// The coefficients are padded to the next power of two by appending zeros. + /// This typically wouldn't be used directly, but instead a [crate::blob::Blob] would be + /// converted to a [PolynomialEvalForm] using [crate::blob::Blob::to_polynomial_eval_form]. pub fn new(evals: Vec) -> Self { let underlying_blob_len_in_bytes = evals.len() * BYTES_PER_FIELD_ELEMENT; let next_power_of_two = evals.len().next_power_of_two(); let mut padded_evals = evals; - // TODO: does it make sense to pad evaluations with zeros? This changes the - // polynomial... padded_evals.resize(next_power_of_two, Fr::zero()); Self { @@ -151,6 +162,8 @@ impl PolynomialEvalForm { helpers::to_byte_array(&self.evaluations, self.len_underlying_blob_bytes) } + /// Converts the polynomial to coefficient form. This is done by performing + /// an IFFT on the evaluations. pub fn to_coeff_form(&self) -> Result { let coeffs = GeneralEvaluationDomain::::new(self.len()) .ok_or(PolynomialError::FFTError( diff --git a/tests/kzg_test.rs b/tests/kzg_test.rs index 9f13e59..2ba7a56 100644 --- a/tests/kzg_test.rs +++ b/tests/kzg_test.rs @@ -193,7 +193,7 @@ mod tests { use ark_bn254::Fq; let blob = Blob::from_raw_data(GETTYSBURG_ADDRESS_BYTES); - let fn_output = KZG_3000.blob_to_kzg_commitment(&blob).unwrap(); + let fn_output = KZG_3000.commit_blob(&blob).unwrap(); let commitment_from_da = G1Affine::new_unchecked( Fq::from_str( "2961155957874067312593973807786254905069537311739090798303675273531563528369", From c3653a8a9414e9ffa6333512f4f54b31e358280e Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 16:38:38 -0500 Subject: [PATCH 17/22] style: switch poly eval and coeff forms to have eval at top of file Eval is the most important one, so should be prominent and easier to find/look at. --- src/polynomial.rs | 126 +++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/src/polynomial.rs b/src/polynomial.rs index 3dda8e0..9139d19 100644 --- a/src/polynomial.rs +++ b/src/polynomial.rs @@ -4,12 +4,15 @@ use ark_poly::{EvaluationDomain, GeneralEvaluationDomain}; use ark_std::Zero; #[derive(Clone, Debug, PartialEq)] -pub struct PolynomialCoeffForm { - /// coeffs contains the coefficients of the polynomial, padded with 0s to - /// the next power of two. Hence if the polynomial is created with +pub struct PolynomialEvalForm { + /// evaluations contains the evaluations of the polynomial, padded with 0s + /// to the next power of two. Hence if the polynomial is created with /// coefficients [1, 2, 3], the internal representation will be [1, 2, - /// 3, 0]. - coeffs: Vec, + /// 3, 0]. Note that this changes the polynomial! This is an inconsistency in our + /// current representations. Polynomials are the objects that get committed, not the + /// underlying Blobs. + /// TODO: do we also want to force blobs to be of powers-of-two length? + evaluations: Vec, /// Number of bytes in the underlying blob, which was used to create the /// polynomial. This is passed as is when converting between Coefficient /// and Evaluation forms, so that the blob can be reconstructed with the @@ -22,32 +25,32 @@ pub struct PolynomialCoeffForm { len_underlying_blob_bytes: usize, } -impl PolynomialCoeffForm { - /// Creates a new [PolynomialCoeffForm] from the given coefficients, passed as a vector of `Fr`. +impl PolynomialEvalForm { + /// Creates a new [PolynomialEvalForm] from the given coefficients, passed as a vector of `Fr`. /// The coefficients are padded to the next power of two by appending zeros. /// This typically wouldn't be used directly, but instead a [crate::blob::Blob] would be - /// converted to a [PolynomialCoeffForm] using [crate::blob::Blob::to_polynomial_coeff_form]. - pub fn new(coeffs: Vec) -> Self { - let underlying_blob_len_in_bytes = coeffs.len() * BYTES_PER_FIELD_ELEMENT; - let next_power_of_two = coeffs.len().next_power_of_two(); - let mut padded_coeffs = coeffs; - padded_coeffs.resize(next_power_of_two, Fr::zero()); + /// converted to a [PolynomialEvalForm] using [crate::blob::Blob::to_polynomial_eval_form]. + pub fn new(evals: Vec) -> Self { + let underlying_blob_len_in_bytes = evals.len() * BYTES_PER_FIELD_ELEMENT; + let next_power_of_two = evals.len().next_power_of_two(); + let mut padded_evals = evals; + padded_evals.resize(next_power_of_two, Fr::zero()); Self { - coeffs: padded_coeffs, + evaluations: padded_evals, len_underlying_blob_bytes: underlying_blob_len_in_bytes, } } - pub fn coeffs(&self) -> &[Fr] { - &self.coeffs + pub fn evaluations(&self) -> &[Fr] { + &self.evaluations } - /// Returns the number of coefficients in the polynomial. Note that this - /// returns the number of coefficients in the padded polynomial, not the - /// number of coefficients in the original polynomial. + /// Returns the number of evaluations in the polynomial. Note that this + /// returns the number of evaluations in the padded polynomial, not the + /// number of evaluations in the original polynomial. pub fn len(&self) -> usize { - self.coeffs.len() + self.evaluations.len() } /// TODO: we should deprecate this. See comment in the struct. @@ -62,41 +65,38 @@ impl PolynomialCoeffForm { } pub fn get_at_index(&self, i: usize) -> Option<&Fr> { - self.coeffs.get(i) + self.evaluations.get(i) } /// Checks if the polynomial has no elements. pub fn is_empty(&self) -> bool { - self.coeffs.is_empty() + self.evaluations.is_empty() } /// Converts all `Fr` elements in the `Polynomial` to a single byte vector. pub fn to_bytes_be(&self) -> Vec { - helpers::to_byte_array(&self.coeffs, self.len_underlying_blob_bytes) + helpers::to_byte_array(&self.evaluations, self.len_underlying_blob_bytes) } - /// Converts the polynomial to evaluation form. This is done by performing - /// an FFT on the coefficients. - pub fn to_eval_form(&self) -> Result { - let evals = GeneralEvaluationDomain::::new(self.len()) + /// Converts the polynomial to coefficient form. This is done by performing + /// an IFFT on the evaluations. + pub fn to_coeff_form(&self) -> Result { + let coeffs = GeneralEvaluationDomain::::new(self.len()) .ok_or(PolynomialError::FFTError( - "Failed to construct domain for FFT".to_string(), + "Failed to construct domain for IFFT".to_string(), ))? - .fft(&self.coeffs); - Ok(PolynomialEvalForm::new(evals)) + .ifft(&self.evaluations); + Ok(PolynomialCoeffForm::new(coeffs)) } } #[derive(Clone, Debug, PartialEq)] -pub struct PolynomialEvalForm { - /// evaluations contains the evaluations of the polynomial, padded with 0s - /// to the next power of two. Hence if the polynomial is created with +pub struct PolynomialCoeffForm { + /// coeffs contains the coefficients of the polynomial, padded with 0s to + /// the next power of two. Hence if the polynomial is created with /// coefficients [1, 2, 3], the internal representation will be [1, 2, - /// 3, 0]. Note that this changes the polynomial! This is an inconsistency in our - /// current representations. Polynomials are the objects that get committed, not the - /// underlying Blobs. - /// TODO: do we also want to force blobs to be of powers-of-two length? - evaluations: Vec, + /// 3, 0]. + coeffs: Vec, /// Number of bytes in the underlying blob, which was used to create the /// polynomial. This is passed as is when converting between Coefficient /// and Evaluation forms, so that the blob can be reconstructed with the @@ -109,32 +109,32 @@ pub struct PolynomialEvalForm { len_underlying_blob_bytes: usize, } -impl PolynomialEvalForm { - /// Creates a new [PolynomialEvalForm] from the given coefficients, passed as a vector of `Fr`. +impl PolynomialCoeffForm { + /// Creates a new [PolynomialCoeffForm] from the given coefficients, passed as a vector of `Fr`. /// The coefficients are padded to the next power of two by appending zeros. /// This typically wouldn't be used directly, but instead a [crate::blob::Blob] would be - /// converted to a [PolynomialEvalForm] using [crate::blob::Blob::to_polynomial_eval_form]. - pub fn new(evals: Vec) -> Self { - let underlying_blob_len_in_bytes = evals.len() * BYTES_PER_FIELD_ELEMENT; - let next_power_of_two = evals.len().next_power_of_two(); - let mut padded_evals = evals; - padded_evals.resize(next_power_of_two, Fr::zero()); + /// converted to a [PolynomialCoeffForm] using [crate::blob::Blob::to_polynomial_coeff_form]. + pub fn new(coeffs: Vec) -> Self { + let underlying_blob_len_in_bytes = coeffs.len() * BYTES_PER_FIELD_ELEMENT; + let next_power_of_two = coeffs.len().next_power_of_two(); + let mut padded_coeffs = coeffs; + padded_coeffs.resize(next_power_of_two, Fr::zero()); Self { - evaluations: padded_evals, + coeffs: padded_coeffs, len_underlying_blob_bytes: underlying_blob_len_in_bytes, } } - pub fn evaluations(&self) -> &[Fr] { - &self.evaluations + pub fn coeffs(&self) -> &[Fr] { + &self.coeffs } - /// Returns the number of evaluations in the polynomial. Note that this - /// returns the number of evaluations in the padded polynomial, not the - /// number of evaluations in the original polynomial. + /// Returns the number of coefficients in the polynomial. Note that this + /// returns the number of coefficients in the padded polynomial, not the + /// number of coefficients in the original polynomial. pub fn len(&self) -> usize { - self.evaluations.len() + self.coeffs.len() } /// TODO: we should deprecate this. See comment in the struct. @@ -149,27 +149,27 @@ impl PolynomialEvalForm { } pub fn get_at_index(&self, i: usize) -> Option<&Fr> { - self.evaluations.get(i) + self.coeffs.get(i) } /// Checks if the polynomial has no elements. pub fn is_empty(&self) -> bool { - self.evaluations.is_empty() + self.coeffs.is_empty() } /// Converts all `Fr` elements in the `Polynomial` to a single byte vector. pub fn to_bytes_be(&self) -> Vec { - helpers::to_byte_array(&self.evaluations, self.len_underlying_blob_bytes) + helpers::to_byte_array(&self.coeffs, self.len_underlying_blob_bytes) } - /// Converts the polynomial to coefficient form. This is done by performing - /// an IFFT on the evaluations. - pub fn to_coeff_form(&self) -> Result { - let coeffs = GeneralEvaluationDomain::::new(self.len()) + /// Converts the polynomial to evaluation form. This is done by performing + /// an FFT on the coefficients. + pub fn to_eval_form(&self) -> Result { + let evals = GeneralEvaluationDomain::::new(self.len()) .ok_or(PolynomialError::FFTError( - "Failed to construct domain for IFFT".to_string(), + "Failed to construct domain for FFT".to_string(), ))? - .ifft(&self.evaluations); - Ok(PolynomialCoeffForm::new(coeffs)) + .fft(&self.coeffs); + Ok(PolynomialEvalForm::new(evals)) } } From e61ddcf7a1a89176e1c980a907ead0f26a33a6c8 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 18:01:38 -0500 Subject: [PATCH 18/22] test: add test to check length of polynomials during conversions --- tests/kzg_test.rs | 2 +- tests/polynomial_test.rs | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/kzg_test.rs b/tests/kzg_test.rs index 2ba7a56..6d4866c 100644 --- a/tests/kzg_test.rs +++ b/tests/kzg_test.rs @@ -256,7 +256,7 @@ mod tests { } #[test] - fn test_compute_proof() { + fn test_compute_kzg_proof() { use rand::Rng; let mut kzg = KZG_INSTANCE.clone(); diff --git a/tests/polynomial_test.rs b/tests/polynomial_test.rs index f8c29bb..db6f1de 100644 --- a/tests/polynomial_test.rs +++ b/tests/polynomial_test.rs @@ -1,6 +1,10 @@ #[cfg(test)] mod tests { - use rust_kzg_bn254::blob::Blob; + use ark_bn254::Fr; + use rust_kzg_bn254::{ + blob::Blob, + polynomial::{PolynomialCoeffForm, PolynomialEvalForm}, + }; const GETTYSBURG_ADDRESS_BYTES: &[u8] = "Fourscore and seven years ago our fathers brought forth, on this continent, a new nation, conceived in liberty, and dedicated to the proposition that all men are created equal. Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived, and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting-place for those who here gave their lives, that that nation might live. It is altogether fitting and proper that we should do this. But, in a larger sense, we cannot dedicate, we cannot consecrate—we cannot hallow—this ground. The brave men, living and dead, who struggled here, have consecrated it far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us—that from these honored dead we take increased devotion to that cause for which they here gave the last full measure of devotion—that we here highly resolve that these dead shall not have died in vain—that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.".as_bytes(); #[test] @@ -58,6 +62,37 @@ mod tests { ); } + #[test] + fn test_polynomial_lengths() { + let poly_coeff = + PolynomialCoeffForm::new(vec![Fr::from(1u8), Fr::from(2u8), Fr::from(3u8)]); + assert_eq!( + poly_coeff.coeffs().len(), + 4, + "poly should be padded to the next power of 2" + ); + + let poly_evals = PolynomialEvalForm::new(vec![Fr::from(1u8), Fr::from(2u8), Fr::from(3u8)]); + assert_eq!( + poly_evals.evaluations().len(), + 4, + "poly should be padded to the next power of 2" + ); + } + + #[test] + fn test_transform_length_stays_same() { + let poly_coeff = + PolynomialCoeffForm::new(vec![Fr::from(1u8), Fr::from(2u8), Fr::from(3u8)]); + let poly_eval = poly_coeff.to_eval_form().unwrap(); + let poly_coeff_back = poly_eval.to_coeff_form().unwrap(); + assert_eq!( + poly_coeff.coeffs().len(), + poly_coeff_back.coeffs().len(), + "length of poly should remain the same when converting between forms" + ); + } + #[test] fn test_transform_form_large_blob() { let blob = Blob::from_raw_data(GETTYSBURG_ADDRESS_BYTES); From 194b32b849d9a097e75ba97e0c97dbd0a0d5fc81 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 18:27:57 -0500 Subject: [PATCH 19/22] docs: move readme docs to lib.rs rust docs --- Cargo.toml | 16 +++--- README.md | 58 ++++----------------- benches/bench_g1_ifft.rs | 4 +- benches/bench_kzg_commit.rs | 4 +- benches/bench_kzg_commit_large_blobs.rs | 4 +- benches/bench_kzg_proof.rs | 4 +- benches/bench_kzg_setup.rs | 6 +-- benches/bench_kzg_verify.rs | 4 +- src/blob.rs | 2 +- src/kzg.rs | 12 ++++- src/lib.rs | 67 +++++++++++++++++++++++++ tests/kzg_test.rs | 24 ++++----- 12 files changed, 119 insertions(+), 86 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index feaa015..6b9f2e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,17 +8,16 @@ description = "This library offers a set of functions for generating and interac readme = "README.md" repository = "https://github.com/Layr-Labs/rust-kzg-bn254" license-file = "LICENSE" -exclude = [ - "tests/*", - "benches/*", -] +exclude = ["tests/*", "benches/*"] +# TODO: is this needed for the image to show up in the rust docs? +include = ["./kzg_commitment_diagram.png"] [dependencies] ark-bn254 = "0.5.0" -ark-ec = {version = "0.5.0", features = ["parallel"]} -ark-ff = {version = "0.5.0", features = ["parallel"]} +ark-ec = { version = "0.5.0", features = ["parallel"] } +ark-ff = { version = "0.5.0", features = ["parallel"] } ark-serialize = "0.5.0" -ark-std = {version = "0.5.0", features = ["parallel"]} +ark-std = { version = "0.5.0", features = ["parallel"] } directories = "5.0.1" hex-literal = "0.4.1" rand = "0.8.5" @@ -28,7 +27,7 @@ num-bigint = "0.4" rayon = "^1.5" num-traits = "0.2" byteorder = "1.4" -ark-poly = {version = "0.5.0", features = ["parallel"]} +ark-poly = { version = "0.5.0", features = ["parallel"] } crossbeam-channel = "0.5" num_cpus = "1.13.0" sys-info = "0.9" @@ -109,4 +108,3 @@ panic = 'unwind' incremental = false codegen-units = 16 rpath = false - diff --git a/README.md b/README.md index afb0cd3..fe55226 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,10 @@ # rust-kzg-bn254 -## Description +[![Docs](https://docs.rs/rust-kzg-bn254/badge.svg)](https://docs.rs/rust-kzg-bn254/latest/rust_kzg_bn254/) +[![Crate](https://img.shields.io/crates/v/rust-kzg-bn254.svg)](https://crates.io/crates/rust-kzg-bn254) This library offers a set of functions for generating and interacting with bn254 KZG commitments and proofs in rust, with the motivation of supporting fraud and validity proof logic in EigenDA rollup integrations. -## Warning & Disclaimer - -This code is unaudited and under construction. This is experimental software and is provided on an "as is" and "as available" basis and may not work at all. It should not be used in production. - -## Setup for testing - -1. To test, please download the provided G1 and G2 points from [DA Resources](https://github.com/Layr-Labs/eigenda/tree/master/inabox/resources/kzg), -2. Specify these files in the `kzg.setup()` function, leave the `g2_power_of2_path` empty, and specify `srs_order` to be 3000. - ## Configuring with the EigenDA KZG trusted setup 1. Follow the setup instructions to download the G1 and G2 powers of 2 points from the [Operator Setup Guide](https://github.com/Layr-Labs/eigenda-operator-setup) @@ -21,47 +13,15 @@ This code is unaudited and under construction. This is experimental software and ## Quick Start -See the `test_compute_kzg_proof` function in [./tests/kzg_test.rs](./tests/kzg_test.rs#) for an end to end usage of the library. - -## Requirements - -1. SRS points required are in the same format as provided by EigenDA. -2. Committing is performed in Lagrange format. The required IFFT is done within the function and is not required to be performed separately. -3. For proof generation, the data is treated as evaluation of polynomial. The required (I)FFT is performed by the compute function and is not required to be performed separately. - -## Library Design / Architecture +See the `test_compute_kzg_proof` function in [./tests/kzg_test.rs](./tests/kzg_test.rs) for an end to end usage of the library. -The main purpose of this library is to allow taking a piece of data, committing to it, and then generating and verifying proofs against that commitment. +Also make sure to check out the examples in our [docs](https://docs.rs/rust-kzg-bn254/latest/rust_kzg_bn254/). -### Data Types - -The main data pipeline goes: -> user data -> Blob -> Polynomial -> KZG Commitment / Proof - -- User Data: bytes array - - meaningful to users (typically will be a rollup batch) -- Blob: bn254 field elements array - - meaningful to EigenDA network - - Obtained from User Data by inserting zeroes every 31 bytes to make every 32 byte an element of bn254. -- Polynomial: bn254 field elements array, interpreted as coefficients or evaluations of a polynomial - - meaningful when committing and generating/verifying proofs - - Obtained from Blob by appending zeroes to make the length a power of 2, and then interpreting the array as coefficients or evaluations of a polynomial. -- Kzg: struct storing the SRS points used to generate commitments and proofs -- SRS points: bn254 group elements - - inner producted with the polynomial to generate commitments - -The [Blob](./src/blob.rs) and [Polynomial](./src/polynomial.rs) structs are mostly [Plain Old Data](https://en.wikipedia.org/wiki/Passive_data_structure) with constructor and few helper methods. The interesting stuff happens in the [KZG](./src/kzg.rs) struct, which has methods for committing to a blob, polynomial in coeff or eval form, and generating and verifying proofs. - -Our current codebase has the types PolynomialEvalForm and PolynomialCoeffForm to represent the polynomial in evaluation and coefficient form respectively. However, we do not have types to represent the two forms of srs points. They are implicitly assumed to be in monomial form when loaded, and an IFFT is performed before taking the inner product with the polynomial in evaluation form. - -### KZG Commitments - -Below diagram explains the different types involved between polynomials, SRS points, and KZG commitments. -A KZG commitment can be taken by an inner product between (poly_coeff, srs_monomial) or (poly_eval, srs_lagrange). FFT and IFFT operations can be performed to convert between these forms. - -![KZG Commitments](./kzg_commitment_diagram.png) +## Setup for testing +1. To test, please download the provided G1 and G2 points from [DA Resources](https://github.com/Layr-Labs/eigenda/tree/master/inabox/resources/kzg), +2. Specify these files in the `kzg.setup()` function, leave the `g2_power_of2_path` empty, and specify `srs_order` to be 3000. -### KZG Proofs +## Warning & Disclaimer -TODO: Add diagram for KZG Proofs +This code is unaudited and under construction. This is experimental software and is provided on an "as is" and "as available" basis and may not work at all. It should not be used in production. \ No newline at end of file diff --git a/benches/bench_g1_ifft.rs b/benches/bench_g1_ifft.rs index b475dee..1d4ee7a 100644 --- a/benches/bench_g1_ifft.rs +++ b/benches/bench_g1_ifft.rs @@ -1,5 +1,5 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use rust_kzg_bn254::kzg::Kzg; +use rust_kzg_bn254::kzg::KZG; use std::time::Duration; fn generate_powers_of_2(limit: u64) -> Vec { @@ -16,7 +16,7 @@ fn generate_powers_of_2(limit: u64) -> Vec { fn bench_g1_ifft(c: &mut Criterion) { c.bench_function("bench_g1_ifft", |b| { - let kzg = Kzg::setup( + let kzg = KZG::setup( "tests/test-files/mainnet-data/g1.131072.point", "", "tests/test-files/mainnet-data/g2.point.powerOf2", diff --git a/benches/bench_kzg_commit.rs b/benches/bench_kzg_commit.rs index d8e108b..1979394 100644 --- a/benches/bench_kzg_commit.rs +++ b/benches/bench_kzg_commit.rs @@ -1,11 +1,11 @@ use criterion::{criterion_group, criterion_main, Criterion}; use rand::Rng; -use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; +use rust_kzg_bn254::{blob::Blob, kzg::KZG}; use std::time::Duration; fn bench_kzg_commit(c: &mut Criterion) { let mut rng = rand::thread_rng(); - let mut kzg = Kzg::setup( + let mut kzg = KZG::setup( "tests/test-files/mainnet-data/g1.131072.point", "", "tests/test-files/mainnet-data/g2.point.powerOf2", diff --git a/benches/bench_kzg_commit_large_blobs.rs b/benches/bench_kzg_commit_large_blobs.rs index 7181413..1b504e3 100644 --- a/benches/bench_kzg_commit_large_blobs.rs +++ b/benches/bench_kzg_commit_large_blobs.rs @@ -1,11 +1,11 @@ use criterion::{criterion_group, criterion_main, Criterion}; use rand::Rng; -use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; +use rust_kzg_bn254::{blob::Blob, kzg::KZG}; use std::time::Duration; fn bench_kzg_commit(c: &mut Criterion) { let mut rng = rand::thread_rng(); - let mut kzg = Kzg::setup( + let mut kzg = KZG::setup( "tests/test-files/mainnet-data/g1.32mb.point", "", "tests/test-files/mainnet-data/g2.point.powerOf2", diff --git a/benches/bench_kzg_proof.rs b/benches/bench_kzg_proof.rs index e8813f0..d15cda1 100644 --- a/benches/bench_kzg_proof.rs +++ b/benches/bench_kzg_proof.rs @@ -1,11 +1,11 @@ use criterion::{criterion_group, criterion_main, Criterion}; use rand::Rng; -use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; +use rust_kzg_bn254::{blob::Blob, kzg::KZG}; use std::time::Duration; fn bench_kzg_proof(c: &mut Criterion) { let mut rng = rand::thread_rng(); - let mut kzg = Kzg::setup( + let mut kzg = KZG::setup( "tests/test-files/mainnet-data/g1.131072.point", "", "tests/test-files/mainnet-data/g2.point.powerOf2", diff --git a/benches/bench_kzg_setup.rs b/benches/bench_kzg_setup.rs index 29f5f91..87bbc6e 100644 --- a/benches/bench_kzg_setup.rs +++ b/benches/bench_kzg_setup.rs @@ -1,11 +1,11 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use rust_kzg_bn254::kzg::Kzg; +use rust_kzg_bn254::kzg::KZG; use std::time::Duration; fn bench_kzg_setup(c: &mut Criterion) { c.bench_function("bench_kzg_setup", |b| { b.iter(|| { - Kzg::setup( + KZG::setup( "tests/test-files/g1.point", "tests/test-files/g2.point", "tests/test-files/g2.point.powerOf2", @@ -16,7 +16,7 @@ fn bench_kzg_setup(c: &mut Criterion) { }); b.iter(|| { - Kzg::setup( + KZG::setup( "tests/test-files/mainnet-data/g1.131072.point", "", "tests/test-files/mainnet-data/g2.point.powerOf2", diff --git a/benches/bench_kzg_verify.rs b/benches/bench_kzg_verify.rs index 0fe8266..21f0834 100644 --- a/benches/bench_kzg_verify.rs +++ b/benches/bench_kzg_verify.rs @@ -1,11 +1,11 @@ use criterion::{criterion_group, criterion_main, Criterion}; use rand::Rng; -use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; +use rust_kzg_bn254::{blob::Blob, kzg::KZG}; use std::time::Duration; fn bench_kzg_verify(c: &mut Criterion) { let mut rng = rand::thread_rng(); - let mut kzg = Kzg::setup( + let mut kzg = KZG::setup( "tests/test-files/mainnet-data/g1.131072.point", "", "tests/test-files/mainnet-data/g2.point.powerOf2", diff --git a/src/blob.rs b/src/blob.rs index 533ef5e..2dd9a55 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -5,7 +5,7 @@ use crate::{ /// A blob which is Eigen DA spec aligned. /// TODO: we should probably move to a transparent repr like -/// https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.FixedBytes.html +/// #[derive(Clone, Debug, PartialEq, Eq)] pub struct Blob { blob_data: Vec, diff --git a/src/kzg.rs b/src/kzg.rs index 6c425cc..b20a2a1 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -19,8 +19,16 @@ use std::{ io::{self, BufReader}, }; +/// Main interesting struct of the rust-kzg-bn254 crate. +/// [Kzg] is a struct that holds the SRS points in monomial form, and +/// provides methods for committing to a blob, (either via a [Blob] itself, +/// or a [PolynomialCoeffForm] or [PolynomialEvalForm]), and generating and verifying proofs. +/// +/// The [Blob] and [PolynomialCoeffForm]/[PolynomialEvalForm] structs are mostly +/// with +/// constructor and few helper methods. #[derive(Debug, PartialEq, Clone)] -pub struct Kzg { +pub struct KZG { // SRS points are stored in monomial form, ready to be used for commitments with polynomials // in coefficient form. To commit against a polynomial in evaluation form, we need to transform // the SRS points to lagrange form using IFFT. @@ -39,7 +47,7 @@ struct Params { completed_setup: bool, } -impl Kzg { +impl KZG { pub fn setup( path_to_g1_points: &str, path_to_g2_points: &str, diff --git a/src/lib.rs b/src/lib.rs index f562aef..8a6d31f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,70 @@ +/*! +## Library Design / Architecture + +The main purpose of this library is to allow taking a piece of data, committing to it, and then generating and verifying proofs against that commitment. + +### Data Types + +The main data pipeline goes: +> user data -> [blob::Blob] -> [polynomial::PolynomialEvalForm]/[polynomial::PolynomialCoeffForm] -> KZG Commitment / Proof + +- User Data: bytes array + - meaningful to users (typically will be a rollup batch) +- Blob: bn254 field elements array + - meaningful to EigenDA network + - Obtained from User Data by inserting zeroes every 31 bytes to make every 32 byte an element of bn254. +- Polynomial: bn254 field elements array, interpreted as coefficients or evaluations of a polynomial + - meaningful when committing and generating/verifying proofs + - Obtained from Blob by appending zeroes to make the length a power of 2, and then interpreting the array as coefficients or evaluations of a polynomial. +- KZG: struct storing the SRS points used to generate commitments and proofs +- SRS points: bn254 group elements + - inner producted with the polynomial to generate commitments + +The Blob and Polynomial structs are mostly +[Plain Old Data](https://en.wikipedia.org/wiki/Passive_data_structure) with constructor and few helper methods. +The interesting stuff happens in the [kzg::KZG] struct, +which has methods for committing to a blob, polynomial in coeff or eval form, +and generating and verifying proofs. + +Our current codebase has the types PolynomialEvalForm and PolynomialCoeffForm to represent the polynomial in evaluation and coefficient form respectively. However, we do not have types to represent the two forms of srs points. They are implicitly assumed to be in monomial form when loaded, and an IFFT is performed before taking the inner product with the polynomial in evaluation form. + +### KZG Commitments + +A KZG commitment can be taken by an inner product between (poly_coeff, srs_monomial) or (poly_eval, srs_lagrange). FFT and IFFT operations can be performed to convert between these forms. + +![KZG](../kzg_commitment_diagram.png) + +### KZG Proofs + +TODO + +## Examples + +### Commit to a some user data +```rust +use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; + +let kzg = Kzg::setup( + "tests/test-files/mainnet-data/g1.131072.point", + "", + "tests/test-files/mainnet-data/g2.point.powerOf2", + 268435456, + 131072, +).unwrap(); + +let rollup_data: &[u8] = "some rollup batcher data".as_bytes(); +let blob = Blob::from_raw_data(rollup_data); +let poly = blob.to_polynomial_eval_form(); +let commitment = kzg.commit_eval_form(&poly).unwrap(); +``` + +### Generate a proof for a piece of data +```rust +// TODO +``` + +*/ + mod arith; pub mod blob; pub mod consts; diff --git a/tests/kzg_test.rs b/tests/kzg_test.rs index 6d4866c..bfa5b35 100644 --- a/tests/kzg_test.rs +++ b/tests/kzg_test.rs @@ -6,7 +6,7 @@ mod tests { blob::Blob, errors::KzgError, helpers, - kzg::Kzg, + kzg::KZG, polynomial::{PolynomialCoeffForm, PolynomialEvalForm}, }; use std::{ @@ -18,9 +18,9 @@ mod tests { use ark_std::{str::FromStr, One}; // Function to determine the setup based on an environment variable - fn determine_setup() -> Kzg { + fn determine_setup() -> KZG { match env::var("KZG_ENV") { - Ok(val) if val == "mainnet-data" => Kzg::setup( + Ok(val) if val == "mainnet-data" => KZG::setup( "tests/test-files/mainnet-data/g1.131072.point", "", "tests/test-files/mainnet-data/g2.point.powerOf2", @@ -28,7 +28,7 @@ mod tests { 131072, ) .unwrap(), - _ => Kzg::setup( + _ => KZG::setup( "tests/test-files/g1.point", "tests/test-files/g2.point", "tests/test-files/g2.point.powerOf2", @@ -41,8 +41,8 @@ mod tests { // Define a static variable for setup lazy_static! { - static ref KZG_INSTANCE: Kzg = determine_setup(); - static ref KZG_3000: Kzg = Kzg::setup( + static ref KZG_INSTANCE: KZG = determine_setup(); + static ref KZG_3000: KZG = KZG::setup( "tests/test-files/g1.point", "tests/test-files/g2.point", "tests/test-files/g2.point.powerOf2", @@ -71,7 +71,7 @@ mod tests { #[test] fn test_kzg_setup_errors() { - let kzg1 = Kzg::setup("tests/test-files/g1.point", "", "", 3000, 3000); + let kzg1 = KZG::setup("tests/test-files/g1.point", "", "", 3000, 3000); assert_eq!( kzg1, Err(KzgError::GenericError( @@ -79,7 +79,7 @@ mod tests { )) ); - let mut kzg2 = Kzg::setup( + let mut kzg2 = KZG::setup( "tests/test-files/g1.point", "tests/test-files/g2.point", "tests/test-files/g2.point.powerOf2", @@ -97,7 +97,7 @@ mod tests { )) ); - let kzg3 = Kzg::setup( + let kzg3 = KZG::setup( "tests/test-files/g1.point", "tests/test-files/g2.point", "tests/test-files/g2.point.powerOf2", @@ -118,7 +118,7 @@ mod tests { use rust_kzg_bn254::helpers::is_on_curve_g2; use std::io::BufRead; - let kzg = Kzg::setup( + let kzg = KZG::setup( "tests/test-files/g1.point", "", "tests/test-files/g2.point.powerOf2", @@ -159,8 +159,8 @@ mod tests { use rand::Rng; let mut rng = rand::thread_rng(); - let mut kzg_clone1: Kzg = KZG_3000.clone(); - let mut kzg_clone2: Kzg = KZG_3000.clone(); + let mut kzg_clone1: KZG = KZG_3000.clone(); + let mut kzg_clone2: KZG = KZG_3000.clone(); (0..10000).for_each(|_| { let blob_length: u64 = rand::thread_rng().gen_range(35..40000); From 8ca7620934bbd1d88e5dd43084196a32b0c4cdfe Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 18:33:25 -0500 Subject: [PATCH 20/22] docs: fix doc code that was failing --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8a6d31f..4efaead 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,9 +42,9 @@ TODO ### Commit to a some user data ```rust -use rust_kzg_bn254::{blob::Blob, kzg::Kzg}; +use rust_kzg_bn254::{blob::Blob, kzg::KZG}; -let kzg = Kzg::setup( +let kzg = KZG::setup( "tests/test-files/mainnet-data/g1.131072.point", "", "tests/test-files/mainnet-data/g2.point.powerOf2", From 54626b1bf005f2569ae99168a0e614c26947d5b6 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 19:01:56 -0500 Subject: [PATCH 21/22] style: cargo fmt --- src/kzg.rs | 27 +++++---- src/lib.rs | 145 +++++++++++++++++++++++++--------------------- src/polynomial.rs | 25 ++++---- 3 files changed, 108 insertions(+), 89 deletions(-) diff --git a/src/kzg.rs b/src/kzg.rs index b20a2a1..f986b38 100644 --- a/src/kzg.rs +++ b/src/kzg.rs @@ -21,12 +21,13 @@ use std::{ /// Main interesting struct of the rust-kzg-bn254 crate. /// [Kzg] is a struct that holds the SRS points in monomial form, and -/// provides methods for committing to a blob, (either via a [Blob] itself, -/// or a [PolynomialCoeffForm] or [PolynomialEvalForm]), and generating and verifying proofs. -/// -/// The [Blob] and [PolynomialCoeffForm]/[PolynomialEvalForm] structs are mostly -/// with -/// constructor and few helper methods. +/// provides methods for committing to a blob, (either via a [Blob] itself, +/// or a [PolynomialCoeffForm] or [PolynomialEvalForm]), and generating and +/// verifying proofs. +/// +/// The [Blob] and [PolynomialCoeffForm]/[PolynomialEvalForm] structs are mostly +/// with +/// constructor and few helper methods. #[derive(Debug, PartialEq, Clone)] pub struct KZG { // SRS points are stored in monomial form, ready to be used for commitments with polynomials @@ -112,9 +113,10 @@ impl KZG { Ok(chunks) } - /// Similar to [Kzg::data_setup_mins], but mainly used for setting up Kzg for testing purposes. - /// Used to specify the number of chunks and chunk length. - /// These parameters are then used to calculate the FFT params required for FFT operations. + /// Similar to [Kzg::data_setup_mins], but mainly used for setting up Kzg + /// for testing purposes. Used to specify the number of chunks and chunk + /// length. These parameters are then used to calculate the FFT params + /// required for FFT operations. pub fn data_setup_custom( &mut self, num_of_nodes: u64, @@ -128,7 +130,8 @@ impl KZG { } /// Used to specify the number of chunks and chunk length. - /// These parameters are then used to calculate the FFT params required for FFT operations. + /// These parameters are then used to calculate the FFT params required for + /// FFT operations. pub fn data_setup_mins( &mut self, min_chunk_length: u64, @@ -519,8 +522,8 @@ impl KZG { } } - /// commit to a [Blob], by transforming it into a [PolynomialEvalForm] and then - /// calling [Kzg::commit_eval_form]. + /// commit to a [Blob], by transforming it into a [PolynomialEvalForm] and + /// then calling [Kzg::commit_eval_form]. pub fn commit_blob(&self, blob: &Blob) -> Result { let polynomial = blob.to_polynomial_eval_form(); self.commit_eval_form(&polynomial) diff --git a/src/lib.rs b/src/lib.rs index 4efaead..f39a07b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,69 +1,82 @@ -/*! -## Library Design / Architecture - -The main purpose of this library is to allow taking a piece of data, committing to it, and then generating and verifying proofs against that commitment. - -### Data Types - -The main data pipeline goes: -> user data -> [blob::Blob] -> [polynomial::PolynomialEvalForm]/[polynomial::PolynomialCoeffForm] -> KZG Commitment / Proof - -- User Data: bytes array - - meaningful to users (typically will be a rollup batch) -- Blob: bn254 field elements array - - meaningful to EigenDA network - - Obtained from User Data by inserting zeroes every 31 bytes to make every 32 byte an element of bn254. -- Polynomial: bn254 field elements array, interpreted as coefficients or evaluations of a polynomial - - meaningful when committing and generating/verifying proofs - - Obtained from Blob by appending zeroes to make the length a power of 2, and then interpreting the array as coefficients or evaluations of a polynomial. -- KZG: struct storing the SRS points used to generate commitments and proofs -- SRS points: bn254 group elements - - inner producted with the polynomial to generate commitments - -The Blob and Polynomial structs are mostly -[Plain Old Data](https://en.wikipedia.org/wiki/Passive_data_structure) with constructor and few helper methods. -The interesting stuff happens in the [kzg::KZG] struct, -which has methods for committing to a blob, polynomial in coeff or eval form, -and generating and verifying proofs. - -Our current codebase has the types PolynomialEvalForm and PolynomialCoeffForm to represent the polynomial in evaluation and coefficient form respectively. However, we do not have types to represent the two forms of srs points. They are implicitly assumed to be in monomial form when loaded, and an IFFT is performed before taking the inner product with the polynomial in evaluation form. - -### KZG Commitments - -A KZG commitment can be taken by an inner product between (poly_coeff, srs_monomial) or (poly_eval, srs_lagrange). FFT and IFFT operations can be performed to convert between these forms. - -![KZG](../kzg_commitment_diagram.png) - -### KZG Proofs - -TODO - -## Examples - -### Commit to a some user data -```rust -use rust_kzg_bn254::{blob::Blob, kzg::KZG}; - -let kzg = KZG::setup( - "tests/test-files/mainnet-data/g1.131072.point", - "", - "tests/test-files/mainnet-data/g2.point.powerOf2", - 268435456, - 131072, -).unwrap(); - -let rollup_data: &[u8] = "some rollup batcher data".as_bytes(); -let blob = Blob::from_raw_data(rollup_data); -let poly = blob.to_polynomial_eval_form(); -let commitment = kzg.commit_eval_form(&poly).unwrap(); -``` - -### Generate a proof for a piece of data -```rust -// TODO -``` - -*/ +//! ## Library Design / Architecture +//! +//! The main purpose of this library is to allow taking a piece of data, +//! committing to it, and then generating and verifying proofs against that +//! commitment. +//! +//! ### Data Types +//! +//! The main data pipeline goes: +//! > user data -> [blob::Blob] -> +//! > [polynomial::PolynomialEvalForm]/[polynomial::PolynomialCoeffForm] -> KZG +//! > Commitment / Proof +//! +//! - User Data: bytes array +//! - meaningful to users (typically will be a rollup batch) +//! - Blob: bn254 field elements array +//! - meaningful to EigenDA network +//! - Obtained from User Data by inserting zeroes every 31 bytes to make every +//! 32 byte an element of bn254. +//! - Polynomial: bn254 field elements array, interpreted as coefficients or +//! evaluations of a polynomial +//! - meaningful when committing and generating/verifying proofs +//! - Obtained from Blob by appending zeroes to make the length a power of 2, +//! and then interpreting the array as coefficients or evaluations of a +//! polynomial. +//! - KZG: struct storing the SRS points used to generate commitments and proofs +//! - SRS points: bn254 group elements +//! - inner producted with the polynomial to generate commitments +//! +//! The Blob and Polynomial structs are mostly +//! [Plain Old Data](https://en.wikipedia.org/wiki/Passive_data_structure) with constructor and few helper methods. +//! The interesting stuff happens in the [kzg::KZG] struct, +//! which has methods for committing to a blob, polynomial in coeff or eval +//! form, and generating and verifying proofs. +//! +//! Our current codebase has the types PolynomialEvalForm and +//! PolynomialCoeffForm to represent the polynomial in evaluation and +//! coefficient form respectively. However, we do not have types to represent +//! the two forms of srs points. They are implicitly assumed to be in monomial +//! form when loaded, and an IFFT is performed before taking the inner product +//! with the polynomial in evaluation form. +//! +//! ### KZG Commitments +//! +//! A KZG commitment can be taken by an inner product between (poly_coeff, +//! srs_monomial) or (poly_eval, srs_lagrange). FFT and IFFT operations can be +//! performed to convert between these forms. +//! +//! ![KZG](../kzg_commitment_diagram.png) +//! +//! ### KZG Proofs +//! +//! TODO +//! +//! ## Examples +//! +//! ### Commit to a some user data +//! ```rust +//! use rust_kzg_bn254::{blob::Blob, kzg::KZG}; +//! +//! let kzg = KZG::setup( +//! "tests/test-files/mainnet-data/g1.131072.point", +//! "", +//! "tests/test-files/mainnet-data/g2.point.powerOf2", +//! 268435456, +//! 131072, +//! ).unwrap(); +//! +//! let rollup_data: &[u8] = "some rollup batcher data".as_bytes(); +//! let blob = Blob::from_raw_data(rollup_data); +//! let poly = blob.to_polynomial_eval_form(); +//! let commitment = kzg.commit_eval_form(&poly).unwrap(); +//! ``` +//! +//! ### Generate a proof for a piece of data +//! ```rust +//! TODO +//! ``` +//! mod arith; pub mod blob; diff --git a/src/polynomial.rs b/src/polynomial.rs index 9139d19..ca6ba16 100644 --- a/src/polynomial.rs +++ b/src/polynomial.rs @@ -8,9 +8,9 @@ pub struct PolynomialEvalForm { /// evaluations contains the evaluations of the polynomial, padded with 0s /// to the next power of two. Hence if the polynomial is created with /// coefficients [1, 2, 3], the internal representation will be [1, 2, - /// 3, 0]. Note that this changes the polynomial! This is an inconsistency in our - /// current representations. Polynomials are the objects that get committed, not the - /// underlying Blobs. + /// 3, 0]. Note that this changes the polynomial! This is an inconsistency + /// in our current representations. Polynomials are the objects that get + /// committed, not the underlying Blobs. /// TODO: do we also want to force blobs to be of powers-of-two length? evaluations: Vec, /// Number of bytes in the underlying blob, which was used to create the @@ -26,10 +26,11 @@ pub struct PolynomialEvalForm { } impl PolynomialEvalForm { - /// Creates a new [PolynomialEvalForm] from the given coefficients, passed as a vector of `Fr`. - /// The coefficients are padded to the next power of two by appending zeros. - /// This typically wouldn't be used directly, but instead a [crate::blob::Blob] would be - /// converted to a [PolynomialEvalForm] using [crate::blob::Blob::to_polynomial_eval_form]. + /// Creates a new [PolynomialEvalForm] from the given coefficients, passed + /// as a vector of `Fr`. The coefficients are padded to the next power + /// of two by appending zeros. This typically wouldn't be used directly, + /// but instead a [crate::blob::Blob] would be converted to a + /// [PolynomialEvalForm] using [crate::blob::Blob::to_polynomial_eval_form]. pub fn new(evals: Vec) -> Self { let underlying_blob_len_in_bytes = evals.len() * BYTES_PER_FIELD_ELEMENT; let next_power_of_two = evals.len().next_power_of_two(); @@ -110,10 +111,12 @@ pub struct PolynomialCoeffForm { } impl PolynomialCoeffForm { - /// Creates a new [PolynomialCoeffForm] from the given coefficients, passed as a vector of `Fr`. - /// The coefficients are padded to the next power of two by appending zeros. - /// This typically wouldn't be used directly, but instead a [crate::blob::Blob] would be - /// converted to a [PolynomialCoeffForm] using [crate::blob::Blob::to_polynomial_coeff_form]. + /// Creates a new [PolynomialCoeffForm] from the given coefficients, passed + /// as a vector of `Fr`. The coefficients are padded to the next power + /// of two by appending zeros. This typically wouldn't be used directly, + /// but instead a [crate::blob::Blob] would be converted to a + /// [PolynomialCoeffForm] using + /// [crate::blob::Blob::to_polynomial_coeff_form]. pub fn new(coeffs: Vec) -> Self { let underlying_blob_len_in_bytes = coeffs.len() * BYTES_PER_FIELD_ELEMENT; let next_power_of_two = coeffs.len().next_power_of_two(); From 5c8e0acc6da650760e3e787e73f6af7b1cb26513 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Tue, 7 Jan 2025 19:08:07 -0500 Subject: [PATCH 22/22] test: fix cargo doc test --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f39a07b..5e7e416 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ //! //! ### Generate a proof for a piece of data //! ```rust -//! TODO +//! // TODO: //! ``` //!