Repository: cloudflare/daphne
Pull request #630 (2024-06-26 17:08:12)

pine: Specify number of proofs and algorithm ID dynamically
Based on #629 (merge that first).
Partially addresses #618.

Currently the number of FLPs to generate is a generic constant and thus determined at compile time. For taskprov, the number of proofs will be configured at runtime. To make this easier, move this parameter into `PineConfig`.

This change also moves the VDAF algorithm ID to `PineConfig`. This is needed for a different reason however: we will have more than one instance of PINE, for different field sizes and XOFs.


Patch:
diff --git a/crates/daphne/benches/pine.rs b/crates/daphne/benches/pine.rs
index c986357..ffe5465 100644
--- a/crates/daphne/benches/pine.rs
+++ b/crates/daphne/benches/pine.rs
@@ -2,7 +2,7 @@
 // SPDX-License-Identifier: BSD-3-Clause
 
 use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
-use daphne::pine::Pine64;
+use daphne::pine::Pine;
 use prio::{
     field::random_vector,
     flp::Type,
@@ -17,7 +17,7 @@ fn pine(c: &mut Criterion) {
         (100_000, 320 * 6),
         (1_000_000, 1_000 * 8),
     ] {
-        let pine = Pine64::new(1.0, dimension, 15, chunk_len).unwrap();
+        let pine = Pine::new_64(1.0, dimension, 15, chunk_len).unwrap();
         let measurement = vec![0.0; dimension];
         let wr_joint_rand_seed = Seed::generate().unwrap();
         let nonce = [0; 16];
diff --git a/crates/daphne/src/pine/flp.rs b/crates/daphne/src/pine/flp.rs
index acb3752..c0ea024 100644
--- a/crates/daphne/src/pine/flp.rs
+++ b/crates/daphne/src/pine/flp.rs
@@ -30,7 +30,7 @@ use prio::{
 use rand::Rng;
 
 use super::{
-    chunk_count, dst, f64_to_field, field_to_f64, PineConfig, NUM_WR_SUCCESSES, NUM_WR_TESTS,
+    chunk_count, f64_to_field, field_to_f64, PineConfig, NUM_WR_SUCCESSES, NUM_WR_TESTS,
     USAGE_WR_JOINT_RAND,
 };
 
@@ -152,8 +152,11 @@ impl<F: FftFriendlyFieldElement> PineType<F> {
         gradient: &[F],
         wr_joint_rand_seed: &Seed<16>,
     ) -> [F; NUM_WR_TESTS] {
-        let mut xof =
-            XofTurboShake128::seed_stream(wr_joint_rand_seed, &dst(USAGE_WR_JOINT_RAND), &[]);
+        let mut xof = XofTurboShake128::seed_stream(
+            wr_joint_rand_seed,
+            &self.cfg.dst(USAGE_WR_JOINT_RAND),
+            &[],
+        );
         let mut buf = vec![0_u8; chunk_count(4, NUM_WR_TESTS * self.cfg.dimension)];
         xof.fill(&mut buf[..]);
 
@@ -507,7 +510,7 @@ mod tests {
     fn encode_gradient() {
         let dimension = 10;
         let frac_bits = 4;
-        let pine = Pine::<Field128, 1>::new(100.0, dimension, frac_bits, 4).unwrap();
+        let pine = Pine::new_128(100.0, dimension, frac_bits, 4).unwrap();
 
         // We use whole numbers here so that we can test gradient decoding without losing any
         // precision.
@@ -548,7 +551,7 @@ mod tests {
     #[test]
     fn encode_wr_tests() {
         let mut rng = thread_rng();
-        let pine = Pine::<Field128, 1>::new(10.0, 10, 15, 4).unwrap();
+        let pine = Pine::new_128(10.0, 10, 15, 4).unwrap();
         let gradient = iter::repeat_with(|| rng.gen_range(-0.1..0.1)).take(10);
 
         let (input, wr_test_results) = pine
@@ -579,7 +582,7 @@ mod tests {
         }
     }
 
-    impl<F: FftFriendlyFieldElement> Pine<F, 1> {
+    impl<F: FftFriendlyFieldElement> Pine<F> {
         fn run_valid_test_case(&self, gradient: impl Iterator<Item = f64>) {
             let (mut input, wr_test_results) = self
                 .flp
@@ -594,28 +597,28 @@ mod tests {
     #[test]
     fn valid() {
         const DIM: usize = 1000;
-        let pine = Pine::<Field128, 1>::new(1000.0, DIM, 15, 27).unwrap();
+        let pine = Pine::new_128(1000.0, DIM, 15, 27).unwrap();
         pine.run_valid_test_case((0..DIM).map(|i| i as f64 * 0.01));
     }
 
     #[test]
     fn valid_small_dimension() {
         const DIM: usize = 1;
-        let pine = Pine::<Field128, 1>::new(1.0, DIM, 15, 4).unwrap();
+        let pine = Pine::new_128(1.0, DIM, 15, 4).unwrap();
         pine.run_valid_test_case([0.75].into_iter());
     }
 
     #[test]
     fn valid_negative_values() {
         const DIM: usize = 1337;
-        let pine = Pine::<Field128, 1>::new(1000.0, DIM, 15, 4).unwrap();
+        let pine = Pine::new_128(1000.0, DIM, 15, 4).unwrap();
         pine.run_valid_test_case((0..DIM).map(|i| i as f64 * -0.01));
     }
 
     #[test]
     fn valid_random_values() {
         const DIM: usize = 1000;
-        let pine = Pine::<Field128, 1>::new(100.0, DIM, 15, 99).unwrap();
+        let pine = Pine::new_128(100.0, DIM, 15, 99).unwrap();
         let mut rng = thread_rng();
         pine.run_valid_test_case(iter::repeat_with(|| rng.gen_range(-0.1..0.1)).take(DIM));
     }
@@ -623,7 +626,7 @@ mod tests {
     #[test]
     fn invalid_mutated_gradient() {
         const DIM: usize = 10;
-        let pine = Pine::<Field128, 1>::new(100.0, DIM, 15, 4).unwrap();
+        let pine = Pine::new_128(100.0, DIM, 15, 4).unwrap();
         let (mut input, wr_test_results) = pine
             .flp
             .encode_with_wr_joint_rand([0.0; DIM].into_iter(), &Seed::generate().unwrap())
@@ -639,7 +642,7 @@ mod tests {
     #[test]
     fn invalid_mutated_sq_norm() {
         const DIM: usize = 10;
-        let pine = Pine::<Field128, 1>::new(100.0, DIM, 15, 4).unwrap();
+        let pine = Pine::new_128(100.0, DIM, 15, 4).unwrap();
         let (mut input, wr_test_results) = pine
             .flp
             .encode_with_wr_joint_rand([0.0; DIM].into_iter(), &Seed::generate().unwrap())
@@ -659,7 +662,7 @@ mod tests {
     #[test]
     fn invalid_mutated_wr_result() {
         const DIM: usize = 10;
-        let pine = Pine::<Field128, 1>::new(100.0, DIM, 15, 4).unwrap();
+        let pine = Pine::new_128(100.0, DIM, 15, 4).unwrap();
         let (mut input, wr_test_results) = pine
             .flp
             .encode_with_wr_joint_rand([0.0; DIM].into_iter(), &Seed::generate().unwrap())
@@ -680,7 +683,7 @@ mod tests {
     #[test]
     fn invalid_mutated_success_bit() {
         const DIM: usize = 10;
-        let pine = Pine::<Field128, 1>::new(100.0, DIM, 15, 4).unwrap();
+        let pine = Pine::new_128(100.0, DIM, 15, 4).unwrap();
         let (mut input, wr_test_results) = pine
             .flp
             .encode_with_wr_joint_rand([0.0; DIM].into_iter(), &Seed::generate().unwrap())
@@ -702,7 +705,7 @@ mod tests {
     #[test]
     fn invalid_dot_prod_mismatch() {
         const DIM: usize = 10;
-        let pine = Pine::<Field128, 1>::new(100.0, DIM, 15, 4).unwrap();
+        let pine = Pine::new_128(100.0, DIM, 15, 4).unwrap();
         let (mut input, wr_test_results) = pine
             .flp
             .encode_with_wr_joint_rand([0.1; DIM].into_iter(), &Seed::generate().unwrap())
diff --git a/crates/daphne/src/pine/mod.rs b/crates/daphne/src/pine/mod.rs
index c5bf0de..cd14de1 100644
--- a/crates/daphne/src/pine/mod.rs
+++ b/crates/daphne/src/pine/mod.rs
@@ -24,8 +24,6 @@ const ALPHA: f64 = 8.7;
 const NUM_WR_TESTS: usize = 100;
 const NUM_WR_SUCCESSES: usize = 100;
 
-const VDAF_ID: u32 = 0xffff_ffff;
-
 const DST_SIZE: usize = 7;
 
 const USAGE_MEAS_SHARE: u16 = 1;
@@ -41,12 +39,37 @@ const USAGE_WR_JOINT_RAND_PART: u16 = 10;
 
 /// The PINE VDAF.
 #[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Pine<F, const PROOFS: u8> {
+pub struct Pine<F> {
     pub flp: PineType<F>,
 }
 
-pub type Pine128 = Pine<Field128, 1>;
-pub type Pine64 = Pine<Field64, 3>;
+pub type Pine128 = Pine<Field128>;
+
+impl Pine128 {
+    /// Construct an instance of [`Pine128`] with the provided parameters.
+    pub fn new_128(
+        norm_bound: f64,
+        dimension: usize,
+        frac_bits: usize,
+        chunk_len: usize,
+    ) -> Result<Self, VdafError> {
+        Self::new(norm_bound, dimension, frac_bits, chunk_len, 1, 0xffff_ffff)
+    }
+}
+
+pub type Pine64 = Pine<Field64>;
+
+impl Pine64 {
+    /// Construct an instance of [`Pine64`] with the provided parameters.
+    pub fn new_64(
+        norm_bound: f64,
+        dimension: usize,
+        frac_bits: usize,
+        chunk_len: usize,
+    ) -> Result<Self, VdafError> {
+        Self::new(norm_bound, dimension, frac_bits, chunk_len, 3, 0xffff_ffff)
+    }
+}
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub(crate) struct PineConfig<F> {
@@ -57,6 +80,8 @@ pub(crate) struct PineConfig<F> {
     sq_norm_bits: usize,
     wr_test_bound: F,
     wr_test_bits: usize,
+    num_proofs: u8,
+    algorithm_id: u32,
 
     // FLP parameters
     chunk_len: usize,
@@ -66,7 +91,17 @@ pub(crate) struct PineConfig<F> {
     gadget_calls: usize,
 }
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
+impl<F> PineConfig<F> {
+    fn dst(&self, usage: u16) -> [u8; DST_SIZE] {
+        let mut dst = [0; DST_SIZE];
+        dst[0] = 0; // VERSION, draft-cheng-cfrg-vdaf-pine-00
+        dst[1..5].copy_from_slice(&self.algorithm_id.to_be_bytes()); // algo
+        dst[5..].copy_from_slice(&usage.to_be_bytes()); // usage
+        dst
+    }
+}
+
+impl<F: FftFriendlyFieldElement> Pine<F> {
     /// Construct an instance of the Pine VDAF.
     ///
     /// # Parameters
@@ -80,11 +115,17 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
     ///
     /// * `chunk_len`: FLP parameter used to for proof generation. Any positive integer can be
     ///   used. The optimal value depends on the other parameters.
-    pub fn new(
+    ///
+    /// * `num_proofs`: The number of FLP proofs to generate.
+    ///
+    /// * `algorithm_id`: The VDAF algorithm ID.
+    pub(crate) fn new(
         norm_bound: f64,
         dimension: usize,
         frac_bits: usize,
         chunk_len: usize,
+        num_proofs: u8,
+        algorithm_id: u32,
     ) -> Result<Self, VdafError> {
         // 24 is the largest number of fractional bits that we know we can support.
         if frac_bits > 24 {
@@ -190,6 +231,8 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
                     sq_norm_bits,
                     wr_test_bound,
                     wr_test_bits,
+                    num_proofs,
+                    algorithm_id,
                     chunk_len,
                     bit_checked_len,
                     encoded_gradient_len,
@@ -245,14 +288,6 @@ fn field_to_f64<F: FftFriendlyFieldElement>(x: F, two_to_frac_bits: f64) -> Resu
     Ok(out)
 }
 
-fn dst(usage: u16) -> [u8; DST_SIZE] {
-    let mut dst = [0; DST_SIZE];
-    dst[0] = 0; // VERSION, draft-cheng-cfrg-vdaf-pine-00
-    dst[1..5].copy_from_slice(&VDAF_ID.to_be_bytes()); // algo
-    dst[5..].copy_from_slice(&usage.to_be_bytes()); // usage
-    dst
-}
-
 /// Compute the number of bits required to encode `x` in binary.
 fn bits(x: u64) -> usize {
     if x == 0 {
@@ -497,7 +532,7 @@ mod tests {
                 expected_wr_test_bits: 15,
             },
         ] {
-            let pine = Pine128::new(
+            let pine = Pine::new_128(
                 t.norm_bound,
                 10, // not used by test
                 t.frac_bits,
diff --git a/crates/daphne/src/pine/msg.rs b/crates/daphne/src/pine/msg.rs
index 8663f53..f8f14a4 100644
--- a/crates/daphne/src/pine/msg.rs
+++ b/crates/daphne/src/pine/msg.rs
@@ -45,9 +45,9 @@ impl Encode for PublicShare {
     }
 }
 
-impl<F, const PROOFS: u8> ParameterizedDecode<Pine<F, PROOFS>> for PublicShare {
+impl<F> ParameterizedDecode<Pine<F>> for PublicShare {
     fn decode_with_param(
-        _pine: &Pine<F, PROOFS>,
+        _pine: &Pine<F>,
         bytes: &mut std::io::Cursor<&[u8]>,
     ) -> Result<Self, CodecError> {
         Ok(Self {
@@ -138,11 +138,9 @@ impl<F: FftFriendlyFieldElement> Encode for InputShare<F> {
     }
 }
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> ParameterizedDecode<(&Pine<F, PROOFS>, usize)>
-    for InputShare<F>
-{
+impl<F: FftFriendlyFieldElement> ParameterizedDecode<(&Pine<F>, usize)> for InputShare<F> {
     fn decode_with_param(
-        (pine, agg_id): &(&Pine<F, PROOFS>, usize),
+        (pine, agg_id): &(&Pine<F>, usize),
         bytes: &mut std::io::Cursor<&[u8]>,
     ) -> Result<Self, CodecError> {
         match agg_id {
@@ -152,7 +150,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> ParameterizedDecode<(&Pine<F,
                     .take(pine.flp.cfg.encoded_input_len)
                     .collect::<Result<Vec<_>, _>>()?,
                 proofs_share: iter::repeat_with(|| F::decode(bytes))
-                    .take(pine.flp.proof_len() * usize::from(PROOFS))
+                    .take(pine.flp.proof_len() * usize::from(pine.flp.cfg.num_proofs))
                     .collect::<Result<Vec<_>, _>>()?,
                 wr_blind: Seed::decode(bytes)?,
                 vf_blind: Seed::decode(bytes)?,
diff --git a/crates/daphne/src/pine/vdaf.rs b/crates/daphne/src/pine/vdaf.rs
index c7c07b7..b5970c3 100644
--- a/crates/daphne/src/pine/vdaf.rs
+++ b/crates/daphne/src/pine/vdaf.rs
@@ -19,9 +19,9 @@ use rand::prelude::*;
 use crate::pine::USAGE_QUERY_RAND;
 
 use super::{
-    dst, field_to_f64, msg, Pine, USAGE_MEAS_SHARE, USAGE_PROOF_SHARE, USAGE_PROVE_RAND,
+    field_to_f64, msg, Pine, USAGE_MEAS_SHARE, USAGE_PROOF_SHARE, USAGE_PROVE_RAND,
     USAGE_VF_JOINT_RAND, USAGE_VF_JOINT_RAND_PART, USAGE_VF_JOINT_RAND_SEED,
-    USAGE_WR_JOINT_RAND_PART, USAGE_WR_JOINT_RAND_SEED, VDAF_ID,
+    USAGE_WR_JOINT_RAND_PART, USAGE_WR_JOINT_RAND_SEED,
 };
 
 const NUM_SHARD_RAND_SEEDS: usize = 7;
@@ -43,11 +43,9 @@ impl<F: FftFriendlyFieldElement> Encode for PineVec<F> {
     }
 }
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> ParameterizedDecode<(&Pine<F, PROOFS>, &())>
-    for PineVec<F>
-{
+impl<F: FftFriendlyFieldElement> ParameterizedDecode<(&Pine<F>, &())> for PineVec<F> {
     fn decode_with_param(
-        (pine, _agg_param): &(&Pine<F, PROOFS>, &()),
+        (pine, _agg_param): &(&Pine<F>, &()),
         bytes: &mut std::io::Cursor<&[u8]>,
     ) -> Result<Self, CodecError> {
         iter::repeat_with(|| F::decode(bytes))
@@ -77,7 +75,7 @@ impl<F: FftFriendlyFieldElement> Aggregatable for PineVec<F> {
 
 // Vdaf
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> Vdaf for Pine<F, PROOFS> {
+impl<F: FftFriendlyFieldElement> Vdaf for Pine<F> {
     type Measurement = Vec<f64>;
     type AggregateResult = Vec<f64>;
     type AggregationParam = ();
@@ -87,7 +85,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Vdaf for Pine<F, PROOFS> {
     type AggregateShare = PineVec<F>;
 
     fn algorithm_id(&self) -> u32 {
-        VDAF_ID
+        self.flp.cfg.algorithm_id
     }
 
     fn num_aggregators(&self) -> usize {
@@ -95,15 +93,16 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Vdaf for Pine<F, PROOFS> {
     }
 }
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
+impl<F: FftFriendlyFieldElement> Pine<F> {
     fn joint_rand_part(
+        &self,
         agg_id: u8,
         blind: &[u8; 16],
         nonce: &[u8; 16],
         meas_share: &[F],
         usage: u16,
     ) -> Seed<16> {
-        let mut xof = XofTurboShake128::init(blind, &dst(usage));
+        let mut xof = XofTurboShake128::init(blind, &self.flp.cfg.dst(usage));
         xof.update(&[agg_id]);
         xof.update(nonce);
         let mut buf = Vec::with_capacity(F::ENCODED_SIZE);
@@ -115,31 +114,31 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
         xof.into_seed()
     }
 
-    fn joint_rand_seed(joint_rand_parts: &[Seed<16>; 2], usage: u16) -> Seed<16> {
-        let mut xof = XofTurboShake128::init(&[0; 16], &dst(usage));
+    fn joint_rand_seed(&self, joint_rand_parts: &[Seed<16>; 2], usage: u16) -> Seed<16> {
+        let mut xof = XofTurboShake128::init(&[0; 16], &self.flp.cfg.dst(usage));
         xof.update(joint_rand_parts[0].as_ref());
         xof.update(joint_rand_parts[1].as_ref());
         xof.into_seed()
     }
 
     fn helper_meas_share(&self, seed: &[u8; 16]) -> Vec<F> {
-        let mut xof = XofTurboShake128::init(seed, &dst(USAGE_MEAS_SHARE));
+        let mut xof = XofTurboShake128::init(seed, &self.flp.cfg.dst(USAGE_MEAS_SHARE));
         xof.update(&[1]); // agg_id
         xof.into_seed_stream()
             .into_field_vec(self.flp.cfg.encoded_input_len)
     }
 
     fn helper_proof_share(&self, seed: &[u8; 16]) -> Vec<F> {
-        let mut xof = XofTurboShake128::init(seed, &dst(USAGE_PROOF_SHARE));
-        xof.update(&[PROOFS, 1]); // PROOFS, agg_id
+        let mut xof = XofTurboShake128::init(seed, &self.flp.cfg.dst(USAGE_PROOF_SHARE));
+        xof.update(&[self.flp.cfg.num_proofs, 1]); // num_proofs, agg_id
         xof.into_seed_stream()
-            .into_field_vec(self.flp.proof_len() * usize::from(PROOFS))
+            .into_field_vec(self.flp.proof_len() * usize::from(self.flp.cfg.num_proofs))
     }
 }
 
 // Client
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> Client<16> for Pine<F, PROOFS> {
+impl<F: FftFriendlyFieldElement> Client<16> for Pine<F> {
     fn shard(
         &self,
         gradient: &Vec<f64>,
@@ -150,7 +149,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Client<16> for Pine<F, PROOFS
     }
 }
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
+impl<F: FftFriendlyFieldElement> Pine<F> {
     pub(crate) fn shard_with_rand(
         &self,
         gradient: &[f64],
@@ -176,14 +175,14 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
         debug_assert_eq!(meas_share_1.len(), self.flp.cfg.encoded_input_len);
 
         let wr_joint_rand_parts = [
-            Self::joint_rand_part(
+            self.joint_rand_part(
                 0,
                 &wr_blind_0,
                 nonce,
                 &meas_share_0,
                 USAGE_WR_JOINT_RAND_PART,
             ),
-            Self::joint_rand_part(
+            self.joint_rand_part(
                 1,
                 &wr_blind_1,
                 nonce,
@@ -195,7 +194,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
         // Run the wraparound tests and complete the Leader's measurement share.
         {
             let wr_joint_rand_seed =
-                Self::joint_rand_seed(&wr_joint_rand_parts, USAGE_WR_JOINT_RAND_SEED);
+                self.joint_rand_seed(&wr_joint_rand_parts, USAGE_WR_JOINT_RAND_SEED);
 
             let i = meas.len();
             let wr_test_results = self
@@ -212,14 +211,14 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
         debug_assert_eq!(meas_share_0.len(), self.flp.cfg.encoded_input_len);
 
         let vf_joint_rand_parts = [
-            Self::joint_rand_part(
+            self.joint_rand_part(
                 0,
                 &vf_blind_0,
                 nonce,
                 &meas_share_0,
                 USAGE_VF_JOINT_RAND_PART,
             ),
-            Self::joint_rand_part(
+            self.joint_rand_part(
                 1,
                 &vf_blind_1,
                 nonce,
@@ -232,24 +231,27 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
         let proofs_share_0 = {
             let vf_joint_rands = {
                 let vf_joint_rand_seed =
-                    Self::joint_rand_seed(&vf_joint_rand_parts, USAGE_VF_JOINT_RAND_SEED);
+                    self.joint_rand_seed(&vf_joint_rand_parts, USAGE_VF_JOINT_RAND_SEED);
 
                 XofTurboShake128::seed_stream(
                     &vf_joint_rand_seed,
-                    &dst(USAGE_VF_JOINT_RAND),
-                    &[PROOFS],
+                    &self.flp.cfg.dst(USAGE_VF_JOINT_RAND),
+                    &[self.flp.cfg.num_proofs],
                 )
-                .into_field_vec(self.flp.joint_rand_len() * usize::from(PROOFS))
+                .into_field_vec(self.flp.joint_rand_len() * usize::from(self.flp.cfg.num_proofs))
             };
 
             let prove_rands = {
-                let mut xof = XofTurboShake128::init(&prove_seed, &dst(USAGE_PROVE_RAND));
-                xof.update(&[PROOFS]);
-                xof.into_seed_stream()
-                    .into_field_vec(self.flp.prove_rand_len() * usize::from(PROOFS))
+                let mut xof =
+                    XofTurboShake128::init(&prove_seed, &self.flp.cfg.dst(USAGE_PROVE_RAND));
+                xof.update(&[self.flp.cfg.num_proofs]);
+                xof.into_seed_stream().into_field_vec(
+                    self.flp.prove_rand_len() * usize::from(self.flp.cfg.num_proofs),
+                )
             };
 
-            let mut proofs = Vec::with_capacity(self.flp.proof_len() * usize::from(PROOFS));
+            let mut proofs =
+                Vec::with_capacity(self.flp.proof_len() * usize::from(self.flp.cfg.num_proofs));
             for (prove_rand, vf_joint_rand) in prove_rands
                 .chunks(self.flp.prove_rand_len())
                 .zip(vf_joint_rands.chunks(self.flp.joint_rand_len()))
@@ -308,7 +310,7 @@ impl<F: FftFriendlyFieldElement> PartialEq for PinePrepState<F> {
 
 impl<F: FftFriendlyFieldElement> Eq for PinePrepState<F> {}
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> Aggregator<16, 16> for Pine<F, PROOFS> {
+impl<F: FftFriendlyFieldElement> Aggregator<16, 16> for Pine<F> {
     type PrepareState = PinePrepState<F>;
     type PrepareShare = msg::PrepShare<F>;
     type PrepareMessage = msg::Prep;
@@ -368,7 +370,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Aggregator<16, 16> for Pine<F
             }
         };
 
-        let wr_joint_rand_part = Self::joint_rand_part(
+        let wr_joint_rand_part = self.joint_rand_part(
             agg_id.try_into().unwrap(),
             wr_blind.as_ref(),
             nonce,
@@ -379,7 +381,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Aggregator<16, 16> for Pine<F
         let corrected_wr_joint_rand_seed = {
             let mut corrected_wr_joint_rand_parts = wr_joint_rand_parts.clone();
             corrected_wr_joint_rand_parts[agg_id] = wr_joint_rand_part.clone();
-            Self::joint_rand_seed(&corrected_wr_joint_rand_parts, USAGE_WR_JOINT_RAND_SEED)
+            self.joint_rand_seed(&corrected_wr_joint_rand_parts, USAGE_WR_JOINT_RAND_SEED)
         };
 
         // Run the wraparound tests.
@@ -392,7 +394,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Aggregator<16, 16> for Pine<F
             meas_share.extend_from_slice(&wr_test_results);
         }
 
-        let vf_joint_rand_part = Self::joint_rand_part(
+        let vf_joint_rand_part = self.joint_rand_part(
             agg_id.try_into().unwrap(),
             vf_blind.as_ref(),
             nonce,
@@ -403,7 +405,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Aggregator<16, 16> for Pine<F
         let corrected_vf_joint_rand_seed = {
             let mut corrected_vf_joint_rand_parts = vf_joint_rand_parts.clone();
             corrected_vf_joint_rand_parts[agg_id] = vf_joint_rand_part.clone();
-            Self::joint_rand_seed(&corrected_vf_joint_rand_parts, USAGE_VF_JOINT_RAND_SEED)
+            self.joint_rand_seed(&corrected_vf_joint_rand_parts, USAGE_VF_JOINT_RAND_SEED)
         };
 
         // Query the proofs.
@@ -411,22 +413,24 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Aggregator<16, 16> for Pine<F
             let corrected_vf_joint_rands = {
                 XofTurboShake128::seed_stream(
                     &corrected_vf_joint_rand_seed,
-                    &dst(USAGE_VF_JOINT_RAND),
-                    &[PROOFS],
+                    &self.flp.cfg.dst(USAGE_VF_JOINT_RAND),
+                    &[self.flp.cfg.num_proofs],
                 )
-                .into_field_vec(self.flp.joint_rand_len() * usize::from(PROOFS))
+                .into_field_vec(self.flp.joint_rand_len() * usize::from(self.flp.cfg.num_proofs))
             };
 
             let query_rands = {
-                let mut xof = XofTurboShake128::init(verify_key, &dst(USAGE_QUERY_RAND));
-                xof.update(&[PROOFS]);
+                let mut xof =
+                    XofTurboShake128::init(verify_key, &self.flp.cfg.dst(USAGE_QUERY_RAND));
+                xof.update(&[self.flp.cfg.num_proofs]);
                 xof.update(nonce);
-                xof.into_seed_stream()
-                    .into_field_vec(self.flp.query_rand_len() * usize::from(PROOFS))
+                xof.into_seed_stream().into_field_vec(
+                    self.flp.query_rand_len() * usize::from(self.flp.cfg.num_proofs),
+                )
             };
 
             let mut verifiers_share =
-                Vec::with_capacity(self.flp.verifier_len() * usize::from(PROOFS));
+                Vec::with_capacity(self.flp.verifier_len() * usize::from(self.flp.cfg.num_proofs));
             for (proof_share, (vf_joint_rand, query_rand)) in
                 proofs_share.chunks(self.flp.proof_len()).zip(
                     corrected_vf_joint_rands
@@ -498,12 +502,12 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Aggregator<16, 16> for Pine<F
             }
         }
 
-        let wr_joint_rand_seed = Self::joint_rand_seed(
+        let wr_joint_rand_seed = self.joint_rand_seed(
             &[wr_joint_rand_part_0, wr_joint_rand_part_1],
             USAGE_WR_JOINT_RAND_SEED,
         );
 
-        let vf_joint_rand_seed = Self::joint_rand_seed(
+        let vf_joint_rand_seed = self.joint_rand_seed(
             &[vf_joint_rand_part_0, vf_joint_rand_part_1],
             USAGE_VF_JOINT_RAND_SEED,
         );
@@ -550,7 +554,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Aggregator<16, 16> for Pine<F
 
 // Collector
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> Collector for Pine<F, PROOFS> {
+impl<F: FftFriendlyFieldElement> Collector for Pine<F> {
     fn unshard<M: IntoIterator<Item = PineVec<F>>>(
         &self,
         _agg_param: &(),
@@ -590,14 +594,14 @@ mod tests {
         },
     };
 
-    use crate::pine::{msg, vdaf::PineVec, Pine128, Pine64};
+    use crate::pine::{msg, vdaf::PineVec, Pine};
 
     use assert_matches::assert_matches;
 
     #[test]
     fn run_128() {
         let dimension = 100;
-        let pine = Pine128::new(1000.0, dimension, 15, 4).unwrap();
+        let pine = Pine::new_128(1000.0, dimension, 15, 4).unwrap();
         let result = run_vdaf(
             &pine,
             &(),
@@ -616,7 +620,7 @@ mod tests {
     #[test]
     fn run_64() {
         let dimension = 100;
-        let pine = Pine64::new(1000.0, dimension, 15, 100).unwrap();
+        let pine = Pine::new_64(1000.0, dimension, 15, 100).unwrap();
         let result = run_vdaf(
             &pine,
             &(),
@@ -637,7 +641,7 @@ mod tests {
     fn aggregate() {
         let dimension = 100;
         let reports = 5;
-        let pine = Pine128::new(1000.0, dimension, 15, 100).unwrap();
+        let pine = Pine::new_128(1000.0, dimension, 15, 100).unwrap();
 
         let mut out_shares_0 = Vec::new();
         let mut out_shares_1 = Vec::new();
@@ -670,7 +674,7 @@ mod tests {
     #[test]
     fn prep_failure_mutated_pub_share_wr_joint_rand() {
         let dimension = 100;
-        let pine = Pine128::new(1000.0, dimension, 15, 4).unwrap();
+        let pine = Pine::new_128(1000.0, dimension, 15, 4).unwrap();
 
         let nonce = [0; 16];
         let (mut public_share, input_shares) = pine.shard(&vec![1.0; dimension], &nonce).unwrap();
@@ -689,7 +693,7 @@ mod tests {
     #[test]
     fn prep_failure_mutated_pub_share_vf_joint_rand() {
         let dimension = 100;
-        let pine = Pine128::new(1000.0, dimension, 15, 4).unwrap();
+        let pine = Pine::new_128(1000.0, dimension, 15, 4).unwrap();
 
         let nonce = [0; 16];
         let (mut public_share, input_shares) = pine.shard(&vec![1.0; dimension], &nonce).unwrap();
@@ -708,7 +712,7 @@ mod tests {
     #[test]
     fn prep_failure_mutated_input_share_proof() {
         let dimension = 100;
-        let pine = Pine128::new(1000.0, dimension, 15, 4).unwrap();
+        let pine = Pine::new_128(1000.0, dimension, 15, 4).unwrap();
 
         let nonce = [0; 16];
         let (public_share, mut input_shares) = pine.shard(&vec![1.0; dimension], &nonce).unwrap();
@@ -727,7 +731,7 @@ mod tests {
     #[test]
     fn prep_failure_mutated_input_share_meas() {
         let dimension = 100;
-        let pine = Pine128::new(1000.0, dimension, 15, 4).unwrap();
+        let pine = Pine::new_128(1000.0, dimension, 15, 4).unwrap();
 
         let nonce = [0; 16];
         let (public_share, mut input_shares) = pine.shard(&vec![1.0; dimension], &nonce).unwrap();


Test patch:
diff --git a/crates/daphne/src/pine/test_vec/mod.rs b/crates/daphne/src/pine/test_vec/mod.rs
index dbbd38b..b142c25 100644
--- a/crates/daphne/src/pine/test_vec/mod.rs
+++ b/crates/daphne/src/pine/test_vec/mod.rs
@@ -53,7 +53,7 @@ struct TestVec {
     agg_result: Vec<f64>,
 }
 
-impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
+impl<F: FftFriendlyFieldElement> Pine<F> {
     fn run_test_vec(&self, test_vec: &TestVec) {
         // Check that the test vector parameters have the values we expect.
         //
@@ -65,7 +65,7 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
         assert_eq!(test_vec.num_wr_checks, NUM_WR_TESTS);
         assert_eq!(test_vec.num_wr_successes, NUM_WR_SUCCESSES);
         assert_eq!(test_vec.shares, 2);
-        assert_eq!(test_vec.proofs, usize::from(PROOFS));
+        assert_eq!(test_vec.proofs, usize::from(self.flp.cfg.num_proofs));
 
         let mut out_shares_0 = Vec::new();
         let mut out_shares_1 = Vec::new();
@@ -176,13 +176,13 @@ impl<F: FftFriendlyFieldElement, const PROOFS: u8> Pine<F, PROOFS> {
 mod tests {
     use super::*;
 
-    use crate::pine::{Pine128, Pine64};
+    use crate::pine::Pine;
 
     #[test]
     fn run_64() {
         let test_vec =
             serde_json::from_str::<TestVec>(include_str!("00/Pine_Field64.json")).unwrap();
-        Pine64::new(
+        Pine::new_64(
             test_vec.l2_norm_bound,
             test_vec.dimension,
             test_vec.num_frac_bits,
@@ -196,7 +196,7 @@ mod tests {
     fn run_128() {
         let test_vec =
             serde_json::from_str::<TestVec>(include_str!("00/Pine_Field128.json")).unwrap();
-        Pine128::new(
+        Pine::new_128(
             test_vec.l2_norm_bound,
             test_vec.dimension,
             test_vec.num_frac_bits,