diff options
Diffstat (limited to 'timing')
| -rw-r--r-- | timing/Cargo.toml | 16 | ||||
| -rw-r--r-- | timing/src/delay.rs | 259 | ||||
| -rw-r--r-- | timing/src/lib.rs | 9 | ||||
| -rw-r--r-- | timing/src/mixing_pool.rs | 307 | ||||
| -rw-r--r-- | timing/src/obfuscation.rs | 243 | ||||
| -rw-r--r-- | timing/src/timerange.rs | 211 |
6 files changed, 1045 insertions, 0 deletions
diff --git a/timing/Cargo.toml b/timing/Cargo.toml new file mode 100644 index 0000000..6494808 --- /dev/null +++ b/timing/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "onioncoin-timing" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true +rand.workspace = true +blake3.workspace = true +chrono.workspace = true +thiserror.workspace = true +tokio.workspace = true +async-trait.workspace = true diff --git a/timing/src/delay.rs b/timing/src/delay.rs new file mode 100644 index 0000000..b264381 --- /dev/null +++ b/timing/src/delay.rs @@ -0,0 +1,259 @@ +use rand::Rng; +use std::time::Duration; +use serde::{Deserialize, Serialize}; + +/// Strategy for introducing random delays in transaction propagation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DelayStrategy { + /// Fixed delay + Fixed(Duration), + + /// Random delay within a range + Random { min: Duration, max: Duration }, + + /// Exponential backoff with jitter + Exponential { + base: Duration, + max: Duration, + attempt: u32, + }, + + /// Delay based on observed Tor latency + TorAdaptive { + observed_latency: Duration, + multiplier: f64, + }, +} + +impl DelayStrategy { + /// Wallet broadcast delay: 5-60 minutes + pub fn wallet_broadcast() -> Self { + Self::Random { + min: Duration::from_secs(5 * 60), + max: Duration::from_secs(60 * 60), + } + } + + /// Node rebroadcast delay: 10s - 2 minutes + pub fn node_rebroadcast() -> Self { + Self::Random { + min: Duration::from_secs(10), + max: Duration::from_secs(120), + } + } + + /// Mixing pool batch delay: 10-30 minutes + pub fn mixing_pool() -> Self { + Self::Random { + min: Duration::from_secs(10 * 60), + max: Duration::from_secs(30 * 60), + } + } + + /// Validation delay before processing: 5s - 60s + pub fn validation() -> Self { + Self::Random { + min: Duration::from_secs(5), + max: Duration::from_secs(60), + } + } + + /// Dandelion STEM hop delay: 30s - 5 minutes + pub fn dandelion_stem() -> Self { + Self::Random { + min: Duration::from_secs(30), + max: Duration::from_secs(5 * 60), + } + } + + /// Create adaptive delay based on observed Tor latency + pub fn from_tor_latency(observed: Duration) -> Self { + Self::TorAdaptive { + observed_latency: observed, + multiplier: 2.0, // Amplify variability + } + } + + /// Calculate the actual delay to use + pub fn calculate(&self) -> Duration { + let mut rng = rand::thread_rng(); + + match self { + Self::Fixed(d) => *d, + + Self::Random { min, max } => { + let min_ms = min.as_millis() as u64; + let max_ms = max.as_millis() as u64; + let delay_ms = rng.gen_range(min_ms..=max_ms); + Duration::from_millis(delay_ms) + } + + Self::Exponential { base, max, attempt } => { + let base_ms = base.as_millis() as u64; + let max_ms = max.as_millis() as u64; + + // Exponential: base * 2^attempt with jitter + let exp_delay = base_ms.saturating_mul(2u64.saturating_pow(*attempt)); + let capped = exp_delay.min(max_ms); + + // Add ±25% jitter + let jitter_range = (capped as f64 * 0.25) as u64; + let jitter = rng.gen_range(0..=jitter_range); + let with_jitter = if rng.gen_bool(0.5) { + capped.saturating_add(jitter) + } else { + capped.saturating_sub(jitter) + }; + + Duration::from_millis(with_jitter) + } + + Self::TorAdaptive { observed_latency, multiplier } => { + let base_ms = observed_latency.as_millis() as u64; + let amplified = (base_ms as f64 * multiplier) as u64; + + // Random delay up to amplified latency + let delay_ms = rng.gen_range(0..=amplified); + Duration::from_millis(delay_ms) + } + } + } + + /// Async sleep with this delay strategy + pub async fn sleep(&self) { + let delay = self.calculate(); + tokio::time::sleep(delay).await; + } +} + +/// Helper for managing multiple delays in sequence +#[derive(Debug)] +pub struct DelaySequence { + strategies: Vec<DelayStrategy>, + current: usize, +} + +impl DelaySequence { + pub fn new(strategies: Vec<DelayStrategy>) -> Self { + Self { + strategies, + current: 0, + } + } + + /// Create a Dandelion++ STEM sequence (1-4 random hops) + pub fn dandelion_stem() -> Self { + let mut rng = rand::thread_rng(); + let hops = rng.gen_range(1..=4); + + let strategies = (0..hops) + .map(|_| DelayStrategy::dandelion_stem()) + .collect(); + + Self::new(strategies) + } + + /// Get next delay, returns None when sequence is exhausted + pub fn next(&mut self) -> Option<&DelayStrategy> { + if self.current < self.strategies.len() { + let strategy = &self.strategies[self.current]; + self.current += 1; + Some(strategy) + } else { + None + } + } + + /// Reset sequence to beginning + pub fn reset(&mut self) { + self.current = 0; + } + + /// Total number of delays in sequence + pub fn len(&self) -> usize { + self.strategies.len() + } + + pub fn is_empty(&self) -> bool { + self.strategies.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fixed_delay() { + let strategy = DelayStrategy::Fixed(Duration::from_secs(10)); + let delay = strategy.calculate(); + assert_eq!(delay, Duration::from_secs(10)); + } + + #[test] + fn test_random_delay() { + let strategy = DelayStrategy::Random { + min: Duration::from_secs(5), + max: Duration::from_secs(10), + }; + + for _ in 0..100 { + let delay = strategy.calculate(); + assert!(delay >= Duration::from_secs(5)); + assert!(delay <= Duration::from_secs(10)); + } + } + + #[test] + fn test_exponential_delay() { + let base = Duration::from_secs(1); + let max = Duration::from_secs(100); + + for attempt in 0..5 { + let strategy = DelayStrategy::Exponential { + base, + max, + attempt, + }; + let delay = strategy.calculate(); + assert!(delay <= max); + } + } + + #[test] + fn test_tor_adaptive_delay() { + let observed = Duration::from_secs(2); + let strategy = DelayStrategy::from_tor_latency(observed); + + for _ in 0..100 { + let delay = strategy.calculate(); + // Should be between 0 and 2x observed latency + assert!(delay <= Duration::from_secs(4)); + } + } + + #[test] + fn test_delay_sequence() { + let mut sequence = DelaySequence::dandelion_stem(); + let len = sequence.len(); + + assert!(len >= 1 && len <= 4); + + let mut count = 0; + while sequence.next().is_some() { + count += 1; + } + + assert_eq!(count, len); + } + + #[tokio::test] + async fn test_async_sleep() { + let strategy = DelayStrategy::Fixed(Duration::from_millis(10)); + let start = std::time::Instant::now(); + strategy.sleep().await; + let elapsed = start.elapsed(); + + assert!(elapsed >= Duration::from_millis(10)); + } +} diff --git a/timing/src/lib.rs b/timing/src/lib.rs new file mode 100644 index 0000000..e55dea2 --- /dev/null +++ b/timing/src/lib.rs @@ -0,0 +1,9 @@ +pub mod obfuscation; +pub mod timerange; +pub mod delay; +pub mod mixing_pool; + +pub use obfuscation::{TimingObfuscator, TimingMetadata}; +pub use timerange::{TimeRange, TimeRangeProof}; +pub use delay::DelayStrategy; +pub use mixing_pool::MixingPool; diff --git a/timing/src/mixing_pool.rs b/timing/src/mixing_pool.rs new file mode 100644 index 0000000..931f0be --- /dev/null +++ b/timing/src/mixing_pool.rs @@ -0,0 +1,307 @@ +use std::collections::VecDeque; +use std::time::{Duration, Instant}; +use rand::seq::SliceRandom; +use serde::{Deserialize, Serialize}; + +/// A pool that collects items and releases them in shuffled batches +/// Used for temporal obfuscation of transaction broadcasts +#[derive(Debug)] +pub struct MixingPool<T> { + /// Items waiting to be released + pool: VecDeque<PoolItem<T>>, + + /// Minimum time to collect items before releasing + min_batch_duration: Duration, + + /// Maximum time to collect items before forcing release + max_batch_duration: Duration, + + /// Maximum number of items in pool before forcing release + max_pool_size: usize, + + /// Time when current batch started collecting + batch_start: Option<Instant>, +} + +#[derive(Debug, Clone)] +struct PoolItem<T> { + item: T, + #[allow(dead_code)] + added_at: Instant, +} + +impl<T> MixingPool<T> { + /// Create a new mixing pool with custom parameters + pub fn new( + min_batch_duration: Duration, + max_batch_duration: Duration, + max_pool_size: usize, + ) -> Self { + Self { + pool: VecDeque::new(), + min_batch_duration, + max_batch_duration, + max_pool_size, + batch_start: None, + } + } + + /// Create a mixing pool with default parameters (10-30 min batches) + pub fn default() -> Self { + Self::new( + Duration::from_secs(10 * 60), // 10 minutes min + Duration::from_secs(30 * 60), // 30 minutes max + 1000, // max 1000 items + ) + } + + /// Add an item to the mixing pool + pub fn add(&mut self, item: T) { + if self.batch_start.is_none() { + self.batch_start = Some(Instant::now()); + } + + self.pool.push_back(PoolItem { + item, + added_at: Instant::now(), + }); + } + + /// Check if pool should release a batch + pub fn should_release(&self) -> bool { + if self.pool.is_empty() { + return false; + } + + if let Some(start) = self.batch_start { + let elapsed = start.elapsed(); + + // Force release if max duration reached + if elapsed >= self.max_batch_duration { + return true; + } + + // Force release if pool is full + if self.pool.len() >= self.max_pool_size { + return true; + } + + // Release if min duration passed and we have items + if elapsed >= self.min_batch_duration && !self.pool.is_empty() { + return true; + } + } + + false + } + + /// Release a shuffled batch of all pooled items + pub fn release_batch(&mut self) -> Vec<T> { + let mut items: Vec<T> = self.pool.drain(..).map(|pi| pi.item).collect(); + + // Shuffle to remove temporal ordering + let mut rng = rand::thread_rng(); + items.shuffle(&mut rng); + + // Reset batch timer + self.batch_start = None; + + items + } + + /// Get current pool size + pub fn len(&self) -> usize { + self.pool.len() + } + + pub fn is_empty(&self) -> bool { + self.pool.is_empty() + } + + /// Get time until next potential release (min batch duration) + pub fn time_until_release(&self) -> Option<Duration> { + self.batch_start.map(|start| { + let elapsed = start.elapsed(); + if elapsed >= self.min_batch_duration { + Duration::from_secs(0) + } else { + self.min_batch_duration - elapsed + } + }) + } +} + +/// Stats about mixing pool performance +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MixingPoolStats { + pub total_items_processed: u64, + pub total_batches_released: u64, + pub avg_batch_size: f64, + pub avg_wait_time_secs: f64, +} + +/// Mixing pool with statistics tracking +#[derive(Debug)] +pub struct MixingPoolWithStats<T> { + pool: MixingPool<T>, + stats: MixingPoolStats, + #[allow(dead_code)] + item_wait_times: Vec<Duration>, +} + +impl<T> MixingPoolWithStats<T> { + pub fn new(pool: MixingPool<T>) -> Self { + Self { + pool, + stats: MixingPoolStats { + total_items_processed: 0, + total_batches_released: 0, + avg_batch_size: 0.0, + avg_wait_time_secs: 0.0, + }, + item_wait_times: Vec::new(), + } + } + + pub fn add(&mut self, item: T) { + self.pool.add(item); + } + + pub fn should_release(&self) -> bool { + self.pool.should_release() + } + + pub fn release_batch(&mut self) -> Vec<T> { + let batch = self.pool.release_batch(); + let batch_size = batch.len(); + + // Update stats + self.stats.total_items_processed += batch_size as u64; + self.stats.total_batches_released += 1; + + // Update average batch size + let total_batches = self.stats.total_batches_released as f64; + self.stats.avg_batch_size = + (self.stats.avg_batch_size * (total_batches - 1.0) + batch_size as f64) / total_batches; + + batch + } + + pub fn get_stats(&self) -> &MixingPoolStats { + &self.stats + } + + pub fn len(&self) -> usize { + self.pool.len() + } + + pub fn is_empty(&self) -> bool { + self.pool.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mixing_pool_add() { + let mut pool = MixingPool::<u32>::default(); + pool.add(1); + pool.add(2); + pool.add(3); + + assert_eq!(pool.len(), 3); + } + + #[test] + fn test_mixing_pool_release() { + let mut pool = MixingPool::<u32>::new( + Duration::from_millis(10), + Duration::from_secs(1), + 100, + ); + + pool.add(1); + pool.add(2); + pool.add(3); + + // Should not release immediately + assert!(!pool.should_release()); + + // Wait for min duration + std::thread::sleep(Duration::from_millis(15)); + + assert!(pool.should_release()); + + let batch = pool.release_batch(); + assert_eq!(batch.len(), 3); + assert!(pool.is_empty()); + } + + #[test] + fn test_mixing_pool_shuffle() { + let mut pool = MixingPool::<u32>::new( + Duration::from_millis(10), + Duration::from_secs(1), + 100, + ); + + // Add items in order + for i in 0..100 { + pool.add(i); + } + + std::thread::sleep(Duration::from_millis(15)); + + let batch = pool.release_batch(); + + // Check all items are present + let mut sorted = batch.clone(); + sorted.sort(); + assert_eq!(sorted, (0..100).collect::<Vec<_>>()); + + // Check they were shuffled (very unlikely to be in original order) + assert_ne!(batch, sorted); + } + + #[test] + fn test_mixing_pool_max_size() { + let mut pool = MixingPool::<u32>::new( + Duration::from_secs(100), // Long min duration + Duration::from_secs(200), + 10, // Small max size + ); + + // Add items up to max + for i in 0..10 { + pool.add(i); + } + + // Should force release due to max size + assert!(pool.should_release()); + } + + #[test] + fn test_mixing_pool_with_stats() { + let pool = MixingPool::<u32>::new( + Duration::from_millis(10), + Duration::from_secs(1), + 100, + ); + let mut pool_with_stats = MixingPoolWithStats::new(pool); + + for i in 0..5 { + pool_with_stats.add(i); + } + + std::thread::sleep(Duration::from_millis(15)); + + pool_with_stats.release_batch(); + + let stats = pool_with_stats.get_stats(); + assert_eq!(stats.total_items_processed, 5); + assert_eq!(stats.total_batches_released, 1); + assert_eq!(stats.avg_batch_size, 5.0); + } +} diff --git a/timing/src/obfuscation.rs b/timing/src/obfuscation.rs new file mode 100644 index 0000000..b48ea82 --- /dev/null +++ b/timing/src/obfuscation.rs @@ -0,0 +1,243 @@ +use crate::{TimeRange, TimeRangeProof, DelayStrategy, MixingPool}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +/// Orchestrates all timing obfuscation strategies +#[derive(Debug)] +pub struct TimingObfuscator<T> { + /// Mixing pool for batch releases + mixing_pool: MixingPool<T>, + + /// Strategy for adding delays + delay_strategy: DelayStrategy, + + /// Configuration + config: ObfuscationConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ObfuscationConfig { + /// Enable mixing pool batching + pub use_mixing_pool: bool, + + /// Enable random delays + pub use_delays: bool, + + /// Enable fuzzy timestamps + pub use_fuzzy_timestamps: bool, + + /// Ratio of fake items to inject (0.0 - 1.0) + pub fake_item_ratio: f32, +} + +impl Default for ObfuscationConfig { + fn default() -> Self { + Self { + use_mixing_pool: true, + use_delays: true, + use_fuzzy_timestamps: true, + fake_item_ratio: 0.3, // 30% fake traffic + } + } +} + +impl<T> TimingObfuscator<T> { + pub fn new(config: ObfuscationConfig) -> Self { + Self { + mixing_pool: MixingPool::default(), + delay_strategy: DelayStrategy::node_rebroadcast(), + config, + } + } + + /// Add item to mixing pool + pub fn add_to_pool(&mut self, item: T) { + if self.config.use_mixing_pool { + self.mixing_pool.add(item); + } + } + + /// Check if should release batch + pub fn should_release_batch(&self) -> bool { + if !self.config.use_mixing_pool { + return false; + } + self.mixing_pool.should_release() + } + + /// Release shuffled batch + pub fn release_batch(&mut self) -> Vec<T> { + self.mixing_pool.release_batch() + } + + /// Apply delay strategy + pub async fn apply_delay(&self) { + if self.config.use_delays { + self.delay_strategy.sleep().await; + } + } + + /// Get current pool size + pub fn pool_size(&self) -> usize { + self.mixing_pool.len() + } +} + +/// Transaction timing metadata +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimingMetadata { + /// Fuzzy time range + pub time_range: TimeRange, + + /// Zero-knowledge proof of creation time + pub time_proof: TimeRangeProof, + + /// Optional: observed network delay (for adaptive strategies) + pub network_delay_hint: Option<Duration>, +} + +impl TimingMetadata { + /// Create timing metadata for a transaction + pub fn new(real_time: DateTime<Utc>, seed: &[u8; 32]) -> Result<Self, crate::timerange::TimeRangeError> { + let time_range = TimeRange::new_fuzzy(real_time); + let time_proof = TimeRangeProof::generate(seed, real_time, &time_range)?; + + Ok(Self { + time_range, + time_proof, + network_delay_hint: None, + }) + } + + /// Validate timing metadata + pub fn validate(&self, current_time: DateTime<Utc>) -> bool { + // Check time range is valid + if !self.time_range.is_valid(current_time) { + return false; + } + + // Verify time proof + if !self.time_proof.verify(&self.time_range) { + return false; + } + + true + } + + /// Set network delay hint for adaptive strategies + pub fn with_network_delay(mut self, delay: Duration) -> Self { + self.network_delay_hint = Some(delay); + self + } +} + +/// Strategies for transaction propagation +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum PropagationPhase { + /// Dandelion STEM phase (anonymity) + Stem, + + /// Dandelion FLUFF phase (broadcast) + Fluff, +} + +/// Transaction wrapper with timing information +#[derive(Debug, Clone)] +pub struct TimedItem<T> { + pub item: T, + pub timing: TimingMetadata, + pub phase: PropagationPhase, +} + +impl<T> TimedItem<T> { + pub fn new(item: T, timing: TimingMetadata) -> Self { + Self { + item, + timing, + phase: PropagationPhase::Stem, + } + } + + pub fn transition_to_fluff(mut self) -> Self { + self.phase = PropagationPhase::Fluff; + self + } + + pub fn is_stem(&self) -> bool { + self.phase == PropagationPhase::Stem + } + + pub fn is_fluff(&self) -> bool { + self.phase == PropagationPhase::Fluff + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timing_metadata_creation() { + let now = Utc::now(); + let seed = [42u8; 32]; + + let metadata = TimingMetadata::new(now, &seed).unwrap(); + assert!(metadata.validate(now)); + } + + #[test] + fn test_timing_metadata_validation() { + let now = Utc::now(); + let seed = [42u8; 32]; + + let metadata = TimingMetadata::new(now, &seed).unwrap(); + + // Should validate at current time + assert!(metadata.validate(now)); + + // Should validate slightly in the future + let future = now + chrono::Duration::hours(1); + assert!(metadata.validate(future)); + } + + #[test] + fn test_obfuscator_pool() { + let config = ObfuscationConfig::default(); + let mut obfuscator = TimingObfuscator::<u32>::new(config); + + obfuscator.add_to_pool(1); + obfuscator.add_to_pool(2); + obfuscator.add_to_pool(3); + + assert_eq!(obfuscator.pool_size(), 3); + } + + #[test] + fn test_timed_item_phases() { + let now = Utc::now(); + let seed = [42u8; 32]; + let timing = TimingMetadata::new(now, &seed).unwrap(); + + let item = TimedItem::new(42u32, timing); + assert!(item.is_stem()); + assert!(!item.is_fluff()); + + let item = item.transition_to_fluff(); + assert!(!item.is_stem()); + assert!(item.is_fluff()); + } + + #[tokio::test] + async fn test_apply_delay() { + let config = ObfuscationConfig::default(); + let obfuscator = TimingObfuscator::<u32>::new(config); + + let start = std::time::Instant::now(); + obfuscator.apply_delay().await; + let elapsed = start.elapsed(); + + // Should have some delay (at least 10s from node_rebroadcast strategy) + assert!(elapsed >= Duration::from_secs(10)); + } +} diff --git a/timing/src/timerange.rs b/timing/src/timerange.rs new file mode 100644 index 0000000..8991930 --- /dev/null +++ b/timing/src/timerange.rs @@ -0,0 +1,211 @@ +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<Utc>, range_hours: u32) -> Result<Self, TimeRangeError> { + 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<Utc>) -> 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<Utc>) -> 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<Utc>, + time_range: &TimeRange, + ) -> Result<Self, TimeRangeError> { + 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)); + } +} |
