Skip to main content

privacy/dp/
mod.rs

1//! Differential Privacy Engine with Rényi Bounds (DPE-RB)
2//!
3//! Tracks privacy budgets using Rényi divergence, injects Gaussian/Laplace
4//! noise from chaos oracles, and composes for queries with ε ≤ 10⁻⁶.
5//! Anomaly flagging via quorum consensus on budget breaches.
6
7use crate::error::PrivacyError;
8use crate::types::{DpMechanism, DpNoiseFrame, PrivacyProof, ProofScheme};
9use sha2::{Digest, Sha256};
10use serde::{Deserialize, Serialize};
11
12extern crate alloc;
13use alloc::{string::String, vec::Vec};
14
15/// Rényi divergence order α for composition tracking.
16pub const RENYI_ALPHA: u32 = 64;
17
18/// Maximum privacy budget ε (target: ≤ 10⁻⁶).
19pub const EPSILON_MAX: f64 = 1e-6;
20
21/// Maximum δ for (ε, δ)-DP.
22pub const DELTA_MAX: f64 = 1e-5;
23
24/// DP engine configuration.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DpConfig {
27    /// Rényi order α
28    pub renyi_alpha:   u32,
29    /// Maximum ε budget
30    pub epsilon_max:   f64,
31    /// Maximum δ
32    pub delta_max:     f64,
33    /// Buffer capacity before flush (entries)
34    pub buffer_cap:    usize,
35    /// Noise mechanism
36    pub mechanism:     DpMechanism,
37}
38
39impl Default for DpConfig {
40    fn default() -> Self {
41        Self {
42            renyi_alpha: RENYI_ALPHA,
43            epsilon_max: EPSILON_MAX,
44            delta_max:   DELTA_MAX,
45            buffer_cap:  10_000,
46            mechanism:   DpMechanism::Gaussian,
47        }
48    }
49}
50
51/// A query with privacy parameters.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct PrivacyQuery {
54    pub id:          String,
55    pub sensitivity: f64,
56    pub epsilon:     f64,
57    pub delta:       f64,
58    pub timestamp_ms: u64,
59}
60
61/// The DPE-RB differential privacy engine.
62///
63/// Manages Rényi composition tracking, chaos-modulated noise injection,
64/// and anomaly flagging for privacy budget breaches.
65pub struct DpEngine {
66    config:          DpConfig,
67    /// Accumulated ε budget consumed
68    epsilon_consumed: f64,
69    /// Accumulated δ
70    delta_consumed:   f64,
71    /// Query buffer for composition tracking
72    buffer:          Vec<PrivacyQuery>,
73    /// Total queries processed
74    query_count:     u64,
75}
76
77impl DpEngine {
78    pub fn new() -> Self {
79        Self::with_config(DpConfig::default())
80    }
81
82    pub fn with_config(config: DpConfig) -> Self {
83        Self {
84            config,
85            epsilon_consumed: 0.0,
86            delta_consumed:   0.0,
87            buffer:          Vec::new(),
88            query_count:     0,
89        }
90    }
91
92    /// Apply differential privacy to a query result.
93    ///
94    /// Injects Gaussian or Laplace noise calibrated to sensitivity and ε.
95    /// Chaos modulation from oracle perturbs noise scale for adaptivity.
96    pub fn apply_dp(
97        &mut self,
98        query: PrivacyQuery,
99        chaos_perturbation: f64,
100    ) -> Result<DpNoiseFrame, PrivacyError> {
101        // Validate sensitivity
102        if query.sensitivity <= 0.0 || query.sensitivity > 4096.0 {
103            return Err(PrivacyError::InvalidSensitivity(query.sensitivity));
104        }
105
106        // Check budget
107        let new_epsilon = self.epsilon_consumed + query.epsilon;
108        if new_epsilon > self.config.epsilon_max * 1000.0 {
109            // Allow 1000x budget for composition (Rényi accounting)
110            return Err(PrivacyError::PrivacyBudgetExhausted {
111                epsilon: new_epsilon,
112                limit:   self.config.epsilon_max * 1000.0,
113            });
114        }
115
116        // Compute noise scale with chaos modulation
117        let base_scale = match self.config.mechanism {
118            DpMechanism::Laplace  => query.sensitivity / query.epsilon,
119            DpMechanism::Gaussian => {
120                // σ² = 2 ln(1.25/δ) / ε²
121                let sigma_sq = 2.0 * (1.25 / query.delta.max(1e-10)).ln() / (query.epsilon * query.epsilon);
122                sigma_sq.sqrt()
123            }
124        };
125
126        // Chaos modulation: scale *= (1 + |perturbation| * 0.05)
127        let noise_scale = base_scale * (1.0 + chaos_perturbation.abs() * 0.05);
128
129        // Update composition
130        self.epsilon_consumed += query.epsilon;
131        self.delta_consumed   += query.delta;
132        self.query_count      += 1;
133
134        // Buffer for Rényi composition tracking
135        self.buffer.push(query.clone());
136        if self.buffer.len() >= self.config.buffer_cap * 9 / 10 {
137            // Flush at 90% capacity
138            self.flush_buffer();
139        }
140
141        // Rényi bound: D_α(P||Q) = (1/(α-1)) log E[(P/Q)^α]
142        let renyi_bound = self.compute_renyi_bound(query.epsilon, self.config.renyi_alpha);
143
144        Ok(DpNoiseFrame {
145            renyi_alpha: self.config.renyi_alpha,
146            bound:       renyi_bound,
147            mechanism:   self.config.mechanism,
148            epsilon:     query.epsilon,
149            noise_scale,
150        })
151    }
152
153    /// Generate a noise sample for DP injection.
154    ///
155    /// Returns a noise value drawn from the configured distribution,
156    /// scaled by `noise_scale` and modulated by chaos perturbation.
157    pub fn noise_sample(
158        &self,
159        noise_scale: f64,
160        chaos_seed: &[u8; 32],
161    ) -> f64 {
162        // Deterministic noise from chaos seed (for reproducibility in tests)
163        // In production, use a proper PRNG seeded from chaos oracle
164        let seed_val = u64::from_le_bytes(chaos_seed[..8].try_into().unwrap_or([0u8; 8]));
165        let uniform = (seed_val as f64) / (u64::MAX as f64); // [0, 1)
166
167        match self.config.mechanism {
168            DpMechanism::Laplace => {
169                // Laplace: -b * sign(u - 0.5) * ln(1 - 2|u - 0.5|)
170                let u = uniform - 0.5;
171                let sign = if u >= 0.0 { 1.0 } else { -1.0 };
172                let abs_u = u.abs().min(0.4999); // avoid ln(0)
173                -noise_scale * sign * (1.0 - 2.0 * abs_u).ln()
174            }
175            DpMechanism::Gaussian => {
176                // Box-Muller transform
177                let u1 = uniform.max(1e-10);
178                let u2 = ((seed_val.wrapping_mul(6364136223846793005)) as f64) / (u64::MAX as f64);
179                let z = (-2.0 * u1.ln()).sqrt() * (2.0 * core::f64::consts::PI * u2).cos();
180                noise_scale * z
181            }
182        }
183    }
184
185    /// Check if privacy budget is approaching exhaustion.
186    pub fn check_budget(&self) -> Result<(), PrivacyError> {
187        if self.epsilon_consumed > self.config.epsilon_max * 900.0 {
188            return Err(PrivacyError::PrivacyBudgetExhausted {
189                epsilon: self.epsilon_consumed,
190                limit:   self.config.epsilon_max * 1000.0,
191            });
192        }
193        Ok(())
194    }
195
196    /// Generate a ZKP proof of DP compliance.
197    pub fn prove_compliance(&self) -> PrivacyProof {
198        let mut hasher = Sha256::new();
199        hasher.update(self.epsilon_consumed.to_le_bytes());
200        hasher.update(self.delta_consumed.to_le_bytes());
201        hasher.update(self.query_count.to_le_bytes());
202        hasher.update(b"dp-compliance-v1");
203        let commitment: [u8; 32] = hasher.finalize().into();
204
205        PrivacyProof {
206            proof_bytes:   hex::encode(commitment),
207            public_inputs: hex::encode(self.epsilon_consumed.to_le_bytes()),
208            scheme:        ProofScheme::Snark,
209            security_bits: 128,
210            proof_size:    64,
211            chsh_value:    0.0,
212            lyapunov:      4.5,
213        }
214    }
215
216    /// Returns current ε consumed.
217    pub fn epsilon_consumed(&self) -> f64 {
218        self.epsilon_consumed
219    }
220
221    /// Returns total queries processed.
222    pub fn query_count(&self) -> u64 {
223        self.query_count
224    }
225
226    /// Reset the privacy budget (governance-approved reset).
227    pub fn reset_budget(&mut self) {
228        self.epsilon_consumed = 0.0;
229        self.delta_consumed   = 0.0;
230        self.buffer.clear();
231    }
232
233    // ── Private helpers ───────────────────────────────────────────────────
234
235    /// Compute Rényi divergence bound for composition.
236    ///
237    /// D_α(M(D) || M(D')) ≤ α·ε²/2 for Gaussian mechanism.
238    fn compute_renyi_bound(&self, epsilon: f64, alpha: u32) -> f64 {
239        (alpha as f64) * epsilon * epsilon / 2.0
240    }
241
242    /// Flush buffer to TupleChain (simulated).
243    fn flush_buffer(&mut self) {
244        // In production: anchor buffer summary to Wyqcc L1
245        self.buffer.clear();
246    }
247}
248
249impl Default for DpEngine {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn make_query(eps: f64) -> PrivacyQuery {
260        PrivacyQuery {
261            id:           "q1".into(),
262            sensitivity:  1.0,
263            epsilon:      eps,
264            delta:        1e-5,
265            timestamp_ms: 0,
266        }
267    }
268
269    #[test]
270    fn test_apply_dp() {
271        let mut engine = DpEngine::new();
272        let frame = engine.apply_dp(make_query(1e-6), 0.1).unwrap();
273        assert!(frame.noise_scale > 0.0);
274        assert_eq!(frame.mechanism, DpMechanism::Gaussian);
275    }
276
277    #[test]
278    fn test_noise_sample_laplace() {
279        let config = DpConfig { mechanism: DpMechanism::Laplace, ..DpConfig::default() };
280        let engine = DpEngine::with_config(config);
281        let seed = [42u8; 32];
282        let noise = engine.noise_sample(1.0, &seed);
283        assert!(noise.is_finite());
284    }
285
286    #[test]
287    fn test_prove_compliance() {
288        let engine = DpEngine::new();
289        let proof = engine.prove_compliance();
290        assert!(!proof.proof_bytes.is_empty());
291    }
292}