#![allow(deprecated)]
#[cfg(not(any(feature = "std", test)))]
use crate::math::FloatExt;
use crate::math::{f64_i64, hash_index, i64_f64, u64_f64};
use alice_det_math::ln64;
#[cfg(feature = "std")]
use alice_det_math::exp64;
#[derive(Clone, Debug)]
#[deprecated(
since = "0.4.0",
note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
)]
pub struct XorShift64 {
state: u64,
}
impl XorShift64 {
#[inline]
#[must_use]
pub const fn new(seed: u64) -> Self {
Self {
state: if seed == 0 {
0x853c_49e6_748f_ea9b
} else {
seed
},
}
}
#[cfg(feature = "std")]
#[must_use]
pub fn from_entropy() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0x853c_49e6_748f_ea9b, |d| {
#[allow(clippy::cast_possible_truncation)]
let low = d.as_nanos() as u64;
low
});
Self::new(seed)
}
#[inline]
pub const fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
#[inline(always)]
pub fn next_f64(&mut self) -> f64 {
const RCP_POW2_53: f64 = 1.0 / u64_f64(1u64 << 53);
u64_f64(self.next_u64() >> 11) * RCP_POW2_53
}
#[inline]
pub fn next_f64_range(&mut self, low: f64, high: f64) -> f64 {
(high - low).mul_add(self.next_f64(), low)
}
#[inline]
pub fn next_bool(&mut self, p: f64) -> bool {
self.next_f64() < p
}
}
#[cfg(feature = "std")]
impl Default for XorShift64 {
fn default() -> Self {
Self::from_entropy()
}
}
#[derive(Clone, Debug)]
#[deprecated(
since = "0.4.0",
note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
)]
pub struct LaplaceNoise {
scale: f64,
rng: XorShift64,
}
impl LaplaceNoise {
#[cfg(feature = "std")]
#[must_use]
pub fn new(sensitivity: f64, epsilon: f64) -> Self {
let scale = sensitivity / epsilon;
Self {
scale,
rng: XorShift64::from_entropy(),
}
}
#[must_use]
pub fn with_seed(sensitivity: f64, epsilon: f64, seed: u64) -> Self {
let scale = sensitivity / epsilon;
Self {
scale,
rng: XorShift64::new(seed),
}
}
#[inline]
pub fn sample(&mut self) -> f64 {
let u = self.rng.next_f64() - 0.5;
let sign = if u < 0.0 { -1.0 } else { 1.0 };
-sign * self.scale * ln64(2.0f64.mul_add(-u.abs(), 1.0))
}
#[inline]
pub fn privatize(&mut self, value: f64) -> f64 {
value + self.sample()
}
#[inline]
pub fn privatize_int(&mut self, value: i64) -> i64 {
f64_i64((i64_f64(value) + self.sample()).round())
}
#[inline]
#[must_use]
pub const fn scale(&self) -> f64 {
self.scale
}
}
#[derive(Clone, Debug)]
#[deprecated(
since = "0.4.0",
note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
)]
pub struct RandomizedResponse {
p_true: f64,
rng: XorShift64,
}
impl RandomizedResponse {
#[cfg(feature = "std")]
#[must_use]
pub fn new(epsilon: f64) -> Self {
assert!(
epsilon.is_finite() && epsilon >= 0.0,
"epsilon must be finite and non-negative"
);
let p_true = 1.0 / (1.0 + exp64(-epsilon));
Self {
p_true,
rng: XorShift64::from_entropy(),
}
}
#[must_use]
#[allow(clippy::manual_range_contains)]
pub const fn with_probability(p_true: f64, seed: u64) -> Self {
assert!(
p_true >= 0.5 && p_true <= 1.0,
"p_true must be in [0.5, 1.0]"
);
Self {
p_true,
rng: XorShift64::new(seed),
}
}
#[inline]
pub fn privatize(&mut self, value: bool) -> bool {
if self.rng.next_bool(self.p_true) {
value
} else {
self.rng.next_bool(0.5)
}
}
#[inline]
pub fn privatize_bit(&mut self, bit: u8) -> u8 {
u8::from(self.privatize(bit != 0))
}
#[inline]
#[must_use]
pub const fn p_true(&self) -> f64 {
self.p_true
}
#[must_use]
pub fn estimate_proportion(p_true: f64, n: u64, k: u64) -> f64 {
if n == 0 {
return 0.0;
}
let inv_n = 1.0 / u64_f64(n);
let observed_rate = u64_f64(k) * inv_n;
let inv_p_true = 1.0 / p_true;
let true_rate = 0.5f64.mul_add(p_true, observed_rate - 0.5) * inv_p_true;
true_rate.clamp(0.0, 1.0)
}
}
#[deprecated(
since = "0.4.0",
note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
)]
pub const RAPPOR_BITS: usize = 64;
#[derive(Clone, Debug)]
#[deprecated(
since = "0.4.0",
note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
)]
pub struct Rappor {
f: f64,
p: f64,
q: f64,
rng: XorShift64,
}
impl Rappor {
pub const BITS: usize = RAPPOR_BITS;
#[cfg(feature = "std")]
#[must_use]
pub fn new(f: f64, p: f64, q: f64) -> Self {
Self::with_seed_params(f, p, q, XorShift64::from_entropy().next_u64())
}
#[must_use]
pub fn with_seed_params(f: f64, p: f64, q: f64, seed: u64) -> Self {
Self {
f: f.clamp(0.0, 0.5),
p: p.clamp(0.0, 1.0),
q: q.clamp(0.0, 1.0),
rng: XorShift64::new(seed),
}
}
#[cfg(feature = "std")]
#[must_use]
pub fn default_params() -> Self {
Self::new(0.5, 0.75, 0.25)
}
#[allow(clippy::unused_self)]
fn encode_bloom(&self, value: u64) -> [u8; RAPPOR_BITS] {
use crate::sketch::FnvHasher;
let mut bloom = [0u8; RAPPOR_BITS];
for i in 0..3u128 {
let h = FnvHasher::hash_u128(u128::from(value) | (i << 64));
let idx = hash_index(h) % RAPPOR_BITS;
bloom[idx] = 1;
}
bloom
}
fn permanent_response(&mut self, bloom: &[u8; RAPPOR_BITS]) -> [u8; RAPPOR_BITS] {
let mut result = [0u8; RAPPOR_BITS];
for i in 0..RAPPOR_BITS {
if self.rng.next_bool(self.f) {
result[i] = u8::from(self.rng.next_bool(0.5));
} else {
result[i] = bloom[i];
}
}
result
}
fn instantaneous_response(&mut self, permanent: &[u8; RAPPOR_BITS]) -> [u8; RAPPOR_BITS] {
let mut result = [0u8; RAPPOR_BITS];
for i in 0..RAPPOR_BITS {
if permanent[i] == 1 {
result[i] = u8::from(self.rng.next_bool(self.p));
} else {
result[i] = u8::from(self.rng.next_bool(self.q));
}
}
result
}
pub fn privatize(&mut self, value: u64) -> [u8; RAPPOR_BITS] {
let bloom = self.encode_bloom(value);
let permanent = self.permanent_response(&bloom);
self.instantaneous_response(&permanent)
}
#[must_use]
pub const fn params(&self) -> (f64, f64, f64) {
(self.f, self.p, self.q)
}
}
#[derive(Clone, Debug)]
#[deprecated(
since = "0.4.0",
note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
)]
pub struct PrivacyBudget {
total_epsilon: f64,
max_epsilon: f64,
query_count: u64,
}
impl PrivacyBudget {
#[must_use]
pub const fn new(max_epsilon: f64) -> Self {
Self {
total_epsilon: 0.0,
max_epsilon,
query_count: 0,
}
}
pub fn try_spend(&mut self, epsilon: f64) -> bool {
if !epsilon.is_finite() || epsilon < 0.0 {
return false;
}
if self.total_epsilon + epsilon <= self.max_epsilon {
self.total_epsilon += epsilon;
self.query_count += 1;
true
} else {
false
}
}
#[inline]
#[must_use]
pub fn remaining(&self) -> f64 {
(self.max_epsilon - self.total_epsilon).max(0.0)
}
#[inline]
#[must_use]
pub const fn spent(&self) -> f64 {
self.total_epsilon
}
#[inline]
#[must_use]
pub const fn query_count(&self) -> u64 {
self.query_count
}
#[inline]
#[must_use]
pub fn is_exhausted(&self) -> bool {
self.total_epsilon >= self.max_epsilon
}
pub const fn reset(&mut self) {
self.total_epsilon = 0.0;
self.query_count = 0;
}
}
#[derive(Clone, Debug)]
#[deprecated(
since = "0.4.0",
note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
)]
pub struct PrivateAggregator {
noisy_sum: f64,
count: u64,
noise_scale: f64,
}
impl PrivateAggregator {
#[must_use]
pub const fn new(noise_scale: f64) -> Self {
Self {
noisy_sum: 0.0,
count: 0,
noise_scale,
}
}
#[inline]
pub fn add(&mut self, noisy_value: f64) {
self.noisy_sum += noisy_value;
self.count += 1;
}
#[must_use]
pub fn estimate_mean(&self) -> f64 {
if self.count == 0 {
0.0
} else {
let inv_count = 1.0 / u64_f64(self.count);
self.noisy_sum * inv_count
}
}
#[must_use]
pub const fn estimate_sum(&self) -> f64 {
self.noisy_sum
}
#[must_use]
pub fn standard_error(&self) -> f64 {
if self.count == 0 {
f64::INFINITY
} else {
let inv_sqrt_n = 1.0 / (u64_f64(self.count)).sqrt();
self.noise_scale * core::f64::consts::SQRT_2 * inv_sqrt_n
}
}
#[inline]
#[must_use]
pub const fn count(&self) -> u64 {
self.count
}
pub const fn reset(&mut self) {
self.noisy_sum = 0.0;
self.count = 0;
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn test_xorshift() {
let mut rng = XorShift64::new(12345);
let v1 = rng.next_u64();
let v2 = rng.next_u64();
assert_ne!(v1, v2);
let mut rng2 = XorShift64::new(12345);
assert_eq!(v1, rng2.next_u64());
}
#[test]
fn test_laplace_noise() {
let mut noise = LaplaceNoise::with_seed(1.0, 1.0, 42);
let mut sum = 0.0;
let n = 10000;
for _ in 0..n {
sum += noise.sample();
}
let mean = sum / f64::from(n);
assert!(mean.abs() < 0.1, "mean = {mean}");
}
#[test]
fn test_randomized_response() {
let mut rr = RandomizedResponse::with_probability(0.75, 42);
let mut correct = 0;
let n = 1000;
for i in 0..n {
let truth = i % 2 == 0;
let response = rr.privatize(truth);
if response == truth {
correct += 1;
}
}
assert!(correct > n / 2, "correct = {correct}");
}
#[test]
fn test_rr_proportion_estimation() {
let p_true = 0.8;
let true_rate = 0.3;
let n = 10000u64;
let mut rr = RandomizedResponse::with_probability(p_true, 42);
let mut positive_reports = 0u64;
for i in 0..n {
let truth = (u64_f64(i) / u64_f64(n)) < true_rate;
if rr.privatize(truth) {
positive_reports += 1;
}
}
let estimated = RandomizedResponse::estimate_proportion(p_true, n, positive_reports);
assert!(
(estimated - true_rate).abs() < 0.1,
"estimated = {estimated}, true = {true_rate}"
);
}
#[test]
fn test_privacy_budget() {
let mut budget = PrivacyBudget::new(1.0);
assert!(budget.try_spend(0.3));
assert!(budget.try_spend(0.3));
assert!(budget.try_spend(0.3));
assert!(!budget.try_spend(0.3));
assert_eq!(budget.query_count(), 3);
assert!((budget.spent() - 0.9).abs() < 0.001);
}
#[test]
fn test_private_aggregator() {
let noise_scale = 1.0;
let mut aggregator = PrivateAggregator::new(noise_scale);
let mut noise = LaplaceNoise::with_seed(1.0, 1.0, 42);
let true_value = 100.0;
let n = 10000;
for _ in 0..n {
let privatized = noise.privatize(true_value);
aggregator.add(privatized);
}
let estimated_mean = aggregator.estimate_mean();
assert!(
(estimated_mean - true_value).abs() < 1.0,
"estimated = {estimated_mean}"
);
}
#[cfg(feature = "std")]
#[test]
fn test_rappor() {
let mut rappor = Rappor::default_params();
let encoded1 = rappor.privatize(12345);
let encoded2 = rappor.privatize(12345);
assert_eq!(encoded1.len(), 64);
assert_eq!(encoded2.len(), 64);
}
#[test]
fn test_xorshift_deterministic() {
let mut rng1 = XorShift64::new(42);
let mut rng2 = XorShift64::new(42);
assert_eq!(rng1.next_u64(), rng2.next_u64());
assert_eq!(rng1.next_u64(), rng2.next_u64());
}
#[test]
fn test_xorshift_zero_seed() {
let mut rng = XorShift64::new(0);
assert_ne!(rng.next_u64(), 0);
}
#[test]
fn test_xorshift_f64_range() {
let mut rng = XorShift64::new(42);
for _ in 0..100 {
let v = rng.next_f64();
assert!((0.0..1.0).contains(&v), "v = {v}");
}
}
#[test]
fn test_xorshift_f64_range_custom() {
let mut rng = XorShift64::new(42);
for _ in 0..100 {
let v = rng.next_f64_range(10.0, 20.0);
assert!((10.0..20.0).contains(&v), "v = {v}");
}
}
#[test]
fn test_xorshift_bool() {
let mut rng = XorShift64::new(42);
let mut trues = 0;
let n = 10000;
for _ in 0..n {
if rng.next_bool(0.5) {
trues += 1;
}
}
assert!(trues > 4000 && trues < 6000, "trues = {trues}");
}
#[cfg(feature = "std")]
#[test]
fn test_xorshift_default() {
let mut rng = XorShift64::default();
let v = rng.next_u64();
assert_ne!(v, 0);
}
#[test]
fn test_laplace_scale() {
let noise = LaplaceNoise::with_seed(2.0, 0.5, 42);
assert!((noise.scale() - 4.0).abs() < f64::EPSILON);
}
#[test]
fn test_laplace_privatize_int() {
let mut noise = LaplaceNoise::with_seed(1.0, 1.0, 42);
let result = noise.privatize_int(100);
assert!((result - 100).abs() < 20, "result = {result}");
}
#[test]
fn test_rr_privatize_bit() {
let mut rr = RandomizedResponse::with_probability(0.9, 42);
let bit = rr.privatize_bit(1);
assert!(bit == 0 || bit == 1);
}
#[test]
fn test_rr_p_true() {
let rr = RandomizedResponse::with_probability(0.8, 42);
assert!((rr.p_true() - 0.8).abs() < f64::EPSILON);
}
#[test]
fn test_rr_estimate_zero_n() {
let result = RandomizedResponse::estimate_proportion(0.8, 0, 0);
assert_eq!(result, 0.0);
}
#[test]
fn test_privacy_budget_exhausted() {
let mut budget = PrivacyBudget::new(1.0);
budget.try_spend(0.5);
budget.try_spend(0.5);
assert!(budget.is_exhausted());
}
#[test]
fn test_privacy_budget_remaining() {
let mut budget = PrivacyBudget::new(1.0);
budget.try_spend(0.3);
assert!((budget.remaining() - 0.7).abs() < 0.001);
}
#[test]
fn test_privacy_budget_reset() {
let mut budget = PrivacyBudget::new(1.0);
budget.try_spend(0.5);
budget.reset();
assert_eq!(budget.query_count(), 0);
assert!((budget.remaining() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_private_aggregator_empty() {
let agg = PrivateAggregator::new(1.0);
assert_eq!(agg.estimate_mean(), 0.0);
assert_eq!(agg.count(), 0);
assert!(agg.standard_error().is_infinite());
}
#[test]
fn test_private_aggregator_reset() {
let mut agg = PrivateAggregator::new(1.0);
agg.add(10.0);
agg.add(20.0);
agg.reset();
assert_eq!(agg.count(), 0);
assert_eq!(agg.estimate_mean(), 0.0);
}
#[test]
fn test_private_aggregator_sum() {
let mut agg = PrivateAggregator::new(1.0);
agg.add(10.0);
agg.add(20.0);
assert!((agg.estimate_sum() - 30.0).abs() < f64::EPSILON);
}
#[cfg(feature = "std")]
#[test]
fn test_rappor_params() {
let rappor = Rappor::new(0.3, 0.7, 0.2);
let (f, p, q) = rappor.params();
assert!((f - 0.3).abs() < f64::EPSILON);
assert!((p - 0.7).abs() < f64::EPSILON);
assert!((q - 0.2).abs() < f64::EPSILON);
}
}