1use crate::error::{OptimError, Result};
23use scirs2_core::ndarray::Array1;
24use scirs2_core::numeric::Float;
25use std::fmt::Debug;
26
27use super::hashing::{canonical_array_bytes, sha256};
28use super::model_checking::{ModelCheckOutcome, ModelChecker};
29use super::proofs::ProofSystem;
30use super::types::{
31 Axiom, FormalVerificationRule, PrivacyContext, ProofResult, ProofStrategy, SystemProperty,
32 VerificationCriticality, VerificationResult,
33};
34
35fn decided(
37 verified: bool,
38 message: impl Into<String>,
39 proof: Option<Vec<u8>>,
40) -> VerificationResult {
41 VerificationResult {
42 verified,
43 proof,
44 confidence: 1.0,
47 message: message.into(),
48 }
49}
50
51fn commitment<T: Float + Debug + Send + Sync + 'static>(data: &Array1<T>) -> Option<Vec<u8>> {
53 canonical_array_bytes(data)
54 .ok()
55 .map(|bytes| sha256(&[&bytes]).to_vec())
56}
57
58pub fn default_privacy_rules<T: Float + Debug + Send + Sync + 'static>(
60) -> Vec<FormalVerificationRule<T>> {
61 vec![
62 FormalVerificationRule {
63 name: "released_values_are_finite".to_string(),
64 specification: "AG(forall i. finite(data[i]))".to_string(),
65 criticality: VerificationCriticality::Safety,
66 verify_fn: Box::new(|data: &Array1<T>, _context: &PrivacyContext| {
67 let offender = data.iter().position(|value| !value.is_finite());
68 match offender {
69 None => decided(
70 true,
71 format!("all {} released values are finite", data.len()),
72 commitment(data),
73 ),
74 Some(index) => decided(
75 false,
76 format!(
77 "element {index} of the release is not finite; an infinite or NaN \
78 gradient defeats the noise calibration"
79 ),
80 commitment(data),
81 ),
82 }
83 }),
84 },
85 FormalVerificationRule {
86 name: "release_is_non_empty".to_string(),
87 specification: "AG(len(data) > 0)".to_string(),
88 criticality: VerificationCriticality::Correctness,
89 verify_fn: Box::new(|data: &Array1<T>, _context: &PrivacyContext| {
90 decided(
91 !data.is_empty(),
92 if data.is_empty() {
93 "the release is empty, so no property of it can be verified".to_string()
94 } else {
95 format!("the release carries {} values", data.len())
96 },
97 commitment(data),
98 )
99 }),
100 },
101 FormalVerificationRule {
102 name: "l2_norm_is_finite".to_string(),
103 specification: "AG(finite(||data||_2))".to_string(),
104 criticality: VerificationCriticality::Safety,
105 verify_fn: Box::new(|data: &Array1<T>, _context: &PrivacyContext| {
106 let sum_of_squares = data
107 .iter()
108 .filter_map(|value| value.to_f64())
109 .map(|value| value * value)
110 .sum::<f64>();
111 let norm = sum_of_squares.sqrt();
112 decided(
113 norm.is_finite(),
114 format!("the L2 norm of the release is {norm}"),
115 commitment(data),
116 )
117 }),
118 },
119 FormalVerificationRule {
120 name: "epsilon_budget_is_positive_and_finite".to_string(),
121 specification: "AG(epsilon > 0 && finite(epsilon))".to_string(),
122 criticality: VerificationCriticality::Correctness,
123 verify_fn: Box::new(|_data: &Array1<T>, context: &PrivacyContext| {
124 let epsilon = context.epsilon_budget;
125 decided(
126 epsilon.is_finite() && epsilon > 0.0,
127 format!(
128 "epsilon is {epsilon}; a non-positive or infinite epsilon is not a privacy \
129 guarantee"
130 ),
131 None,
132 )
133 }),
134 },
135 FormalVerificationRule {
136 name: "delta_budget_is_in_the_unit_interval".to_string(),
137 specification: "AG(0 <= delta && delta < 1)".to_string(),
138 criticality: VerificationCriticality::Correctness,
139 verify_fn: Box::new(|_data: &Array1<T>, context: &PrivacyContext| {
140 let delta = context.delta_budget;
141 decided(
142 delta.is_finite() && (0.0..1.0).contains(&delta),
143 format!("delta is {delta}; it must lie in [0, 1)"),
144 None,
145 )
146 }),
147 },
148 FormalVerificationRule {
149 name: "privacy_mechanism_is_named".to_string(),
150 specification: "AG(mechanism != \"\")".to_string(),
151 criticality: VerificationCriticality::Correctness,
152 verify_fn: Box::new(|_data: &Array1<T>, context: &PrivacyContext| {
153 let named = !context.privacy_mechanism.trim().is_empty();
154 decided(
155 named,
156 if named {
157 format!("mechanism is `{}`", context.privacy_mechanism)
158 } else {
159 "no privacy mechanism is recorded for this release".to_string()
160 },
161 None,
162 )
163 }),
164 },
165 FormalVerificationRule {
166 name: "gdpr_data_handling_flags_are_declared".to_string(),
167 specification: "AG(data_minimization && purpose_limitation && storage_limitation)"
168 .to_string(),
169 criticality: VerificationCriticality::Optional,
170 verify_fn: Box::new(|_data: &Array1<T>, context: &PrivacyContext| {
171 let mut missing = Vec::new();
172 if !context.data_minimization {
173 missing.push("data_minimization");
174 }
175 if !context.purpose_limitation {
176 missing.push("purpose_limitation");
177 }
178 if !context.storage_limitation {
179 missing.push("storage_limitation");
180 }
181 decided(
182 missing.is_empty(),
183 if missing.is_empty() {
184 "all three GDPR data-handling principles are declared".to_string()
185 } else {
186 format!(
187 "undeclared data-handling principles: {}",
188 missing.join(", ")
189 )
190 },
191 None,
192 )
193 }),
194 },
195 ]
196}
197
198pub struct FormalVerificationEngine<T: Float + Debug + Send + Sync + 'static> {
200 verification_rules: Vec<FormalVerificationRule<T>>,
202 proof_system: ProofSystem<T>,
204 model_checker: ModelChecker<T>,
206 theorem_prover: TheoremProver<T>,
208}
209
210impl<T: Float + Debug + Send + Sync + 'static> FormalVerificationEngine<T> {
211 pub fn new() -> Self {
213 Self {
214 verification_rules: default_privacy_rules(),
215 proof_system: ProofSystem::new(),
216 model_checker: ModelChecker::new(),
217 theorem_prover: TheoremProver::new(),
218 }
219 }
220
221 pub fn empty() -> Self {
226 Self {
227 verification_rules: Vec::new(),
228 proof_system: ProofSystem::empty(),
229 model_checker: ModelChecker::new(),
230 theorem_prover: TheoremProver::empty(),
231 }
232 }
233
234 pub fn rule_count(&self) -> usize {
236 self.verification_rules.len()
237 }
238
239 pub fn rule_names(&self) -> Vec<String> {
241 self.verification_rules
242 .iter()
243 .map(|rule| rule.name.clone())
244 .collect()
245 }
246
247 pub fn add_rule(&mut self, rule: FormalVerificationRule<T>) {
249 self.verification_rules.push(rule);
250 }
251
252 pub fn proof_system(&self) -> &ProofSystem<T> {
254 &self.proof_system
255 }
256
257 pub fn theorem_prover(&self) -> &TheoremProver<T> {
259 &self.theorem_prover
260 }
261
262 pub fn theorem_prover_mut(&mut self) -> &mut TheoremProver<T> {
264 &mut self.theorem_prover
265 }
266
267 pub fn model_checker_mut(&mut self) -> &mut ModelChecker<T> {
269 &mut self.model_checker
270 }
271
272 pub fn verify_all_properties(
277 &self,
278 data: &Array1<T>,
279 context: &PrivacyContext,
280 ) -> Result<Vec<VerificationResult>> {
281 if self.verification_rules.is_empty() {
282 return Err(OptimError::InvalidState(
283 "no formal verification rules are registered; there is nothing to verify, which \
284 is not the same as everything passing"
285 .to_string(),
286 ));
287 }
288 Ok(self
289 .verification_rules
290 .iter()
291 .map(|rule| (rule.verify_fn)(data, context))
292 .collect())
293 }
294
295 pub fn require_all_properties(
298 &self,
299 data: &Array1<T>,
300 context: &PrivacyContext,
301 ) -> Result<Vec<VerificationResult>> {
302 let results = self.verify_all_properties(data, context)?;
303 let mut failures = Vec::new();
304 for (rule, result) in self.verification_rules.iter().zip(results.iter()) {
305 let critical = matches!(
306 rule.criticality,
307 VerificationCriticality::Safety | VerificationCriticality::Correctness
308 );
309 if critical && !result.verified {
310 failures.push(format!("{}: {}", rule.name, result.message));
311 }
312 }
313 if failures.is_empty() {
314 Ok(results)
315 } else {
316 Err(OptimError::InvalidState(format!(
317 "formal verification failed: {}",
318 failures.join("; ")
319 )))
320 }
321 }
322
323 pub fn check_model_property(&self, property: &SystemProperty) -> Result<ModelCheckOutcome> {
325 self.model_checker.check_property(property)
326 }
327}
328
329impl<T: Float + Debug + Send + Sync + 'static> Default for FormalVerificationEngine<T> {
330 fn default() -> Self {
331 Self::new()
332 }
333}
334
335pub struct TheoremProver<T: Float + Debug + Send + Sync + 'static> {
343 axioms: Vec<Axiom<T>>,
345 strategies: Vec<ProofStrategy<T>>,
347}
348
349impl<T: Float + Debug + Send + Sync + 'static> TheoremProver<T> {
350 pub fn new() -> Self {
352 let mut prover = Self::empty();
353 for axiom in default_axioms() {
354 prover.add_axiom(axiom);
355 }
356 prover.add_strategy(all_axioms_strategy());
357 prover
358 }
359
360 pub fn empty() -> Self {
362 Self {
363 axioms: Vec::new(),
364 strategies: Vec::new(),
365 }
366 }
367
368 pub fn add_axiom(&mut self, axiom: Axiom<T>) {
370 self.axioms.push(axiom);
371 }
372
373 pub fn add_strategy(&mut self, strategy: ProofStrategy<T>) {
375 self.strategies.push(strategy);
376 }
377
378 pub fn axiom_names(&self) -> Vec<String> {
380 self.axioms.iter().map(|axiom| axiom.name.clone()).collect()
381 }
382
383 pub fn prove(&self, goal: &str, data: &Array1<T>) -> Result<ProofResult> {
385 if self.axioms.is_empty() && self.strategies.is_empty() {
386 return Err(OptimError::UnsupportedOperation(format!(
387 "cannot prove `{goal}`: the prover has neither axioms nor proof strategies \
388 registered"
389 )));
390 }
391
392 for axiom in &self.axioms {
394 if axiom.name == goal {
395 let holds = (axiom.verify_fn)(data);
396 return Ok(ProofResult {
397 proven: holds,
398 proof_steps: vec![format!(
399 "evaluated axiom `{}` ({}) on the release: {holds}",
400 axiom.name, axiom.statement
401 )],
402 used_axioms: vec![axiom.name.clone()],
403 confidence: 1.0,
404 });
405 }
406 }
407
408 let mut attempted = Vec::new();
410 for strategy in &self.strategies {
411 let result = (strategy.apply_fn)(data, &self.axioms);
412 if result.proven {
413 return Ok(result);
414 }
415 attempted.push(format!(
416 "strategy `{}` did not discharge the goal",
417 strategy.name
418 ));
419 }
420
421 Ok(ProofResult {
422 proven: false,
423 proof_steps: attempted,
424 used_axioms: Vec::new(),
425 confidence: 1.0,
426 })
427 }
428}
429
430impl<T: Float + Debug + Send + Sync + 'static> Default for TheoremProver<T> {
431 fn default() -> Self {
432 Self::new()
433 }
434}
435
436pub fn default_axioms<T: Float + Debug + Send + Sync + 'static>() -> Vec<Axiom<T>> {
438 vec![
439 Axiom {
440 name: "values_are_finite".to_string(),
441 statement: "forall i. finite(data[i])".to_string(),
442 verify_fn: Box::new(|data: &Array1<T>| data.iter().all(|value| value.is_finite())),
443 },
444 Axiom {
445 name: "release_is_non_empty".to_string(),
446 statement: "len(data) > 0".to_string(),
447 verify_fn: Box::new(|data: &Array1<T>| !data.is_empty()),
448 },
449 Axiom {
450 name: "l2_norm_is_finite".to_string(),
451 statement: "finite(||data||_2)".to_string(),
452 verify_fn: Box::new(|data: &Array1<T>| {
453 data.iter()
454 .filter_map(|value| value.to_f64())
455 .map(|value| value * value)
456 .sum::<f64>()
457 .sqrt()
458 .is_finite()
459 }),
460 },
461 ]
462}
463
464pub fn all_axioms_strategy<T: Float + Debug + Send + Sync + 'static>() -> ProofStrategy<T> {
466 ProofStrategy {
467 name: "conjunction_of_axioms".to_string(),
468 apply_fn: Box::new(|data: &Array1<T>, axioms: &[Axiom<T>]| {
469 if axioms.is_empty() {
470 return ProofResult {
471 proven: false,
472 proof_steps: vec!["no axioms are registered".to_string()],
473 used_axioms: Vec::new(),
474 confidence: 1.0,
475 };
476 }
477 let mut steps = Vec::with_capacity(axioms.len());
478 let mut used = Vec::with_capacity(axioms.len());
479 let mut all_hold = true;
480 for axiom in axioms {
481 let holds = (axiom.verify_fn)(data);
482 steps.push(format!("{} => {holds}", axiom.statement));
483 used.push(axiom.name.clone());
484 all_hold &= holds;
485 }
486 ProofResult {
487 proven: all_hold,
488 proof_steps: steps,
489 used_axioms: used,
490 confidence: 1.0,
491 }
492 }),
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 fn context(epsilon: f64, delta: f64) -> PrivacyContext {
501 PrivacyContext {
502 epsilon_budget: epsilon,
503 delta_budget: delta,
504 privacy_mechanism: "dp_sgd".to_string(),
505 data_minimization: true,
506 purpose_limitation: true,
507 storage_limitation: true,
508 }
509 }
510
511 fn good_data() -> Array1<f64> {
512 Array1::from(vec![0.25, -0.5, 1.0])
513 }
514
515 #[test]
516 fn an_engine_with_no_rules_errors_instead_of_reporting_success() {
517 let engine: FormalVerificationEngine<f64> = FormalVerificationEngine::empty();
520 let outcome = engine.verify_all_properties(&good_data(), &context(1.0, 1e-5));
521 let message = match outcome {
522 Err(err) => err.to_string(),
523 Ok(results) => panic!("an empty engine reported {} results", results.len()),
524 };
525 assert!(message.contains("nothing to verify"), "got: {message}");
526 }
527
528 #[test]
529 fn the_default_engine_registers_real_rules_and_passes_a_clean_release() {
530 let engine: FormalVerificationEngine<f64> = FormalVerificationEngine::new();
531 assert!(engine.rule_count() >= 7, "{:?}", engine.rule_names());
532 let results = match engine.verify_all_properties(&good_data(), &context(1.0, 1e-5)) {
533 Ok(results) => results,
534 Err(err) => panic!("verification failed: {err}"),
535 };
536 assert_eq!(results.len(), engine.rule_count());
537 assert!(
538 results.iter().all(|result| result.verified),
539 "a clean release must satisfy every rule: {:?}",
540 results
541 .iter()
542 .filter(|result| !result.verified)
543 .map(|result| result.message.clone())
544 .collect::<Vec<_>>()
545 );
546 assert!(engine
547 .require_all_properties(&good_data(), &context(1.0, 1e-5))
548 .is_ok());
549 }
550
551 #[test]
552 fn a_non_finite_release_is_reported_as_a_failure() {
553 let engine: FormalVerificationEngine<f64> = FormalVerificationEngine::new();
554 let data = Array1::from(vec![0.25, f64::INFINITY, 1.0]);
555 let results = match engine.verify_all_properties(&data, &context(1.0, 1e-5)) {
556 Ok(results) => results,
557 Err(err) => panic!("verification failed: {err}"),
558 };
559 let finite_rule = results
560 .iter()
561 .find(|result| result.message.contains("not finite"));
562 assert!(
563 finite_rule.is_some(),
564 "the finiteness rule must fail and say which element"
565 );
566 assert!(
567 engine
568 .require_all_properties(&data, &context(1.0, 1e-5))
569 .is_err(),
570 "a safety-critical failure must be an error"
571 );
572 }
573
574 #[test]
575 fn an_invalid_privacy_context_is_reported_as_a_failure() {
576 let engine: FormalVerificationEngine<f64> = FormalVerificationEngine::new();
577 for (epsilon, delta) in [(0.0, 1e-5), (-1.0, 1e-5), (1.0, 1.0), (1.0, -0.1)] {
578 let outcome = engine.require_all_properties(&good_data(), &context(epsilon, delta));
579 assert!(
580 outcome.is_err(),
581 "epsilon={epsilon}, delta={delta} must not verify"
582 );
583 }
584 }
585
586 #[test]
587 fn every_rule_result_commits_to_the_checked_vector() {
588 let engine: FormalVerificationEngine<f64> = FormalVerificationEngine::new();
589 let left = match engine.verify_all_properties(&good_data(), &context(1.0, 1e-5)) {
590 Ok(results) => results,
591 Err(err) => panic!("verification failed: {err}"),
592 };
593 let other = Array1::from(vec![0.25, -0.5, 1.5]);
594 let right = match engine.verify_all_properties(&other, &context(1.0, 1e-5)) {
595 Ok(results) => results,
596 Err(err) => panic!("verification failed: {err}"),
597 };
598 let left_proofs: Vec<_> = left.iter().filter_map(|r| r.proof.clone()).collect();
599 let right_proofs: Vec<_> = right.iter().filter_map(|r| r.proof.clone()).collect();
600 assert!(
601 !left_proofs.is_empty(),
602 "data rules must carry a commitment"
603 );
604 assert_ne!(
605 left_proofs, right_proofs,
606 "the commitment must depend on the checked values"
607 );
608 }
609
610 #[test]
611 fn a_prover_with_nothing_registered_refuses_to_prove() {
612 let prover: TheoremProver<f64> = TheoremProver::empty();
613 assert!(prover.prove("values_are_finite", &good_data()).is_err());
614 }
615
616 #[test]
617 fn the_default_prover_discharges_a_true_axiom_and_refutes_a_false_one() {
618 let prover: TheoremProver<f64> = TheoremProver::new();
619 let proven = match prover.prove("values_are_finite", &good_data()) {
620 Ok(result) => result,
621 Err(err) => panic!("prove failed: {err}"),
622 };
623 assert!(proven.proven);
624 assert_eq!(proven.used_axioms, vec!["values_are_finite".to_string()]);
625
626 let bad = Array1::from(vec![f64::NAN]);
627 let refuted = match prover.prove("values_are_finite", &bad) {
628 Ok(result) => result,
629 Err(err) => panic!("prove failed: {err}"),
630 };
631 assert!(
632 !refuted.proven,
633 "a NaN release must not satisfy the finiteness axiom"
634 );
635 }
636
637 #[test]
638 fn an_unknown_goal_falls_through_to_the_strategies_and_is_not_asserted() {
639 let prover: TheoremProver<f64> = TheoremProver::new();
640 let result = match prover.prove("dp_sgd_is_epsilon_dp", &good_data()) {
641 Ok(result) => result,
642 Err(err) => panic!("prove failed: {err}"),
643 };
644 assert!(!result.proof_steps.is_empty());
647 assert!(result.confidence <= 1.0);
648 }
649
650 #[test]
651 fn the_conjunction_strategy_fails_on_a_bad_release() {
652 let prover: TheoremProver<f64> = TheoremProver::new();
653 let empty = Array1::from(Vec::<f64>::new());
654 let result = match prover.prove("anything_at_all", &empty) {
655 Ok(result) => result,
656 Err(err) => panic!("prove failed: {err}"),
657 };
658 assert!(
659 !result.proven,
660 "an empty release violates release_is_non_empty"
661 );
662 }
663}