1use 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
15pub const RENYI_ALPHA: u32 = 64;
17
18pub const EPSILON_MAX: f64 = 1e-6;
20
21pub const DELTA_MAX: f64 = 1e-5;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DpConfig {
27 pub renyi_alpha: u32,
29 pub epsilon_max: f64,
31 pub delta_max: f64,
33 pub buffer_cap: usize,
35 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#[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
61pub struct DpEngine {
66 config: DpConfig,
67 epsilon_consumed: f64,
69 delta_consumed: f64,
71 buffer: Vec<PrivacyQuery>,
73 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 pub fn apply_dp(
97 &mut self,
98 query: PrivacyQuery,
99 chaos_perturbation: f64,
100 ) -> Result<DpNoiseFrame, PrivacyError> {
101 if query.sensitivity <= 0.0 || query.sensitivity > 4096.0 {
103 return Err(PrivacyError::InvalidSensitivity(query.sensitivity));
104 }
105
106 let new_epsilon = self.epsilon_consumed + query.epsilon;
108 if new_epsilon > self.config.epsilon_max * 1000.0 {
109 return Err(PrivacyError::PrivacyBudgetExhausted {
111 epsilon: new_epsilon,
112 limit: self.config.epsilon_max * 1000.0,
113 });
114 }
115
116 let base_scale = match self.config.mechanism {
118 DpMechanism::Laplace => query.sensitivity / query.epsilon,
119 DpMechanism::Gaussian => {
120 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 let noise_scale = base_scale * (1.0 + chaos_perturbation.abs() * 0.05);
128
129 self.epsilon_consumed += query.epsilon;
131 self.delta_consumed += query.delta;
132 self.query_count += 1;
133
134 self.buffer.push(query.clone());
136 if self.buffer.len() >= self.config.buffer_cap * 9 / 10 {
137 self.flush_buffer();
139 }
140
141 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 pub fn noise_sample(
158 &self,
159 noise_scale: f64,
160 chaos_seed: &[u8; 32],
161 ) -> f64 {
162 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); match self.config.mechanism {
168 DpMechanism::Laplace => {
169 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); -noise_scale * sign * (1.0 - 2.0 * abs_u).ln()
174 }
175 DpMechanism::Gaussian => {
176 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 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 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 pub fn epsilon_consumed(&self) -> f64 {
218 self.epsilon_consumed
219 }
220
221 pub fn query_count(&self) -> u64 {
223 self.query_count
224 }
225
226 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 fn compute_renyi_bound(&self, epsilon: f64, alpha: u32) -> f64 {
239 (alpha as f64) * epsilon * epsilon / 2.0
240 }
241
242 fn flush_buffer(&mut self) {
244 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}