From 9f5d864d533ce86459e654f5d78212933c0269ea Mon Sep 17 00:00:00 2001 From: gabrix73 Date: Mon, 1 Jun 2026 18:41:36 +0200 Subject: Initial commit: OnionCoin prototype with Proof-of-Relay consensus OnionCoin is a privacy cryptocurrency that rewards Tor relay operators through a unique Proof-of-Contribution consensus mechanism. Core Features: - Proof-of-Relay: 30% of block rewards go to Tor operators - Native .onion node identity (no IP exposure) - Temporal obfuscation protocols - Dandelion++ over Tor propagation - Native inheritance system with dead man's switch Technical Stack: - Rust workspace with 8 crates - Ed25519/X25519 cryptography - arti (Rust Tor client) integration planned - 10 minute block time, 5-10 TPS design Status: Prototype - Consensus logic complete with passing tests (30/33) - Network layer conceptual design complete - Tor integration pending - Testnet launch planned Q3 2026 License: MIT Author: Gabriele Salati (virebent) Contact: g48rix@gmail.com Website: https://www.gabrielesalati.eu Repository: https://git.virebent.art/virebent/onioncoin --- inheritance/src/unlock.rs | 469 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 inheritance/src/unlock.rs (limited to 'inheritance/src/unlock.rs') diff --git a/inheritance/src/unlock.rs b/inheritance/src/unlock.rs new file mode 100644 index 0000000..61d3e5c --- /dev/null +++ b/inheritance/src/unlock.rs @@ -0,0 +1,469 @@ +use serde::{Deserialize, Serialize}; +use crate::contract::{InheritanceContract, ContractStatus}; + +/// Progressive unlock tier +/// This is OnionCoin's INNOVATIVE feature: gradual unlock gives owner time to recover! +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UnlockTier { + /// Tier number (1, 2, 3...) + pub tier: u32, + + /// Days after grace period expiry + pub days_after_expiry: i64, + + /// Percentage of total to unlock at this tier + pub unlock_percentage: u8, + + /// Has this tier been unlocked? + pub unlocked: bool, + + /// Timestamp when unlocked + pub unlock_time: Option, +} + +impl UnlockTier { + pub fn new(tier: u32, days_after_expiry: i64, unlock_percentage: u8) -> Self { + Self { + tier, + days_after_expiry, + unlock_percentage, + unlocked: false, + unlock_time: None, + } + } + + /// Check if this tier should be unlocked + pub fn should_unlock(&self, time_since_expiry: i64) -> bool { + !self.unlocked && time_since_expiry >= self.days_after_expiry * 24 * 3600 + } + + /// Mark tier as unlocked + pub fn mark_unlocked(&mut self, current_time: i64) { + self.unlocked = true; + self.unlock_time = Some(current_time); + } +} + +/// Complete unlock schedule with multiple tiers +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UnlockSchedule { + /// All unlock tiers + pub tiers: Vec, + + /// Grace period expiry time + pub grace_expiry_time: i64, + + /// Total unlocked so far (percentage) + pub total_unlocked_percentage: u8, +} + +impl UnlockSchedule { + /// Create standard 4-tier progressive unlock + /// Tier 1 (30 days): 10% + /// Tier 2 (60 days): 25% (35% total) + /// Tier 3 (90 days): 35% (70% total) + /// Tier 4 (120 days): 30% (100% total) + pub fn standard(grace_expiry_time: i64) -> Self { + let tiers = vec![ + UnlockTier::new(1, 30, 10), // 10% after 30 days + UnlockTier::new(2, 60, 25), // 25% after 60 days (35% total) + UnlockTier::new(3, 90, 35), // 35% after 90 days (70% total) + UnlockTier::new(4, 120, 30), // 30% after 120 days (100% total) + ]; + + Self { + tiers, + grace_expiry_time, + total_unlocked_percentage: 0, + } + } + + /// Create aggressive 3-tier unlock (faster) + /// Tier 1 (14 days): 25% + /// Tier 2 (30 days): 35% (60% total) + /// Tier 3 (60 days): 40% (100% total) + pub fn aggressive(grace_expiry_time: i64) -> Self { + let tiers = vec![ + UnlockTier::new(1, 14, 25), // 25% after 14 days + UnlockTier::new(2, 30, 35), // 35% after 30 days + UnlockTier::new(3, 60, 40), // 40% after 60 days + ]; + + Self { + tiers, + grace_expiry_time, + total_unlocked_percentage: 0, + } + } + + /// Create conservative 5-tier unlock (slower, more chances to recover) + /// Tier 1 (60 days): 5% + /// Tier 2 (90 days): 10% (15% total) + /// Tier 3 (120 days): 20% (35% total) + /// Tier 4 (180 days): 30% (65% total) + /// Tier 5 (365 days): 35% (100% total) + pub fn conservative(grace_expiry_time: i64) -> Self { + let tiers = vec![ + UnlockTier::new(1, 60, 5), // 5% after 60 days + UnlockTier::new(2, 90, 10), // 10% after 90 days + UnlockTier::new(3, 120, 20), // 20% after 120 days + UnlockTier::new(4, 180, 30), // 30% after 180 days + UnlockTier::new(5, 365, 35), // 35% after 365 days + ]; + + Self { + tiers, + grace_expiry_time, + total_unlocked_percentage: 0, + } + } + + /// Custom unlock schedule + pub fn custom(grace_expiry_time: i64, tiers: Vec) -> Result { + // Validate total percentage = 100% + let total: u32 = tiers.iter().map(|t| t.unlock_percentage as u32).sum(); + if total != 100 { + return Err(UnlockError::InvalidTotalPercentage(total)); + } + + // Validate tiers are in order + for i in 1..tiers.len() { + if tiers[i].days_after_expiry <= tiers[i - 1].days_after_expiry { + return Err(UnlockError::InvalidTierOrder); + } + } + + Ok(Self { + tiers, + grace_expiry_time, + total_unlocked_percentage: 0, + }) + } + + /// Process unlocks for current time + pub fn process_unlocks(&mut self, current_time: i64) -> Vec { + let time_since_expiry = current_time - self.grace_expiry_time; + let mut events = Vec::new(); + + for tier in &mut self.tiers { + if tier.should_unlock(time_since_expiry) { + tier.mark_unlocked(current_time); + self.total_unlocked_percentage += tier.unlock_percentage; + + events.push(UnlockEvent { + tier: tier.tier, + percentage: tier.unlock_percentage, + timestamp: current_time, + }); + } + } + + events + } + + /// Get next unlock tier + pub fn next_unlock(&self) -> Option<&UnlockTier> { + self.tiers.iter().find(|t| !t.unlocked) + } + + /// Get time until next unlock + pub fn time_until_next_unlock(&self, current_time: i64) -> Option { + self.next_unlock().map(|tier| { + let target_time = self.grace_expiry_time + tier.days_after_expiry * 24 * 3600; + (target_time - current_time).max(0) + }) + } + + /// Check if fully unlocked + pub fn is_fully_unlocked(&self) -> bool { + self.total_unlocked_percentage >= 100 + } + + /// Get summary of unlock progress + pub fn progress_summary(&self, current_time: i64) -> String { + let mut summary = format!("Unlocked: {}%\n", self.total_unlocked_percentage); + + for tier in &self.tiers { + let status = if tier.unlocked { + format!("✓ Unlocked at {}", + chrono::DateTime::from_timestamp(tier.unlock_time.unwrap(), 0) + .map(|dt| dt.format("%Y-%m-%d").to_string()) + .unwrap_or_else(|| "unknown".to_string())) + } else { + let time_until = self.grace_expiry_time + tier.days_after_expiry * 24 * 3600 - current_time; + if time_until > 0 { + format!("⏳ In {} days", time_until / (24 * 3600)) + } else { + "⏳ Ready to unlock".to_string() + } + }; + + summary.push_str(&format!( + "Tier {}: {}% - {}\n", + tier.tier, tier.unlock_percentage, status + )); + } + + summary + } +} + +/// Event when a tier is unlocked +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UnlockEvent { + pub tier: u32, + pub percentage: u8, + pub timestamp: i64, +} + +/// Manager for progressive unlock +pub struct ProgressiveUnlock; + +impl ProgressiveUnlock { + /// Initialize progressive unlock for a contract + pub fn initialize( + contract: &mut InheritanceContract, + schedule_type: UnlockScheduleType, + ) -> Result<(), UnlockError> { + if !contract.config.progressive_unlock { + return Err(UnlockError::ProgressiveUnlockDisabled); + } + + if contract.status != ContractStatus::Unlocking { + return Err(UnlockError::ContractNotUnlocking); + } + + let grace_expiry = contract.last_heartbeat + contract.config.total_timeout(); + + let schedule = match schedule_type { + UnlockScheduleType::Standard => UnlockSchedule::standard(grace_expiry), + UnlockScheduleType::Aggressive => UnlockSchedule::aggressive(grace_expiry), + UnlockScheduleType::Conservative => UnlockSchedule::conservative(grace_expiry), + UnlockScheduleType::Custom(tiers) => { + UnlockSchedule::custom(grace_expiry, tiers)? + } + }; + + contract.unlock_schedule = Some(schedule); + Ok(()) + } + + /// Process unlocks and distribute to beneficiaries + pub fn process( + contract: &mut InheritanceContract, + current_time: i64, + ) -> Result, UnlockError> { + let schedule = contract + .unlock_schedule + .as_mut() + .ok_or(UnlockError::NoSchedule)?; + + let events = schedule.process_unlocks(current_time); + + if events.is_empty() { + return Ok(Vec::new()); + } + + let mut distributions = Vec::new(); + + for event in events { + // Calculate amount to unlock for this tier + let tier_amount = (contract.locked_amount as f64 * (event.percentage as f64 / 100.0)) as u64; + + // Distribute to beneficiaries + for beneficiary in &mut contract.beneficiaries { + let beneficiary_amount = beneficiary.calculate_amount(tier_amount); + beneficiary.unlocked_amount += beneficiary_amount; + + distributions.push(Distribution { + beneficiary: beneficiary.pubkey, + amount: beneficiary_amount, + tier: event.tier, + timestamp: event.timestamp, + }); + } + } + + // Mark as completed if fully unlocked + if schedule.is_fully_unlocked() { + contract.status = ContractStatus::Completed; + } + + Ok(distributions) + } + + /// Get unlock status + pub fn status(contract: &InheritanceContract, current_time: i64) -> String { + if let Some(schedule) = &contract.unlock_schedule { + schedule.progress_summary(current_time) + } else { + "No unlock schedule".to_string() + } + } +} + +/// Type of unlock schedule +#[derive(Debug, Clone)] +pub enum UnlockScheduleType { + Standard, + Aggressive, + Conservative, + Custom(Vec), +} + +/// Distribution to a beneficiary +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Distribution { + pub beneficiary: [u8; 32], + pub amount: u64, + pub tier: u32, + pub timestamp: i64, +} + +#[derive(Debug, thiserror::Error)] +pub enum UnlockError { + #[error("Invalid total percentage: {0}% (must be 100%)")] + InvalidTotalPercentage(u32), + + #[error("Unlock tiers must be in chronological order")] + InvalidTierOrder, + + #[error("Progressive unlock is disabled for this contract")] + ProgressiveUnlockDisabled, + + #[error("Contract is not in unlocking state")] + ContractNotUnlocking, + + #[error("No unlock schedule configured")] + NoSchedule, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contract::InheritanceConfig; + + #[test] + fn test_unlock_tier() { + let mut tier = UnlockTier::new(1, 30, 10); + + assert!(!tier.unlocked); + assert!(tier.should_unlock(31 * 24 * 3600)); + assert!(!tier.should_unlock(29 * 24 * 3600)); + + tier.mark_unlocked(1000); + assert!(tier.unlocked); + assert_eq!(tier.unlock_time, Some(1000)); + } + + #[test] + fn test_standard_schedule() { + let schedule = UnlockSchedule::standard(0); + + assert_eq!(schedule.tiers.len(), 4); + assert_eq!(schedule.total_unlocked_percentage, 0); + + let total: u32 = schedule.tiers.iter().map(|t| t.unlock_percentage as u32).sum(); + assert_eq!(total, 100); + } + + #[test] + fn test_schedule_processing() { + let mut schedule = UnlockSchedule::standard(0); + + // 31 days after expiry - should unlock tier 1 (10%) + let events = schedule.process_unlocks(31 * 24 * 3600); + + assert_eq!(events.len(), 1); + assert_eq!(events[0].tier, 1); + assert_eq!(events[0].percentage, 10); + assert_eq!(schedule.total_unlocked_percentage, 10); + } + + #[test] + fn test_multiple_tier_unlock() { + let mut schedule = UnlockSchedule::standard(0); + + // 91 days after expiry - should unlock tiers 1, 2, and 3 + let events = schedule.process_unlocks(91 * 24 * 3600); + + assert_eq!(events.len(), 3); + assert_eq!(schedule.total_unlocked_percentage, 70); // 10 + 25 + 35 + } + + #[test] + fn test_full_unlock() { + let mut schedule = UnlockSchedule::standard(0); + + schedule.process_unlocks(121 * 24 * 3600); + + assert!(schedule.is_fully_unlocked()); + assert_eq!(schedule.total_unlocked_percentage, 100); + } + + #[test] + fn test_custom_schedule() { + let tiers = vec![ + UnlockTier::new(1, 10, 50), + UnlockTier::new(2, 20, 50), + ]; + + let schedule = UnlockSchedule::custom(0, tiers); + assert!(schedule.is_ok()); + } + + #[test] + fn test_invalid_custom_schedule() { + // Invalid total percentage + let tiers = vec![ + UnlockTier::new(1, 10, 50), + UnlockTier::new(2, 20, 40), // Total = 90% + ]; + + let schedule = UnlockSchedule::custom(0, tiers); + assert!(matches!( + schedule.unwrap_err(), + UnlockError::InvalidTotalPercentage(90) + )); + } + + #[test] + fn test_progressive_unlock_with_contract() { + use crate::contract::Beneficiary; + + let beneficiaries = vec![ + Beneficiary::new([1u8; 32], 60).unwrap(), + Beneficiary::new([2u8; 32], 40).unwrap(), + ]; + + let mut contract = InheritanceContract::new( + [0u8; 32], + beneficiaries, + InheritanceConfig::default(), + 10000, + 0, + ) + .unwrap(); + + // Simulate unlocking state + contract.status = ContractStatus::Unlocking; + + // Initialize progressive unlock + ProgressiveUnlock::initialize(&mut contract, UnlockScheduleType::Standard).unwrap(); + + assert!(contract.unlock_schedule.is_some()); + + // Process unlocks after 31 days + let grace_expiry = contract.config.total_timeout(); + let current_time = grace_expiry + 31 * 24 * 3600; + + let distributions = ProgressiveUnlock::process(&mut contract, current_time).unwrap(); + + // Tier 1 unlocks 10% = 1000 ONC + // Beneficiary 1: 60% of 1000 = 600 + // Beneficiary 2: 40% of 1000 = 400 + assert_eq!(distributions.len(), 2); + assert_eq!(distributions[0].amount, 600); + assert_eq!(distributions[1].amount, 400); + } +} -- cgit v1.2.3