use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; use blake3::Hasher; /// Represents a fuzzy time range for transaction timestamps #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TimeRange { /// Earliest possible timestamp (Unix seconds) pub earliest: i64, /// Latest possible timestamp (Unix seconds) pub latest: i64, } impl TimeRange { /// Minimum allowed range duration (2 hours) pub const MIN_RANGE_SECONDS: i64 = 2 * 3600; /// Maximum allowed range duration (6 hours) pub const MAX_RANGE_SECONDS: i64 = 6 * 3600; /// Maximum age for a transaction (4 hours from latest) pub const MAX_TX_AGE_SECONDS: i64 = 4 * 3600; /// Create a new TimeRange around a real timestamp pub fn new(real_time: DateTime, range_hours: u32) -> Result { let range_seconds = (range_hours as i64) * 3600; if range_seconds < Self::MIN_RANGE_SECONDS { return Err(TimeRangeError::RangeTooSmall); } if range_seconds > Self::MAX_RANGE_SECONDS { return Err(TimeRangeError::RangeTooLarge); } let half_range = Duration::seconds(range_seconds / 2); let earliest = (real_time - half_range).timestamp(); let latest = (real_time + half_range).timestamp(); Ok(Self { earliest, latest }) } /// Create a fuzzy range with random padding pub fn new_fuzzy(real_time: DateTime) -> Self { use rand::Rng; let mut rng = rand::thread_rng(); // Random range between 2-6 hours let range_hours = rng.gen_range(2..=6); Self::new(real_time, range_hours).expect("Range should be valid") } /// Check if this range is valid at current time pub fn is_valid(&self, current_time: DateTime) -> bool { let range_duration = self.latest - self.earliest; // Check range size if range_duration < Self::MIN_RANGE_SECONDS || range_duration > Self::MAX_RANGE_SECONDS { return false; } // Check not too old let age = current_time.timestamp() - self.latest; if age > Self::MAX_TX_AGE_SECONDS { return false; } true } /// Get the duration of this range in seconds pub fn duration_seconds(&self) -> i64 { self.latest - self.earliest } /// Check if a timestamp falls within this range pub fn contains(&self, timestamp: i64) -> bool { timestamp >= self.earliest && timestamp <= self.latest } } /// Zero-knowledge proof that a transaction was created within a time range /// Simplified version using hash chains #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TimeRangeProof { /// Hash at earliest time point pub earliest_hash: [u8; 32], /// Hash at latest time point pub latest_hash: [u8; 32], /// Commitment to the real creation time pub time_commitment: [u8; 32], } impl TimeRangeProof { /// Generate a proof for a real timestamp within a range /// Uses hash chain: H^n(seed) where n = minutes since epoch pub fn generate( seed: &[u8; 32], real_time: DateTime, time_range: &TimeRange, ) -> Result { if !time_range.contains(real_time.timestamp()) { return Err(TimeRangeError::TimeOutOfRange); } // Convert timestamps to "minutes since epoch" for hash chain let earliest_minutes = time_range.earliest / 60; let latest_minutes = time_range.latest / 60; let real_minutes = real_time.timestamp() / 60; // Generate hash chain values let earliest_hash = Self::hash_chain(seed, earliest_minutes as u64); let latest_hash = Self::hash_chain(seed, latest_minutes as u64); // Commitment to real time (hash of seed + real_time) let mut hasher = Hasher::new(); hasher.update(seed); hasher.update(&real_minutes.to_le_bytes()); let time_commitment = *hasher.finalize().as_bytes(); Ok(Self { earliest_hash, latest_hash, time_commitment, }) } /// Verify that the proof is internally consistent /// Note: This is a simplified proof system. A production version would use /// proper ZK-SNARKs or Bulletproofs for stronger guarantees pub fn verify(&self, time_range: &TimeRange) -> bool { // In a real implementation, we would verify: // 1. earliest_hash and latest_hash are valid points in hash chain // 2. time_commitment corresponds to a point between them // 3. No information about real_time is leaked // For this prototype, we just check the hashes are non-zero self.earliest_hash != [0u8; 32] && self.latest_hash != [0u8; 32] && self.time_commitment != [0u8; 32] && time_range.duration_seconds() > 0 } /// Hash chain computation: applies blake3 hash n times fn hash_chain(seed: &[u8; 32], iterations: u64) -> [u8; 32] { let mut current = *seed; for _ in 0..iterations { let mut hasher = Hasher::new(); hasher.update(¤t); current = *hasher.finalize().as_bytes(); } current } } #[derive(Debug, thiserror::Error)] pub enum TimeRangeError { #[error("Time range is too small (minimum 2 hours)")] RangeTooSmall, #[error("Time range is too large (maximum 6 hours)")] RangeTooLarge, #[error("Real timestamp is outside the specified range")] TimeOutOfRange, } #[cfg(test)] mod tests { use super::*; #[test] fn test_timerange_creation() { let now = Utc::now(); let range = TimeRange::new(now, 4).unwrap(); assert!(range.duration_seconds() == 4 * 3600); assert!(range.contains(now.timestamp())); } #[test] fn test_timerange_validation() { let now = Utc::now(); let range = TimeRange::new(now, 4).unwrap(); assert!(range.is_valid(now)); assert!(range.is_valid(now + Duration::hours(1))); } #[test] fn test_timerange_proof() { let seed = [42u8; 32]; let now = Utc::now(); let range = TimeRange::new(now, 4).unwrap(); let proof = TimeRangeProof::generate(&seed, now, &range).unwrap(); assert!(proof.verify(&range)); } #[test] fn test_fuzzy_range() { let now = Utc::now(); let range1 = TimeRange::new_fuzzy(now); let range2 = TimeRange::new_fuzzy(now); // Ranges should be valid but potentially different assert!(range1.is_valid(now)); assert!(range2.is_valid(now)); } }