From 3c60992a83a33fd6dbf0d71677ba90bb378e6f60 Mon Sep 17 00:00:00 2001 From: Erik Taubeneck Date: Fri, 25 Oct 2024 15:51:44 -0700 Subject: [PATCH 1/6] make IndistinguishableHybridReports paddable, add PaddingStep in hybrid protocol --- ipa-core/src/protocol/hybrid/mod.rs | 21 ++++-- ipa-core/src/protocol/hybrid/step.rs | 2 + .../src/protocol/ipa_prf/oprf_padding/mod.rs | 66 +++++++++++++++++++ ipa-core/src/report/hybrid.rs | 19 ++++++ 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/ipa-core/src/protocol/hybrid/mod.rs b/ipa-core/src/protocol/hybrid/mod.rs index 482f6e939..a777b7448 100644 --- a/ipa-core/src/protocol/hybrid/mod.rs +++ b/ipa-core/src/protocol/hybrid/mod.rs @@ -1,5 +1,7 @@ pub(crate) mod step; +use step::HybridStep as Step; + use crate::{ error::Error, ff::{ @@ -9,12 +11,14 @@ use crate::{ helpers::query::DpMechanism, protocol::{ context::{ShardedContext, UpgradableContext}, - ipa_prf::{oprf_padding::PaddingParameters, shuffle::Shuffle}, + ipa_prf::{ + oprf_padding::{apply_dp_padding, PaddingParameters}, + shuffle::Shuffle, + }, }, report::hybrid::IndistinguishableHybridReport, secret_sharing::replicated::semi_honest::AdditiveShare as Replicated, }; - // In theory, we could support (runtime-configured breakdown count) ≤ (compile-time breakdown count) // ≤ 2^|bk|, with all three values distinct, but at present, there is no runtime configuration and // the latter two must be equal. The implementation of `move_single_value_to_bucket` does support a @@ -61,10 +65,10 @@ impl BreakdownKey<256> for BA8 {} /// # Panics /// Propagates errors from config issues or while running the protocol pub async fn hybrid_protocol<'ctx, C, BK, V, HV, const SS_BITS: usize, const B: usize>( - _ctx: C, + ctx: C, input_rows: Vec>, _dp_params: DpMechanism, - _dp_padding_params: PaddingParameters, + dp_padding_params: PaddingParameters, ) -> Result>, Error> where C: UpgradableContext + 'ctx + Shuffle + ShardedContext, @@ -75,5 +79,14 @@ where if input_rows.is_empty() { return Ok(vec![Replicated::ZERO; B]); } + + // Apply DP padding for OPRF + let _padded_input_rows = apply_dp_padding::<_, IndistinguishableHybridReport, B>( + ctx.narrow(&Step::PaddingDp), + input_rows, + &dp_padding_params, + ) + .await?; + unimplemented!("protocol::hybrid::hybrid_protocol is not fully implemented") } diff --git a/ipa-core/src/protocol/hybrid/step.rs b/ipa-core/src/protocol/hybrid/step.rs index 5de0051be..aed51ad1e 100644 --- a/ipa-core/src/protocol/hybrid/step.rs +++ b/ipa-core/src/protocol/hybrid/step.rs @@ -3,4 +3,6 @@ use ipa_step_derive::CompactStep; #[derive(CompactStep)] pub(crate) enum HybridStep { ReshardByTag, + #[step(child = crate::protocol::ipa_prf::oprf_padding::step::PaddingDpStep, name="padding_dp")] + PaddingDp, } 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 207dd2a43..0bf47dedd 100644 --- a/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs @@ -28,6 +28,7 @@ use crate::{ }, RecordId, }, + report::hybrid::IndistinguishableHybridReport, secret_sharing::{ replicated::{semi_honest::AdditiveShare, ReplicatedSecretSharing}, SharedValue, @@ -130,6 +131,71 @@ pub trait Paddable { Self: Sized; } +impl Paddable for IndistinguishableHybridReport +where + BK: BooleanArray + U128Conversions, + V: BooleanArray, +{ + fn add_padding_items, const B: usize>( + direction_to_excluded_helper: Direction, + padding_input_rows: &mut VC, + 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 = IndistinguishableHybridReport::new( + match_key_shares, + AdditiveShare::ZERO, + AdditiveShare::ZERO, + ); + padding_input_rows.extend(std::iter::once(row)); + } + } + } + } + } + Ok(total_number_of_fake_rows) + } + + fn add_zero_shares>( + padding_input_rows: &mut VC, + total_number_of_fake_rows: u32, + ) { + for _ in 0..total_number_of_fake_rows as usize { + let row = IndistinguishableHybridReport::new( + AdditiveShare::ZERO, + AdditiveShare::ZERO, + AdditiveShare::ZERO, + ); + + padding_input_rows.extend(std::iter::once(row)); + } + } +} + impl Paddable for OPRFIPAInputRow where BK: BooleanArray + U128Conversions, diff --git a/ipa-core/src/report/hybrid.rs b/ipa-core/src/report/hybrid.rs index 62d66a797..0fa796b7f 100644 --- a/ipa-core/src/report/hybrid.rs +++ b/ipa-core/src/report/hybrid.rs @@ -358,6 +358,25 @@ where breakdown_key: Replicated, } +impl IndistinguishableHybridReport +where + BK: SharedValue, + V: SharedValue, +{ + #[must_use] + pub fn new( + match_key: Replicated, + value: Replicated, + breakdown_key: Replicated, + ) -> Self { + Self { + match_key, + value, + breakdown_key, + } + } +} + impl From> for IndistinguishableHybridReport where BK: SharedValue, From 638f9d0925aa210468b9c54e3a18d930948f58c2 Mon Sep 17 00:00:00 2001 From: Erik Taubeneck Date: Mon, 28 Oct 2024 11:07:02 -0700 Subject: [PATCH 2/6] use pub fields instead of new method --- .../src/protocol/ipa_prf/oprf_padding/mod.rs | 20 +++++++-------- ipa-core/src/report/hybrid.rs | 25 +++---------------- 2 files changed, 13 insertions(+), 32 deletions(-) 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 0bf47dedd..cc7ad3697 100644 --- a/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs @@ -166,11 +166,11 @@ where Direction::Left => AdditiveShare::new(BA64::ZERO, dummy_mk), Direction::Right => AdditiveShare::new(dummy_mk, BA64::ZERO), }; - let row = IndistinguishableHybridReport::new( - match_key_shares, - AdditiveShare::ZERO, - AdditiveShare::ZERO, - ); + let row = IndistinguishableHybridReport { + match_key: match_key_shares, + value: AdditiveShare::ZERO, + breakdown_key: AdditiveShare::ZERO, + }; padding_input_rows.extend(std::iter::once(row)); } } @@ -185,11 +185,11 @@ where total_number_of_fake_rows: u32, ) { for _ in 0..total_number_of_fake_rows as usize { - let row = IndistinguishableHybridReport::new( - AdditiveShare::ZERO, - AdditiveShare::ZERO, - AdditiveShare::ZERO, - ); + let row = IndistinguishableHybridReport { + match_key: AdditiveShare::ZERO, + value: AdditiveShare::ZERO, + breakdown_key: AdditiveShare::ZERO, + }; padding_input_rows.extend(std::iter::once(row)); } diff --git a/ipa-core/src/report/hybrid.rs b/ipa-core/src/report/hybrid.rs index 0fa796b7f..9de913dcb 100644 --- a/ipa-core/src/report/hybrid.rs +++ b/ipa-core/src/report/hybrid.rs @@ -353,28 +353,9 @@ where BK: SharedValue, V: SharedValue, { - match_key: Replicated, - value: Replicated, - breakdown_key: Replicated, -} - -impl IndistinguishableHybridReport -where - BK: SharedValue, - V: SharedValue, -{ - #[must_use] - pub fn new( - match_key: Replicated, - value: Replicated, - breakdown_key: Replicated, - ) -> Self { - Self { - match_key, - value, - breakdown_key, - } - } + pub match_key: Replicated, + pub value: Replicated, + pub breakdown_key: Replicated, } impl From> for IndistinguishableHybridReport From f0ea6bfab7ec0f9d4f38c33e15547e4e0ac2aa03 Mon Sep 17 00:00:00 2001 From: Erik Taubeneck Date: Mon, 28 Oct 2024 11:52:14 -0700 Subject: [PATCH 3/6] add test for hybrid padding --- .../src/protocol/ipa_prf/oprf_padding/mod.rs | 103 ++++++++++++++++++ ipa-core/src/test_fixture/hybrid.rs | 41 +++++++ 2 files changed, 144 insertions(+) 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 cc7ad3697..25205ff98 100644 --- a/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs @@ -492,6 +492,7 @@ mod tests { }, RecordId, }, + report::hybrid::IndistinguishableHybridReport, secret_sharing::replicated::semi_honest::AdditiveShare, test_fixture::{Reconstruct, Runner, TestWorld}, }; @@ -517,6 +518,31 @@ mod tests { Ok(input) } + pub async fn set_up_apply_dp_padding_pass_for_indistinguishable_reports< + C, + BK, + V, + const B: usize, + >( + ctx: C, + padding_params: PaddingParameters, + ) -> Result>, Error> + where + C: Context, + BK: BooleanArray + U128Conversions, + V: 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; @@ -591,6 +617,83 @@ mod tests { } } + #[tokio::test] + pub async fn indistinguishable_report_noise_in_dp_padding_pass() { + // Note: This is a close copy of the test `oprf_noise_in_dp_padding_pass` + // Which will make this easier to delete the former test + // when we remove the oprf protocol. + type BK = BA8; + type V = BA3; + 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_indistinguishable_reports::<_, BK, V, 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 match_key_counts: HashMap = HashMap::new(); + for row in result_reconstructed { + assert!(row.value == 0); + assert!(row.breakdown_key == 0); // since we set AggregationPadding::NoAggPadding + assert!(row.match_key != 0); + + let count = match_key_counts.entry(row.match_key).or_insert(0); + *count += 1; + } + // Now look at now many times a match_key occured + let mut sample_per_cardinality: BTreeMap = BTreeMap::new(); + for cardinality in match_key_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, diff --git a/ipa-core/src/test_fixture/hybrid.rs b/ipa-core/src/test_fixture/hybrid.rs index 63ecf73e5..84a48dcbf 100644 --- a/ipa-core/src/test_fixture/hybrid.rs +++ b/ipa-core/src/test_fixture/hybrid.rs @@ -1,11 +1,52 @@ use std::collections::{HashMap, HashSet}; +use crate::{ + ff::{boolean_array::BooleanArray, U128Conversions}, + report::hybrid::IndistinguishableHybridReport, + secret_sharing::{replicated::semi_honest::AdditiveShare as Replicated, IntoShares}, + test_fixture::sharing::Reconstruct, +}; + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq)] pub enum TestHybridRecord { TestImpression { match_key: u64, breakdown_key: u32 }, TestConversion { match_key: u64, value: u32 }, } +#[derive(PartialEq, Eq)] +pub struct TestIndistinguishableHybridReport { + pub match_key: u64, + pub value: u32, + pub breakdown_key: u32, +} + +impl Reconstruct + for [&IndistinguishableHybridReport; 3] +where + BK: BooleanArray + U128Conversions + IntoShares>, + V: BooleanArray + U128Conversions + IntoShares>, +{ + fn reconstruct(&self) -> TestIndistinguishableHybridReport { + let [s0, s1, s2] = self; + + let match_key = [&s0.match_key, &s1.match_key, &s2.match_key] + .reconstruct() + .as_u128(); + + let breakdown_key = [&s0.breakdown_key, &s1.breakdown_key, &s2.breakdown_key] + .reconstruct() + .as_u128(); + + let value = [&s0.value, &s1.value, &s2.value].reconstruct().as_u128(); + + TestIndistinguishableHybridReport { + match_key: match_key.try_into().unwrap(), + breakdown_key: breakdown_key.try_into().unwrap(), + value: value.try_into().unwrap(), + } + } +} + struct HashmapEntry { breakdown_key: u32, total_value: u32, From d71ea88dea3d5b8f3bcf9d0dc83d0528b4117c8f Mon Sep 17 00:00:00 2001 From: Erik Taubeneck Date: Tue, 29 Oct 2024 09:32:49 -0700 Subject: [PATCH 4/6] clean up implementation of Paddable trait on IndistinguishableHybridReport --- .../src/protocol/ipa_prf/oprf_padding/mod.rs | 47 +++++++++---------- ipa-core/src/report/hybrid.rs | 26 ++++++++++ ipa-core/src/secret_sharing/replicated/mod.rs | 8 ++++ 3 files changed, 55 insertions(+), 26 deletions(-) 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 25205ff98..a0b748742 100644 --- a/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs @@ -2,6 +2,8 @@ pub(crate) mod distributions; pub mod insecure; pub mod step; +use std::iter::{repeat, repeat_with}; + #[cfg(any(test, feature = "test-fixture", feature = "cli"))] pub use insecure::DiscreteDp as InsecureDiscreteDp; use rand::Rng; @@ -157,23 +159,22 @@ where 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 = IndistinguishableHybridReport { - match_key: match_key_shares, - value: AdditiveShare::ZERO, - breakdown_key: AdditiveShare::ZERO, - }; - padding_input_rows.extend(std::iter::once(row)); - } - } + padding_input_rows.extend( + repeat_with(|| { + let dummy_mk: BA64 = rng.gen(); + repeat(IndistinguishableHybridReport::from( + AdditiveShare::new_excluding_direction( + dummy_mk, + direction_to_excluded_helper, + ), + )) + .take(cardinality as usize) + }) + // this means there will be `sample` many unique + // matchkeys to add each with cardinality = `cardinality` + .take(sample as usize) + .flatten(), + ); } } } @@ -184,15 +185,9 @@ where padding_input_rows: &mut VC, total_number_of_fake_rows: u32, ) { - for _ in 0..total_number_of_fake_rows as usize { - let row = IndistinguishableHybridReport { - match_key: AdditiveShare::ZERO, - value: AdditiveShare::ZERO, - breakdown_key: AdditiveShare::ZERO, - }; - - padding_input_rows.extend(std::iter::once(row)); - } + padding_input_rows.extend( + repeat(IndistinguishableHybridReport::ZERO).take(total_number_of_fake_rows as usize), + ); } } diff --git a/ipa-core/src/report/hybrid.rs b/ipa-core/src/report/hybrid.rs index 9de913dcb..95604e2fe 100644 --- a/ipa-core/src/report/hybrid.rs +++ b/ipa-core/src/report/hybrid.rs @@ -358,6 +358,32 @@ where pub breakdown_key: Replicated, } +impl IndistinguishableHybridReport +where + BK: SharedValue, + V: SharedValue, +{ + pub const ZERO: Self = Self { + match_key: Replicated::::ZERO, + value: Replicated::::ZERO, + breakdown_key: Replicated::::ZERO, + }; +} + +impl From> for IndistinguishableHybridReport +where + BK: SharedValue, + V: SharedValue, +{ + fn from(match_key: Replicated) -> Self { + Self { + match_key, + value: Replicated::::ZERO, + breakdown_key: Replicated::::ZERO, + } + } +} + impl From> for IndistinguishableHybridReport where BK: SharedValue, diff --git a/ipa-core/src/secret_sharing/replicated/mod.rs b/ipa-core/src/secret_sharing/replicated/mod.rs index dcf51494e..e2c7617e4 100644 --- a/ipa-core/src/secret_sharing/replicated/mod.rs +++ b/ipa-core/src/secret_sharing/replicated/mod.rs @@ -2,12 +2,20 @@ pub mod malicious; pub mod semi_honest; use super::{SecretSharing, SharedValue}; +use crate::helpers::Direction; pub trait ReplicatedSecretSharing: SecretSharing { fn new(a: V, b: V) -> Self; fn left(&self) -> V; fn right(&self) -> V; + fn new_excluding_direction(v: V, direction: Direction) -> Self { + match direction { + Direction::Left => Self::new(V::ZERO, v), + Direction::Right => Self::new(v, V::ZERO), + } + } + fn map T, R: ReplicatedSecretSharing, T: SharedValue>(&self, f: F) -> R { R::new(f(self.left()), f(self.right())) } From 947be2ef5bbd5b86731ae9dfb878ed03499ec4b2 Mon Sep 17 00:00:00 2001 From: Erik Taubeneck Date: Tue, 29 Oct 2024 09:50:43 -0700 Subject: [PATCH 5/6] add documentation --- ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) 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 a0b748742..0d74ad6a7 100644 --- a/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs +++ b/ipa-core/src/protocol/ipa_prf/oprf_padding/mod.rs @@ -138,6 +138,12 @@ where BK: BooleanArray + U128Conversions, V: BooleanArray, { + /// Given an extendable collection of `IndistinguishableHybridReport`s, + /// this function will pad the collection with dummy reports. The reports + /// have a random `match_key` and zeros for `breakdown_key` and `value`. + /// Dummies need to be added at every possible cardinality of `match_key`s, + /// e.g., we add sets of dummies with the same `match_key` at each possible cardinality. + /// The number of sets at each cardinality is random, and determined by `padding_params`. fn add_padding_items, const B: usize>( direction_to_excluded_helper: Direction, padding_input_rows: &mut VC, @@ -181,6 +187,8 @@ where Ok(total_number_of_fake_rows) } + /// Given an extendable collection of `IndistinguishableHybridReport`s, + /// this function ads `total_number_of_fake_rows` of Reports with zeros in all fields. fn add_zero_shares>( padding_input_rows: &mut VC, total_number_of_fake_rows: u32, From 52c78c0a9d95a9dae8e718fc58afbf742adf777b Mon Sep 17 00:00:00 2001 From: Erik Taubeneck Date: Tue, 29 Oct 2024 11:16:17 -0700 Subject: [PATCH 6/6] update reconstruct with PR feedback --- ipa-core/src/test_fixture/hybrid.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/ipa-core/src/test_fixture/hybrid.rs b/ipa-core/src/test_fixture/hybrid.rs index 84a48dcbf..3b8cc2460 100644 --- a/ipa-core/src/test_fixture/hybrid.rs +++ b/ipa-core/src/test_fixture/hybrid.rs @@ -27,17 +27,21 @@ where V: BooleanArray + U128Conversions + IntoShares>, { fn reconstruct(&self) -> TestIndistinguishableHybridReport { - let [s0, s1, s2] = self; - - let match_key = [&s0.match_key, &s1.match_key, &s2.match_key] + let match_key = self + .each_ref() + .map(|v| v.match_key.clone()) .reconstruct() .as_u128(); - - let breakdown_key = [&s0.breakdown_key, &s1.breakdown_key, &s2.breakdown_key] + let breakdown_key = self + .each_ref() + .map(|v| v.breakdown_key.clone()) + .reconstruct() + .as_u128(); + let value = self + .each_ref() + .map(|v| v.value.clone()) .reconstruct() .as_u128(); - - let value = [&s0.value, &s1.value, &s2.value].reconstruct().as_u128(); TestIndistinguishableHybridReport { match_key: match_key.try_into().unwrap(),