1use rand::rngs::StdRng;
7use rand::{Rng, SeedableRng};
8use rand_distr::{Distribution, Normal};
9
10use crate::error::FederationError;
11use crate::types::{DiffPrivacyProof, NoiseMechanism};
12
13pub struct DiffPrivacyEngine {
15 epsilon: f64,
17 delta: f64,
19 sensitivity: f64,
21 clipping_norm: f64,
23 mechanism: NoiseMechanism,
25 rng: StdRng,
27}
28
29impl DiffPrivacyEngine {
30 pub fn gaussian(
34 epsilon: f64,
35 delta: f64,
36 sensitivity: f64,
37 clipping_norm: f64,
38 ) -> Result<Self, FederationError> {
39 if epsilon <= 0.0 {
40 return Err(FederationError::InvalidEpsilon(epsilon));
41 }
42 if delta <= 0.0 || delta >= 1.0 {
43 return Err(FederationError::InvalidDelta(delta));
44 }
45 Ok(Self {
46 epsilon,
47 delta,
48 sensitivity,
49 clipping_norm,
50 mechanism: NoiseMechanism::Gaussian,
51 rng: StdRng::from_rng(rand::thread_rng()).unwrap(),
52 })
53 }
54
55 pub fn laplace(
57 epsilon: f64,
58 sensitivity: f64,
59 clipping_norm: f64,
60 ) -> Result<Self, FederationError> {
61 if epsilon <= 0.0 {
62 return Err(FederationError::InvalidEpsilon(epsilon));
63 }
64 Ok(Self {
65 epsilon,
66 delta: 0.0,
67 sensitivity,
68 clipping_norm,
69 mechanism: NoiseMechanism::Laplace,
70 rng: StdRng::from_rng(rand::thread_rng()).unwrap(),
71 })
72 }
73
74 pub fn with_seed(mut self, seed: u64) -> Self {
76 self.rng = StdRng::seed_from_u64(seed);
77 self
78 }
79
80 fn gaussian_sigma(&self) -> f64 {
82 self.sensitivity * (2.0_f64 * (1.25_f64 / self.delta).ln()).sqrt() / self.epsilon
83 }
84
85 fn laplace_scale(&self) -> f64 {
87 self.sensitivity / self.epsilon
88 }
89
90 pub fn clip_gradients(&self, gradients: &mut [f64]) {
92 let norm: f64 = gradients.iter().map(|x| x * x).sum::<f64>().sqrt();
93 if norm > self.clipping_norm {
94 let scale = self.clipping_norm / norm;
95 for g in gradients.iter_mut() {
96 *g *= scale;
97 }
98 }
99 }
100
101 pub fn add_noise(&mut self, params: &mut [f64]) -> DiffPrivacyProof {
105 self.clip_gradients(params);
106
107 match self.mechanism {
108 NoiseMechanism::Gaussian => {
109 let sigma = self.gaussian_sigma();
110 let normal = Normal::new(0.0, sigma).unwrap();
111 for p in params.iter_mut() {
112 *p += normal.sample(&mut self.rng);
113 }
114 DiffPrivacyProof {
115 epsilon: self.epsilon,
116 delta: self.delta,
117 mechanism: NoiseMechanism::Gaussian,
118 sensitivity: self.sensitivity,
119 clipping_norm: self.clipping_norm,
120 noise_scale: sigma,
121 noised_parameter_count: params.len() as u64,
122 }
123 }
124 NoiseMechanism::Laplace => {
125 let b = self.laplace_scale();
126 for p in params.iter_mut() {
127 let u: f64 = self.rng.gen::<f64>() - 0.5;
129 let noise = -b * u.signum() * (1.0 - 2.0 * u.abs()).ln();
130 *p += noise;
131 }
132 DiffPrivacyProof {
133 epsilon: self.epsilon,
134 delta: 0.0,
135 mechanism: NoiseMechanism::Laplace,
136 sensitivity: self.sensitivity,
137 clipping_norm: self.clipping_norm,
138 noise_scale: b,
139 noised_parameter_count: params.len() as u64,
140 }
141 }
142 }
143 }
144
145 pub fn add_noise_scalar(&mut self, value: &mut f64) -> f64 {
147 let mut v = [*value];
148 self.add_noise(&mut v);
149 *value = v[0];
150 v[0]
151 }
152
153 pub fn epsilon(&self) -> f64 {
155 self.epsilon
156 }
157
158 pub fn delta(&self) -> f64 {
160 self.delta
161 }
162}
163
164pub struct PrivacyAccountant {
171 epsilon_limit: f64,
173 target_delta: f64,
175 rdp_alphas: Vec<(f64, f64)>,
178 history: Vec<ExportRecord>,
180}
181
182#[derive(Clone, Debug)]
184pub struct ExportRecord {
185 pub timestamp_s: u64,
187 pub epsilon: f64,
189 pub delta: f64,
191 pub mechanism: NoiseMechanism,
193 pub parameter_count: u64,
195}
196
197impl PrivacyAccountant {
198 pub fn new(epsilon_limit: f64, target_delta: f64) -> Self {
200 let alphas: Vec<f64> = vec![
202 1.5, 1.75, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0,
203 1024.0,
204 ];
205 let rdp_alphas = alphas.into_iter().map(|a| (a, 0.0)).collect();
206 Self {
207 epsilon_limit,
208 target_delta,
209 rdp_alphas,
210 history: Vec::new(),
211 }
212 }
213
214 fn gaussian_rdp(alpha: f64, sigma: f64) -> f64 {
216 alpha / (2.0 * sigma * sigma)
217 }
218
219 fn rdp_to_dp(alpha: f64, rdp_epsilon: f64, delta: f64) -> f64 {
221 rdp_epsilon - (delta.ln()) / (alpha - 1.0)
222 }
223
224 pub fn record_gaussian(&mut self, sigma: f64, epsilon: f64, delta: f64, parameter_count: u64) {
226 for (alpha, rdp_eps) in &mut self.rdp_alphas {
228 *rdp_eps += Self::gaussian_rdp(*alpha, sigma);
229 }
230 self.history.push(ExportRecord {
231 timestamp_s: 0,
232 epsilon,
233 delta,
234 mechanism: NoiseMechanism::Gaussian,
235 parameter_count,
236 });
237 }
238
239 pub fn record_laplace(&mut self, epsilon: f64, parameter_count: u64) {
241 for (alpha, rdp_eps) in &mut self.rdp_alphas {
244 if *alpha > 1.0 {
245 *rdp_eps += *alpha * epsilon / (*alpha - 1.0);
246 }
247 }
248 self.history.push(ExportRecord {
249 timestamp_s: 0,
250 epsilon,
251 delta: 0.0,
252 mechanism: NoiseMechanism::Laplace,
253 parameter_count,
254 });
255 }
256
257 pub fn current_epsilon(&self) -> f64 {
259 self.rdp_alphas
260 .iter()
261 .map(|(alpha, rdp_eps)| Self::rdp_to_dp(*alpha, *rdp_eps, self.target_delta))
262 .fold(f64::INFINITY, f64::min)
263 }
264
265 pub fn remaining_budget(&self) -> f64 {
267 (self.epsilon_limit - self.current_epsilon()).max(0.0)
268 }
269
270 pub fn can_afford(&self, additional_epsilon: f64) -> bool {
272 self.current_epsilon() + additional_epsilon <= self.epsilon_limit
273 }
274
275 pub fn is_exhausted(&self) -> bool {
277 self.current_epsilon() >= self.epsilon_limit
278 }
279
280 pub fn budget_fraction_used(&self) -> f64 {
282 self.current_epsilon() / self.epsilon_limit
283 }
284
285 pub fn export_count(&self) -> usize {
287 self.history.len()
288 }
289
290 pub fn history(&self) -> &[ExportRecord] {
292 &self.history
293 }
294
295 pub fn epsilon_limit(&self) -> f64 {
297 self.epsilon_limit
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn gaussian_engine_creates() {
307 let engine = DiffPrivacyEngine::gaussian(1.0, 1e-5, 1.0, 1.0);
308 assert!(engine.is_ok());
309 }
310
311 #[test]
312 fn invalid_epsilon_rejected() {
313 let engine = DiffPrivacyEngine::gaussian(0.0, 1e-5, 1.0, 1.0);
314 assert!(engine.is_err());
315 let engine = DiffPrivacyEngine::gaussian(-1.0, 1e-5, 1.0, 1.0);
316 assert!(engine.is_err());
317 }
318
319 #[test]
320 fn invalid_delta_rejected() {
321 let engine = DiffPrivacyEngine::gaussian(1.0, 0.0, 1.0, 1.0);
322 assert!(engine.is_err());
323 let engine = DiffPrivacyEngine::gaussian(1.0, 1.0, 1.0, 1.0);
324 assert!(engine.is_err());
325 }
326
327 #[test]
328 fn gradient_clipping() {
329 let engine = DiffPrivacyEngine::gaussian(1.0, 1e-5, 1.0, 1.0).unwrap();
330 let mut grads = vec![3.0, 4.0]; engine.clip_gradients(&mut grads);
332 let norm: f64 = grads.iter().map(|x| x * x).sum::<f64>().sqrt();
333 assert!((norm - 1.0).abs() < 1e-6); }
335
336 #[test]
337 fn gradient_no_clip_when_small() {
338 let engine = DiffPrivacyEngine::gaussian(1.0, 1e-5, 1.0, 10.0).unwrap();
339 let mut grads = vec![3.0, 4.0]; engine.clip_gradients(&mut grads);
341 assert!((grads[0] - 3.0).abs() < 1e-10);
342 assert!((grads[1] - 4.0).abs() < 1e-10);
343 }
344
345 #[test]
346 fn add_noise_gaussian_deterministic() {
347 let mut engine = DiffPrivacyEngine::gaussian(1.0, 1e-5, 1.0, 100.0)
348 .unwrap()
349 .with_seed(42);
350 let mut params = vec![1.0, 2.0, 3.0];
351 let original = params.clone();
352 let proof = engine.add_noise(&mut params);
353 assert_eq!(proof.mechanism, NoiseMechanism::Gaussian);
354 assert_eq!(proof.noised_parameter_count, 3);
355 assert!(params
357 .iter()
358 .zip(original.iter())
359 .any(|(a, b)| (a - b).abs() > 1e-10));
360 }
361
362 #[test]
363 fn add_noise_laplace_deterministic() {
364 let mut engine = DiffPrivacyEngine::laplace(1.0, 1.0, 100.0)
365 .unwrap()
366 .with_seed(42);
367 let mut params = vec![1.0, 2.0, 3.0];
368 let proof = engine.add_noise(&mut params);
369 assert_eq!(proof.mechanism, NoiseMechanism::Laplace);
370 assert_eq!(proof.noised_parameter_count, 3);
371 }
372
373 #[test]
374 fn privacy_accountant_initial_state() {
375 let acc = PrivacyAccountant::new(10.0, 1e-5);
376 assert_eq!(acc.export_count(), 0);
377 assert!(!acc.is_exhausted());
378 assert!(acc.can_afford(1.0));
379 assert!(acc.remaining_budget() > 9.9);
380 }
381
382 #[test]
383 fn privacy_accountant_tracks_gaussian() {
384 let mut acc = PrivacyAccountant::new(10.0, 1e-5);
385 acc.record_gaussian(1.0, 1.0, 1e-5, 100);
387 assert_eq!(acc.export_count(), 1);
388 let eps = acc.current_epsilon();
389 assert!(eps > 0.0);
390 assert!(eps < 10.0);
391 }
392
393 #[test]
394 fn privacy_accountant_composition() {
395 let mut acc = PrivacyAccountant::new(10.0, 1e-5);
396 let eps_after_1 = {
397 acc.record_gaussian(1.0, 1.0, 1e-5, 100);
398 acc.current_epsilon()
399 };
400 acc.record_gaussian(1.0, 1.0, 1e-5, 100);
401 let eps_after_2 = acc.current_epsilon();
402 assert!(eps_after_2 > eps_after_1);
404 }
405
406 #[test]
407 fn privacy_accountant_exhaustion() {
408 let mut acc = PrivacyAccountant::new(1.0, 1e-5);
409 for _ in 0..100 {
411 acc.record_gaussian(0.1, 10.0, 1e-5, 10);
412 }
413 assert!(acc.is_exhausted());
414 assert!(!acc.can_afford(0.1));
415 }
416}