diff --git a/src/vdaf.rs b/src/vdaf.rs index 8dc8b2fe..2c68f2e4 100644 --- a/src/vdaf.rs +++ b/src/vdaf.rs @@ -298,6 +298,11 @@ pub trait Aggregator: Vda agg_param: &Self::AggregationParam, output_shares: M, ) -> Result; + + /// Validates an aggregation parameter with respect to all previous aggregaiton parameters used + /// for the same input share. `prev` MUST be sorted from least to most recently used. + #[must_use] + fn is_agg_param_valid(cur: &Self::AggregationParam, prev: &[Self::AggregationParam]) -> bool; } /// Aggregator that implements differential privacy with Aggregator-side noise addition. diff --git a/src/vdaf/dummy.rs b/src/vdaf/dummy.rs index 6903cd54..5b969bc1 100644 --- a/src/vdaf/dummy.rs +++ b/src/vdaf/dummy.rs @@ -166,6 +166,10 @@ impl vdaf::Aggregator<0, 16> for Vdaf { } Ok(aggregate_share) } + + fn is_agg_param_valid(_cur: &Self::AggregationParam, _prev: &[Self::AggregationParam]) -> bool { + true + } } impl vdaf::Client<16> for Vdaf { diff --git a/src/vdaf/poplar1.rs b/src/vdaf/poplar1.rs index 514ed390..1d396e4d 100644 --- a/src/vdaf/poplar1.rs +++ b/src/vdaf/poplar1.rs @@ -17,6 +17,7 @@ use crate::{ use bitvec::{prelude::Lsb0, vec::BitVec}; use rand_core::RngCore; use std::{ + collections::BTreeSet, convert::TryFrom, fmt::Debug, io::{Cursor, Read}, @@ -1245,6 +1246,39 @@ impl, const SEED_SIZE: usize> Aggregator output_shares, ) } + + /// Validates that no aggregation parameter with the same level as `cur` has been used with the + /// same input share before. `prev` contains the aggregation parameters used for the same input. + /// `prev` MUST be sorted from least to most recently used. + fn is_agg_param_valid(cur: &Poplar1AggregationParam, prev: &[Poplar1AggregationParam]) -> bool { + // Exit early if there are no previous aggregation params to compare to, i.e., this is the + // first time the input share has been processed + if prev.is_empty() { + return true; + } + + // Unpack this agg param and the last one in the list + let Poplar1AggregationParam { + level: cur_level, + prefixes: cur_prefixes, + } = cur; + let Poplar1AggregationParam { + level: last_level, + prefixes: last_prefixes, + } = prev.last().as_ref().unwrap(); + let last_prefixes_set = BTreeSet::from_iter(last_prefixes); + + // Check that the level increased. + if cur_level <= last_level { + return false; + } + + // Check that current prefixes are extensions of the last level's prefixes. + cur_prefixes.iter().all(|cur_prefix| { + let last_prefix = cur_prefix.prefix(*last_level as usize); + last_prefixes_set.contains(&last_prefix) + }) + } } impl, const SEED_SIZE: usize> Collector for Poplar1 { @@ -1979,6 +2013,57 @@ mod tests { assert_matches!(err, CodecError::Other(_)); } + // Tests Poplar1::is_valid() functionality. This unit test is translated from + // https://github.com/cfrg/draft-irtf-cfrg-vdaf/blob/a4874547794818573acd8734874c9784043b1140/poc/tests/test_vdaf_poplar1.py#L187 + #[test] + fn agg_param_validity() { + // The actual Poplar instance doesn't matter for the parameter validity tests + type V = Poplar1; + + // Helper function for making aggregation params + fn make_agg_param(bitstrings: &[&[u8]]) -> Result { + Poplar1AggregationParam::try_from_prefixes( + bitstrings + .iter() + .map(|v| { + let bools = v.iter().map(|&b| b != 0).collect::>(); + IdpfInput::from_bools(&bools) + }) + .collect(), + ) + } + + // Test `is_valid` returns False on repeated levels, and True otherwise. + let agg_params = [ + make_agg_param(&[&[0], &[1]]).unwrap(), + make_agg_param(&[&[0, 0]]).unwrap(), + make_agg_param(&[&[0, 0], &[1, 0]]).unwrap(), + ]; + assert!(V::is_agg_param_valid(&agg_params[0], &[])); + assert!(V::is_agg_param_valid(&agg_params[1], &agg_params[..1])); + assert!(!V::is_agg_param_valid(&agg_params[2], &agg_params[..2])); + + // Test `is_valid` accepts level jumps. + let agg_params = [ + make_agg_param(&[&[0], &[1]]).unwrap(), + make_agg_param(&[&[0, 1, 0], &[0, 1, 1], &[1, 0, 1], &[1, 1, 1]]).unwrap(), + ]; + assert!(V::is_agg_param_valid(&agg_params[1], &agg_params[..1])); + + // Test `is_valid` rejects unconnected prefixes. + let agg_params = [ + make_agg_param(&[&[0]]).unwrap(), + make_agg_param(&[&[0, 1, 0], &[0, 1, 1], &[1, 0, 1], &[1, 1, 1]]).unwrap(), + ]; + assert!(!V::is_agg_param_valid(&agg_params[1], &agg_params[..1])); + + // Test that the `Poplar1AggregationParam` constructor rejects unsorted and duplicate + // prefixes. + assert!(make_agg_param(&[&[1], &[0]]).is_err()); + assert!(make_agg_param(&[&[1, 0, 0], &[0, 1, 1]]).is_err()); + assert!(make_agg_param(&[&[0, 0, 0], &[0, 1, 0], &[0, 1, 0]]).is_err()); + } + #[derive(Debug, Deserialize)] struct HexEncoded(#[serde(with = "hex")] Vec); diff --git a/src/vdaf/prio2.rs b/src/vdaf/prio2.rs index ba725d90..680f09ea 100644 --- a/src/vdaf/prio2.rs +++ b/src/vdaf/prio2.rs @@ -325,6 +325,11 @@ impl Aggregator<32, 16> for Prio2 { Ok(agg_share) } + + /// Returns `true` iff `prev.is_empty()` + fn is_agg_param_valid(_cur: &Self::AggregationParam, prev: &[Self::AggregationParam]) -> bool { + prev.is_empty() + } } impl Collector for Prio2 { diff --git a/src/vdaf/prio3.rs b/src/vdaf/prio3.rs index f6e42d97..f0d482de 100644 --- a/src/vdaf/prio3.rs +++ b/src/vdaf/prio3.rs @@ -1441,6 +1441,11 @@ where Ok(agg_share) } + + /// Returns `true` iff `prev.is_empty()` + fn is_agg_param_valid(_cur: &Self::AggregationParam, prev: &[Self::AggregationParam]) -> bool { + prev.is_empty() + } } #[cfg(feature = "experimental")]