-
Notifications
You must be signed in to change notification settings - Fork 68
Optimize SRS serialization: parallel uncompressed format, file caching, buffered I/O #325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,11 @@ | ||
| use derivative::Derivative; | ||
| use gkr_engine::StructuredReferenceString; | ||
| use halo2curves::group::prime::PrimeCurveAffine; | ||
| use halo2curves::group::UncompressedEncoding; | ||
| use halo2curves::{pairing::Engine, CurveAffine}; | ||
| use rayon::prelude::*; | ||
| use serdes::{ExpSerde, SerdeResult}; | ||
| use std::io::{Read, Write}; | ||
|
|
||
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Derivative)] | ||
| #[derivative(Default(bound = ""))] | ||
|
|
@@ -34,7 +38,7 @@ where | |
|
|
||
| /// Structured reference string for univariate KZG polynomial commitment scheme. | ||
| /// The univariate polynomial here is of coefficient form. | ||
| #[derive(Clone, Debug, PartialEq, Eq, Derivative, ExpSerde)] | ||
| #[derive(Clone, Debug, PartialEq, Eq, Derivative)] | ||
| #[derivative(Default(bound = ""))] | ||
| pub struct CoefFormUniKZGSRS<E: Engine> | ||
| where | ||
|
|
@@ -48,6 +52,79 @@ where | |
| pub tau_g2: E::G2Affine, | ||
| } | ||
|
|
||
| /// Custom ExpSerde implementation with parallel deserialization for fast SRS loading. | ||
| /// Uses UNCOMPRESSED format to avoid expensive square root computation during load. | ||
| impl<E: Engine> ExpSerde for CoefFormUniKZGSRS<E> | ||
| where | ||
| E::G1Affine: ExpSerde | ||
| + CurveAffine<ScalarExt = E::Fr, CurveExt = E::G1> | ||
| + UncompressedEncoding | ||
| + Send | ||
| + Sync, | ||
|
Comment on lines
+59
to
+63
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The E::G1Affine: CurveAffine<ScalarExt = E::Fr, CurveExt = E::G1>
+ UncompressedEncoding
+ Send
+ Sync, |
||
| E::G2Affine: CurveAffine<ScalarExt = E::Fr, CurveExt = E::G2> + ExpSerde, | ||
| { | ||
| fn serialize_into<W: Write>(&self, mut writer: W) -> SerdeResult<()> { | ||
| // Serialize length | ||
| self.powers_of_tau.len().serialize_into(&mut writer)?; | ||
|
|
||
| // Get uncompressed point size | ||
| let point_size = | ||
| std::mem::size_of::<<E::G1Affine as UncompressedEncoding>::Uncompressed>(); | ||
| let total_size = self.powers_of_tau.len() * point_size; | ||
|
|
||
| // Pre-allocate buffer and convert all points to uncompressed format in parallel | ||
| let mut buffer = vec![0u8; total_size]; | ||
| buffer | ||
| .par_chunks_mut(point_size) | ||
| .zip(self.powers_of_tau.par_iter()) | ||
| .for_each(|(chunk, point)| { | ||
| let uncompressed = point.to_uncompressed(); | ||
| chunk.copy_from_slice(uncompressed.as_ref()); | ||
| }); | ||
|
|
||
| // Write all at once | ||
| writer.write_all(&buffer)?; | ||
|
|
||
| // Serialize tau_g2 | ||
| self.tau_g2.serialize_into(&mut writer)?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn deserialize_from<R: Read>(mut reader: R) -> SerdeResult<Self> { | ||
| // Read length | ||
| let len = usize::deserialize_from(&mut reader)?; | ||
|
|
||
| // Uncompressed G1 point size (e.g. BN256: 64 bytes) | ||
| let point_size = | ||
| std::mem::size_of::<<E::G1Affine as UncompressedEncoding>::Uncompressed>(); | ||
| let total_bytes = len * point_size; | ||
|
|
||
| let mut buffer = vec![0u8; total_bytes]; | ||
| reader.read_exact(&mut buffer)?; | ||
|
|
||
| // Parse points in parallel - using from_uncompressed_unchecked (no square root needed) | ||
| let powers_of_tau: Vec<E::G1Affine> = buffer | ||
| .par_chunks(point_size) | ||
| .map(|chunk| { | ||
| let mut uncompressed = | ||
| <E::G1Affine as UncompressedEncoding>::Uncompressed::default(); | ||
| uncompressed.as_mut().copy_from_slice(chunk); | ||
| E::G1Affine::from_uncompressed_unchecked(&uncompressed) | ||
| .into_option() | ||
| .ok_or(serdes::SerdeError::DeserializeError) | ||
|
Comment on lines
+112
to
+114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using from_uncompressed_unchecked skips critical validation checks, such as ensuring the point lies on the curve and belongs to the correct subgroup. While this is a significant performance optimization for loading trusted SRS files, it introduces a security risk if the SRS file is tampered with or loaded from an untrusted source. Consider adding a comment warning about this or providing a way to perform validation if needed. |
||
| }) | ||
| .collect::<SerdeResult<Vec<_>>>()?; | ||
|
|
||
| // Read tau_g2 | ||
| let tau_g2 = E::G2Affine::deserialize_from(&mut reader)?; | ||
|
|
||
| Ok(Self { | ||
| powers_of_tau, | ||
| tau_g2, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| impl<E: Engine> StructuredReferenceString for CoefFormUniKZGSRS<E> | ||
| where | ||
| <E as Engine>::G1Affine: ExpSerde + CurveAffine<ScalarExt = E::Fr, CurveExt = E::G1>, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ use gkr_engine::{ | |||||||||
| }; | ||||||||||
| use polynomials::{MultiLinearPoly, MultilinearExtension, MutableMultilinearExtension}; | ||||||||||
|
|
||||||||||
| /// Initialize PCS for testing without SRS caching (always regenerates SRS) | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docstring states that this function initializes PCS 'without SRS caching', but the implementation was changed to use a hardcoded path in /tmp, which enables caching. This documentation should be updated to reflect the actual behavior.
Suggested change
|
||||||||||
| #[allow(clippy::type_complexity)] | ||||||||||
| pub fn expander_pcs_init_testing_only<FieldConfig: FieldEngine, PCS: ExpanderPCS<FieldConfig>>( | ||||||||||
| n_input_vars: usize, | ||||||||||
|
|
@@ -14,19 +15,44 @@ pub fn expander_pcs_init_testing_only<FieldConfig: FieldEngine, PCS: ExpanderPCS | |||||||||
| <PCS::SRS as StructuredReferenceString>::PKey, | ||||||||||
| <PCS::SRS as StructuredReferenceString>::VKey, | ||||||||||
| PCS::ScratchPad, | ||||||||||
| ) { | ||||||||||
| let srs_path = format!("/tmp/kzg_srs_{}.bin", n_input_vars); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a hardcoded path in /tmp with a predictable filename is insecure on multi-user systems as it is vulnerable to symlink attacks. Additionally, it can lead to permission issues or collisions if multiple users run the same tests. It is recommended to use a more secure location or at least use std::env::temp_dir() to respect environment variables and improve cross-platform compatibility. |
||||||||||
| expander_pcs_init_with_srs_path::<FieldConfig, PCS>(n_input_vars, mpi_config, Some(&srs_path)) | ||||||||||
|
Comment on lines
+19
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoding the temporary path to
Suggested change
|
||||||||||
| } | ||||||||||
|
|
||||||||||
| /// Initialize PCS with optional SRS file caching | ||||||||||
| /// | ||||||||||
| /// # Arguments | ||||||||||
| /// * `n_input_vars` - Number of input variables (determines SRS size as 2^n) | ||||||||||
| /// * `mpi_config` - MPI configuration | ||||||||||
| /// * `srs_path` - Optional path to SRS cache file: | ||||||||||
| /// - `Some(path)`: If file exists, load SRS from it; otherwise generate and save to it | ||||||||||
| /// - `None`: Always regenerate SRS (no caching) | ||||||||||
| #[allow(clippy::type_complexity)] | ||||||||||
| pub fn expander_pcs_init_with_srs_path<FieldConfig: FieldEngine, PCS: ExpanderPCS<FieldConfig>>( | ||||||||||
| n_input_vars: usize, | ||||||||||
| mpi_config: &impl MPIEngine, | ||||||||||
| srs_path: Option<&str>, | ||||||||||
| ) -> ( | ||||||||||
| PCS::Params, | ||||||||||
| <PCS::SRS as StructuredReferenceString>::PKey, | ||||||||||
| <PCS::SRS as StructuredReferenceString>::VKey, | ||||||||||
| PCS::ScratchPad, | ||||||||||
| ) { | ||||||||||
| let mut rng = test_rng(); | ||||||||||
|
|
||||||||||
| let pcs_params = | ||||||||||
| <PCS as ExpanderPCS<FieldConfig>>::gen_params(n_input_vars, mpi_config.world_size()); | ||||||||||
|
|
||||||||||
| let pcs_setup = <PCS as ExpanderPCS<FieldConfig>>::gen_or_load_srs_for_testing( | ||||||||||
| &pcs_params, | ||||||||||
| mpi_config, | ||||||||||
| &mut rng, | ||||||||||
| None, | ||||||||||
| srs_path, | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| let (pcs_proving_key, pcs_verification_key) = pcs_setup.into_keys(); | ||||||||||
|
|
||||||||||
| let pcs_scratch = <PCS as ExpanderPCS<FieldConfig>>::init_scratch_pad(&pcs_params, mpi_config); | ||||||||||
|
|
||||||||||
| ( | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,8 +68,8 @@ impl<V: ExpSerde> ExpSerde for Vec<V> { | |
| } | ||
|
|
||
| fn deserialize_from<R: Read>(mut reader: R) -> SerdeResult<Self> { | ||
| let mut v = Self::default(); | ||
| let len = usize::deserialize_from(&mut reader)?; | ||
| let mut v = Vec::with_capacity(len); | ||
|
Comment on lines
71
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pre-allocating a Vec with with_capacity(len) where len is read directly from the input stream can lead to Out-Of-Memory (OOM) vulnerabilities if the input is malicious or corrupted (e.g., a very large len value). It is safer to bound the maximum allowed length or use a more conservative allocation strategy. |
||
| for _ in 0..len { | ||
| v.push(V::deserialize_from(&mut reader)?); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A 64MB buffer capacity for BufReader and BufWriter is quite large and likely excessive. Since the SRS data is mostly read or written in large chunks (via read_exact or write_all), BufReader/BufWriter will often bypass the internal buffer for these large operations. A smaller, more standard buffer size (e.g., 64KB or 1MB) would be more memory-efficient without sacrificing performance.