diff --git a/Cargo.lock b/Cargo.lock index aad67d968..136ef75a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,6 +750,8 @@ dependencies = [ "autocfg", "num-integer", "num-traits", + "rand 0.8.5", + "serde", ] [[package]] @@ -781,6 +783,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.2.4" @@ -799,6 +812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", + "num-bigint", "num-integer", "num-traits", ] @@ -899,6 +913,10 @@ dependencies = [ "itertools 0.11.0", "modinverse", "num-bigint", + "num-integer", + "num-iter", + "num-rational 0.4.1", + "num-traits", "once_cell", "prio", "rand 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index ee7899f91..a758133b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } @@ -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"] @@ -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"] diff --git a/benches/speed_tests.rs b/benches/speed_tests.rs index 09b0aaefa..39d18519b 100644 --- a/benches/speed_tests.rs +++ b/benches/speed_tests.rs @@ -7,6 +7,16 @@ 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 = "multithreaded")] +use prio::flp::gadgets::ParallelSumMultithreaded; #[cfg(feature = "prio2")] use prio::vdaf::prio2::Prio2; use prio::{ @@ -49,6 +59,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::::from_integer(BigUint::from(u128::MAX)).pow(2), + Ratio::::from_integer(BigUint::from(u64::MAX)), + Ratio::::from_integer(BigUint::from(u32::MAX)), + Ratio::::from_integer(BigUint::from(5u8)), + Ratio::::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 @@ -312,7 +346,7 @@ fn prio3(c: &mut Criterion) { BenchmarkId::new("serial", dimension), &dimension, |b, dimension| { - let vdaf: Prio3, _, 16> = + let vdaf: Prio3, _, 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); @@ -329,7 +363,7 @@ fn prio3(c: &mut Criterion) { BenchmarkId::new("parallel", dimension), &dimension, |b, dimension| { - let vdaf: Prio3, _, 16> = + let vdaf: Prio3, _, 16> = Prio3::new_fixedpoint_boundedl2_vec_sum_multithreaded( num_shares, *dimension, ) @@ -350,7 +384,7 @@ fn prio3(c: &mut Criterion) { BenchmarkId::new("series", dimension), &dimension, |b, dimension| { - let vdaf: Prio3, _, 16> = + let vdaf: Prio3, _, 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); @@ -379,7 +413,7 @@ fn prio3(c: &mut Criterion) { BenchmarkId::new("parallel", dimension), &dimension, |b, dimension| { - let vdaf: Prio3, _, 16> = + let vdaf: Prio3, _, 16> = Prio3::new_fixedpoint_boundedl2_vec_sum_multithreaded( num_shares, *dimension, ) @@ -413,7 +447,7 @@ fn prio3(c: &mut Criterion) { BenchmarkId::new("serial", dimension), &dimension, |b, dimension| { - let vdaf: Prio3, _, 16> = + let vdaf: Prio3, _, 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); @@ -430,7 +464,7 @@ fn prio3(c: &mut Criterion) { BenchmarkId::new("parallel", dimension), &dimension, |b, dimension| { - let vdaf: Prio3, _, 16> = + let vdaf: Prio3, _, 16> = Prio3::new_fixedpoint_boundedl2_vec_sum_multithreaded( num_shares, *dimension, ) @@ -451,7 +485,7 @@ fn prio3(c: &mut Criterion) { BenchmarkId::new("series", dimension), &dimension, |b, dimension| { - let vdaf: Prio3, _, 16> = + let vdaf: Prio3, _, 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); @@ -480,7 +514,7 @@ fn prio3(c: &mut Criterion) { BenchmarkId::new("parallel", dimension), &dimension, |b, dimension| { - let vdaf: Prio3, _, 16> = + let vdaf: Prio3, _, 16> = Prio3::new_fixedpoint_boundedl2_vec_sum_multithreaded( num_shares, *dimension, ) @@ -737,9 +771,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")))] diff --git a/binaries/src/bin/vdaf_message_sizes.rs b/binaries/src/bin/vdaf_message_sizes.rs index e1f4c48cc..cd65e0e5c 100644 --- a/binaries/src/bin/vdaf_message_sizes.rs +++ b/binaries/src/bin/vdaf_message_sizes.rs @@ -1,5 +1,6 @@ use fixed::{types::extra::U15, FixedI16}; use fixed_macro::fixed; + use prio::{ codec::Encode, vdaf::{ diff --git a/src/dp.rs b/src/dp.rs index 829bafd4f..d51a7dffd 100644 --- a/src/dp.rs +++ b/src/dp.rs @@ -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." + )] + 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), +} + +/// 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); + +impl Rational { + /// Construct a [`Rational`] number from numerator `n` and denominator `d`. Errors if denominator is zero. + pub fn from_unsigned(n: T, d: T) -> Result + where + T: Into, + { + // we don't want to expose BigUint in the public api, hence the Into bound + let d = d.into(); + if d == 0 { + Err(DpError::ZeroDenominator) + } else { + Ok(Rational(Ratio::::new(n.into().into(), d.into()))) + } + } +} + +impl TryFrom 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 { + match BigRational::from_float(value) { + Some(y) => Ok(Rational(Ratio::::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). @@ -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]]. /// /// [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, +} + +impl ZCdpBudget { + /// 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 {} -/// 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. + type Budget: DifferentialPrivacyBudget; -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; } + +pub mod distributions; diff --git a/src/dp/distributions.rs b/src/dp/distributions.rs new file mode 100644 index 000000000..7cb22c617 --- /dev/null +++ b/src/dp/distributions.rs @@ -0,0 +1,605 @@ +// Copyright (c) 2023 ISRG +// SPDX-License-Identifier: MPL-2.0 +// +// This file contains code covered by the following copyright and permission notice +// and has been modified by ISRG and collaborators. +// +// Copyright (c) 2022 President and Fellows of Harvard College +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// Copyright 2020 Thomas Steinke +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The following code is adapted from the opendp implementation to reduce dependencies: +// https://github.com/opendp/opendp/blob/main/rust/src/traits/samplers/cks20 + +//! Implementation of a sampler from the Discrete Gaussian Distribution. +//! +//! Follows +//! Clément Canonne, Gautam Kamath, Thomas Steinke. The Discrete Gaussian for Differential Privacy. 2020. +//! + +use num_bigint::{BigInt, BigUint, UniformBigUint}; +use num_integer::Integer; +use num_iter::range_inclusive; +use num_rational::Ratio; +use num_traits::{One, Zero}; +use rand::{distributions::uniform::UniformSampler, distributions::Distribution, Rng}; + +use super::{ + DifferentialPrivacyBudget, DifferentialPrivacyDistribution, DifferentialPrivacyStrategy, + DpError, ZCdpBudget, +}; + +/// Sample from the Bernoulli(gamma) distribution, where $gamma /leq 1$. +/// +/// `sample_bernoulli(gamma, rng)` returns numbers distributed as $Bernoulli(gamma)$. +/// using the given random number generator for base randomness. The procedure is as described +/// on page 30 of [[CKS20]]. +/// +/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf +fn sample_bernoulli(gamma: &Ratio, rng: &mut R) -> bool { + let d = gamma.denom(); + assert!(!d.is_zero()); + assert!(gamma <= &Ratio::::one()); + + // sample uniform biguint in {1,...,d} + // uses the implementation of rand::Uniform for num_bigint::BigUint + let s = UniformBigUint::sample_single_inclusive(BigUint::one(), d, rng); + + s <= *gamma.numer() +} + +/// Sample from the Bernoulli(exp(-gamma)) distribution where `gamma` is in `[0,1]`. +/// +/// `sample_bernoulli_exp1(gamma, rng)` returns numbers distributed as $Bernoulli(exp(-gamma))$, +/// using the given random number generator for base randomness. Follows Algorithm 1 of [[CKS20]], +/// splitting the branches into two non-recursive functions. This is the `gamma in [0,1]` branch. +/// +/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf +fn sample_bernoulli_exp1(gamma: &Ratio, rng: &mut R) -> bool { + assert!(!gamma.denom().is_zero()); + assert!(gamma <= &Ratio::::one()); + + let mut k = BigUint::one(); + loop { + if sample_bernoulli(&(gamma / k.clone()), rng) { + k += 1u8; + } else { + return k.is_odd(); + } + } +} + +/// Sample from the Bernoulli(exp(-gamma)) distribution. +/// +/// `sample_bernoulli_exp(gamma, rng)` returns numbers distributed as $Bernoulli(exp(-gamma))$, +/// using the given random number generator for base randomness. Follows Algorithm 1 of [[CKS20]], +/// splitting the branches into two non-recursive functions. This is the `gamma > 1` branch. +/// +/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf +fn sample_bernoulli_exp(gamma: &Ratio, rng: &mut R) -> bool { + assert!(!gamma.denom().is_zero()); + for _ in range_inclusive(BigUint::one(), gamma.floor().to_integer()) { + if !sample_bernoulli_exp1(&Ratio::::one(), rng) { + return false; + } + } + sample_bernoulli_exp1(&(gamma - gamma.floor()), rng) +} + +/// Sample from the geometric distribution with parameter 1 - exp(-gamma). +/// +/// `sample_geometric_exp(gamma, rng)` returns numbers distributed according to +/// $Geometric(1 - exp(-gamma))$, using the given random number generator for base randomness. +/// The code follows all but the last three lines of Algorithm 2 in [[CKS20]]. +/// +/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf +fn sample_geometric_exp(gamma: &Ratio, rng: &mut R) -> BigUint { + let (s, t) = (gamma.numer(), gamma.denom()); + assert!(!t.is_zero()); + if gamma.is_zero() { + return BigUint::zero(); + } + + // sampler for uniform biguint in {0...t-1} + // uses the implementation of rand::Uniform for num_bigint::BigUint + let usampler = UniformBigUint::new(BigUint::zero(), t); + let mut u = usampler.sample(rng); + + while !sample_bernoulli_exp1(&Ratio::::new(u.clone(), t.clone()), rng) { + u = usampler.sample(rng); + } + + let mut v = BigUint::zero(); + loop { + if sample_bernoulli_exp1(&Ratio::::one(), rng) { + v += 1u8; + } else { + break; + } + } + + // we do integer division, so the following term equals floor((u + t*v)/s) + (u + t * v) / s +} + +/// Sample from the discrete Laplace distribution. +/// +/// `sample_discrete_laplace(scale, rng)` returns numbers distributed according to +/// $\mathcal{L}_\mathbb{Z}(0, scale)$, using the given random number generator for base randomness. +/// This follows Algorithm 2 of [[CKS20]], using a subfunction for geometric sampling. +/// +/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf +fn sample_discrete_laplace(scale: &Ratio, rng: &mut R) -> BigInt { + let (s, t) = (scale.numer(), scale.denom()); + assert!(!t.is_zero()); + if s.is_zero() { + return BigInt::zero(); + } + + loop { + let negative = sample_bernoulli(&Ratio::::new(BigUint::one(), 2u8.into()), rng); + let y: BigInt = sample_geometric_exp(&scale.recip(), rng).into(); + if negative && y.is_zero() { + continue; + } else { + return if negative { -y } else { y }; + } + } +} + +/// Sample from the discrete Gaussian distribution. +/// +/// `sample_discrete_gaussian(sigma, rng)` returns `BigInt` numbers distributed as +/// $\mathcal{N}_\mathbb{Z}(0, sigma^2)$, using the given random number generator for base +/// randomness. Follows Algorithm 3 from [[CKS20]]. +/// +/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf +fn sample_discrete_gaussian(sigma: &Ratio, rng: &mut R) -> BigInt { + assert!(!sigma.denom().is_zero()); + if sigma.is_zero() { + return 0.into(); + } + let t = sigma.floor() + BigUint::one(); + + // no need to compute these parts of the probability term every iteration + let summand = sigma.pow(2) / t.clone(); + // compute probability of accepting the laplace sample y + let prob = |term: Ratio| term.pow(2) * (sigma.pow(2) * BigUint::from(2u8)).recip(); + + loop { + let y = sample_discrete_laplace(&t, rng); + + // absolute value without type conversion + let y_abs: Ratio = BigUint::new(y.to_u32_digits().1).into(); + + // unsigned subtraction-followed-by-square + let prob: Ratio = if y_abs < summand { + prob(summand.clone() - y_abs) + } else { + prob(y_abs - summand.clone()) + }; + + if sample_bernoulli_exp(&prob, rng) { + return y; + } + } +} + +/// Samples `BigInt` numbers according to the discrete Gaussian distribution with mean zero. +/// The distribution is defined over the integers, represented by arbitrary-precision integers. +/// The sampling procedure follows [[CKS20]]. +/// +/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf +#[derive(Clone, Debug)] +pub struct DiscreteGaussian { + /// The standard deviation of the distribution. + std: Ratio, +} + +impl DiscreteGaussian { + /// Create a new sampler from the Discrete Gaussian Distribution with the given + /// standard deviation and mean zero. Errors if the input has denominator zero. + pub fn new(std: Ratio) -> Result { + if std.denom().is_zero() { + return Err(DpError::ZeroDenominator); + } + Ok(DiscreteGaussian { std }) + } +} + +impl Distribution for DiscreteGaussian { + fn sample(&self, rng: &mut R) -> BigInt + where + R: Rng + ?Sized, + { + sample_discrete_gaussian(&self.std, rng) + } +} + +impl DifferentialPrivacyDistribution for DiscreteGaussian {} + +/// A DP strategy using the discrete gaussian distribution. +pub struct DiscreteGaussianDpStrategy +where + B: DifferentialPrivacyBudget, +{ + budget: B, +} + +/// A DP strategy using the discrete gaussian distribution providing zero-concentrated DP. +pub type ZCdpDiscreteGaussian = DiscreteGaussianDpStrategy; + +impl DifferentialPrivacyStrategy for DiscreteGaussianDpStrategy { + type Budget = ZCdpBudget; + type Distribution = DiscreteGaussian; + type Sensitivity = Ratio; + + fn from_budget(budget: ZCdpBudget) -> DiscreteGaussianDpStrategy { + DiscreteGaussianDpStrategy { budget } + } + + /// Create a new sampler from the Discrete Gaussian Distribution with a standard + /// deviation calibrated to provide `1/2 epsilon^2` zero-concentrated differential + /// privacy when added to the result of an integer-valued function with sensitivity + /// `sensitivity`, following Theorem 4 from [[CKS20]] + /// + /// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf + fn create_distribution( + &self, + sensitivity: Ratio, + ) -> Result { + DiscreteGaussian::new(sensitivity / self.budget.epsilon.clone()) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::dp::Rational; + use crate::vdaf::prg::SeedStreamSha3; + + use num_bigint::{BigUint, Sign, ToBigInt, ToBigUint}; + use num_traits::{One, Signed, ToPrimitive}; + use rand::{distributions::Distribution, SeedableRng}; + use statrs::distribution::{ChiSquared, ContinuousCDF, Normal}; + use std::collections::HashMap; + + #[test] + fn test_discrete_gaussian() { + let sampler = + DiscreteGaussian::new(Ratio::::from_integer(BigUint::from(5u8))).unwrap(); + + // check samples are consistent + let mut rng = SeedStreamSha3::from_seed([0u8; 16]); + let samples: Vec = (0..10) + .map(|_| i8::try_from(sampler.sample(&mut rng)).unwrap()) + .collect(); + let samples1: Vec = (0..10) + .map(|_| i8::try_from(sampler.sample(&mut rng)).unwrap()) + .collect(); + assert_eq!(samples, vec!(5, -7, 10, -6, 0, -2, -3, 5, -2, -3)); + assert_eq!(samples1, vec!(0, 4, 0, 1, 4, -5, 7, 10, 3, -9)); + } + + #[test] + /// Make sure that the distribution created by `create_distribution` + /// of `ZCdpDicreteGaussian` is the same one as manually creating one + /// by using the constructor of `DiscreteGaussian` directly. + fn test_zcdp_discrete_gaussian() { + // sample from a manually created distribution + let sampler1 = + DiscreteGaussian::new(Ratio::::from_integer(BigUint::from(4u8))).unwrap(); + let mut rng = SeedStreamSha3::from_seed([0u8; 16]); + let samples1: Vec = (0..10) + .map(|_| i8::try_from(sampler1.sample(&mut rng)).unwrap()) + .collect(); + + // sample from the distribution created by the `zcdp` strategy + let zcdp = ZCdpDiscreteGaussian { + budget: ZCdpBudget::new(Rational::try_from(0.25).unwrap()), + }; + let sampler2 = zcdp + .create_distribution(Ratio::::from_integer(1u8.into())) + .unwrap(); + let mut rng2 = SeedStreamSha3::from_seed([0u8; 16]); + let samples2: Vec = (0..10) + .map(|_| i8::try_from(sampler2.sample(&mut rng2)).unwrap()) + .collect(); + + assert_eq!(samples2, samples1); + } + + pub fn test_mean BigInt>( + mut sampler: FS, + hyp_mean: f64, + hyp_var: f64, + alpha: f64, + n: u32, + ) -> bool { + // we test if the mean from our sampler is within the given error margin assuimng its + // normally distributed with mean hyp_mean and variance sqrt(hyp_var/n) + // this assumption is from the central limit theorem + + // inverse cdf (quantile function) is F s.t. P[X<=F(p)]=p for X ~ N(0,1) + // (i.e. X from the standard normal distribution) + let probit = |p| Normal::new(0.0, 1.0).unwrap().inverse_cdf(p); + + // x such that the probability of a N(0,1) variable attaining + // a value outside of (-x, x) is alpha + let z_stat = probit(alpha / 2.).abs(); + + // confidence interval for the mean + let abs_p_tol = Ratio::::from_float(z_stat * (hyp_var / n as f64).sqrt()).unwrap(); + + // take n samples from the distribution, compute empirical mean + let emp_mean = Ratio::::new((0..n).map(|_| sampler()).sum::(), n.into()); + + (emp_mean - Ratio::::from_float(hyp_mean).unwrap()).abs() < abs_p_tol + } + + fn histogram( + d: &Vec, + bin_bounds: &[Option<(BigInt, BigInt)>], + smallest: BigInt, + largest: BigInt, + ) -> HashMap, u64> { + // a binned histogram of the samples in `d` + // used for chi_square test + + fn insert(hist: &mut HashMap, key: &T, val: u64) + where + T: Eq + std::hash::Hash + Clone, + { + *hist.entry(key.clone()).or_default() += val; + } + + // regular histogram + let mut hist = HashMap::::new(); + //binned histogram + let mut bin_hist = HashMap::, u64>::new(); + + for val in d { + // throw outliers with bound bins + if val < &smallest || val > &largest { + insert(&mut bin_hist, &None, 1); + } else { + insert(&mut hist, val, 1); + } + } + // sort values into their bins + for (a, b) in bin_bounds.iter().flatten() { + for i in range_inclusive(a.clone(), b.clone()) { + if let Some(count) = hist.get(&i) { + insert(&mut bin_hist, &Some((a.clone(), b.clone())), *count); + } + } + } + bin_hist + } + + fn discrete_gauss_cdf_approx( + sigma: &BigUint, + bin_bounds: &[Option<(BigInt, BigInt)>], + ) -> HashMap, f64> { + // approximate bin probabilties from theoretical distribution + // formula is eq. (1) on page 3 of [[CKS20]] + // + // [CKS20]: https://arxiv.org/pdf/2004.00010.pdf + let sigma = BigInt::from_biguint(Sign::Plus, sigma.clone()); + let exp_sum = |lower: &BigInt, upper: &BigInt| { + range_inclusive(lower.clone(), upper.clone()) + .map(|x: BigInt| { + f64::exp( + Ratio::::new(-(x.pow(2)), 2 * sigma.pow(2)) + .to_f64() + .unwrap(), + ) + }) + .sum::() + }; + // denominator is approximate up to 10 times the variance + // outside of that probabilities should be very small + // so the error will be negligible for the test + let denom = exp_sum(&(-10i8 * sigma.pow(2)), &(10i8 * sigma.pow(2))); + + // compute probabilities for each bin + let mut cdf = HashMap::new(); + let mut p_outside = 1.0; // probability of not landing inside bin boundaries + for (a, b) in bin_bounds.iter().flatten() { + let entry = exp_sum(a, b) / denom; + assert!(!entry.is_zero() && entry.is_finite()); + cdf.insert(Some((a.clone(), b.clone())), entry); + p_outside -= entry; + } + cdf.insert(None, p_outside); + cdf + } + + fn chi_square(sigma: &BigUint, n_bins: usize, alpha: f64) -> bool { + // perform pearsons chi-squared test on the discrete gaussian sampler + + let sigma_signed = BigInt::from_biguint(Sign::Plus, sigma.clone()); + + // cut off at 3 times the std. and collect all outliers in a seperate bin + let global_bound = 3u8 * sigma_signed; + + // bounds of bins + let lower_bounds = range_inclusive(-global_bound.clone(), global_bound.clone()).step_by( + ((2u8 * global_bound.clone()) / BigInt::from(n_bins)) + .try_into() + .unwrap(), + ); + let mut bin_bounds: Vec> = std::iter::zip( + lower_bounds.clone().take(n_bins), + lower_bounds.map(|x: BigInt| x - 1u8).skip(1), + ) + .map(Some) + .collect(); + bin_bounds.push(None); // bin for outliers + + // approximate bin probabilities + let cdf = discrete_gauss_cdf_approx(sigma, &bin_bounds); + + // chi2 stat wants at least 5 expected entries per bin + // so we choose n_samples in a way that gives us that + let n_samples = cdf + .values() + .map(|val| f64::ceil(5.0 / *val) as u32) + .max() + .unwrap(); + + // collect that number of samples + let mut rng = SeedStreamSha3::from_seed([0u8; 16]); + let samples: Vec = (1..n_samples) + .map(|_| { + sample_discrete_gaussian(&Ratio::::from_integer(sigma.clone()), &mut rng) + }) + .collect(); + + // make a histogram from the samples + let hist = histogram(&samples, &bin_bounds, -global_bound.clone(), global_bound); + + // compute pearsons chi-squared test statistic + let stat: f64 = bin_bounds + .iter() + .map(|key| { + let expected = cdf.get(&(key.clone())).unwrap() * n_samples as f64; + if let Some(val) = hist.get(&(key.clone())) { + (*val as f64 - expected).powf(2.) / expected + } else { + 0.0 + } + }) + .sum::(); + + let chi2 = ChiSquared::new((cdf.len() - 1) as f64).unwrap(); + // the probability of observing X >= stat for X ~ chi-squared + // (the "p-value") + let p = 1.0 - chi2.cdf(stat); + + p > alpha + } + + #[test] + fn empirical_test_gauss() { + [100, 2000, 20000].iter().for_each(|p| { + let mut rng = SeedStreamSha3::from_seed([0u8; 16]); + let sampler = || { + sample_discrete_gaussian( + &Ratio::::from_integer((*p).to_biguint().unwrap()), + &mut rng, + ) + }; + let mean = 0.0; + let var = (p * p) as f64; + assert!( + test_mean(sampler, mean, var, 0.00001, 1000), + "Empirical evaluation of discrete Gaussian({:?}) sampler mean failed.", + p + ); + }); + // we only do chi square for std 100 because it's expensive + assert!(chi_square(&(100u8.to_biguint().unwrap()), 10, 0.05)); + } + + #[test] + fn empirical_test_bernoulli_mean() { + [2u8, 5u8, 7u8, 9u8].iter().for_each(|p| { + let mut rng = SeedStreamSha3::from_seed([0u8; 16]); + let sampler = || { + if sample_bernoulli( + &Ratio::::new(BigUint::one(), (*p).into()), + &mut rng, + ) { + BigInt::one() + } else { + BigInt::zero() + } + }; + let mean = 1. / (*p as f64); + let var = mean * (1. - mean); + assert!( + test_mean(sampler, mean, var, 0.00001, 1000), + "Empirical evaluation of the Bernoulli(1/{:?}) distribution mean failed", + p + ); + }) + } + + #[test] + fn empirical_test_geometric_mean() { + [2u8, 5u8, 7u8, 9u8].iter().for_each(|p| { + let mut rng = SeedStreamSha3::from_seed([0u8; 16]); + let sampler = || { + sample_geometric_exp( + &Ratio::::new(BigUint::one(), (*p).into()), + &mut rng, + ) + .to_bigint() + .unwrap() + }; + let p_prob = 1. - f64::exp(-(1. / *p as f64)); + let mean = (1. - p_prob) / p_prob; + let var = (1. - p_prob) / p_prob.powi(2); + assert!( + test_mean(sampler, mean, var, 0.0001, 1000), + "Empirical evaluation of the Geometric(1-exp(-1/{:?})) distribution mean failed", + p + ); + }) + } + + #[test] + fn empirical_test_laplace_mean() { + [2u8, 5u8, 7u8, 9u8].iter().for_each(|p| { + let mut rng = SeedStreamSha3::from_seed([0u8; 16]); + let sampler = || { + sample_discrete_laplace( + &Ratio::::new(BigUint::one(), (*p).into()), + &mut rng, + ) + }; + let mean = 0.0; + let var = (1. / *p as f64).powi(2); + assert!( + test_mean(sampler, mean, var, 0.0001, 1000), + "Empirical evaluation of the Laplace(0,1/{:?}) distribution mean failed", + p + ); + }) + } +} diff --git a/src/flp.rs b/src/flp.rs index ab67e9c7b..e9142dc00 100644 --- a/src/flp.rs +++ b/src/flp.rs @@ -46,6 +46,8 @@ //! //! [draft-irtf-cfrg-vdaf-06]: https://datatracker.ietf.org/doc/draft-irtf-cfrg-vdaf/05/ +#[cfg(feature = "experimental")] +use crate::dp::DifferentialPrivacyStrategy; use crate::fft::{discrete_fourier_transform, discrete_fourier_transform_inv_finish, FftError}; use crate::field::{FftFriendlyFieldElement, FieldElement, FieldElementWithInteger, FieldError}; use crate::fp::log2; @@ -104,6 +106,11 @@ pub enum FlpError { #[error("Field error: {0}")] Field(#[from] FieldError), + #[cfg(feature = "experimental")] + /// An error happened during noising. + #[error("differential privacy error: {0}")] + DifferentialPrivacy(#[from] crate::dp::DpError), + /// Unit test error. #[cfg(test)] #[error("test failed: {0}")] @@ -545,6 +552,21 @@ pub trait Type: Sized + Eq + Clone + Debug { } } +/// A type which supports adding noise to aggregate shares for Server Differential Privacy. +#[cfg(feature = "experimental")] +pub trait TypeWithNoise: Type +where + S: DifferentialPrivacyStrategy, +{ + /// Add noise to the aggregate share to obtain differential privacy. + fn add_noise_to_result( + &self, + dp_strategy: &S, + agg_result: &mut [Self::Field], + num_measurements: usize, + ) -> Result<(), FlpError>; +} + /// A gadget, a non-affine arithmetic circuit that is called when evaluating a validity circuit. pub trait Gadget: Debug { /// Evaluates the gadget on input `inp` and returns the output. diff --git a/src/flp/types/fixedpoint_l2.rs b/src/flp/types/fixedpoint_l2.rs index d2bbea710..c7d883ff5 100644 --- a/src/flp/types/fixedpoint_l2.rs +++ b/src/flp/types/fixedpoint_l2.rs @@ -2,7 +2,9 @@ //! A [`Type`] for summing vectors of fixed point numbers where the //! [L2 norm](https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm) -//! of each vector is bounded by `1`. +//! of each vector is bounded by `1` and adding [discrete Gaussian +//! noise](https://arxiv.org/abs/2004.00010) in order to achieve server +//! differential privacy. //! //! In the following a high level overview over the inner workings of this type //! is given and implementation details are discussed. It is not necessary for @@ -16,6 +18,21 @@ //! the norm of the vector is equal to the submitted norm, while the encoding //! guarantees that the submitted norm lies in the correct range. //! +//! The bound on the L2 norm allows calibration of discrete Gaussian noise added +//! after aggregation, making the procedure differentially private. +//! +//! ### Submission layout +//! +//! The client submissions contain a share of their vector and the norm +//! they claim it has. +//! The submission is a vector of field elements laid out as follows: +//! ```text +//! |---- bits_per_entry * entries ----|---- bits_for_norm ----| +//! ^ ^ +//! \- the input vector entries | +//! \- the encoded norm +//! ``` +//! //! ### Different number encodings //! //! Let `n` denote the number of bits of the chosen fixed-point type. @@ -63,6 +80,14 @@ //! two's complement integers, the computation that happens in //! [`CompatibleFloat::to_field_integer`] is actually simpler. //! +//! ### Value `1` +//! +//! We actually do not allow the submitted norm or vector entries to be +//! exactly `1`, but rather require them to be strictly less. Supporting `1` would +//! entail a more fiddly encoding and is not necessary for our usecase. +//! The largest representable vector entry can be computed by `dec(2^n-1)`. +//! For example, it is `0.999969482421875` for `n = 16`. +//! //! ### Norm computation //! //! The L2 norm of a vector xs of numbers in `[-1,1)` is given by: @@ -114,6 +139,17 @@ //! This means that the valid norms are exactly those representable with `2n-2` //! bits. //! +//! ### Noise and Differential Privacy +//! +//! Bounding the submission norm bounds the impact that changing a single +//! client's submission can have on the aggregate. That is, the so-called +//! L2-sensitivity of the procedure is equal to two times the norm bound, namely +//! `2^n`. Therefore, adding discrete Gaussian noise with standard deviation +//! `sigma = `(2^n)/epsilon` for some `epsilon` will make the procedure [`(epsilon^2)/2` +//! zero-concentrated differentially private](https://arxiv.org/abs/2004.00010). +//! `epsilon` is given as a parameter to the `add_noise_to_result` function, as part of the +//! `dp_strategy` argument of type [`ZCdpDiscreteGaussian`]. +//! //! ### Differences in the computation because of distribution //! //! In `decode_result()`, what is decoded are not the submitted entries of a @@ -128,39 +164,26 @@ //! ### Naming in the implementation //! //! The following names are used: -//! - `self.bits_per_entry` is `n` -//! - `self.entries` is `d` -//! - `self.bits_for_norm` is `2n-2` -//! -//! ### Submission layout +//! - `self.bits_per_entry` is `n` +//! - `self.entries` is `d` +//! - `self.bits_for_norm` is `2n-2` //! -//! The client submissions contain a share of their vector and the norm -//! they claim it has. -//! The submission is a vector of field elements laid out as follows: -//! ```text -//! |---- bits_per_entry * entries ----|---- bits_for_norm ----| -//! ^ ^ -//! \- the input vector entries | -//! \- the encoded norm -//! ``` -//! -//! ### Value `1` -//! -//! We actually do not allow the submitted norm or vector entries to be -//! exactly `1`, but rather require them to be strictly less. Supporting `1` would -//! entail a more fiddly encoding and is not necessary for our usecase. -//! The largest representable vector entry can be computed by `dec(2^n-1)`. -//! For example, it is `0.999969482421875` for `n = 16`. pub mod compatible_float; -use crate::field::{FftFriendlyFieldElement, FieldElementWithInteger, FieldElementWithIntegerExt}; +use crate::dp::{distributions::ZCdpDiscreteGaussian, DifferentialPrivacyStrategy, DpError}; +use crate::field::{Field128, FieldElement, FieldElementWithInteger, FieldElementWithIntegerExt}; use crate::flp::gadgets::{BlindPolyEval, ParallelSumGadget, PolyEval}; use crate::flp::types::fixedpoint_l2::compatible_float::CompatibleFloat; -use crate::flp::{FlpError, Gadget, Type}; +use crate::flp::{FlpError, Gadget, Type, TypeWithNoise}; use crate::polynomial::poly_range_check; +use crate::vdaf::prg::SeedStreamSha3; use fixed::traits::Fixed; - +use num_bigint::{BigInt, BigUint, TryFromBigIntError}; +use num_integer::Integer; +use num_rational::Ratio; +use rand::{distributions::Distribution, Rng}; +use rand_core::SeedableRng; use std::{convert::TryFrom, convert::TryInto, fmt::Debug, marker::PhantomData}; /// The fixed point vector sum data type. Each measurement is a vector of fixed point numbers of @@ -172,6 +195,10 @@ use std::{convert::TryFrom, convert::TryInto, fmt::Debug, marker::PhantomData}; /// particular, exactly the following types are supported: /// `FixedI16`, `FixedI32` and `FixedI64`. /// +/// The type implements the [`TypeWithNoise`] trait. The `add_noise_to_result` function adds +/// discrete Gaussian noise to an aggregate share, calibrated to the passed privacy budget. +/// This will result in the aggregate satisfying zero-concentrated differential privacy. +/// /// Depending on the size of the vector that needs to be transmitted, a corresponding field type has /// to be chosen for `F`. For a `n`-bit fixed point type and a `d`-dimensional vector, the field /// modulus needs to be larger than `d * 2^(2n-2)` so there are no overflows during norm validity @@ -179,15 +206,14 @@ use std::{convert::TryFrom, convert::TryInto, fmt::Debug, marker::PhantomData}; #[derive(Clone, PartialEq, Eq)] pub struct FixedPointBoundedL2VecSum< T: Fixed, - F: FftFriendlyFieldElement, - SPoly: ParallelSumGadget> + Clone, - SBlindPoly: ParallelSumGadget> + Clone, + SPoly: ParallelSumGadget> + Clone, + SBlindPoly: ParallelSumGadget> + Clone, > { bits_per_entry: usize, entries: usize, bits_for_norm: usize, - range_01_checker: Vec, - norm_summand_poly: Vec, + range_01_checker: Vec, + norm_summand_poly: Vec, phantom: PhantomData<(T, SPoly, SBlindPoly)>, // range/position constants @@ -201,12 +227,11 @@ pub struct FixedPointBoundedL2VecSum< gadget1_chunk_len: usize, } -impl Debug for FixedPointBoundedL2VecSum +impl Debug for FixedPointBoundedL2VecSum where T: Fixed, - F: FftFriendlyFieldElement, - SPoly: ParallelSumGadget> + Clone, - SBlindPoly: ParallelSumGadget> + Clone, + SPoly: ParallelSumGadget> + Clone, + SBlindPoly: ParallelSumGadget> + Clone, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("FixedPointBoundedL2VecSum") @@ -216,21 +241,19 @@ where } } -impl FixedPointBoundedL2VecSum +impl FixedPointBoundedL2VecSum where T: Fixed, - F: FftFriendlyFieldElement, - SPoly: ParallelSumGadget> + Clone, - SBlindPoly: ParallelSumGadget> + Clone, - u128: TryFrom, + SPoly: ParallelSumGadget> + Clone, + SBlindPoly: ParallelSumGadget> + Clone, { /// Return a new [`FixedPointBoundedL2VecSum`] type parameter. Each value of this type is a /// fixed point vector with `entries` entries. pub fn new(entries: usize) -> Result { // (0) initialize constants - let fi_one = F::Integer::from(F::one()); + let fi_one = u128::from(Field128::one()); - // (I) Check that the fixed type `F` is compatible. + // (I) Check that the fixed type is compatible. // // We only support fixed types that encode values in [-1,1]. // These have a single integer bit. @@ -246,7 +269,7 @@ where let bits_per_entry: usize = (::INT_NBITS + ::FRAC_NBITS) .try_into() .map_err(|_| FlpError::Encode("Could not convert u32 into usize.".to_string()))?; - if !F::valid_integer_bitlength(bits_per_entry) { + if !Field128::valid_integer_bitlength(bits_per_entry) { return Err(FlpError::Encode(format!( "fixed point type bit length ({bits_per_entry}) too large for field modulus", ))); @@ -256,7 +279,7 @@ where // // Valid norms encoded as field integers lie in [0,2^(2*bits - 2)). let bits_for_norm = 2 * bits_per_entry - 2; - if !F::valid_integer_bitlength(bits_for_norm) { + if !Field128::valid_integer_bitlength(bits_for_norm) { return Err(FlpError::Encode(format!( "maximal norm bit length ({bits_for_norm}) too large for field modulus", ))); @@ -273,11 +296,7 @@ where ))); if let Some(val) = (entries as u128).checked_mul(1 << bits_for_norm) { - if let Ok(modulus) = u128::try_from(F::modulus()) { - if val >= modulus { - return err; - } - } else { + if val >= Field128::modulus() { return err; } } else { @@ -293,7 +312,11 @@ where // p(y) = 2^(2n-2) + -(2^n) * y + 1 * y^2 let linear_part = fi_one << bits_per_entry; let constant_part = fi_one << (bits_per_entry + bits_per_entry - 2); - let norm_summand_poly = vec![F::from(constant_part), -F::from(linear_part), F::one()]; + let norm_summand_poly = vec![ + Field128::from(constant_part), + -Field128::from(linear_part), + Field128::one(), + ]; // Compute chunk length and number of calls for parallel sum gadgets. let len0 = bits_per_entry * entries + bits_for_norm; @@ -323,21 +346,72 @@ where gadget1_chunk_len, }) } + + /// This noising function can be called on the aggregate share to make + /// the entire aggregation process differentially private. The noise is + /// calibrated to result in a guarantee of `1/2 * epsilon^2` zero-concentrated + /// differential privacy, where `epsilon` is given by `dp_strategy.budget`. + fn add_noise( + &self, + dp_strategy: &ZCdpDiscreteGaussian, + agg_result: &mut [Field128], + rng: &mut R, + ) -> Result<(), FlpError> { + // generate and add discrete gaussian noise for each entry + + // 0. Compute sensitivity of aggregation, namely 2^n. + let sensitivity = BigUint::from(2u128).pow(self.bits_per_entry as u32); + // Also create a BigInt containing the field modulus. + let modulus = BigInt::from(Field128::modulus()); + + // 1. initialize sampler + let sampler = dp_strategy.create_distribution(Ratio::from_integer(sensitivity))?; + + // 2. Generate noise for each slice entry and apply it. + for entry in agg_result.iter_mut() { + // (a) Generate noise. + let noise: BigInt = sampler.sample(rng); + + // (b) Put noise into field. + // + // The noise is generated as BigInt, but has to fit into the Field128, + // which has modulus `Field128::modulus()`. Thus we use `BigInt::mod_floor()` + // to calculate `noise mod modulus`. This value fits into `u128`, and + // can be then put into the field. + // + // Note: we cannot use the operator `%` here, since it is not the mathematical + // modulus operation: for negative inputs and positive modulus it gives a + // negative result! + let noise: BigInt = noise.mod_floor(&modulus); + let noise: u128 = noise.try_into().map_err(|e: TryFromBigIntError| { + FlpError::DifferentialPrivacy(DpError::BigIntConversion(e)) + })?; + let f_noise = Field128::from(Field128::valid_integer_try_from::(noise)?); + + // (c) Apply noise to each entry of the aggregate share. + *entry += f_noise; + } + + Ok(()) + } } -impl Type for FixedPointBoundedL2VecSum +impl Type for FixedPointBoundedL2VecSum where - T: Fixed + CompatibleFloat, - F: FftFriendlyFieldElement, - SPoly: ParallelSumGadget> + Eq + Clone + 'static, - SBlindPoly: ParallelSumGadget> + Eq + Clone + 'static, + T: Fixed + CompatibleFloat, + SPoly: ParallelSumGadget> + Eq + Clone + 'static, + SBlindPoly: ParallelSumGadget> + Eq + Clone + 'static, { const ID: u32 = 0xFFFF0000; type Measurement = Vec; type AggregateResult = Vec; - type Field = F; + type Field = Field128; + + fn encode_measurement(&self, fp_entries: &Vec) -> Result, FlpError> { + if fp_entries.len() != self.entries { + return Err(FlpError::Encode("unexpected input length".into())); + } - fn encode_measurement(&self, fp_entries: &Vec) -> Result, FlpError> { // Convert the fixed-point encoded input values to field integers. We do // this once here because we need them for encoding but also for // computing the norm. @@ -346,10 +420,10 @@ where // (I) Vector entries. // Encode the integer entries bitwise, and write them into the `encoded` // vector. - let mut encoded: Vec = - vec![F::zero(); self.bits_per_entry * self.entries + self.bits_for_norm]; + let mut encoded: Vec = + vec![Field128::zero(); self.bits_per_entry * self.entries + self.bits_for_norm]; for (l, entry) in integer_entries.clone().enumerate() { - F::fill_with_bitvector_representation( + Field128::fill_with_bitvector_representation( &entry, &mut encoded[l * self.bits_per_entry..(l + 1) * self.bits_per_entry], )?; @@ -357,12 +431,12 @@ where // (II) Vector norm. // Compute the norm of the input vector. - let field_entries = integer_entries.map(|x| F::from(x)); + let field_entries = integer_entries.map(Field128::from); let norm = compute_norm_of_entries(field_entries, self.bits_per_entry)?; - let norm_int = F::Integer::from(norm); + let norm_int = u128::from(norm); // Write the norm into the `entries` vector. - F::fill_with_bitvector_representation( + Field128::fill_with_bitvector_representation( &norm_int, &mut encoded[self.range_norm_begin..self.range_norm_end], )?; @@ -370,7 +444,11 @@ where Ok(encoded) } - fn decode_result(&self, data: &[F], num_measurements: usize) -> Result, FlpError> { + fn decode_result( + &self, + data: &[Field128], + num_measurements: usize, + ) -> Result, FlpError> { if data.len() != self.entries { return Err(FlpError::Decode("unexpected input length".into())); } @@ -384,13 +462,13 @@ where }; let mut res = Vec::with_capacity(data.len()); for d in data { - let decoded = >::to_float(*d, num_measurements); + let decoded = ::to_float(*d, num_measurements); res.push(decoded); } Ok(res) } - fn gadget(&self) -> Vec>> { + fn gadget(&self) -> Vec>> { // This gadget checks that a field element is zero or one. // It is called for all the "bits" of the encoded entries // and of the encoded norm. @@ -411,15 +489,15 @@ where fn valid( &self, - g: &mut Vec>>, - input: &[F], - joint_rand: &[F], + g: &mut Vec>>, + input: &[Field128], + joint_rand: &[Field128], num_shares: usize, - ) -> Result { + ) -> Result { self.valid_call_check(input, joint_rand)?; - let f_num_shares = F::from(F::valid_integer_try_from(num_shares)?); - let constant_part_multiplier = F::one() / f_num_shares; + let f_num_shares = Field128::from(Field128::valid_integer_try_from::(num_shares)?); + let constant_part_multiplier = Field128::one() / f_num_shares; // Ensure that all submitted field elements are either 0 or 1. // This is done for: @@ -436,9 +514,9 @@ where // `ParallelSum` gadget. For a similar application see the `CountVec` // type. let range_check = { - let mut outp = F::zero(); + let mut outp = Field128::zero(); let mut r = joint_rand[0]; - let mut padded_chunk = vec![F::zero(); 2 * self.gadget0_chunk_len]; + let mut padded_chunk = vec![Field128::zero(); 2 * self.gadget0_chunk_len]; for chunk in input[..self.range_norm_end].chunks(self.gadget0_chunk_len) { let d = chunk.len(); @@ -481,17 +559,17 @@ where // decode the bit-encoded entries into elements in the range [0,2^n): let decoded_entries: Result, _> = input[0..self.entries * self.bits_per_entry] .chunks(self.bits_per_entry) - .map(F::decode_from_bitvector_representation) + .map(Field128::decode_from_bitvector_representation) .collect(); // run parallel sum gadget on the decoded entries let computed_norm = { - let mut outp = F::zero(); + let mut outp = Field128::zero(); // Chunks which are too short need to be extended with a share of the // encoded zero value, that is: 1/num_shares * (2^(n-1)) - let fi_one = F::Integer::from(F::one()); - let zero_enc = F::from(fi_one << (self.bits_per_entry - 1)); + let fi_one = u128::from(Field128::one()); + let zero_enc = Field128::from(fi_one << (self.bits_per_entry - 1)); let zero_enc_share = zero_enc * constant_part_multiplier; for chunk in decoded_entries?.chunks(self.gadget1_chunk_len) { @@ -513,7 +591,7 @@ where // The submitted norm is also decoded from its bit-encoding, and // compared with the computed norm. let submitted_norm_enc = &input[self.range_norm_begin..self.range_norm_end]; - let submitted_norm = F::decode_from_bitvector_representation(submitted_norm_enc)?; + let submitted_norm = Field128::decode_from_bitvector_representation(submitted_norm_enc)?; let norm_check = computed_norm - submitted_norm; @@ -523,7 +601,7 @@ where Ok(out) } - fn truncate(&self, input: Vec) -> Result, FlpError> { + fn truncate(&self, input: Vec) -> Result, FlpError> { self.truncate_call_check(&input)?; let mut decoded_vector = vec![]; @@ -532,7 +610,7 @@ where let start = i_entry * self.bits_per_entry; let end = (i_entry + 1) * self.bits_per_entry; - let decoded = F::decode_from_bitvector_representation(&input[start..end])?; + let decoded = Field128::decode_from_bitvector_representation(&input[start..end])?; decoded_vector.push(decoded); } Ok(decoded_vector) @@ -576,16 +654,32 @@ where } } +impl TypeWithNoise + for FixedPointBoundedL2VecSum +where + T: Fixed + CompatibleFloat, + SPoly: ParallelSumGadget> + Eq + Clone + 'static, + SBlindPoly: ParallelSumGadget> + Eq + Clone + 'static, +{ + fn add_noise_to_result( + &self, + dp_strategy: &ZCdpDiscreteGaussian, + agg_result: &mut [Self::Field], + _num_measurements: usize, + ) -> Result<(), FlpError> { + self.add_noise(dp_strategy, agg_result, &mut SeedStreamSha3::from_entropy()) + } +} + /// Compute the square of the L2 norm of a vector of fixed-point numbers encoded as field elements. /// /// * `entries` - Iterator over the vector entries. /// * `bits_per_entry` - Number of bits one entry has. -fn compute_norm_of_entries(entries: Fs, bits_per_entry: usize) -> Result +fn compute_norm_of_entries(entries: Fs, bits_per_entry: usize) -> Result where - F: FieldElementWithInteger, - Fs: IntoIterator, + Fs: IntoIterator, { - let fi_one = F::Integer::from(F::one()); + let fi_one = u128::from(Field128::one()); // The value that is computed here is: // sum_{y in entries} 2^(2n-2) + -(2^n) * y + 1 * y^2 @@ -594,7 +688,7 @@ where // more information. // // Initialize `norm_accumulator`. - let mut norm_accumulator = F::zero(); + let mut norm_accumulator = Field128::zero(); // constants let linear_part = fi_one << bits_per_entry; // = 2^(2n-2) @@ -602,7 +696,8 @@ where // Add term for a given `entry` to `norm_accumulator`. for entry in entries.into_iter() { - let summand = entry * entry + F::from(constant_part) - F::from(linear_part) * (entry); + let summand = + entry * entry + Field128::from(constant_part) - Field128::from(linear_part) * (entry); norm_accumulator += summand; } Ok(norm_accumulator) @@ -611,12 +706,15 @@ where #[cfg(test)] mod tests { use super::*; + use crate::dp::{Rational, ZCdpBudget}; use crate::field::{random_vector, Field128, FieldElement}; use crate::flp::gadgets::ParallelSum; use crate::flp::types::test_utils::{flp_validity_test, ValidityTestCase}; + use crate::vdaf::prg::SeedStreamSha3; use fixed::types::extra::{U127, U14, U63}; use fixed::{FixedI128, FixedI16, FixedI64}; use fixed_macro::fixed; + use rand::SeedableRng; #[test] fn test_bounded_fpvec_sum_parallel_fp16() { @@ -664,14 +762,14 @@ mod tests { fn test_fixed(fp_vec: Vec, enc_vec: Vec) where - F: CompatibleFloat, + F: CompatibleFloat, { let n: usize = (F::INT_NBITS + F::FRAC_NBITS).try_into().unwrap(); type Ps = ParallelSum>; type Psb = ParallelSum>; - let vsum: FixedPointBoundedL2VecSum = + let vsum: FixedPointBoundedL2VecSum = FixedPointBoundedL2VecSum::new(3).unwrap(); let one = Field128::one(); // Round trip @@ -686,6 +784,34 @@ mod tests { vec!(0.25, 0.125, 0.0625) ); + // Noise + let mut v = vsum + .truncate(vsum.encode_measurement(&fp_vec).unwrap()) + .unwrap(); + let strategy = ZCdpDiscreteGaussian::from_budget(ZCdpBudget::new( + Rational::from_unsigned(100u8, 3u8).unwrap(), + )); + vsum.add_noise(&strategy, &mut v, &mut SeedStreamSha3::from_seed([0u8; 16])) + .unwrap(); + assert_eq!( + vsum.decode_result(&v, 1).unwrap(), + match n { + // sensitivity depends on encoding so the noise differs + 16 => vec!(0.307464599609375, 0.087310791015625, 0.080108642578125), + 32 => vec!( + 0.29547774279490113, + 0.14262904040515423, + 0.024025703314691782 + ), + 64 => vec!( + 0.26282219659181755, + 0.18725036621584434, + 0.18477686410976318 + ), + _ => panic!("unsupported bitsize"), + } + ); + // encoded norm does not match computed norm let mut input: Vec = vsum.encode_measurement(&fp_vec).unwrap(); assert_eq!(input[0], Field128::zero()); @@ -782,7 +908,6 @@ mod tests { // fixed point too large , - Field128, ParallelSum>, ParallelSum>, >>::new(3) @@ -790,7 +915,6 @@ mod tests { // vector too large , - Field128, ParallelSum>, ParallelSum>, >>::new(3000000000) @@ -798,7 +922,6 @@ mod tests { // fixed point type has more than one int bit , - Field128, ParallelSum>, ParallelSum>, >>::new(3) diff --git a/src/flp/types/fixedpoint_l2/compatible_float.rs b/src/flp/types/fixedpoint_l2/compatible_float.rs index 2f2794dbd..404bec125 100644 --- a/src/flp/types/fixedpoint_l2/compatible_float.rs +++ b/src/flp/types/fixedpoint_l2/compatible_float.rs @@ -9,15 +9,15 @@ use fixed::{FixedI16, FixedI32, FixedI64}; /// Assign a `Float` type to this type and describe how to represent this type as an integer of the /// given field, and how to represent a field element as the assigned `Float` type. -pub trait CompatibleFloat { +pub trait CompatibleFloat { /// Represent a field element as `Float`, given the number of clients `c`. - fn to_float(t: F, c: u128) -> f64; + fn to_float(t: Field128, c: u128) -> f64; /// Represent a value of this type as an integer in the given field. - fn to_field_integer(&self) -> ::Integer; + fn to_field_integer(&self) -> ::Integer; } -impl CompatibleFloat for FixedI16 { +impl CompatibleFloat for FixedI16 { fn to_float(d: Field128, c: u128) -> f64 { to_float_bits(d, c, 16) } @@ -32,7 +32,7 @@ impl CompatibleFloat for FixedI16 { } } -impl CompatibleFloat for FixedI32 { +impl CompatibleFloat for FixedI32 { fn to_float(d: Field128, c: u128) -> f64 { to_float_bits(d, c, 32) } @@ -47,7 +47,7 @@ impl CompatibleFloat for FixedI32 { } } -impl CompatibleFloat for FixedI64 { +impl CompatibleFloat for FixedI64 { fn to_float(d: Field128, c: u128) -> f64 { to_float_bits(d, c, 64) } diff --git a/src/vdaf/prg.rs b/src/vdaf/prg.rs index 4779af42c..3bfc746a4 100644 --- a/src/vdaf/prg.rs +++ b/src/vdaf/prg.rs @@ -47,6 +47,7 @@ impl Seed { Ok(Self::from_bytes(seed)) } + /// Construct seed from a byte slice. pub(crate) fn from_bytes(seed: [u8; SEED_SIZE]) -> Self { Self(seed) } diff --git a/src/vdaf/prio3.rs b/src/vdaf/prio3.rs index a6eca0c2c..589fb642a 100644 --- a/src/vdaf/prio3.rs +++ b/src/vdaf/prio3.rs @@ -28,7 +28,11 @@ //! [draft-irtf-cfrg-vdaf-06]: https://datatracker.ietf.org/doc/draft-irtf-cfrg-vdaf/06/ use super::prg::PrgSha3; +#[cfg(feature = "experimental")] +use super::AggregatorWithNoise; use crate::codec::{CodecError, Decode, Encode, ParameterizedDecode}; +#[cfg(feature = "experimental")] +use crate::dp::DifferentialPrivacyStrategy; use crate::field::{decode_fieldvec, FftFriendlyFieldElement, FieldElement}; use crate::field::{Field128, Field64}; #[cfg(feature = "multithreaded")] @@ -42,6 +46,8 @@ use crate::flp::types::fixedpoint_l2::{ }; use crate::flp::types::{Average, Count, Histogram, Sum, SumVec}; use crate::flp::Type; +#[cfg(feature = "experimental")] +use crate::flp::TypeWithNoise; use crate::prng::Prng; use crate::vdaf::prg::{Prg, Seed}; use crate::vdaf::{ @@ -148,7 +154,6 @@ impl Prio3Sum { pub type Prio3FixedPointBoundedL2VecSum = Prio3< FixedPointBoundedL2VecSum< Fx, - Field128, ParallelSum>, ParallelSum>, >, @@ -157,7 +162,7 @@ pub type Prio3FixedPointBoundedL2VecSum = Prio3< >; #[cfg(feature = "experimental")] -impl> Prio3FixedPointBoundedL2VecSum { +impl Prio3FixedPointBoundedL2VecSum { /// Construct an instance of this VDAF with the given number of aggregators and number of /// vector entries. pub fn new_fixedpoint_boundedl2_vec_sum( @@ -180,7 +185,6 @@ impl> Prio3FixedPointBoundedL2VecSum { pub type Prio3FixedPointBoundedL2VecSumMultithreaded = Prio3< FixedPointBoundedL2VecSum< Fx, - Field128, ParallelSumMultithreaded>, ParallelSumMultithreaded>, >, @@ -189,7 +193,7 @@ pub type Prio3FixedPointBoundedL2VecSumMultithreaded = Prio3< >; #[cfg(all(feature = "experimental", feature = "multithreaded"))] -impl> Prio3FixedPointBoundedL2VecSumMultithreaded { +impl Prio3FixedPointBoundedL2VecSumMultithreaded { /// Construct an instance of this VDAF with the given number of aggregators and number of /// vector entries. pub fn new_fixedpoint_boundedl2_vec_sum_multithreaded( @@ -1154,6 +1158,27 @@ where } } +#[cfg(feature = "experimental")] +impl AggregatorWithNoise + for Prio3 +where + T: TypeWithNoise, + P: Prg, + S: DifferentialPrivacyStrategy, +{ + fn add_noise_to_agg_share( + &self, + dp_strategy: &S, + _agg_param: &Self::AggregationParam, + agg_share: &mut Self::AggregateShare, + num_measurements: usize, + ) -> Result<(), VdafError> { + self.typ + .add_noise_to_result(dp_strategy, &mut agg_share.0, num_measurements)?; + Ok(()) + } +} + impl Collector for Prio3 where T: Type, @@ -1384,9 +1409,9 @@ mod tests { fn test_fixed_vec( fp_0: Fx, - prio3: Prio3, PrgSha3, 16>, + prio3: Prio3, PrgSha3, 16>, ) where - Fx: Fixed + CompatibleFloat + std::ops::Neg, + Fx: Fixed + CompatibleFloat + std::ops::Neg, PE: Eq + ParallelSumGadget> + Clone + 'static, BPE: Eq + ParallelSumGadget> + Clone + 'static, { @@ -1476,9 +1501,9 @@ mod tests { fp_4_inv: Fx, fp_8_inv: Fx, fp_16_inv: Fx, - prio3: Prio3, PrgSha3, 16>, + prio3: Prio3, PrgSha3, 16>, ) where - Fx: Fixed + CompatibleFloat + std::ops::Neg, + Fx: Fixed + CompatibleFloat + std::ops::Neg, PE: Eq + ParallelSumGadget> + Clone + 'static, BPE: Eq + ParallelSumGadget> + Clone + 'static, { diff --git a/supply-chain/config.toml b/supply-chain/config.toml index f54827024..17678f837 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -189,7 +189,7 @@ criteria = "safe-to-run" [[exemptions.ppv-lite86]] version = "0.2.16" -criteria = "safe-to-run" +criteria = "safe-to-deploy" [[exemptions.proc-macro-error]] version = "1.0.4" @@ -205,7 +205,7 @@ criteria = "safe-to-run" [[exemptions.rand]] version = "0.8.5" -criteria = "safe-to-run" +criteria = "safe-to-deploy" [[exemptions.rand_distr]] version = "0.2.2" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index c328a9851..ab71fcff7 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -448,6 +448,12 @@ criteria = "safe-to-deploy" version = "0.1.45" notes = "All code written or reviewed by Josh Stone." +[[audits.firefox.audits.num-iter]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "0.1.43" +notes = "All code written or reviewed by Josh Stone." + [[audits.firefox.audits.num-rational]] who = "Josh Stone " criteria = "safe-to-deploy" diff --git a/tests/discrete_gauss.rs b/tests/discrete_gauss.rs new file mode 100644 index 000000000..301825167 --- /dev/null +++ b/tests/discrete_gauss.rs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MPL-2.0 + +use num_bigint::{BigInt, BigUint}; +use num_rational::Ratio; +use num_traits::FromPrimitive; +use prio::dp::distributions::DiscreteGaussian; +use prio::vdaf::prg::SeedStreamSha3; +use rand::distributions::Distribution; +use rand::SeedableRng; +use serde::Deserialize; + +/// A test vector of discrete Gaussian samples, produced by the python reference +/// implementation for [[CKS20]]. The script used to generate the test vector can +/// be found in this gist: +/// https://gist.github.com/ooovi/529c00fc8a7eafd068cd076b78fc424e +/// The python reference implementation is here: +/// https://github.com/IBM/discrete-gaussian-differential-privacy +/// +/// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf +#[derive(Debug, Eq, PartialEq, Deserialize)] +pub struct DiscreteGaussTestVector { + #[serde(with = "hex")] + seed: [u8; 16], + std_num: u128, + std_denom: u128, + samples: Vec, +} + +#[test] +fn discrete_gauss_reference() { + let test_vectors: Vec = vec![ + serde_json::from_str(include_str!(concat!("test_vectors/discrete_gauss_3.json"))).unwrap(), + serde_json::from_str(include_str!(concat!("test_vectors/discrete_gauss_9.json"))).unwrap(), + serde_json::from_str(include_str!(concat!( + "test_vectors/discrete_gauss_100.json" + ))) + .unwrap(), + serde_json::from_str(include_str!(concat!( + "test_vectors/discrete_gauss_41293847.json" + ))) + .unwrap(), + serde_json::from_str(include_str!(concat!( + "test_vectors/discrete_gauss_9999999999999999999999.json" + ))) + .unwrap(), + ]; + + for test_vector in test_vectors { + let sampler = DiscreteGaussian::new(Ratio::::new( + test_vector.std_num.into(), + test_vector.std_denom.into(), + )) + .unwrap(); + + // check samples are consistent + let mut rng = SeedStreamSha3::from_seed(test_vector.seed); + let samples: Vec = (0..test_vector.samples.len()) + .map(|_| sampler.sample(&mut rng)) + .collect(); + + assert_eq!( + samples, + test_vector + .samples + .iter() + .map(|&s| BigInt::from_i128(s).unwrap()) + .collect::>() + ); + } +} diff --git a/tests/test_vectors/discrete_gauss_100.json b/tests/test_vectors/discrete_gauss_100.json new file mode 100644 index 000000000..4d137585e --- /dev/null +++ b/tests/test_vectors/discrete_gauss_100.json @@ -0,0 +1,56 @@ +{ + "samples": [ + 9, + -28, + 34, + -171, + -140, + -46, + -21, + 79, + 66, + -93, + 54, + 96, + 29, + 126, + 99, + 87, + -96, + -50, + -183, + 38, + 59, + -21, + 124, + -8, + 271, + -92, + -14, + 40, + -16, + 84, + 73, + -21, + 145, + -33, + 145, + 102, + 11, + 43, + 108, + 229, + -9, + -105, + -64, + 55, + -100, + 133, + 121, + 7, + -10 + ], + "seed": "000102030405060708090a0b0c0d0e0f", + "std_denom": 1, + "std_num": 100 +} diff --git a/tests/test_vectors/discrete_gauss_2.342.json b/tests/test_vectors/discrete_gauss_2.342.json new file mode 100644 index 000000000..7d9508c44 --- /dev/null +++ b/tests/test_vectors/discrete_gauss_2.342.json @@ -0,0 +1,56 @@ +{ + "samples": [ + -1, + 4, + 2, + 0, + 0, + -2, + 1, + -5, + 2, + -1, + 0, + 0, + 0, + 3, + -6, + 5, + 2, + 1, + -1, + -3, + 0, + 2, + -3, + -2, + 2, + -3, + 1, + 1, + 2, + -3, + -1, + -1, + 4, + 2, + -2, + 1, + -1, + 0, + -3, + 1, + 2, + 1, + -1, + 3, + 1, + 2, + -3, + -2, + 0 + ], + "seed": "000102030405060708090a0b0c0d0e0f", + "std_denom": 500, + "std_num": 1171 +} diff --git a/tests/test_vectors/discrete_gauss_3.json b/tests/test_vectors/discrete_gauss_3.json new file mode 100644 index 000000000..9498d4994 --- /dev/null +++ b/tests/test_vectors/discrete_gauss_3.json @@ -0,0 +1,56 @@ +{ + "samples": [ + 3, + -2, + 3, + -3, + 2, + -2, + 3, + 0, + 2, + -2, + 0, + 4, + 1, + 1, + 1, + 1, + -1, + -5, + 1, + -1, + 5, + 0, + 2, + 1, + -4, + -1, + 2, + 0, + 6, + -2, + 5, + -5, + 4, + 1, + 0, + 2, + 4, + -1, + 1, + 1, + 2, + -1, + 2, + 3, + -4, + 0, + -5, + 1, + 0 + ], + "seed": "000102030405060708090a0b0c0d0e0f", + "std_denom": 1, + "std_num": 3 +} diff --git a/tests/test_vectors/discrete_gauss_41293847.json b/tests/test_vectors/discrete_gauss_41293847.json new file mode 100644 index 000000000..0b1d29a60 --- /dev/null +++ b/tests/test_vectors/discrete_gauss_41293847.json @@ -0,0 +1,56 @@ +{ + "samples": [ + 70857263, + 18330198, + -6609980, + 8669846, + -2681659, + -54892738, + 60147921, + 79172197, + -16075661, + 30555472, + 15383293, + -35100486, + 20242670, + 6488930, + 29500131, + -4688140, + 43525994, + -86625040, + -14695416, + 36287578, + -10307891, + 28542331, + 5302354, + 26002909, + 5312479, + 44968188, + 39980678, + 20935504, + 11830162, + 24571010, + -5634824, + -23220702, + 15182292, + -35332392, + 19512814, + 78329581, + 66818430, + 19835766, + 30069715, + 9733652, + 57043716, + 7039071, + 57974818, + -16362391, + 11112340, + -4794388, + 21465160, + -39209211, + -32553325 + ], + "seed": "000102030405060708090a0b0c0d0e0f", + "std_denom": 1, + "std_num": 41293847 +} diff --git a/tests/test_vectors/discrete_gauss_9.json b/tests/test_vectors/discrete_gauss_9.json new file mode 100644 index 000000000..9e273b115 --- /dev/null +++ b/tests/test_vectors/discrete_gauss_9.json @@ -0,0 +1,56 @@ +{ + "samples": [ + 17, + -7, + -3, + -18, + -1, + -1, + -2, + -6, + 4, + 4, + 6, + 1, + 8, + -10, + 5, + -2, + 2, + -14, + -14, + -3, + 3, + -2, + 16, + -1, + 28, + -11, + -1, + 8, + 1, + -6, + 7, + -3, + -12, + 3, + 1, + 5, + -10, + -5, + -1, + 4, + -5, + 3, + -2, + -1, + 2, + -5, + 4, + 12, + 3 + ], + "seed": "000102030405060708090a0b0c0d0e0f", + "std_denom": 1, + "std_num": 9 +} diff --git a/tests/test_vectors/discrete_gauss_9999999999999999999999.json b/tests/test_vectors/discrete_gauss_9999999999999999999999.json new file mode 100644 index 000000000..baf0acd95 --- /dev/null +++ b/tests/test_vectors/discrete_gauss_9999999999999999999999.json @@ -0,0 +1,56 @@ +{ + "samples": [ + 8563533359778814917517, + -4547278178581420790424, + 12328330069970863742495, + -17068676313673721296322, + 3013363462824264479248, + 17000140898647308338458, + 15305155613246405095770, + -21199406902639250150074, + 12144433268410875970969, + 16126826580699617913309, + 3378319318863394876114, + -3931138587493688461562, + -14132891197188514107529, + -6306079673435155346359, + -3923441892847593054161, + -15343913081587795695543, + 6358156846193247612690, + 7419825606177014954794, + 5188566641087329402683, + -6758430220371638235303, + -4790100404639726653559, + -16014248963401012891620, + -3070071895986196677964, + -5848874668833745227877, + -18435830148517064469147, + -17848621274303274471834, + 5282430088172496749183, + 8576946480142860155499, + -2828646354360531511240, + -942850098400867509851, + 9425651555597696445218, + -2539281423953275968251, + 11187378493262462376834, + -6116678358159812997083, + -3436435729276833415628, + 200162077639402997265, + 1902018077580024126206, + 3514979444113021823498, + -10598978329592410882809, + -4432092109314175600675, + -9433517318412068100406, + 3751331871729952870001, + -2157632349890764345554, + 636693770470222913362, + -13762970481594739356390, + -24485079813076283116966, + -214843260419243736734, + -8207508139915530781866, + 11326059754769588764304 + ], + "seed": "000102030405060708090a0b0c0d0e0f", + "std_denom": 1, + "std_num": 9999999999999999999999 +}