Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding differential privacy to FixedPointBoundedL2VecSum #578

Merged
merged 1 commit into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ fiat-crypto = { version = "0.1.20", optional = true }
fixed = { version = "1.23", optional = true }
getrandom = { version = "0.2.10", features = ["std"] }
hmac = { version = "0.12.1", optional = true }
num-bigint = { version = "0.4.3", optional = true, features = ["rand", "serde"] }
num-integer = { version = "0.1.45", optional = true }
num-iter = { version = "0.1.43", optional = true }
num-rational = { version = "0.4.1", optional = true }
num-traits = { version = "0.2.15", optional = true }
rand = { version = "0.8", optional = true }
rand_core = "0.6.4"
rayon = { version = "1.7.0", optional = true }
serde = { version = "1.0", features = ["derive"] }
Expand Down Expand Up @@ -50,7 +56,7 @@ zipf = "7.0.1"

[features]
default = ["crypto-dependencies"]
experimental = ["bitvec", "fiat-crypto", "fixed"]
experimental = ["bitvec", "fiat-crypto", "fixed", "num-bigint", "num-rational", "num-traits", "num-integer", "num-iter", "rand"]
multithreaded = ["rayon"]
prio2 = ["crypto-dependencies", "hmac", "sha2"]
crypto-dependencies = ["aes", "ctr", "cmac"]
Expand All @@ -69,6 +75,11 @@ harness = false
name = "cycle_counts"
harness = false

