prio/dp.rs
1// SPDX-License-Identifier: MPL-2.0
2
3//! Differential privacy (DP) primitives.
4//!
5//! There are three main traits defined in this module:
6//!
7//! - `DifferentialPrivacyBudget`: Implementors should be types of DP-budgets,
8//! i.e., methods to measure the amount of privacy provided by DP-mechanisms.
9//! Examples: zCDP, ApproximateDP (Epsilon-Delta), PureDP
10//!
11//! - `DifferentialPrivacyDistribution`: Distribution from which noise is sampled.
12//! Examples: DiscreteGaussian, DiscreteLaplace
13//!
14//! - `DifferentialPrivacyStrategy`: This is a combination of choices for budget and distribution.
15//! Examples: zCDP-DiscreteGaussian, EpsilonDelta-DiscreteGaussian
16//!
17use num_bigint::{BigInt, BigUint, TryFromBigIntError};
18use num_rational::{BigRational, Ratio};
19use serde::{Deserialize, Serialize};
20
21/// Errors propagated by methods in this module.
22#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum DpError {
25 /// Tried to use an invalid float as privacy parameter.
26 #[error(
27 "DP error: input value was not a valid privacy parameter. \
28 It should to be a non-negative, finite float."
29 )]
30 InvalidFloat,
31
32 /// Tried to construct a rational number with zero denominator.
33 #[error("DP error: input denominator was zero.")]
34 ZeroDenominator,
35
36 /// Tried to convert BigInt into something incompatible.
37 #[error("DP error: {0}")]
38 BigIntConversion(#[from] TryFromBigIntError<BigInt>),
39
40 /// Invalid parameter value.
41 #[error("invalid parameter: {0}")]
42 InvalidParameter(String),
43}
44
45/// Positive arbitrary precision rational number to represent DP and noise distribution parameters in
46/// protocol messages and manipulate them without rounding errors.
47#[derive(Clone, Debug)]
48pub struct Rational(Ratio<BigUint>);
49
50impl Rational {
51 /// Construct a [`Rational`] number from numerator `n` and denominator `d`. Errors if denominator is zero.
52 pub fn from_unsigned<T>(n: T, d: T) -> Result<Self, DpError>
53 where
54 T: Into<u128>,
55 {
56 // we don't want to expose BigUint in the public api, hence the Into<u128> bound
57 let d = d.into();
58 if d == 0 {
59 Err(DpError::ZeroDenominator)
60 } else {
61 Ok(Rational(Ratio::<BigUint>::new(n.into().into(), d.into())))
62 }
63 }
64}
65
66impl TryFrom<f32> for Rational {
67 type Error = DpError;
68 /// Constructs a `Rational` from a given `f32` value.
69 ///
70 /// The special float values (NaN, positive and negative infinity) result in
71 /// an error. All other values are represented exactly, without rounding errors.
72 fn try_from(value: f32) -> Result<Self, DpError> {
73 match BigRational::from_float(value) {
74 Some(y) => Ok(Rational(Ratio::<BigUint>::new(
75 y.numer().clone().try_into()?,
76 y.denom().clone().try_into()?,
77 ))),
78 None => Err(DpError::InvalidFloat)?,
79 }
80 }
81}
82
83/// Marker trait for differential privacy budgets (regardless of the specific accounting method).
84pub trait DifferentialPrivacyBudget {}
85
86/// Marker trait for differential privacy scalar noise distributions.
87pub trait DifferentialPrivacyDistribution {}
88
89/// Zero-concentrated differential privacy (ZCDP) budget as defined in [[BS16]].
90///
91/// [BS16]: https://arxiv.org/pdf/1605.02065.pdf
92#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Ord, PartialOrd)]
93pub struct ZCdpBudget {
94 epsilon: Ratio<BigUint>,
95}
96
97impl ZCdpBudget {
98 /// Create a budget for parameter `epsilon`, using the notation from [[CKS20]] where `rho = (epsilon**2)/2`
99 /// for a `rho`-ZCDP budget.
100 ///
101 /// [CKS20]: https://arxiv.org/pdf/2004.00010.pdf
102 // TODO(#1095): This should be fallible, and it should return an error if epsilon is zero.
103 pub fn new(epsilon: Rational) -> Self {
104 Self { epsilon: epsilon.0 }
105 }
106}
107
108impl DifferentialPrivacyBudget for ZCdpBudget {}
109
110/// Pure differential privacy budget. (ε-DP or (ε, 0)-DP)
111#[derive(Clone, Debug, Eq, PartialEq, Serialize, Ord, PartialOrd)]
112pub struct PureDpBudget {
113 epsilon: Ratio<BigUint>,
114}
115
116impl PureDpBudget {
117 /// Create a budget for parameter `epsilon`.
118 pub fn new(epsilon: Rational) -> Result<Self, DpError> {
119 if epsilon.0.numer() == &BigUint::ZERO {
120 return Err(DpError::InvalidParameter("epsilon cannot be zero".into()));
121 }
122 Ok(Self { epsilon: epsilon.0 })
123 }
124}
125
126impl DifferentialPrivacyBudget for PureDpBudget {}
127
128/// This module encapsulates a deserialization helper struct. It is needed so we can wrap its
129/// derived `Deserialize` implementation in a customized `Deserialize` implementation, which makes
130/// use of the budget's constructor to enforce input validation invariants.
131mod budget_serde {
132 use num_bigint::BigUint;
133 use num_rational::Ratio;
134 use serde::{de, Deserialize};
135
136 #[derive(Deserialize)]
137 pub struct PureDpBudget {
138 epsilon: Ratio<BigUint>,
139 }
140
141 impl<'de> Deserialize<'de> for super::PureDpBudget {
142 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
143 where
144 D: serde::Deserializer<'de>,
145 {
146 let helper = PureDpBudget::deserialize(deserializer)?;
147 super::PureDpBudget::new(super::Rational(helper.epsilon))
148 .map_err(|_| de::Error::custom("epsilon cannot be zero"))
149 }
150 }
151}
152
153/// Strategy to make aggregate results differentially private, e.g. by adding noise from a specific
154/// type of distribution instantiated with a given DP budget.
155pub trait DifferentialPrivacyStrategy {
156 /// The type of the DP budget, i.e. the variant of differential privacy that can be obtained
157 /// by using this strategy.
158 type Budget: DifferentialPrivacyBudget;
159
160 /// The distribution type this strategy will use to generate the noise.
161 type Distribution: DifferentialPrivacyDistribution;
162
163 /// The type the sensitivity used for privacy analysis has.
164 type Sensitivity;
165
166 /// Create a strategy from a differential privacy budget. The distribution created with
167 /// `create_distribution` should provide the amount of privacy specified here.
168 fn from_budget(b: Self::Budget) -> Self;
169
170 /// Create a new distribution parametrized s.t. adding samples to the result of a function
171 /// with sensitivity `s` will yield differential privacy of the DP variant given in the
172 /// `Budget` type. Can error upon invalid parameters.
173 fn create_distribution(&self, s: Self::Sensitivity) -> Result<Self::Distribution, DpError>;
174}
175
176pub mod distributions;
177mod rand_bigint;
178
179#[cfg(test)]
180mod tests {
181 use serde_json::json;
182
183 use super::PureDpBudget;
184
185 #[test]
186 fn budget_deserialization() {
187 serde_json::from_value::<PureDpBudget>(json!({"epsilon": [[1], [1]]})).unwrap();
188 serde_json::from_value::<PureDpBudget>(json!({"epsilon": [[0], [1]]})).unwrap_err();
189 serde_json::from_value::<PureDpBudget>(json!({"epsilon": [[1], [0]]})).unwrap_err();
190 }
191}