diff --git a/ipa-core/build.rs b/ipa-core/build.rs index 2cfd8252b..ed45e74f2 100644 --- a/ipa-core/build.rs +++ b/ipa-core/build.rs @@ -21,6 +21,7 @@ track_steps!( prf_sharding::step, shuffle::step, aggregation::step, + oprf_padding::step, step, }, dp::step, diff --git a/ipa-core/src/error.rs b/ipa-core/src/error.rs index 6a2eb5eae..fd1e0b63e 100644 --- a/ipa-core/src/error.rs +++ b/ipa-core/src/error.rs @@ -82,10 +82,14 @@ pub enum Error { ParallelDZKPValidationFailed, #[error("Inconsistent shares")] InconsistentShares, + #[error("Inconsistent padding")] + InconsistentPadding, #[error("The Masks cannot be set safely, i.e. without deleting non-zero field elements")] DZKPMasks, #[error("Attempt to operate on zero records")] ZeroRecords, + #[error("DP related error: {0}")] + DPPaddingError(#[from] crate::protocol::ipa_prf::oprf_padding::insecure::DpError), #[error("Epsilon submitted to query is out of bounds")] EpsilonOutOfBounds, #[error("Missing total records in {0}")] diff --git a/ipa-core/src/helpers/mod.rs b/ipa-core/src/helpers/mod.rs index 819de8c4d..e33a2ec99 100644 --- a/ipa-core/src/helpers/mod.rs +++ b/ipa-core/src/helpers/mod.rs @@ -303,6 +303,18 @@ impl Role { pub const fn eq(self, other: Self) -> bool { matches!((self, other), (H1, H1) | (H2, H2) | (H3, H3)) } + + /// Returns the direction to the peer with the specified role. + /// + /// If `self == role`, returns `None`. + #[must_use] + pub const fn direction_to(&self, role: Role) -> Option { + match (self, role) { + (H1, H2) | (H2, H3) | (H3, H1) => Some(Direction::Right), + (H1, H3) | (H2, H1) | (H3, H2) => Some(Direction::Left), + (H1, H1) | (H2, H2) | (H3, H3) => None, + } + } } impl From for &'static str { diff --git a/ipa-core/src/protocol/context/prss.rs b/ipa-core/src/protocol/context/prss.rs index 9aeae6d04..d06564234 100644 --- a/ipa-core/src/protocol/context/prss.rs +++ b/ipa-core/src/protocol/context/prss.rs @@ -1,7 +1,7 @@ //! Metric-aware PRSS decorators use generic_array::{ArrayLength, GenericArray}; -use rand_core::{Error, RngCore}; +use rand_core::{CryptoRng, Error, RngCore}; use crate::{ helpers::{Direction, Role}, @@ -145,3 +145,5 @@ impl RngCore for InstrumentedSequentialSharedRandomness<'_> { self.inner.try_fill_bytes(dest) } } + +impl CryptoRng for InstrumentedSequentialSharedRandomness<'_> {} diff --git a/ipa-core/src/protocol/ipa_prf/aggregation/breakdown_reveal.rs b/ipa-core/src/protocol/ipa_prf/aggregation/breakdown_reveal.rs index 405b1fdbd..b08a3ece9 100644 --- a/ipa-core/src/protocol/ipa_prf/aggregation/breakdown_reveal.rs +++ b/ipa-core/src/protocol/ipa_prf/aggregation/breakdown_reveal.rs @@ -19,8 +19,11 @@ use crate::{ basics::semi_honest_reveal, context::Context, ipa_prf::{ - aggregation::step::AggregationStep, prf_sharding::SecretSharedAttributionOutputs, - shuffle::shuffle_attribution_outputs, BreakdownKey, + aggregation::step::AggregationStep, + oprf_padding::{apply_dp_padding, PaddingParameters}, + prf_sharding::{AttributionOutputs, SecretSharedAttributionOutputs}, + shuffle::shuffle_attribution_outputs, + BreakdownKey, }, BooleanProtocols, RecordId, }, @@ -59,8 +62,18 @@ where BitDecomposed>: for<'a> TransposeFrom<&'a [Replicated; B], Error = Infallible>, { - let atributions = shuffle_attributions(&ctx, attributed_values).await?; - let grouped_tvs = reveal_breakdowns(&ctx, atributions).await?; + let dp_padding_params = PaddingParameters::default(); + // Apply DP padding for Breakdown Reveal Aggregation + let attributed_values_padded = + apply_dp_padding::<_, AttributionOutputs, Replicated>, B>( + ctx.narrow(&AggregationStep::PaddingDp), + attributed_values, + dp_padding_params, + ) + .await?; + + let attributions = shuffle_attributions(&ctx, attributed_values_padded).await?; + let grouped_tvs = reveal_breakdowns(&ctx, attributions).await?; let num_rows = grouped_tvs.max_len; aggregate_values::<_, HV, B>(ctx, grouped_tvs.into_stream(), num_rows).await } diff --git a/ipa-core/src/protocol/ipa_prf/aggregation/step.rs b/ipa-core/src/protocol/ipa_prf/aggregation/step.rs index 845a2d49e..6d1776d54 100644 --- a/ipa-core/src/protocol/ipa_prf/aggregation/step.rs +++ b/ipa-core/src/protocol/ipa_prf/aggregation/step.rs @@ -5,6 +5,8 @@ pub(crate) enum AggregationStep { /// key. Aggregation based on move to bucket approach does not need them. /// When reveal-based aggregation is the default, other steps (such as `MoveToBucket`) /// should be deleted + #[step(child = crate::protocol::ipa_prf::oprf_padding::step::PaddingDpStep, name="padding_dp")] + PaddingDp, #[step(child = crate::protocol::ipa_prf::shuffle::step::OPRFShuffleStep)] Shuffle, RevealStep, diff --git a/ipa-core/src/protocol/ipa_prf/mod.rs b/ipa-core/src/protocol/ipa_prf/mod.rs index c046478e2..1aa585535 100644 --- a/ipa-core/src/protocol/ipa_prf/mod.rs +++ b/ipa-core/src/protocol/ipa_prf/mod.rs @@ -25,6 +25,7 @@ use crate::{ }, ipa_prf::{ boolean_ops::convert_to_fp25519, + oprf_padding::apply_dp_padding, prf_eval::{eval_dy_prf, gen_prf_key}, prf_sharding::{ attribute_cap_aggregate, histograms_ranges_sortkeys, PrfShardedIpaInputRow, @@ -91,7 +92,9 @@ use step::IpaPrfStep as Step; use crate::{ helpers::query::DpMechanism, - protocol::{context::Validator, dp::dp_for_histogram}, + protocol::{ + context::Validator, dp::dp_for_histogram, ipa_prf::oprf_padding::PaddingParameters, + }, }; #[derive(Clone, Debug, Default)] @@ -218,6 +221,7 @@ pub async fn oprf_ipa<'ctx, BK, TV, HV, TS, const SS_BITS: usize, const B: usize input_rows: Vec>, attribution_window_seconds: Option, dp_params: DpMechanism, + dp_padding_params: PaddingParameters, ) -> Result>, Error> where BK: BreakdownKey, @@ -247,7 +251,16 @@ where if input_rows.is_empty() { return Ok(vec![Replicated::ZERO; B]); } - let shuffled = shuffle_inputs(ctx.narrow(&Step::Shuffle), input_rows).await?; + + // Apply DP padding for OPRF + let padded_input_rows = apply_dp_padding::<_, OPRFIPAInputRow, B>( + ctx.narrow(&Step::PaddingDp), + input_rows, + dp_padding_params, + ) + .await?; + + let shuffled = shuffle_inputs(ctx.narrow(&Step::Shuffle), padded_input_rows).await?; let mut prfd_inputs = compute_prf_for_inputs(ctx.clone(), &shuffled).await?; prfd_inputs.sort_by(|a, b| a.prf_of_match_key.cmp(&b.prf_of_match_key)); @@ -376,7 +389,10 @@ pub mod tests { U128Conversions, }, helpers::query::DpMechanism, - protocol::{dp::NoiseParams, ipa_prf::oprf_ipa}, + protocol::{ + dp::NoiseParams, + ipa_prf::{oprf_ipa, oprf_padding::PaddingParameters}, + }, test_executor::run, test_fixture::{ipa::TestRawDataRecord, Reconstruct, Runner, TestWorld}, }; @@ -410,14 +426,22 @@ pub mod tests { test_input(10, 12345, true, 0, 5), test_input(0, 68362, false, 1, 0), test_input(20, 68362, true, 0, 2), - ]; + ]; // trigger value of 2 attributes to earlier source row with breakdown 1 and trigger + // value of 5 attributes to source row with breakdown 2. let dp_params = DpMechanism::NoDp; + let padding_params = PaddingParameters::relaxed(); let mut result: Vec<_> = world .semi_honest(records.into_iter(), |ctx, input_rows| async move { - oprf_ipa::(ctx, input_rows, None, dp_params) - .await - .unwrap() + oprf_ipa::( + ctx, + input_rows, + None, + dp_params, + padding_params, + ) + .await + .unwrap() }) .await .reconstruct(); @@ -432,6 +456,8 @@ pub mod tests { #[test] fn semi_honest_with_dp() { const SS_BITS: usize = 1; + // setting SS_BITS this small will cause clipping in capping + // since per_user_credit_cap == 2^SS_BITS semi_honest_with_dp_internal::(); } #[test] @@ -451,6 +477,7 @@ pub mod tests { let epsilon = 10.0; let dp_params = DpMechanism::Binomial { epsilon }; let per_user_credit_cap = 2_f64.powi(i32::try_from(SS_BITS).unwrap()); + let padding_params = PaddingParameters::relaxed(); let world = TestWorld::default(); let records: Vec = vec![ @@ -462,9 +489,15 @@ pub mod tests { ]; let mut result: Vec<_> = world .semi_honest(records.into_iter(), |ctx, input_rows| async move { - oprf_ipa::(ctx, input_rows, None, dp_params) - .await - .unwrap() + oprf_ipa::( + ctx, + input_rows, + None, + dp_params, + padding_params, + ) + .await + .unwrap() }) .await .reconstruct(); @@ -513,12 +546,19 @@ pub mod tests { let records: Vec = vec![]; let dp_params = DpMechanism::NoDp; + let padding_params = PaddingParameters::no_padding(); let mut result: Vec<_> = world .semi_honest(records.into_iter(), |ctx, input_rows| async move { - oprf_ipa::(ctx, input_rows, None, dp_params) - .await - .unwrap() + oprf_ipa::( + ctx, + input_rows, + None, + dp_params, + padding_params, + ) + .await + .unwrap() }) .await .reconstruct(); @@ -542,12 +582,19 @@ pub mod tests { test_input(0, 68362, false, 1, 0), ]; let dp_params = DpMechanism::NoDp; + let padding_params = PaddingParameters::no_padding(); let mut result: Vec<_> = world .semi_honest(records.into_iter(), |ctx, input_rows| async move { - oprf_ipa::(ctx, input_rows, None, dp_params) - .await - .unwrap() + oprf_ipa::( + ctx, + input_rows, + None, + dp_params, + padding_params, + ) + .await + .unwrap() }) .await .reconstruct(); @@ -590,11 +637,18 @@ pub mod tests { records.shuffle(&mut thread_rng()); let dp_params = DpMechanism::NoDp; + let padding_params = PaddingParameters::no_padding(); let mut result: Vec<_> = world .semi_honest(records.into_iter(), |ctx, input_rows| async move { - oprf_ipa::(ctx, input_rows, None, dp_params) - .await - .unwrap() + oprf_ipa::( + ctx, + input_rows, + None, + dp_params, + padding_params, + ) + .await + .unwrap() }) .await .reconstruct(); diff --git a/ipa-core/src/protocol/ipa_prf/oprf_padding/README.md b/ipa-core/src/protocol/ipa_prf/oprf_padding/README.md index dfa680e97..1106bca88 100644 --- a/ipa-core/src/protocol/ipa_prf/oprf_padding/README.md +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/README.md @@ -44,7 +44,9 @@ The process of drawing a sample from a Truncated Double Geometric will be done b 4. We will use rejection sampleing from a double geometric to sample from a truncated double geometric. ### Sampling from the Geometric Distribuiton -We take the Geometric Distribution to be the probability distribution of the number of failures of Bernoulli trials before the first success, supported on the set $\{0,1,2,...\}$, with $0 < p \leq 1$ the success probability of the Bernoulli trials. +We take the Geometric Distribution to be the probability distribution of the number of failures of Bernoulli trials before the first success, supported on the set $\{0,1,2,...\}$, with $0 < p \leq 1$ the success probability of the Bernoulli trials. + +The mean of the geometric is $\mu = \frac{1-p}{p}$ and variance is $\sigma^2 = \frac{1-p}{p^2}$. ### Sampling from the Double Geometric Distribution We use the following from this [book](https://www.researchgate.net/publication/258697410_The_Laplace_Distribution_and_Generalizations) page 159. @@ -56,7 +58,27 @@ $Y=\theta + X_1 - X_2$ where $X_1$ and $X_2$ are iid geometric variables with success probability $p = 1 - e^{-1/s}$. We use this relation to sample from the double geometric by first drawing two independent samples from $X_1$ and $X_2$ and then computing their difference plus the shift by $\theta$. - +The variance of a double geometric is the sum of the variances of the two independent geometrics, $X_1$ and $X_2$, so is $2 * (\frac{1-p}{p^2})$ ### Samples from the Truncated Double Geometric Distribution Once we can draw samples from a double geometric, we can sample from our desired truncated double geometric by sampling the double geometric with rejection if the sample lies outside the support set $\{0,...,2n\}$. + +The variance of a truncated double geometric distribution is (TODO), but the variance is always less than the variance of the underlying (non-truncated) double geometric distribution. + +# Padding Breakdowns Keys for Reveal Based Aggregation +A new aggregation protocol reveals the breakdown keys in the clear before aggregating the associated secret +shared values. This leaks the number of records for each breakdown key. We can assume that there is a cap +enforced on the number of records for any one matchkey in IPA. Using this sensitivity we can then (with a desired epsilon, +delta) generate a random padding number of dummy rows with each breakdown key. + +# Generating Padding for Matchkeys and Breakdown keys together +1. Would be to try and add the fake breakdown keys to the fake rows already being generated for fake matchkeys. But this +approach has a couple challenges: + 1. We shouldn't add any fake breakdown keys to fake matchkey rows when the matchkey is being added with cardinality + equal to one. Because these rows can be dropped after matching and never have the fake breakdowns revealed. + 2. There may need to be some adjustment made to the DP parameters achieved. + 3. We should not be adding fake breakdown keys to matchkeys that have a cardinality larger than the cap we have established + for the number of breakdowns per user. Otherwise, those breakdown keys would never be revealed as they will be dropped. +2. The second approach we could consider is to add the fake rows for matchkey padding at the start of the protocol and then later +right before Breakdown Reveal Aggregation add the fake rows for breakdown key padding. This approach has the benefit of being more +efficient in that we do not need to compute the OPRF of these fake rows which are added just-in-time for use in aggregation. diff --git a/ipa-core/src/protocol/ipa_prf/oprf_padding/distributions.rs b/ipa-core/src/protocol/ipa_prf/oprf_padding/distributions.rs index be43897b1..efb767830 100644 --- a/ipa-core/src/protocol/ipa_prf/oprf_padding/distributions.rs +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/distributions.rs @@ -139,7 +139,7 @@ impl Distribution for DoubleGeometric { /// Truncated Double Geometric distribution. #[derive(Debug, PartialEq)] pub struct TruncatedDoubleGeometric { - shift_doubled: u32, // move 2 * shift to constructor instead of sample + pub shift_doubled: u32, // move 2 * shift to constructor instead of sample double_geometric: DoubleGeometric, } diff --git a/ipa-core/src/protocol/ipa_prf/oprf_padding/insecure.rs b/ipa-core/src/protocol/ipa_prf/oprf_padding/insecure.rs index 3a0f56cb9..2a353b918 100644 --- a/ipa-core/src/protocol/ipa_prf/oprf_padding/insecure.rs +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/insecure.rs @@ -9,6 +9,8 @@ use crate::protocol::ipa_prf::oprf_padding::distributions::{ BoxMuller, RoundedBoxMuller, TruncatedDoubleGeometric, }; +pub type DpError = Error; + #[derive(Debug, PartialEq, thiserror::Error)] pub enum Error { #[error("Epsilon value must be greater than {}, got {0}", f64::MIN_POSITIVE)] @@ -176,7 +178,7 @@ fn right_hand_side(n: u32, big_delta: u32, epsilon: f64) -> f64 { fn find_smallest_n(big_delta: u32, epsilon: f64, small_delta: f64) -> u32 { // for a fixed set of DP parameters, finds the smallest n that satisfies equation (11) // of https://arxiv.org/pdf/2110.08177.pdf. This gives the narrowest TruncatedDoubleGeometric - // that will satisify the disired DP parameters. + // that will satisfy the desired DP parameters. for n in big_delta.. { if small_delta >= right_hand_side(n, big_delta, epsilon) { return n; @@ -187,6 +189,8 @@ fn find_smallest_n(big_delta: u32, epsilon: f64, small_delta: f64) -> u32 { impl OPRFPaddingDp { // See dp/README.md + /// # Errors + /// will return errors if invalid DP parameters are provided. pub fn new(new_epsilon: f64, new_delta: f64, new_sensitivity: u32) -> Result { // make sure delta and epsilon are in range, i.e. >min and delta<1-min if new_epsilon < f64::MIN_POSITIVE { @@ -200,7 +204,7 @@ impl OPRFPaddingDp { return Err(Error::BadSensitivity(new_sensitivity)); } - // compute smallest shift needed to achieve this delta + // compute the smallest shift needed to achieve this delta let smallest_n = find_smallest_n(new_sensitivity, new_epsilon, new_delta); Ok(Self { @@ -218,10 +222,24 @@ impl OPRFPaddingDp { pub fn sample(&self, rng: &mut R) -> u32 { self.truncated_double_geometric.sample(rng) } + + /// Returns the mean and an upper bound on the standard deviation of the `OPRFPaddingDp` distribution + /// The upper bound is valid if the standard deviation is greater than 1. + /// see `oprf_padding/README.md` + #[must_use] + pub fn mean_and_std_bound(&self) -> (f64, f64) { + let mean = f64::from(self.truncated_double_geometric.shift_doubled) / 2.0; + let s = 1.0 / self.epsilon; + let p = 1.0 - E.powf(-1.0 / s); + let std_bound = (2.0 * (1.0 - p) / pow_u32(p, 2)).sqrt(); + (mean, std_bound) + } } #[cfg(all(test, unit_test))] mod test { + use std::collections::BTreeMap; + use proptest::{prelude::ProptestConfig, proptest}; use rand::{rngs::StdRng, thread_rng, Rng}; use rand_core::SeedableRng; @@ -408,11 +426,21 @@ mod test { } #[test] fn test_oprf_padding_dp() { - let oprf_padding = OPRFPaddingDp::new(1.0, 1e-6, 10); + let oprf_padding = OPRFPaddingDp::new(1.0, 1e-6, 10).unwrap(); let mut rng = rand::thread_rng(); - oprf_padding.unwrap().sample(&mut rng); + let num_samples = 1000; + let mut count_sample_values: BTreeMap = BTreeMap::new(); + + for _ in 0..num_samples { + let sample = oprf_padding.sample(&mut rng); + let sample_count = count_sample_values.entry(sample).or_insert(0); + *sample_count += 1; + } + for (sample, count) in &count_sample_values { + println!("A sample value equal to {sample} occurred {count} time(s)",); + } } fn test_oprf_padding_dp_constructor() { let mut actual = OPRFPaddingDp::new(-1.0, 1e-6, 10); // (epsilon, delta, sensitivity) diff --git a/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs b/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs index 304714b5f..6b5cef31b 100644 --- a/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs @@ -1,5 +1,790 @@ mod distributions; -mod insecure; +pub mod insecure; +pub mod step; #[cfg(any(test, feature = "test-fixture", feature = "cli"))] pub use insecure::DiscreteDp as InsecureDiscreteDp; +use rand::Rng; +use tokio::try_join; + +use crate::{ + error, + error::Error, + ff::{ + boolean::Boolean, + boolean_array::{BooleanArray, BA32, BA64}, + U128Conversions, + }, + helpers::{Direction, Role, TotalRecords}, + protocol::{ + context::{prss::InstrumentedSequentialSharedRandomness, Context}, + ipa_prf::{ + oprf_padding::{ + insecure::OPRFPaddingDp, + step::{PaddingDpStep, SendTotalRows}, + }, + prf_sharding::AttributionOutputs, + OPRFIPAInputRow, + }, + RecordId, + }, + secret_sharing::{ + replicated::{semi_honest::AdditiveShare, ReplicatedSecretSharing}, + SharedValue, + }, +}; + +/// Parameter struct for padding parameters. +#[derive(Default, Copy, Clone, Debug)] +pub struct PaddingParameters { + pub aggregation_padding: AggregationPadding, + pub oprf_padding: OPRFPadding, +} + +#[derive(Copy, Clone, Debug)] +pub enum AggregationPadding { + NoAggPadding, + Parameters { + aggregation_epsilon: f64, + aggregation_delta: f64, + aggregation_padding_sensitivity: u32, + }, +} + +#[derive(Copy, Clone, Debug)] +pub enum OPRFPadding { + NoOPRFPadding, + Parameters { + oprf_epsilon: f64, + oprf_delta: f64, + matchkey_cardinality_cap: u32, + oprf_padding_sensitivity: u32, + }, +} + +impl Default for AggregationPadding { + fn default() -> Self { + AggregationPadding::Parameters { + aggregation_epsilon: 5.0, + aggregation_delta: 1e-6, + aggregation_padding_sensitivity: 10, // for IPA is most natural to set + // equal to the matchkey_cardinality_cap + } + } +} + +impl Default for OPRFPadding { + fn default() -> Self { + OPRFPadding::Parameters { + oprf_epsilon: 5.0, + oprf_delta: 1e-6, + matchkey_cardinality_cap: 10, + oprf_padding_sensitivity: 2, // should remain 2 + } + } +} + +impl PaddingParameters { + #[must_use] + pub fn relaxed() -> Self { + PaddingParameters { + aggregation_padding: AggregationPadding::Parameters { + aggregation_epsilon: 10.0, + aggregation_delta: 1e-4, + aggregation_padding_sensitivity: 3, + }, + oprf_padding: OPRFPadding::Parameters { + oprf_epsilon: 10.0, + oprf_delta: 1e-4, + matchkey_cardinality_cap: 3, + oprf_padding_sensitivity: 2, + }, + } + } + + #[must_use] + pub fn no_padding() -> Self { + PaddingParameters { + aggregation_padding: AggregationPadding::NoAggPadding, + oprf_padding: OPRFPadding::NoOPRFPadding, + } + } +} + +/// Paddable trait to support generation of padding for both `OPRFIPAInputRow`s and `AttributionOutputs` +/// while reusing the code common to both. +pub trait Paddable { + /// # Errors + /// may propagate errors from `OPRFPaddingDp` distribution setup + fn add_padding_items, const B: usize>( + direction_to_excluded_helper: Direction, + padding_input_rows: &mut V, + padding_params: &PaddingParameters, + rng: &mut InstrumentedSequentialSharedRandomness, + ) -> Result + where + Self: Sized; + + fn add_zero_shares>(padding_input_rows: &mut V, total_number_of_fake_rows: u32) + where + Self: Sized; +} + +impl Paddable for OPRFIPAInputRow +where + BK: BooleanArray + U128Conversions, + TV: BooleanArray, + TS: BooleanArray, +{ + fn add_padding_items, const B: usize>( + direction_to_excluded_helper: Direction, + padding_input_rows: &mut V, + padding_params: &PaddingParameters, + rng: &mut InstrumentedSequentialSharedRandomness, + ) -> Result { + let mut total_number_of_fake_rows = 0; + match padding_params.oprf_padding { + OPRFPadding::NoOPRFPadding => {} + OPRFPadding::Parameters { + oprf_epsilon, + oprf_delta, + matchkey_cardinality_cap, + oprf_padding_sensitivity, + } => { + let oprf_padding = + OPRFPaddingDp::new(oprf_epsilon, oprf_delta, oprf_padding_sensitivity)?; + for cardinality in 1..=matchkey_cardinality_cap { + let sample = oprf_padding.sample(rng); + total_number_of_fake_rows += sample * cardinality; + + // this means there will be `sample` many unique + // matchkeys to add each with cardinality = `cardinality` + for _ in 0..sample { + let dummy_mk: BA64 = rng.gen(); + for _ in 0..cardinality { + let match_key_shares = match direction_to_excluded_helper { + Direction::Left => AdditiveShare::new(BA64::ZERO, dummy_mk), + Direction::Right => AdditiveShare::new(dummy_mk, BA64::ZERO), + }; + let row = OPRFIPAInputRow { + match_key: match_key_shares, + is_trigger: AdditiveShare::new(Boolean::FALSE, Boolean::FALSE), + breakdown_key: AdditiveShare::new(BK::ZERO, BK::ZERO), + trigger_value: AdditiveShare::new(TV::ZERO, TV::ZERO), + timestamp: AdditiveShare::new(TS::ZERO, TS::ZERO), + }; + padding_input_rows.extend(std::iter::once(row)); + } + } + } + } + } + Ok(total_number_of_fake_rows) + } + + fn add_zero_shares>( + padding_input_rows: &mut V, + total_number_of_fake_rows: u32, + ) { + for _ in 0..total_number_of_fake_rows as usize { + let row = OPRFIPAInputRow { + match_key: AdditiveShare::new(BA64::ZERO, BA64::ZERO), + is_trigger: AdditiveShare::new(Boolean::FALSE, Boolean::FALSE), + breakdown_key: AdditiveShare::new(BK::ZERO, BK::ZERO), + trigger_value: AdditiveShare::new(TV::ZERO, TV::ZERO), + timestamp: AdditiveShare::new(TS::ZERO, TS::ZERO), + }; + + padding_input_rows.extend(std::iter::once(row)); + } + } +} + +impl Paddable for AttributionOutputs, AdditiveShare> +where + BK: BooleanArray + U128Conversions, + TV: BooleanArray, +{ + fn add_padding_items, const B: usize>( + direction_to_excluded_helper: Direction, + padding_input_rows: &mut V, + padding_params: &PaddingParameters, + rng: &mut InstrumentedSequentialSharedRandomness, + ) -> Result { + // padding for aggregation + let mut total_number_of_fake_rows = 0; + match padding_params.aggregation_padding { + AggregationPadding::NoAggPadding => {} + AggregationPadding::Parameters { + aggregation_epsilon, + aggregation_delta, + aggregation_padding_sensitivity, + } => { + let aggregation_padding = OPRFPaddingDp::new( + aggregation_epsilon, + aggregation_delta, + aggregation_padding_sensitivity, + )?; + let num_breakdowns: u32 = u32::try_from(B).unwrap(); + // for every breakdown, sample how many dummies will be added + for breakdownkey in 0..num_breakdowns { + let sample = aggregation_padding.sample(rng); + total_number_of_fake_rows += sample; + + // now add `sample` many fake rows with this `breakdownkey` + for _ in 0..sample { + let breakdownkey_shares = match direction_to_excluded_helper { + Direction::Left => AdditiveShare::new( + BK::ZERO, + BK::truncate_from(u128::from(breakdownkey)), + ), + Direction::Right => AdditiveShare::new( + BK::truncate_from(u128::from(breakdownkey)), + BK::ZERO, + ), + }; + + let row = AttributionOutputs { + attributed_breakdown_key_bits: breakdownkey_shares, + capped_attributed_trigger_value: AdditiveShare::new(TV::ZERO, TV::ZERO), + }; + + padding_input_rows.extend(std::iter::once(row)); + } + } + } + } + Ok(total_number_of_fake_rows) + } + + fn add_zero_shares>( + padding_input_rows: &mut V, + total_number_of_fake_rows: u32, + ) { + for _ in 0..total_number_of_fake_rows as usize { + let row = AttributionOutputs { + attributed_breakdown_key_bits: AdditiveShare::new(BK::ZERO, BK::ZERO), + capped_attributed_trigger_value: AdditiveShare::new(TV::ZERO, TV::ZERO), + }; + + padding_input_rows.extend(std::iter::once(row)); + } + } +} + +/// # Errors +/// Will propagate errors from `apply_dp_padding_pass` +pub async fn apply_dp_padding( + ctx: C, + mut input: Vec, + padding_params: PaddingParameters, +) -> Result, Error> +where + C: Context, + T: Paddable, +{ + let initial_len = input.len(); + + // H1 and H2 add padding noise + input = apply_dp_padding_pass::( + ctx.narrow(&PaddingDpStep::PaddingDpPass1), + input, + Role::H3, + &padding_params, + ) + .await?; + + // H3 and H1 add padding noise + input = apply_dp_padding_pass::( + ctx.narrow(&PaddingDpStep::PaddingDpPass2), + input, + Role::H2, + &padding_params, + ) + .await?; + + // H2 and H3 add padding noise + input = apply_dp_padding_pass::( + ctx.narrow(&PaddingDpStep::PaddingDpPass3), + input, + Role::H1, + &padding_params, + ) + .await?; + + let after_padding_len = input.len(); + tracing::info!( + "Total number of padding records added: {}. Padding Parameters: {:?}", + after_padding_len - initial_len, + padding_params + ); + + Ok(input) +} + +/// Apply dp padding with one pair of helpers generating the noise +/// Steps +/// 1. Helpers `h_i` and `h_i_plus_one` will get the same rng from PRSS +/// and use it to sample the same random noise for padding from `OPRFPaddingDp`. +/// They will generate secret shares of these fake rows. +/// 2. `h_i` and `h_i_plus_one` will send the send `total_number_of_fake_rows` to `excluded_helper` +/// 3. `excluded_helper` will generate secret shares of zero for as many rows as the `total_number_of_fake_rows` +/// +/// # Errors +/// Will propogate errors from `OPRFPaddingDp`. Will return an error if the two helpers adding noise +/// tell the excluded helper to add different numbers of fake rows. +/// # Panics +/// will panic if not able to fit the received value `v` into a `u32` +pub async fn apply_dp_padding_pass( + ctx: C, + mut input: Vec, + excluded_helper: Role, + padding_params: &PaddingParameters, +) -> Result, Error> +where + C: Context, + T: Paddable, +{ + let total_number_of_fake_rows; + let mut padding_input_rows: Vec = Vec::new(); + let send_ctx = ctx + .narrow(&SendTotalRows::SendNumFakeRecords) + .set_total_records(TotalRecords::ONE); + + if let Some(direction_to_excluded_helper) = ctx.role().direction_to(excluded_helper) { + // Step 1: Helpers `h_i` and `h_i_plus_one` will get the same rng from PRSS + // and use it to sample the same random noise for padding from OPRFPaddingDp. + // They will generate secret shares of these fake rows. + let (mut left, mut right) = ctx.prss_rng(); + let rng = match direction_to_excluded_helper { + Direction::Left => &mut right, + Direction::Right => &mut left, + }; + let total_number_of_fake_rows = T::add_padding_items::, B>( + direction_to_excluded_helper, + &mut padding_input_rows, + padding_params, + rng, + )?; + + // Step 2: `h_i` and `h_i_plus_one` will send the send `total_number_of_fake_rows` to the `excluded_helper`. + // The `excluded_helper` will check that both `h_i` and `h_i_plus_one` have sent the same value + // to prevent any malicious behavior. See oprf_padding/README.md for explanation of why revealing + // the total number of fake rows is okay. + let send_channel = + send_ctx.send_channel::(send_ctx.role().peer(direction_to_excluded_helper)); + send_channel + .send( + RecordId::FIRST, + BA32::truncate_from(u128::from(total_number_of_fake_rows)), + ) + .await?; + } else { + // Step 3: `h_out` will first receive the total_number_of_fake rows from the other + // parties and then `h_out` will set its shares to zero for the fake rows + let recv_channel_right = + send_ctx.recv_channel::(send_ctx.role().peer(Direction::Right)); + let recv_channel_left = + send_ctx.recv_channel::(send_ctx.role().peer(Direction::Left)); + let (from_right, from_left) = try_join!( + recv_channel_right.receive(RecordId::FIRST), + recv_channel_left.receive(RecordId::FIRST), + )?; + if from_right != from_left { + return Err::, error::Error>(Error::InconsistentPadding); + } + total_number_of_fake_rows = u32::try_from(from_right.as_u128()).unwrap(); + + T::add_zero_shares(&mut padding_input_rows, total_number_of_fake_rows); + } + + input.extend(padding_input_rows); + Ok(input) +} + +#[cfg(all(test, unit_test))] +mod tests { + use std::collections::{BTreeMap, HashMap}; + + use crate::{ + error::Error, + ff::{ + boolean_array::{BooleanArray, BA20, BA3, BA32, BA8}, + U128Conversions, + }, + helpers::{Direction, Role, TotalRecords}, + protocol::{ + context::Context, + ipa_prf::{ + oprf_padding::{ + apply_dp_padding_pass, insecure, insecure::OPRFPaddingDp, AggregationPadding, + OPRFPadding, PaddingParameters, + }, + prf_sharding::{tests::PreAggregationTestOutputInDecimal, AttributionOutputs}, + OPRFIPAInputRow, + }, + RecordId, + }, + secret_sharing::replicated::semi_honest::AdditiveShare, + test_fixture::{Reconstruct, Runner, TestWorld}, + }; + + pub async fn set_up_apply_dp_padding_pass_for_oprf( + ctx: C, + padding_params: PaddingParameters, + ) -> Result>, Error> + where + C: Context, + BK: BooleanArray + U128Conversions, + TV: BooleanArray, + TS: BooleanArray, + { + let mut input: Vec> = Vec::new(); + input = apply_dp_padding_pass::, B>( + ctx, + input, + Role::H3, + &padding_params, + ) + .await?; + Ok(input) + } + + #[tokio::test] + pub async fn oprf_noise_in_dp_padding_pass() { + type BK = BA8; + type TV = BA3; + type TS = BA20; + const B: usize = 256; + let world = TestWorld::default(); + let oprf_epsilon = 1.0; + let oprf_delta = 1e-6; + let matchkey_cardinality_cap = 10; + let oprf_padding_sensitivity = 2; + + let result = world + .semi_honest((), |ctx, ()| async move { + let padding_params = PaddingParameters { + oprf_padding: OPRFPadding::Parameters { + oprf_epsilon, + oprf_delta, + matchkey_cardinality_cap, + oprf_padding_sensitivity, + }, + aggregation_padding: AggregationPadding::NoAggPadding, + }; + set_up_apply_dp_padding_pass_for_oprf::<_, BK, TV, TS, B>(ctx, padding_params).await + }) + .await + .map(Result::unwrap); + // check that all three helpers added the same number of dummy shares + assert!(result[0].len() == result[1].len() && result[0].len() == result[2].len()); + + let result_reconstructed = result.reconstruct(); + // check that all fields besides the matchkey are zero and matchkey is not zero + let mut user_id_counts: HashMap = HashMap::new(); + for row in result_reconstructed { + // println!("{row:?}"); + assert!(row.timestamp == 0); + assert!(row.trigger_value == 0); + assert!(!row.is_trigger_report); + assert!(row.breakdown_key == 0); // since we set AggregationPadding::NoAggPadding + assert!(row.user_id != 0); + + let count = user_id_counts.entry(row.user_id).or_insert(0); + *count += 1; + } + // Now look at now many times a user_id occured + let mut sample_per_cardinality: BTreeMap = BTreeMap::new(); + for cardinality in user_id_counts.values() { + let count = sample_per_cardinality.entry(*cardinality).or_insert(0); + *count += 1; + } + let mut distribution_of_samples: BTreeMap = BTreeMap::new(); + + for (cardinality, sample) in sample_per_cardinality { + println!("{sample} user IDs occurred {cardinality} time(s)"); + let count = distribution_of_samples.entry(sample).or_insert(0); + *count += 1; + } + + let oprf_padding = + OPRFPaddingDp::new(oprf_epsilon, oprf_delta, oprf_padding_sensitivity).unwrap(); + + let (mean, std_bound) = oprf_padding.mean_and_std_bound(); + let tolerance_bound = 12.0; + assert!(std_bound > 1.0); // bound on the std only holds if this is true. + println!("mean = {mean}, std_bound = {std_bound}"); + for (sample, count) in &distribution_of_samples { + println!("An OPRFPadding sample value equal to {sample} occurred {count} time(s)",); + assert!( + (f64::from(*sample) - mean).abs() < tolerance_bound * std_bound, + "aggregation noise sample was not within {tolerance_bound} times the standard deviation bound from what was expected." + ); + } + } + + pub async fn set_up_apply_dp_padding_pass_for_agg( + ctx: C, + padding_params: PaddingParameters, + ) -> Result, AdditiveShare>>, Error> + where + C: Context, + BK: BooleanArray + U128Conversions, + TV: BooleanArray, + { + let mut input: Vec, AdditiveShare>> = Vec::new(); + input = apply_dp_padding_pass::< + C, + AttributionOutputs, AdditiveShare>, + B, + >(ctx, input, Role::H3, &padding_params) + .await?; + Ok(input) + } + + #[tokio::test] + pub async fn aggregation_noise_in_dp_padding_pass() { + type BK = BA8; + type TV = BA3; + const B: usize = 256; + let world = TestWorld::default(); + let aggregation_epsilon = 1.0; + let aggregation_delta = 1e-6; + let aggregation_padding_sensitivity = 2; + + let result = world + .semi_honest((), |ctx, ()| async move { + let padding_params = PaddingParameters { + oprf_padding: OPRFPadding::NoOPRFPadding, + aggregation_padding: AggregationPadding::Parameters { + aggregation_epsilon, + aggregation_delta, + aggregation_padding_sensitivity, + }, + }; + set_up_apply_dp_padding_pass_for_agg::<_, BK, TV, B>(ctx, padding_params).await + }) + .await + .map(Result::unwrap); + + // check that all three helpers added the same number of dummy shares + assert!(result[0].len() == result[1].len() && result[0].len() == result[2].len()); + + let result_reconstructed: Vec = result.reconstruct(); + + let mut sample_per_breakdown: HashMap = HashMap::new(); + for row in result_reconstructed { + assert!(row.capped_attributed_trigger_value == 0); + let sample = sample_per_breakdown + .entry(row.attributed_breakdown_key) + .or_insert(0); + *sample += 1; + } + // check that all breakdowns had noise added + assert!(B == sample_per_breakdown.len()); + + let aggregation_padding = OPRFPaddingDp::new( + aggregation_epsilon, + aggregation_delta, + aggregation_padding_sensitivity, + ) + .unwrap(); + + let (mean, std_bound) = aggregation_padding.mean_and_std_bound(); + assert!(std_bound > 1.0); // bound on the std only holds if this is true. + let tolerance_factor = 12.0; + println!( + "mean = {mean}, std_bound = {std_bound}, {tolerance_factor} * std_bound = {}", + tolerance_factor * std_bound + ); + for sample in sample_per_breakdown.values() { + assert!( + (f64::from(*sample) - mean).abs() < tolerance_factor * std_bound, + "aggregation noise sample = {} was not within {tolerance_factor} times the standard deviation bound \ + ({tolerance_factor} * std_bound = {}) from what was expected (mean = {mean}). For Laplace this will fail ~ 0.03% of the time randomly.", + *sample, + tolerance_factor * std_bound, + ); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////// + /// Analysis of Parameters + /// + /// + /// + pub fn expected_number_fake_rows( + padding_params: PaddingParameters, + num_breakdown_keys: u32, + ) -> (f64, f64) { + // print out how many fake rows are expected for both oprf and aggregation + // for the given parameter set. + let mut expected_agg_total_rows = 0.0; + let mut expected_oprf_total_rows = 0.0; + + // padding for aggregation + match padding_params.aggregation_padding { + AggregationPadding::NoAggPadding => { + expected_agg_total_rows = 0.0; + } + AggregationPadding::Parameters { + aggregation_epsilon, + aggregation_delta, + aggregation_padding_sensitivity, + } => { + let aggregation_padding = OPRFPaddingDp::new( + aggregation_epsilon, + aggregation_delta, + aggregation_padding_sensitivity, + ) + .unwrap(); + + let (mean, _) = aggregation_padding.mean_and_std_bound(); + expected_agg_total_rows += f64::from(num_breakdown_keys) * mean; + } + } + + // padding for oprf + match padding_params.oprf_padding { + OPRFPadding::NoOPRFPadding => expected_oprf_total_rows = 0.0, + OPRFPadding::Parameters { + oprf_epsilon, + oprf_delta, + matchkey_cardinality_cap, + oprf_padding_sensitivity, + } => { + let oprf_padding = + OPRFPaddingDp::new(oprf_epsilon, oprf_delta, oprf_padding_sensitivity).unwrap(); + + let (mean, _) = oprf_padding.mean_and_std_bound(); + for cardinality in 0..matchkey_cardinality_cap { + expected_oprf_total_rows += mean * f64::from(cardinality); + } + } + } + (expected_oprf_total_rows, expected_agg_total_rows) + } + + #[test] + #[ignore] + pub fn table_of_padding_parameters() { + // see output https://docs.google.com/spreadsheets/d/1N0WEUkarP_6nd-7W8O9r-Xurh9OImESgAC1Jd_6OfWw/edit?gid=0#gid=0 + let epsilon_values = [0.01, 0.1, 1.0, 5.0, 10.0]; + let delta_values = [1e-9, 1e-8, 1e-7, 1e-6]; + let matchkey_cardinality_cap_values = [10, 100, 1000]; + let num_breakdown_keys_values = [16, 64, 256, 1024]; + println!( + "epsilon, delta, matchkey_cardinality_cap,aggregation_padding_sensitivity,num_breakdown_keys,Expected \ + OPRF total rows,Expected Aggregation total rows ", + ); + for epsilon in epsilon_values { + for delta in delta_values { + for matchkey_cardinality_cap in matchkey_cardinality_cap_values { + let aggregation_padding_sensitivity = matchkey_cardinality_cap; // TODO not necessary to have this + for num_breakdown_keys in num_breakdown_keys_values { + let padding_params = PaddingParameters { + aggregation_padding: AggregationPadding::Parameters { + aggregation_epsilon: epsilon, + aggregation_delta: delta, + aggregation_padding_sensitivity, + }, + oprf_padding: OPRFPadding::Parameters { + oprf_epsilon: epsilon, + oprf_delta: delta, + matchkey_cardinality_cap, + oprf_padding_sensitivity: 2, + }, + }; + // Call the function to get expected number of fake rows + let (expected_oprf_total_rows, expected_agg_total_rows) = + expected_number_fake_rows(padding_params, num_breakdown_keys); + // Print parameters and outcomes + println!( + "{epsilon}, {delta}, {matchkey_cardinality_cap},\ + {aggregation_padding_sensitivity},{num_breakdown_keys},\ + {expected_oprf_total_rows},{expected_agg_total_rows}" + ); + } + } + } + } + } + + /// /////////////////////////////////////////////////////////////////// + /// Below tests are for more foundational components used in building padding. + + /// # Errors + /// Will propogate errors from `OPRFPaddingDp` + pub fn sample_shared_randomness(ctx: &C) -> Result + where + C: Context, + { + let oprf_padding = OPRFPaddingDp::new(1.0, 1e-6, 10_u32)?; + let (mut left, mut right) = ctx.prss_rng(); + let rng = if ctx.role() == Role::H1 { + &mut right + } else if ctx.role() == Role::H2 { + &mut left + } else { + return Ok(0); + }; + let sample = oprf_padding.sample(rng); + Ok(sample) + } + + #[tokio::test] + pub async fn test_sample_shared_randomness() { + println!("in test_sample_shared_randomness"); + let world = TestWorld::default(); + let result = world + .semi_honest( + (), + |ctx, ()| async move { sample_shared_randomness::<_>(&ctx) }, + ) + .await; + assert!(result[0] == result[1]); // H1 and H2 should agree + println!("result = {result:?}",); + } + + pub async fn send_to_helper(ctx: C) -> Result + where + C: Context, + { + let mut num_fake_rows: BA32 = BA32::truncate_from(u128::try_from(0).unwrap()); + + if ctx.role() == Role::H1 { + num_fake_rows = BA32::truncate_from(u128::try_from(2).unwrap()); + } + if ctx.role() == Role::H2 { + num_fake_rows = BA32::truncate_from(u128::try_from(3).unwrap()); + } + let send_ctx = ctx.set_total_records(TotalRecords::ONE); + if ctx.role() == Role::H1 { + let send_channel = send_ctx.send_channel::(send_ctx.role().peer(Direction::Left)); + let _ = send_channel.send(RecordId::FIRST, num_fake_rows).await; + } + + if ctx.role() == Role::H3 { + let recv_channel = + send_ctx.recv_channel::(send_ctx.role().peer(Direction::Right)); + match recv_channel.receive(RecordId::FIRST).await { + Ok(v) => num_fake_rows = v, + Err(e) => return Err(e.into()), + } + } + Ok(num_fake_rows) + } + + #[tokio::test] + pub async fn test_send_to_helper() { + let world = TestWorld::default(); + let result = world + .semi_honest((), |ctx, ()| async move { send_to_helper::<_>(ctx).await }) + .await; + println!("result = {result:?}",); + let value_h1 = result[0].as_ref().expect("Failed to get result for H1"); + let value_h3 = result[2].as_ref().expect("Failed to get result for H3"); + assert_eq!(value_h1, value_h3, "H1 and H3 should agree"); + } +} diff --git a/ipa-core/src/protocol/ipa_prf/oprf_padding/step.rs b/ipa-core/src/protocol/ipa_prf/oprf_padding/step.rs new file mode 100644 index 000000000..3d07a114e --- /dev/null +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/step.rs @@ -0,0 +1,17 @@ +use ipa_step_derive::CompactStep; + +#[derive(CompactStep)] +pub(crate) enum PaddingDpStep { + PaddingDp, + #[step(child = crate::protocol::ipa_prf::oprf_padding::step::SendTotalRows)] + PaddingDpPass1, + #[step(child = crate::protocol::ipa_prf::oprf_padding::step::SendTotalRows)] + PaddingDpPass2, + #[step(child = crate::protocol::ipa_prf::oprf_padding::step::SendTotalRows)] + PaddingDpPass3, +} + +#[derive(CompactStep)] +pub(crate) enum SendTotalRows { + SendNumFakeRecords, +} diff --git a/ipa-core/src/protocol/ipa_prf/prf_sharding/mod.rs b/ipa-core/src/protocol/ipa_prf/prf_sharding/mod.rs index 5e75b3801..5246695e8 100644 --- a/ipa-core/src/protocol/ipa_prf/prf_sharding/mod.rs +++ b/ipa-core/src/protocol/ipa_prf/prf_sharding/mod.rs @@ -307,6 +307,9 @@ where /// /// The `aggregation` module also uses this type to hold chunks of attribution output records by /// specifying vectorized types for `BK` and `TV`. +/// +/// + #[derive(Clone, Debug, Default)] pub struct AttributionOutputs { pub attributed_breakdown_key_bits: BK, diff --git a/ipa-core/src/protocol/ipa_prf/step.rs b/ipa-core/src/protocol/ipa_prf/step.rs index a8f9a570e..71b0c9cbd 100644 --- a/ipa-core/src/protocol/ipa_prf/step.rs +++ b/ipa-core/src/protocol/ipa_prf/step.rs @@ -2,6 +2,8 @@ use ipa_step_derive::CompactStep; #[derive(CompactStep)] pub(crate) enum IpaPrfStep { + #[step(child = crate::protocol::ipa_prf::oprf_padding::step::PaddingDpStep, name="padding_dp")] + PaddingDp, #[step(child = crate::protocol::ipa_prf::shuffle::step::OPRFShuffleStep)] Shuffle, // ConvertInputRowsToPrf, diff --git a/ipa-core/src/query/processor.rs b/ipa-core/src/query/processor.rs index a813913c4..929258a02 100644 --- a/ipa-core/src/query/processor.rs +++ b/ipa-core/src/query/processor.rs @@ -670,7 +670,7 @@ mod tests { attribution_window_seconds: None, num_multi_bits: 3, with_dp: 0, - epsilon: 1.0, + epsilon: 5.0, plaintext_match_keys: true, }), }, diff --git a/ipa-core/src/query/runner/oprf_ipa.rs b/ipa-core/src/query/runner/oprf_ipa.rs index f1df443c3..e33e9904d 100644 --- a/ipa-core/src/query/runner/oprf_ipa.rs +++ b/ipa-core/src/query/runner/oprf_ipa.rs @@ -18,7 +18,7 @@ use crate::{ protocol::{ basics::ShareKnownValue, context::{Context, SemiHonestContext}, - ipa_prf::{oprf_ipa, OPRFIPAInputRow}, + ipa_prf::{oprf_ipa, oprf_padding::PaddingParameters, OPRFIPAInputRow}, step::ProtocolStep::IpaPrf, }, report::{EncryptedOprfReport, EventType}, @@ -119,12 +119,13 @@ where epsilon: config.epsilon, }, }; + let padding_params = PaddingParameters::relaxed(); match config.per_user_credit_cap { - 8 => oprf_ipa::(ctx, input, aws, dp_params).await, - 16 => oprf_ipa::(ctx, input, aws, dp_params).await, - 32 => oprf_ipa::(ctx, input, aws, dp_params).await, - 64 => oprf_ipa::(ctx, input, aws, dp_params).await, - 128 => oprf_ipa::(ctx, input, aws, dp_params).await, + 8 => oprf_ipa::(ctx, input, aws, dp_params, padding_params).await, + 16 => oprf_ipa::(ctx, input, aws, dp_params, padding_params).await, + 32 => oprf_ipa::(ctx, input, aws, dp_params, padding_params).await, + 64 => oprf_ipa::(ctx, input, aws, dp_params, padding_params).await, + 128 => oprf_ipa::(ctx, input, aws, dp_params, padding_params).await, _ => panic!( "Invalid value specified for per-user cap: {:?}. Must be one of 8, 16, 32, 64, or 128.", config.per_user_credit_cap @@ -232,7 +233,7 @@ mod tests { attribution_window_seconds: None, max_breakdown_key: 3, with_dp: 0, - epsilon: 1.0, + epsilon: 5.0, plaintext_match_keys: false, }; let input = BodyStream::from(buffer); diff --git a/ipa-core/src/test_fixture/ipa.rs b/ipa-core/src/test_fixture/ipa.rs index ab4595242..8670f0277 100644 --- a/ipa-core/src/test_fixture/ipa.rs +++ b/ipa-core/src/test_fixture/ipa.rs @@ -7,7 +7,9 @@ use crate::protocol::ipa_prf::prf_sharding::GroupingKey; use crate::{ ff::{PrimeField, Serializable}, helpers::query::{DpMechanism, IpaQueryConfig}, - protocol::{dp::NoiseParams, ipa_prf::OPRFIPAInputRow}, + protocol::{ + dp::NoiseParams, ipa_prf::oprf_padding::PaddingParameters, ipa_prf::OPRFIPAInputRow, + }, secret_sharing::{ replicated::{ malicious::ExtendableField, semi_honest, semi_honest::AdditiveShare as Replicated, @@ -207,13 +209,14 @@ pub async fn test_oprf_ipa( epsilon: config.epsilon, }, }; + let padding_params = PaddingParameters::default(); let result: Vec<_> = if config.per_user_credit_cap == 256 { // Note that many parameters are different in this case, not just the credit cap. // This config is needed for collect_steps coverage. world.semi_honest( records.into_iter(), |ctx, input_rows: Vec>| async move { - oprf_ipa::(ctx, input_rows, aws, dp_params) + oprf_ipa::(ctx, input_rows, aws, dp_params, padding_params) .await .unwrap() }, @@ -225,19 +228,19 @@ pub async fn test_oprf_ipa( |ctx, input_rows: Vec>| async move { match config.per_user_credit_cap { - 8 => oprf_ipa::(ctx, input_rows, aws, dp_params) + 8 => oprf_ipa::(ctx, input_rows, aws, dp_params, padding_params) .await .unwrap(), - 16 => oprf_ipa::(ctx, input_rows, aws, dp_params) + 16 => oprf_ipa::(ctx, input_rows, aws, dp_params, padding_params) .await .unwrap(), - 32 => oprf_ipa::(ctx, input_rows, aws, dp_params) + 32 => oprf_ipa::(ctx, input_rows, aws, dp_params, padding_params) .await .unwrap(), - 64 => oprf_ipa::(ctx, input_rows, aws, dp_params) + 64 => oprf_ipa::(ctx, input_rows, aws, dp_params, padding_params) .await .unwrap(), - 128 => oprf_ipa::(ctx, input_rows, aws, dp_params) + 128 => oprf_ipa::(ctx, input_rows, aws, dp_params, padding_params) .await .unwrap(), _ => diff --git a/ipa-core/tests/common/mod.rs b/ipa-core/tests/common/mod.rs index 7dd0b1eef..56cdc37ce 100644 --- a/ipa-core/tests/common/mod.rs +++ b/ipa-core/tests/common/mod.rs @@ -93,12 +93,18 @@ pub trait CommandExt { } impl CommandExt for Command { + // Have the `silent` function return self and comment out the + // rest of the function to see printing from tests that run all + // the binaries. e.g. when running: `cargo test --test compact_gate --lib + // compact_gate_cap_8_no_window_semi_honest -p ipa-core --no-default-features + // --features "cli web-app real-world-infra test-fixture compact-gate"` fn silent(&mut self) -> &mut Self { if std::env::var("VERBOSE").ok().is_none() { self.arg("--quiet") } else { self.arg("-vv") } + // return self; } }