[[test]]
name = "discrete_gauss"
path = "tests/discrete_gauss.rs"
required-features = ["experimental"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
Expand Down
52 changes: 42 additions & 10 deletions benches/speed_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ use criterion::{BatchSize, Throughput};
use fixed::types::{I1F15, I1F31};
#[cfg(feature = "experimental")]
use fixed_macro::fixed;
#[cfg(feature = "experimental")]
use num_bigint::BigUint;
#[cfg(feature = "experimental")]
use num_rational::Ratio;
#[cfg(feature = "experimental")]
use num_traits::ToPrimitive;
#[cfg(feature = "experimental")]
use prio::dp::distributions::DiscreteGaussian;
#[cfg(feature = "prio2")]
use prio::vdaf::prio2::Prio2;
use prio::{
Expand Down Expand Up @@ -49,6 +57,30 @@ fn prng(c: &mut Criterion) {
group.finish();
}

/// Speed test for generating samples from the discrete gaussian distribution using different
/// standard deviations.
#[cfg(feature = "experimental")]
pub fn dp_noise(c: &mut Criterion) {
let mut group = c.benchmark_group("dp_noise");
let mut rng = StdRng::seed_from_u64(RNG_SEED);

let test_stds = [
Ratio::<BigUint>::from_integer(BigUint::from(u128::MAX)).pow(2),
Ratio::<BigUint>::from_integer(BigUint::from(u64::MAX)),
Ratio::<BigUint>::from_integer(BigUint::from(u32::MAX)),
Ratio::<BigUint>::from_integer(BigUint::from(5u8)),
Ratio::<BigUint>::new(BigUint::from(10000u32), BigUint::from(23u32)),
];
for std in test_stds {
let sampler = DiscreteGaussian::new(std.clone()).unwrap();
group.bench_function(
BenchmarkId::new("discrete_gaussian", std.to_f64().unwrap_or(f64::INFINITY)),
|b| b.iter(|| sampler.sample(&mut rng)),
);
}
group.finish();
}

/// The asymptotic cost of polynomial multiplication is `O(n log n)` using FFT and `O(n^2)` using
/// the naive method. This benchmark demonstrates that the latter has better concrete performance
/// for small polynomials. The result is used to pick the `FFT_THRESHOLD` constant in
Expand Down Expand Up @@ -312,7 +344,7 @@ fn prio3(c: &mut Criterion) {
BenchmarkId::new("serial", dimension),
&dimension,
|b, dimension| {
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F15, _, _, _>, _, 16> =
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F15, _, _>, _, 16> =
Prio3::new_fixedpoint_boundedl2_vec_sum(num_shares, *dimension).unwrap();
let mut measurement = vec![fixed!(0: I1F15); *dimension];
measurement[0] = fixed!(0.5: I1F15);
Expand All @@ -329,7 +361,7 @@ fn prio3(c: &mut Criterion) {
BenchmarkId::new("parallel", dimension),
&dimension,
|b, dimension| {
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F15, _, _, _>, _, 16> =
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F15, _, _>, _, 16> =
Prio3::new_fixedpoint_boundedl2_vec_sum_multithreaded(
num_shares, *dimension,
)
Expand All @@ -350,7 +382,7 @@ fn prio3(c: &mut Criterion) {
BenchmarkId::new("series", dimension),
&dimension,
|b, dimension| {
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F15, _, _, _>, _, 16> =
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F15, _, _>, _, 16> =
Prio3::new_fixedpoint_boundedl2_vec_sum(num_shares, *dimension).unwrap();
let mut measurement = vec![fixed!(0: I1F15); *dimension];
measurement[0] = fixed!(0.5: I1F15);
Expand Down Expand Up @@ -379,7 +411,7 @@ fn prio3(c: &mut Criterion) {
BenchmarkId::new("parallel", dimension),
&dimension,
|b, dimension| {
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F15, _, _, _>, _, 16> =
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F15, _, _>, _, 16> =
Prio3::new_fixedpoint_boundedl2_vec_sum_multithreaded(
num_shares, *dimension,
)
Expand Down Expand Up @@ -413,7 +445,7 @@ fn prio3(c: &mut Criterion) {
BenchmarkId::new("serial", dimension),
&dimension,
|b, dimension| {
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F31, _, _, _>, _, 16> =
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F31, _, _>, _, 16> =
Prio3::new_fixedpoint_boundedl2_vec_sum(num_shares, *dimension).unwrap();
let mut measurement = vec![fixed!(0: I1F31); *dimension];
measurement[0] = fixed!(0.5: I1F31);
Expand All @@ -430,7 +462,7 @@ fn prio3(c: &mut Criterion) {
BenchmarkId::new("parallel", dimension),
&dimension,
|b, dimension| {
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F31, _, _, _>, _, 16> =
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F31, _, _>, _, 16> =
Prio3::new_fixedpoint_boundedl2_vec_sum_multithreaded(
num_shares, *dimension,
)
Expand All @@ -451,7 +483,7 @@ fn prio3(c: &mut Criterion) {
BenchmarkId::new("series", dimension),
&dimension,
|b, dimension| {
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F31, _, _, _>, _, 16> =
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F31, _, _>, _, 16> =
Prio3::new_fixedpoint_boundedl2_vec_sum(num_shares, *dimension).unwrap();
let mut measurement = vec![fixed!(0: I1F31); *dimension];
measurement[0] = fixed!(0.5: I1F31);
Expand Down Expand Up @@ -480,7 +512,7 @@ fn prio3(c: &mut Criterion) {
BenchmarkId::new("parallel", dimension),
&dimension,
|b, dimension| {
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F31, _, _, _>, _, 16> =
let vdaf: Prio3<FixedPointBoundedL2VecSum<I1F31, _, _>, _, 16> =
Prio3::new_fixedpoint_boundedl2_vec_sum_multithreaded(
num_shares, *dimension,
)
Expand Down Expand Up @@ -737,9 +769,9 @@ fn poplar1_generate_zipf_distributed_batch(
}

#[cfg(all(feature = "prio2", feature = "experimental"))]
criterion_group!(benches, poplar1, prio3, prio2, poly_mul, prng, idpf);
criterion_group!(benches, poplar1, prio3, prio2, poly_mul, prng, idpf, dp_noise);
#[cfg(all(not(feature = "prio2"), feature = "experimental"))]
criterion_group!(benches, poplar1, prio3, poly_mul, prng, idpf);
criterion_group!(benches, poplar1, prio3, poly_mul, prng, idpf, dp_noise);
#[cfg(all(feature = "prio2", not(feature = "experimental")))]
criterion_group!(benches, prio3, prio2, prng, poly_mul);
#[cfg(all(not(feature = "prio2"), not(feature = "experimental")))]
Expand Down
1 change: 1 addition & 0 deletions binaries/src/bin/vdaf_message_sizes.rs
cjpatton marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use fixed::{types::extra::U15, FixedI16};
use fixed_macro::fixed;

cjpatton marked this conversation as resolved.
Show resolved Hide resolved
use prio::{
codec::Encode,
vdaf::{
Expand Down
143 changes: 100 additions & 43 deletions src/dp.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,77 @@
// SPDX-License-Identifier: MPL-2.0

//! Differential privacy (DP) primitives.
use std::fmt::Debug;
//!
//! There are three main traits defined in this module:
//!
//! - `DifferentialPrivacyBudget`: Implementors should be types of DP-budgets,
//! i.e., methods to measure the amount of privacy provided by DP-mechanisms.
//! Examples: zCDP, ApproximateDP (Epsilon-Delta), PureDP
//!
//! - `DifferentialPrivacyDistribution`: Distribution from which noise is sampled.
//! Examples: DiscreteGaussian, DiscreteLaplace
//!
//! - `DifferentialPrivacyStrategy`: This is a combination of choices for budget and distribution.
//! Examples: zCDP-DiscreteGaussian, EpsilonDelta-DiscreteGaussian
//!
use num_bigint::{BigInt, BigUint, TryFromBigIntError};
use num_rational::{BigRational, Ratio};

/// Positive rational number to represent DP and noise distribution parameters in protocol messages
/// and manipulate them without rounding errors.
/// Errors propagated by methods in this module.
#[derive(Debug, thiserror::Error)]
pub enum DpError {
/// Tried to use an invalid float as privacy parameter.
#[error(
"DP error: input value was not a valid privacy parameter. \
It should to be a non-negative, finite float."
cjpatton marked this conversation as resolved.
Show resolved Hide resolved
)]
InvalidFloat,

/// Tried to construct a rational number with zero denominator.
#[error("DP error: input denominator was zero.")]
ZeroDenominator,

/// Tried to convert BigInt into something incompatible.
#[error("DP error: {0}")]
BigIntConversion(#[from] TryFromBigIntError<BigInt>),
}

/// Positive arbitrary precision rational number to represent DP and noise distribution parameters in
/// protocol messages and manipulate them without rounding errors.
#[derive(Clone, Debug)]
pub struct Rational {
/// Numerator.
pub numerator: u32,
/// Denominator.
pub denominator: u32,
pub struct Rational(Ratio<BigUint>);

impl Rational {
cjpatton marked this conversation as resolved.
Show resolved Hide resolved
/// Construct a [`Rational`] number from numerator `n` and denominator `d`. Errors if denominator is zero.
pub fn from_unsigned<T>(n: T, d: T) -> Result<Self, DpError>
where
T: Into<u128>,
{
// we don't want to expose BigUint in the public api, hence the Into<u128> bound
let d = d.into();
if d == 0 {
Err(DpError::ZeroDenominator)
} else {
Ok(Rational(Ratio::<BigUint>::new(n.into().into(), d.into())))
}
}
}

impl TryFrom<f32> for Rational {
type Error = DpError;
/// Constructs a `Rational` from a given `f32` value.
///
/// The special float values (NaN, positive and negative infinity) result in
/// an error. All other values are represented exactly, without rounding errors.
fn try_from(value: f32) -> Result<Self, DpError> {
ooovi marked this conversation as resolved.
Show resolved Hide resolved
match BigRational::from_float(value) {
Some(y) => Ok(Rational(Ratio::<BigUint>::new(
y.numer().clone().try_into()?,
y.denom().clone().try_into()?,
))),
None => Err(DpError::InvalidFloat)?,
}
}
}

/// Marker trait for differential privacy budgets (regardless of the specific accounting method).
Expand All @@ -19,50 +80,46 @@ pub trait DifferentialPrivacyBudget {}
/// Marker trait for differential privacy scalar noise distributions.
pub trait DifferentialPrivacyDistribution {}

/// Zero-concentrated differential privacy (zCDP) budget as defined in [[BS16]].
/// Zero-concentrated differential privacy (ZCDP) budget as defined in [[BS16]].
cjpatton marked this conversation as resolved.
Show resolved Hide resolved
///
/// [BS16]: https://arxiv.org/pdf/1605.02065.pdf
pub struct ZeroConcentratedDifferentialPrivacyBudget {
/// Parameter `epsilon`, using the notation from [[CKS20]] where `rho = (epsilon**2)/2`
/// for a `rho`-zCDP budget.
pub struct ZCdpBudget {
epsilon: Ratio<BigUint>,
}

impl ZCdpBudget {
cjpatton marked this conversation as resolved.
Show resolved Hide resolved
/// Create a budget for parameter `epsilon`, using the notation from [[CKS20]] where `rho = (epsilon**2)/2`
/// for a `rho`-ZCDP budget.
///
/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf
pub epsilon: Rational,
pub fn new(epsilon: Rational) -> Self {
Self { epsilon: epsilon.0 }
}
}

impl DifferentialPrivacyBudget for ZeroConcentratedDifferentialPrivacyBudget {}
impl DifferentialPrivacyBudget for ZCdpBudget {}
ooovi marked this conversation as resolved.
Show resolved Hide resolved
cjpatton marked this conversation as resolved.
Show resolved Hide resolved

/// Zero-mean Discrete Gaussian noise distribution.
///
/// The distribution is defined over the integers, represented by arbitrary-precision integers.
#[derive(Clone, Debug)]
pub struct DiscreteGaussian {
/// Standard deviation of the distribution.
pub sigma: Rational,
}
/// Strategy to make aggregate results differentially private, e.g. by adding noise from a specific
/// type of distribution instantiated with a given DP budget.
pub trait DifferentialPrivacyStrategy {
/// The type of the DP budget, i.e. the variant of differential privacy that can be obtained
/// by using this strategy.
cjpatton marked this conversation as resolved.
Show resolved Hide resolved
type Budget: DifferentialPrivacyBudget;
cjpatton marked this conversation as resolved.
Show resolved Hide resolved

impl DifferentialPrivacyDistribution for DiscreteGaussian {}
/// The distribution type this strategy will use to generate the noise.
type Distribution: DifferentialPrivacyDistribution;

/// Strategy to make aggregate shares differentially private, e.g. by adding noise from a specific
/// type of distribution instantiated with a given DP budget
pub trait DifferentialPrivacyStrategy {}
/// The type the sensitivity used for privacy analysis has.
type Sensitivity;

/// A zCDP budget used to create a Discrete Gaussian distribution
pub struct ZCdpDiscreteGaussian {
budget: ZeroConcentratedDifferentialPrivacyBudget,
}
/// Create a strategy from a differential privacy budget. The distribution created with
/// `create_distribution` should provide the amount of privacy specified here.
fn from_budget(b: Self::Budget) -> Self;

impl DifferentialPrivacyStrategy for ZCdpDiscreteGaussian {}

impl ZCdpDiscreteGaussian {
/// Creates a new Discrete Gaussian by following Theorem 4 from [[CKS20]]
///
/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf
pub fn create_distribution(&self, sensitivity: Rational) -> DiscreteGaussian {
let sigma = Rational {
numerator: self.budget.epsilon.denominator * sensitivity.numerator,
denominator: self.budget.epsilon.numerator * sensitivity.denominator,
};
DiscreteGaussian { sigma }
}
/// Create a new distribution parametrized s.t. adding samples to the result of a function
/// with sensitivity `s` will yield differential privacy of the DP variant given in the
/// `Budget` type. Can error upon invalid parameters.
fn create_distribution(&self, s: Self::Sensitivity) -> Result<Self::Distribution, DpError>;
}

pub mod distributions;
Loading