diff options
Diffstat (limited to 'inheritance/src')
| -rw-r--r-- | inheritance/src/contract.rs | 561 | ||||
| -rw-r--r-- | inheritance/src/heartbeat.rs | 447 | ||||
| -rw-r--r-- | inheritance/src/lib.rs | 11 | ||||
| -rw-r--r-- | inheritance/src/recovery.rs | 683 | ||||
| -rw-r--r-- | inheritance/src/secret_sharing.rs | 424 | ||||
| -rw-r--r-- | inheritance/src/unlock.rs | 469 |
6 files changed, 2595 insertions, 0 deletions
diff --git a/inheritance/src/contract.rs b/inheritance/src/contract.rs new file mode 100644 index 0000000..46d0ae0 --- /dev/null +++ b/inheritance/src/contract.rs @@ -0,0 +1,561 @@ +use serde::{Deserialize, Serialize}; +use crate::unlock::UnlockSchedule; +use blake3::Hasher; + +/// Time-locked inheritance contract for OnionCoin +/// WORLD'S FIRST native blockchain inheritance system! +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InheritanceContract { + /// Contract ID (hash of owner + creation time) + pub contract_id: [u8; 32], + + /// Owner's public key + pub owner: [u8; 32], + + /// Beneficiaries with their inheritance percentages + pub beneficiaries: Vec<Beneficiary>, + + /// Configuration + pub config: InheritanceConfig, + + /// Last heartbeat timestamp + pub last_heartbeat: i64, + + /// Contract creation time + pub created_at: i64, + + /// Current status + pub status: ContractStatus, + + /// Unlock schedule (optional for progressive unlock) + pub unlock_schedule: Option<UnlockSchedule>, + + /// Encrypted data (for privacy) + pub encrypted_metadata: Option<Vec<u8>>, + + /// Dispute flag (owner can dispute if alive) + pub dispute_active: bool, + + /// Total amount locked in contract + pub locked_amount: u64, +} + +/// A beneficiary who will receive inheritance +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Beneficiary { + /// Beneficiary's public key + pub pubkey: [u8; 32], + + /// Percentage of inheritance (0-100) + pub percentage: u8, + + /// Optional encrypted name/info + pub encrypted_info: Option<Vec<u8>>, + + /// Has this beneficiary been notified? + pub notified: bool, + + /// Amount unlocked so far + pub unlocked_amount: u64, +} + +impl Beneficiary { + pub fn new(pubkey: [u8; 32], percentage: u8) -> Result<Self, InheritanceError> { + if percentage > 100 { + return Err(InheritanceError::InvalidPercentage(percentage)); + } + + Ok(Self { + pubkey, + percentage, + encrypted_info: None, + notified: false, + unlocked_amount: 0, + }) + } + + /// Calculate absolute amount for this beneficiary + pub fn calculate_amount(&self, total: u64) -> u64 { + (total as f64 * (self.percentage as f64 / 100.0)) as u64 + } +} + +/// Contract configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InheritanceConfig { + /// Heartbeat interval (seconds) - how often owner must check in + /// Default: 90 days + pub heartbeat_interval: i64, + + /// Grace period after missed heartbeat (seconds) + /// Default: 30 days + pub grace_period: i64, + + /// Enable progressive unlock + pub progressive_unlock: bool, + + /// Require Tor-only access for heartbeat + pub tor_only: bool, + + /// Enable encrypted beneficiary list + pub encrypted_beneficiaries: bool, + + /// Require multi-signature for unlock + pub multi_sig_required: bool, + + /// Number of signatures required (if multi_sig enabled) + pub required_signatures: u32, + + /// Enable Shamir secret sharing + pub shamir_enabled: bool, + + /// Shamir threshold (M of N) + pub shamir_threshold: Option<(u8, u8)>, +} + +impl Default for InheritanceConfig { + fn default() -> Self { + Self { + heartbeat_interval: 90 * 24 * 3600, // 90 days + grace_period: 30 * 24 * 3600, // 30 days + progressive_unlock: true, // Default ON + tor_only: true, // Privacy first! + encrypted_beneficiaries: true, + multi_sig_required: false, + required_signatures: 0, + shamir_enabled: false, + shamir_threshold: None, + } + } +} + +impl InheritanceConfig { + /// Total time before unlock starts + pub fn total_timeout(&self) -> i64 { + self.heartbeat_interval + self.grace_period + } + + /// Create a secure config (maximum protections) + pub fn secure() -> Self { + Self { + heartbeat_interval: 180 * 24 * 3600, // 6 months + grace_period: 60 * 24 * 3600, // 2 months + progressive_unlock: true, + tor_only: true, + encrypted_beneficiaries: true, + multi_sig_required: true, + required_signatures: 2, + shamir_enabled: true, + shamir_threshold: Some((3, 5)), // 3 of 5 + } + } + + /// Create a quick config (for testing or urgent cases) + pub fn quick() -> Self { + Self { + heartbeat_interval: 30 * 24 * 3600, // 30 days + grace_period: 7 * 24 * 3600, // 7 days + progressive_unlock: true, + tor_only: false, + encrypted_beneficiaries: false, + multi_sig_required: false, + required_signatures: 0, + shamir_enabled: false, + shamir_threshold: None, + } + } +} + +/// Contract status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ContractStatus { + /// Contract is active, owner is alive + Active, + + /// Heartbeat missed, grace period active + GracePeriod, + + /// Grace period expired, unlocking in progress + Unlocking, + + /// Fully unlocked, inheritance distributed + Completed, + + /// Owner disputed (proved they're alive) + Disputed, + + /// Contract cancelled by owner + Cancelled, +} + +impl InheritanceContract { + /// Create a new inheritance contract + pub fn new( + owner: [u8; 32], + beneficiaries: Vec<Beneficiary>, + config: InheritanceConfig, + locked_amount: u64, + current_time: i64, + ) -> Result<Self, InheritanceError> { + // Validate beneficiaries + Self::validate_beneficiaries(&beneficiaries)?; + + // Generate contract ID + let contract_id = Self::generate_contract_id(&owner, current_time); + + Ok(Self { + contract_id, + owner, + beneficiaries, + config, + last_heartbeat: current_time, + created_at: current_time, + status: ContractStatus::Active, + unlock_schedule: None, + encrypted_metadata: None, + dispute_active: false, + locked_amount, + }) + } + + /// Generate unique contract ID + fn generate_contract_id(owner: &[u8; 32], created_at: i64) -> [u8; 32] { + let mut hasher = Hasher::new(); + hasher.update(owner); + hasher.update(&created_at.to_le_bytes()); + *hasher.finalize().as_bytes() + } + + /// Validate beneficiaries list + fn validate_beneficiaries(beneficiaries: &[Beneficiary]) -> Result<(), InheritanceError> { + if beneficiaries.is_empty() { + return Err(InheritanceError::NoBeneficiaries); + } + + // Check total percentage = 100% + let total: u32 = beneficiaries.iter().map(|b| b.percentage as u32).sum(); + if total != 100 { + return Err(InheritanceError::InvalidTotalPercentage(total)); + } + + // Check for duplicate beneficiaries + let mut seen = std::collections::HashSet::new(); + for b in beneficiaries { + if !seen.insert(b.pubkey) { + return Err(InheritanceError::DuplicateBeneficiary); + } + } + + Ok(()) + } + + /// Record a heartbeat (owner is alive) + pub fn heartbeat(&mut self, current_time: i64) -> Result<(), InheritanceError> { + // Only owner can heartbeat + if self.status == ContractStatus::Cancelled { + return Err(InheritanceError::ContractCancelled); + } + + if self.status == ContractStatus::Completed { + return Err(InheritanceError::ContractCompleted); + } + + self.last_heartbeat = current_time; + + // Reset status if was in grace period + if self.status == ContractStatus::GracePeriod { + self.status = ContractStatus::Active; + } + + Ok(()) + } + + /// Check if heartbeat timeout has been reached + pub fn is_timeout(&self, current_time: i64) -> bool { + let elapsed = current_time - self.last_heartbeat; + elapsed >= self.config.heartbeat_interval + } + + /// Check if grace period has expired + pub fn is_grace_expired(&self, current_time: i64) -> bool { + let elapsed = current_time - self.last_heartbeat; + elapsed >= self.config.total_timeout() + } + + /// Update contract status based on time + pub fn update_status(&mut self, current_time: i64) { + if self.is_grace_expired(current_time) { + if self.status != ContractStatus::Unlocking + && self.status != ContractStatus::Completed + && self.status != ContractStatus::Cancelled { + self.status = ContractStatus::Unlocking; + } + } else if self.is_timeout(current_time) { + if self.status == ContractStatus::Active { + self.status = ContractStatus::GracePeriod; + } + } + } + + /// File a dispute (owner proves they're alive) + pub fn file_dispute(&mut self, current_time: i64) -> Result<(), InheritanceError> { + if self.status == ContractStatus::Completed { + return Err(InheritanceError::ContractCompleted); + } + + self.dispute_active = true; + self.status = ContractStatus::Disputed; + self.last_heartbeat = current_time; // Reset heartbeat + + Ok(()) + } + + /// Cancel contract (owner decides to cancel) + pub fn cancel(&mut self) -> Result<u64, InheritanceError> { + if self.status == ContractStatus::Completed { + return Err(InheritanceError::ContractCompleted); + } + + self.status = ContractStatus::Cancelled; + + // Return locked amount to owner + let amount = self.locked_amount; + self.locked_amount = 0; + + Ok(amount) + } + + /// Calculate time until unlock + pub fn time_until_unlock(&self, current_time: i64) -> i64 { + let total_timeout = self.config.total_timeout(); + let elapsed = current_time - self.last_heartbeat; + (total_timeout - elapsed).max(0) + } + + /// Get human-readable status + pub fn status_description(&self, current_time: i64) -> String { + match self.status { + ContractStatus::Active => { + let days = self.time_until_unlock(current_time) / (24 * 3600); + format!("Active - {} days until timeout", days) + } + ContractStatus::GracePeriod => { + let days = self.time_until_unlock(current_time) / (24 * 3600); + format!("Grace Period - {} days remaining", days) + } + ContractStatus::Unlocking => "Unlocking in progress".to_string(), + ContractStatus::Completed => "Inheritance distributed".to_string(), + ContractStatus::Disputed => "Disputed by owner".to_string(), + ContractStatus::Cancelled => "Cancelled".to_string(), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum InheritanceError { + #[error("No beneficiaries specified")] + NoBeneficiaries, + + #[error("Invalid beneficiary percentage: {0} (must be 0-100)")] + InvalidPercentage(u8), + + #[error("Total percentage must be 100%, got {0}%")] + InvalidTotalPercentage(u32), + + #[error("Duplicate beneficiary detected")] + DuplicateBeneficiary, + + #[error("Contract already cancelled")] + ContractCancelled, + + #[error("Contract already completed")] + ContractCompleted, + + #[error("Heartbeat timeout not reached")] + TimeoutNotReached, + + #[error("Grace period not expired")] + GracePeriodNotExpired, + + #[error("Insufficient locked amount")] + InsufficientAmount, + + #[error("Unauthorized access")] + Unauthorized, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_beneficiary_creation() { + let beneficiary = Beneficiary::new([1u8; 32], 50).unwrap(); + assert_eq!(beneficiary.percentage, 50); + + let invalid = Beneficiary::new([1u8; 32], 101); + assert!(invalid.is_err()); + } + + #[test] + fn test_beneficiary_amount_calculation() { + let beneficiary = Beneficiary::new([1u8; 32], 25).unwrap(); + assert_eq!(beneficiary.calculate_amount(1000), 250); + } + + #[test] + fn test_contract_creation() { + let beneficiaries = vec![ + Beneficiary::new([1u8; 32], 60).unwrap(), + Beneficiary::new([2u8; 32], 40).unwrap(), + ]; + + let contract = InheritanceContract::new( + [0u8; 32], + beneficiaries, + InheritanceConfig::default(), + 10000, + 0, + ); + + assert!(contract.is_ok()); + } + + #[test] + fn test_invalid_total_percentage() { + let beneficiaries = vec![ + Beneficiary::new([1u8; 32], 60).unwrap(), + Beneficiary::new([2u8; 32], 30).unwrap(), // Total = 90%, invalid + ]; + + let contract = InheritanceContract::new( + [0u8; 32], + beneficiaries, + InheritanceConfig::default(), + 10000, + 0, + ); + + assert!(matches!( + contract.unwrap_err(), + InheritanceError::InvalidTotalPercentage(90) + )); + } + + #[test] + fn test_heartbeat() { + let beneficiaries = vec![Beneficiary::new([1u8; 32], 100).unwrap()]; + + let mut contract = InheritanceContract::new( + [0u8; 32], + beneficiaries, + InheritanceConfig::default(), + 10000, + 0, + ) + .unwrap(); + + // Record heartbeat + contract.heartbeat(100).unwrap(); + assert_eq!(contract.last_heartbeat, 100); + } + + #[test] + fn test_timeout_detection() { + let beneficiaries = vec![Beneficiary::new([1u8; 32], 100).unwrap()]; + + let config = InheritanceConfig { + heartbeat_interval: 1000, + grace_period: 500, + ..Default::default() + }; + + let contract = InheritanceContract::new([0u8; 32], beneficiaries, config, 10000, 0) + .unwrap(); + + assert!(!contract.is_timeout(500)); + assert!(contract.is_timeout(1001)); + } + + #[test] + fn test_grace_period() { + let beneficiaries = vec![Beneficiary::new([1u8; 32], 100).unwrap()]; + + let config = InheritanceConfig { + heartbeat_interval: 1000, + grace_period: 500, + ..Default::default() + }; + + let contract = InheritanceContract::new([0u8; 32], beneficiaries, config, 10000, 0) + .unwrap(); + + assert!(!contract.is_grace_expired(1400)); + assert!(contract.is_grace_expired(1501)); + } + + #[test] + fn test_status_updates() { + let beneficiaries = vec![Beneficiary::new([1u8; 32], 100).unwrap()]; + + let config = InheritanceConfig { + heartbeat_interval: 100, + grace_period: 50, + ..Default::default() + }; + + let mut contract = InheritanceContract::new([0u8; 32], beneficiaries, config, 10000, 0) + .unwrap(); + + // Initially active + assert_eq!(contract.status, ContractStatus::Active); + + // After timeout, should enter grace period + contract.update_status(101); + assert_eq!(contract.status, ContractStatus::GracePeriod); + + // After grace period, should start unlocking + contract.update_status(151); + assert_eq!(contract.status, ContractStatus::Unlocking); + } + + #[test] + fn test_dispute() { + let beneficiaries = vec![Beneficiary::new([1u8; 32], 100).unwrap()]; + + let mut contract = InheritanceContract::new( + [0u8; 32], + beneficiaries, + InheritanceConfig::default(), + 10000, + 0, + ) + .unwrap(); + + contract.file_dispute(1000).unwrap(); + + assert_eq!(contract.status, ContractStatus::Disputed); + assert!(contract.dispute_active); + assert_eq!(contract.last_heartbeat, 1000); + } + + #[test] + fn test_cancel_contract() { + let beneficiaries = vec![Beneficiary::new([1u8; 32], 100).unwrap()]; + + let mut contract = InheritanceContract::new( + [0u8; 32], + beneficiaries, + InheritanceConfig::default(), + 10000, + 0, + ) + .unwrap(); + + let returned = contract.cancel().unwrap(); + + assert_eq!(returned, 10000); + assert_eq!(contract.status, ContractStatus::Cancelled); + assert_eq!(contract.locked_amount, 0); + } +} diff --git a/inheritance/src/heartbeat.rs b/inheritance/src/heartbeat.rs new file mode 100644 index 0000000..ef19776 --- /dev/null +++ b/inheritance/src/heartbeat.rs @@ -0,0 +1,447 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use crate::contract::InheritanceContract; + +/// Manages heartbeat system for inheritance contracts +/// Owner must periodically "check in" to prove they're alive +pub struct HeartbeatManager { + /// Contract heartbeat tracking + contracts: HashMap<[u8; 32], HeartbeatRecord>, + + /// Notification settings + notification_settings: NotificationSettings, +} + +/// Record of heartbeat activity for a contract +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HeartbeatRecord { + /// Contract ID + pub contract_id: [u8; 32], + + /// Last heartbeat timestamp + pub last_heartbeat: i64, + + /// Total heartbeats sent + pub heartbeat_count: u64, + + /// Average interval between heartbeats + pub avg_interval_days: f64, + + /// Warnings sent to owner + pub warnings_sent: u32, + + /// Notifications sent to beneficiaries + pub beneficiary_notifications_sent: u32, +} + +/// Notification settings for heartbeat warnings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NotificationSettings { + /// Send warning when X% of interval remaining + pub warning_threshold_percentage: u8, + + /// Send critical warning when in grace period + pub grace_period_warning: bool, + + /// Notify beneficiaries when grace period starts + pub notify_beneficiaries_grace: bool, + + /// Notify beneficiaries on each unlock tier + pub notify_beneficiaries_unlock: bool, +} + +impl Default for NotificationSettings { + fn default() -> Self { + Self { + warning_threshold_percentage: 80, // Warn at 80% of interval + grace_period_warning: true, + notify_beneficiaries_grace: true, + notify_beneficiaries_unlock: true, + } + } +} + +impl HeartbeatManager { + pub fn new() -> Self { + Self { + contracts: HashMap::new(), + notification_settings: NotificationSettings::default(), + } + } + + /// Register a contract for heartbeat monitoring + pub fn register_contract(&mut self, contract_id: [u8; 32], current_time: i64) { + let record = HeartbeatRecord { + contract_id, + last_heartbeat: current_time, + heartbeat_count: 0, + avg_interval_days: 0.0, + warnings_sent: 0, + beneficiary_notifications_sent: 0, + }; + + self.contracts.insert(contract_id, record); + } + + /// Process a heartbeat + pub fn process_heartbeat( + &mut self, + contract: &mut InheritanceContract, + current_time: i64, + ) -> Result<HeartbeatStatus, HeartbeatError> { + // Record heartbeat in contract + contract.heartbeat(current_time) + .map_err(|_| HeartbeatError::ContractError)?; + + // Update heartbeat record + if let Some(record) = self.contracts.get_mut(&contract.contract_id) { + let interval = current_time - record.last_heartbeat; + let interval_days = interval as f64 / (24.0 * 3600.0); + + // Update average interval + let total = record.heartbeat_count as f64; + record.avg_interval_days = + (record.avg_interval_days * total + interval_days) / (total + 1.0); + + record.last_heartbeat = current_time; + record.heartbeat_count += 1; + record.warnings_sent = 0; // Reset warnings + + Ok(HeartbeatStatus::Success { + next_required: current_time + contract.config.heartbeat_interval, + avg_interval_days: record.avg_interval_days, + }) + } else { + Err(HeartbeatError::ContractNotRegistered) + } + } + + /// Check all contracts and generate notifications + pub fn check_contracts( + &mut self, + contracts: &[&InheritanceContract], + current_time: i64, + ) -> Vec<Notification> { + let mut notifications = Vec::new(); + + for contract in contracts { + if let Some(record) = self.contracts.get_mut(&contract.contract_id) { + let time_since_heartbeat = current_time - contract.last_heartbeat; + let interval = contract.config.heartbeat_interval; + + // Calculate percentage of interval elapsed + let percentage_elapsed = (time_since_heartbeat as f64 / interval as f64 * 100.0) as u8; + + // Check for warnings + if percentage_elapsed >= self.notification_settings.warning_threshold_percentage + && record.warnings_sent == 0 + { + notifications.push(Notification { + contract_id: contract.contract_id, + notification_type: NotificationType::WarningOwner, + message: format!( + "{}% of heartbeat interval elapsed. Please send heartbeat soon!", + percentage_elapsed + ), + urgency: Urgency::Medium, + timestamp: current_time, + }); + record.warnings_sent += 1; + } + + // Check if grace period started + if contract.is_timeout(current_time) + && !contract.is_grace_expired(current_time) + && self.notification_settings.grace_period_warning + && record.warnings_sent < 2 + { + let grace_remaining = contract.config.grace_period + - (time_since_heartbeat - interval); + + notifications.push(Notification { + contract_id: contract.contract_id, + notification_type: NotificationType::CriticalOwner, + message: format!( + "CRITICAL: Grace period active! {} days remaining before unlock!", + grace_remaining / (24 * 3600) + ), + urgency: Urgency::Critical, + timestamp: current_time, + }); + + // Notify beneficiaries + if self.notification_settings.notify_beneficiaries_grace + && record.beneficiary_notifications_sent == 0 + { + notifications.push(Notification { + contract_id: contract.contract_id, + notification_type: NotificationType::InfoBeneficiaries, + message: "Inheritance contract has entered grace period. Unlock will begin if owner does not respond.".to_string(), + urgency: Urgency::Low, + timestamp: current_time, + }); + record.beneficiary_notifications_sent += 1; + } + + record.warnings_sent += 1; + } + + // Check if unlocking started + if contract.is_grace_expired(current_time) + && record.beneficiary_notifications_sent < 2 + && self.notification_settings.notify_beneficiaries_unlock + { + notifications.push(Notification { + contract_id: contract.contract_id, + notification_type: NotificationType::UnlockStarted, + message: "Inheritance unlock has begun. Funds will be distributed according to the schedule.".to_string(), + urgency: Urgency::High, + timestamp: current_time, + }); + record.beneficiary_notifications_sent += 1; + } + } + } + + notifications + } + + /// Get heartbeat statistics for a contract + pub fn get_stats(&self, contract_id: &[u8; 32]) -> Option<&HeartbeatRecord> { + self.contracts.get(contract_id) + } + + /// Suggest optimal heartbeat frequency for user + pub fn suggest_frequency(&self, contract_id: &[u8; 32]) -> Option<String> { + self.contracts.get(contract_id).map(|record| { + if record.heartbeat_count < 3 { + "Not enough data yet. Continue sending regular heartbeats.".to_string() + } else if record.avg_interval_days < 30.0 { + format!( + "You're checking in every {:.0} days. Consider spacing out to 30-60 days.", + record.avg_interval_days + ) + } else if record.avg_interval_days > 60.0 { + format!( + "You're checking in every {:.0} days. Consider more frequent heartbeats (30-45 days) for safety.", + record.avg_interval_days + ) + } else { + format!( + "Good frequency! Checking in every {:.0} days.", + record.avg_interval_days + ) + } + }) + } + + /// Create simple heartbeat transaction + /// Just send tiny amount to yourself to prove you're alive + pub fn create_heartbeat_tx(owner: [u8; 32]) -> HeartbeatTransaction { + HeartbeatTransaction { + from: owner, + to: owner, // Send to yourself + amount: 1, // Minimum amount (0.00000001 ONC) + timestamp: chrono::Utc::now().timestamp(), + heartbeat_marker: true, + } + } +} + +impl Default for HeartbeatManager { + fn default() -> Self { + Self::new() + } +} + +/// Result of heartbeat processing +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HeartbeatStatus { + Success { + next_required: i64, + avg_interval_days: f64, + }, + Warning { + message: String, + }, + Error { + message: String, + }, +} + +/// Notification to send to owner or beneficiaries +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Notification { + pub contract_id: [u8; 32], + pub notification_type: NotificationType, + pub message: String, + pub urgency: Urgency, + pub timestamp: i64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum NotificationType { + WarningOwner, + CriticalOwner, + InfoBeneficiaries, + UnlockStarted, + TierUnlocked, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Urgency { + Low, + Medium, + High, + Critical, +} + +/// Simple heartbeat transaction +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HeartbeatTransaction { + pub from: [u8; 32], + pub to: [u8; 32], + pub amount: u64, + pub timestamp: i64, + pub heartbeat_marker: bool, +} + +#[derive(Debug, thiserror::Error)] +pub enum HeartbeatError { + #[error("Contract not registered with heartbeat manager")] + ContractNotRegistered, + + #[error("Contract error occurred")] + ContractError, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contract::{InheritanceContract, Beneficiary, InheritanceConfig}; + + fn create_test_contract() -> InheritanceContract { + let beneficiaries = vec![Beneficiary::new([1u8; 32], 100).unwrap()]; + + InheritanceContract::new( + [0u8; 32], + beneficiaries, + InheritanceConfig::default(), + 10000, + 0, + ) + .unwrap() + } + + #[test] + fn test_heartbeat_manager_registration() { + let mut manager = HeartbeatManager::new(); + let contract_id = [1u8; 32]; + + manager.register_contract(contract_id, 0); + + assert!(manager.contracts.contains_key(&contract_id)); + } + + #[test] + fn test_heartbeat_processing() { + let mut manager = HeartbeatManager::new(); + let mut contract = create_test_contract(); + + manager.register_contract(contract.contract_id, 0); + + let result = manager.process_heartbeat(&mut contract, 1000); + + assert!(result.is_ok()); + assert_eq!(contract.last_heartbeat, 1000); + + let record = manager.get_stats(&contract.contract_id).unwrap(); + assert_eq!(record.heartbeat_count, 1); + } + + #[test] + fn test_average_interval_calculation() { + let mut manager = HeartbeatManager::new(); + let mut contract = create_test_contract(); + + manager.register_contract(contract.contract_id, 0); + + // First heartbeat at day 30 + manager.process_heartbeat(&mut contract, 30 * 24 * 3600).unwrap(); + + // Second heartbeat at day 60 + manager.process_heartbeat(&mut contract, 60 * 24 * 3600).unwrap(); + + let record = manager.get_stats(&contract.contract_id).unwrap(); + assert!((record.avg_interval_days - 30.0).abs() < 1.0); + } + + #[test] + fn test_notification_generation() { + let mut manager = HeartbeatManager::new(); + let mut contract = create_test_contract(); + + // Set short interval for testing + contract.config.heartbeat_interval = 100; + contract.config.grace_period = 50; + + manager.register_contract(contract.contract_id, 0); + + // Check at 85% of interval (should generate warning) + let notifications = manager.check_contracts(&[&contract], 85); + + assert!(!notifications.is_empty()); + assert_eq!(notifications[0].notification_type, NotificationType::WarningOwner); + assert_eq!(notifications[0].urgency, Urgency::Medium); + } + + #[test] + fn test_grace_period_notifications() { + let mut manager = HeartbeatManager::new(); + let mut contract = create_test_contract(); + + contract.config.heartbeat_interval = 100; + contract.config.grace_period = 50; + + manager.register_contract(contract.contract_id, 0); + + // Enter grace period + let notifications = manager.check_contracts(&[&contract], 101); + + // Should have critical warning for owner and info for beneficiaries + assert!(notifications.len() >= 2); + assert!(notifications.iter().any(|n| n.notification_type == NotificationType::CriticalOwner)); + assert!(notifications.iter().any(|n| n.notification_type == NotificationType::InfoBeneficiaries)); + } + + #[test] + fn test_heartbeat_transaction() { + let owner = [42u8; 32]; + let tx = HeartbeatManager::create_heartbeat_tx(owner); + + assert_eq!(tx.from, owner); + assert_eq!(tx.to, owner); // Sent to self + assert_eq!(tx.amount, 1); // Minimal amount + assert!(tx.heartbeat_marker); + } + + #[test] + fn test_frequency_suggestion() { + let mut manager = HeartbeatManager::new(); + let contract_id = [1u8; 32]; + + manager.register_contract(contract_id, 0); + + // Not enough data initially + let suggestion = manager.suggest_frequency(&contract_id).unwrap(); + assert!(suggestion.contains("Not enough data")); + + // Simulate multiple heartbeats + let mut record = manager.contracts.get_mut(&contract_id).unwrap(); + record.heartbeat_count = 5; + record.avg_interval_days = 45.0; + + let suggestion = manager.suggest_frequency(&contract_id).unwrap(); + assert!(suggestion.contains("Good frequency")); + } +} diff --git a/inheritance/src/lib.rs b/inheritance/src/lib.rs new file mode 100644 index 0000000..8f9ca94 --- /dev/null +++ b/inheritance/src/lib.rs @@ -0,0 +1,11 @@ +pub mod contract; +pub mod heartbeat; +pub mod unlock; +pub mod secret_sharing; +pub mod recovery; + +pub use contract::{InheritanceContract, Beneficiary, InheritanceConfig}; +pub use heartbeat::{HeartbeatManager, HeartbeatStatus}; +pub use unlock::{UnlockTier, UnlockSchedule, ProgressiveUnlock}; +pub use secret_sharing::{ShamirShares, ShareDistribution}; +pub use recovery::{RecoveryManager, DisputeResolution}; diff --git a/inheritance/src/recovery.rs b/inheritance/src/recovery.rs new file mode 100644 index 0000000..60c28fc --- /dev/null +++ b/inheritance/src/recovery.rs @@ -0,0 +1,683 @@ +use serde::{Deserialize, Serialize}; +use crate::contract::{InheritanceContract, ContractStatus}; +use std::collections::HashMap; + +/// Manages dispute resolution and anti-scam protections +pub struct RecoveryManager { + /// Active disputes + disputes: HashMap<[u8; 32], Dispute>, + + /// Recovery attempts + recovery_attempts: HashMap<[u8; 32], Vec<RecoveryAttempt>>, + + /// Configuration + config: RecoveryConfig, +} + +/// Configuration for recovery and dispute system +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecoveryConfig { + /// Allow owner to dispute unlock + pub allow_disputes: bool, + + /// Maximum disputes allowed + pub max_disputes: u32, + + /// Cooldown period between disputes (seconds) + pub dispute_cooldown: i64, + + /// Enable multi-signature for disputes + pub multi_sig_disputes: bool, + + /// Required confirmations for recovery + pub required_confirmations: u32, + + /// Enable anti-scam checks + pub anti_scam_enabled: bool, +} + +impl Default for RecoveryConfig { + fn default() -> Self { + Self { + allow_disputes: true, + max_disputes: 3, // Max 3 disputes + dispute_cooldown: 7 * 24 * 3600, // 7 days between disputes + multi_sig_disputes: false, + required_confirmations: 1, + anti_scam_enabled: true, + } + } +} + +/// A dispute filed by the owner +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Dispute { + /// Contract ID + pub contract_id: [u8; 32], + + /// Who filed the dispute + pub filed_by: [u8; 32], + + /// When the dispute was filed + pub filed_at: i64, + + /// Dispute reason + pub reason: DisputeReason, + + /// Dispute status + pub status: DisputeStatus, + + /// Evidence provided + pub evidence: Vec<Evidence>, + + /// Resolution + pub resolution: Option<DisputeResolution>, + + /// Number of previous disputes + pub dispute_count: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DisputeReason { + /// Owner is alive and well + OwnerAlive, + + /// Temporary incapacitation (hospital, etc.) + TemporaryIncapacitation, + + /// Technical issue preventing heartbeat + TechnicalIssue, + + /// Suspected fraudulent beneficiary + FraudSuspected, + + /// Other reason + Other(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DisputeStatus { + /// Pending review + Pending, + + /// Under investigation + UnderReview, + + /// Resolved in favor of owner + ResolvedOwner, + + /// Resolved in favor of beneficiaries + ResolvedBeneficiaries, + + /// Rejected (too many disputes) + Rejected, +} + +/// Evidence for a dispute +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Evidence { + /// Evidence type + pub evidence_type: EvidenceType, + + /// Evidence data (encrypted) + pub data: Vec<u8>, + + /// Timestamp + pub submitted_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EvidenceType { + /// Cryptographic proof of life (signed message) + ProofOfLife, + + /// Medical certificate (encrypted) + MedicalCertificate, + + /// Technical logs + TechnicalLogs, + + /// Witness statement + WitnessStatement, + + /// Other + Other, +} + +/// Resolution of a dispute +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DisputeResolution { + /// Resolution type + pub resolution_type: ResolutionType, + + /// Actions taken + pub actions: Vec<ResolutionAction>, + + /// Resolved at + pub resolved_at: i64, + + /// Notes + pub notes: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ResolutionType { + /// Owner proven alive, reset heartbeat + ResetHeartbeat, + + /// Extend grace period + ExtendGracePeriod { additional_days: i64 }, + + /// Cancel unlock, resume normal operation + CancelUnlock, + + /// Proceed with unlock (owner dispute invalid) + ProceedWithUnlock, + + /// Freeze contract pending investigation + FreezeContract, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ResolutionAction { + HeartbeatReset, + GracePeriodExtended { days: i64 }, + ContractCancelled, + UnlockProceeded, + ContractFrozen, +} + +/// Attempt to recover/access contract +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecoveryAttempt { + /// Who attempted recovery + pub attempted_by: [u8; 32], + + /// When + pub attempted_at: i64, + + /// Method used + pub method: RecoveryMethod, + + /// Success or failure + pub success: bool, + + /// Suspicious indicators + pub suspicious: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum RecoveryMethod { + /// Normal progressive unlock + ProgressiveUnlock, + + /// Shamir secret sharing + ShamirRecovery { shares_used: u8 }, + + /// Emergency recovery key + EmergencyKey, + + /// Dispute resolution + DisputeResolution, +} + +impl RecoveryManager { + pub fn new(config: RecoveryConfig) -> Self { + Self { + disputes: HashMap::new(), + recovery_attempts: HashMap::new(), + config, + } + } + + /// File a dispute + pub fn file_dispute( + &mut self, + contract: &mut InheritanceContract, + filed_by: [u8; 32], + reason: DisputeReason, + current_time: i64, + ) -> Result<(), RecoveryError> { + if !self.config.allow_disputes { + return Err(RecoveryError::DisputesNotAllowed); + } + + // Check if owner + if filed_by != contract.owner { + return Err(RecoveryError::Unauthorized); + } + + // Check max disputes + let previous_disputes = self.disputes.values() + .filter(|d| d.contract_id == contract.contract_id) + .count() as u32; + + if previous_disputes >= self.config.max_disputes { + return Err(RecoveryError::TooManyDisputes(previous_disputes)); + } + + // Check cooldown + if let Some(last_dispute) = self.get_last_dispute(&contract.contract_id) { + let time_since_last = current_time - last_dispute.filed_at; + if time_since_last < self.config.dispute_cooldown { + return Err(RecoveryError::DisputeCooldownActive { + remaining: self.config.dispute_cooldown - time_since_last, + }); + } + } + + // Create dispute + let dispute = Dispute { + contract_id: contract.contract_id, + filed_by, + filed_at: current_time, + reason, + status: DisputeStatus::Pending, + evidence: Vec::new(), + resolution: None, + dispute_count: previous_disputes, + }; + + // File dispute in contract + contract.file_dispute(current_time)?; + + self.disputes.insert(contract.contract_id, dispute); + + Ok(()) + } + + /// Add evidence to dispute + pub fn add_evidence( + &mut self, + contract_id: &[u8; 32], + evidence: Evidence, + ) -> Result<(), RecoveryError> { + let dispute = self.disputes.get_mut(contract_id) + .ok_or(RecoveryError::DisputeNotFound)?; + + if dispute.status != DisputeStatus::Pending + && dispute.status != DisputeStatus::UnderReview + { + return Err(RecoveryError::DisputeAlreadyResolved); + } + + dispute.evidence.push(evidence); + dispute.status = DisputeStatus::UnderReview; + + Ok(()) + } + + /// Resolve a dispute + pub fn resolve_dispute( + &mut self, + contract: &mut InheritanceContract, + resolution: DisputeResolution, + current_time: i64, + ) -> Result<(), RecoveryError> { + let dispute = self.disputes.get_mut(&contract.contract_id) + .ok_or(RecoveryError::DisputeNotFound)?; + + // Apply resolution actions + for action in &resolution.actions { + match action { + ResolutionAction::HeartbeatReset => { + contract.heartbeat(current_time)?; + contract.status = ContractStatus::Active; + } + ResolutionAction::GracePeriodExtended { days } => { + contract.config.grace_period += days * 24 * 3600; + } + ResolutionAction::ContractCancelled => { + contract.cancel()?; + } + ResolutionAction::UnlockProceeded => { + contract.status = ContractStatus::Unlocking; + contract.dispute_active = false; + } + ResolutionAction::ContractFrozen => { + // Implementation would freeze contract + } + } + } + + dispute.status = match resolution.resolution_type { + ResolutionType::ResetHeartbeat | ResolutionType::ExtendGracePeriod { .. } | ResolutionType::CancelUnlock => { + DisputeStatus::ResolvedOwner + } + ResolutionType::ProceedWithUnlock => { + DisputeStatus::ResolvedBeneficiaries + } + ResolutionType::FreezeContract => { + DisputeStatus::UnderReview + } + }; + + dispute.resolution = Some(resolution); + + Ok(()) + } + + /// Record a recovery attempt + pub fn record_recovery_attempt( + &mut self, + contract_id: [u8; 32], + attempt: RecoveryAttempt, + ) { + self.recovery_attempts + .entry(contract_id) + .or_insert_with(Vec::new) + .push(attempt); + } + + /// Check for suspicious activity + pub fn check_suspicious_activity(&self, contract_id: &[u8; 32]) -> SuspiciousActivityReport { + let attempts = self.recovery_attempts.get(contract_id); + + let mut report = SuspiciousActivityReport { + contract_id: *contract_id, + suspicious: false, + indicators: Vec::new(), + }; + + if let Some(attempts) = attempts { + // Check for multiple failed attempts + let failed_attempts = attempts.iter().filter(|a| !a.success).count(); + if failed_attempts > 5 { + report.suspicious = true; + report.indicators.push(format!( + "{} failed recovery attempts", + failed_attempts + )); + } + + // Check for attempts from unknown parties + let unique_attemptors: std::collections::HashSet<_> = + attempts.iter().map(|a| a.attempted_by).collect(); + + if unique_attemptors.len() > 3 { + report.suspicious = true; + report.indicators.push(format!( + "{} different parties attempted recovery", + unique_attemptors.len() + )); + } + + // Check for suspicious timing (many attempts in short time) + if attempts.len() > 10 { + let first = attempts.first().unwrap().attempted_at; + let last = attempts.last().unwrap().attempted_at; + let timespan_hours = (last - first) / 3600; + + if timespan_hours < 24 { + report.suspicious = true; + report.indicators.push(format!( + "{} attempts in {} hours", + attempts.len(), + timespan_hours + )); + } + } + } + + report + } + + /// Get last dispute for a contract + fn get_last_dispute(&self, contract_id: &[u8; 32]) -> Option<&Dispute> { + self.disputes.get(contract_id) + } + + /// Get dispute statistics + pub fn get_dispute_stats(&self, contract_id: &[u8; 32]) -> DisputeStats { + let total_disputes = self.disputes.values() + .filter(|d| d.contract_id == *contract_id) + .count(); + + let resolved_owner = self.disputes.values() + .filter(|d| d.contract_id == *contract_id && d.status == DisputeStatus::ResolvedOwner) + .count(); + + let resolved_beneficiaries = self.disputes.values() + .filter(|d| d.contract_id == *contract_id && d.status == DisputeStatus::ResolvedBeneficiaries) + .count(); + + DisputeStats { + total_disputes, + resolved_owner, + resolved_beneficiaries, + pending: total_disputes - resolved_owner - resolved_beneficiaries, + } + } +} + +impl Default for RecoveryManager { + fn default() -> Self { + Self::new(RecoveryConfig::default()) + } +} + +/// Report of suspicious activity +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SuspiciousActivityReport { + pub contract_id: [u8; 32], + pub suspicious: bool, + pub indicators: Vec<String>, +} + +/// Statistics about disputes +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DisputeStats { + pub total_disputes: usize, + pub resolved_owner: usize, + pub resolved_beneficiaries: usize, + pub pending: usize, +} + +#[derive(Debug, thiserror::Error)] +pub enum RecoveryError { + #[error("Disputes are not allowed for this contract")] + DisputesNotAllowed, + + #[error("Unauthorized: only owner can file disputes")] + Unauthorized, + + #[error("Too many disputes filed: {0}")] + TooManyDisputes(u32), + + #[error("Dispute cooldown active: {remaining} seconds remaining")] + DisputeCooldownActive { remaining: i64 }, + + #[error("Dispute not found")] + DisputeNotFound, + + #[error("Dispute already resolved")] + DisputeAlreadyResolved, + + #[error("Contract error: {0}")] + ContractError(String), +} + +impl From<crate::contract::InheritanceError> for RecoveryError { + fn from(e: crate::contract::InheritanceError) -> Self { + RecoveryError::ContractError(e.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contract::{InheritanceContract, Beneficiary, InheritanceConfig}; + + fn create_test_contract() -> InheritanceContract { + let beneficiaries = vec![Beneficiary::new([1u8; 32], 100).unwrap()]; + + InheritanceContract::new( + [0u8; 32], + beneficiaries, + InheritanceConfig::default(), + 10000, + 0, + ) + .unwrap() + } + + #[test] + fn test_file_dispute() { + let mut manager = RecoveryManager::default(); + let mut contract = create_test_contract(); + + let result = manager.file_dispute( + &mut contract, + [0u8; 32], // Owner + DisputeReason::OwnerAlive, + 1000, + ); + + assert!(result.is_ok()); + assert!(contract.dispute_active); + assert_eq!(contract.status, ContractStatus::Disputed); + } + + #[test] + fn test_unauthorized_dispute() { + let mut manager = RecoveryManager::default(); + let mut contract = create_test_contract(); + + let result = manager.file_dispute( + &mut contract, + [99u8; 32], // Not owner + DisputeReason::OwnerAlive, + 1000, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_max_disputes() { + let mut config = RecoveryConfig::default(); + config.max_disputes = 2; + config.dispute_cooldown = 0; // No cooldown for testing + + let mut manager = RecoveryManager::new(config); + let mut contract = create_test_contract(); + + // First dispute - OK + manager.file_dispute(&mut contract, [0u8; 32], DisputeReason::OwnerAlive, 1000).unwrap(); + + // Reset contract for second dispute + contract.dispute_active = false; + contract.status = ContractStatus::Active; + + // Second dispute - OK + manager.file_dispute(&mut contract, [0u8; 32], DisputeReason::OwnerAlive, 2000).unwrap(); + + // Reset for third dispute + contract.dispute_active = false; + contract.status = ContractStatus::Active; + + // Third dispute - Should fail (max 2) + let result = manager.file_dispute(&mut contract, [0u8; 32], DisputeReason::OwnerAlive, 3000); + assert!(result.is_err()); + } + + #[test] + fn test_add_evidence() { + let mut manager = RecoveryManager::default(); + let mut contract = create_test_contract(); + + manager.file_dispute(&mut contract, [0u8; 32], DisputeReason::OwnerAlive, 1000).unwrap(); + + let evidence = Evidence { + evidence_type: EvidenceType::ProofOfLife, + data: vec![1, 2, 3], + submitted_at: 1100, + }; + + let result = manager.add_evidence(&contract.contract_id, evidence); + assert!(result.is_ok()); + + let dispute = manager.disputes.get(&contract.contract_id).unwrap(); + assert_eq!(dispute.evidence.len(), 1); + assert_eq!(dispute.status, DisputeStatus::UnderReview); + } + + #[test] + fn test_resolve_dispute_reset_heartbeat() { + let mut manager = RecoveryManager::default(); + let mut contract = create_test_contract(); + + manager.file_dispute(&mut contract, [0u8; 32], DisputeReason::OwnerAlive, 1000).unwrap(); + + let resolution = DisputeResolution { + resolution_type: ResolutionType::ResetHeartbeat, + actions: vec![ResolutionAction::HeartbeatReset], + resolved_at: 2000, + notes: None, + }; + + manager.resolve_dispute(&mut contract, resolution, 2000).unwrap(); + + assert_eq!(contract.last_heartbeat, 2000); + assert_eq!(contract.status, ContractStatus::Active); + + let dispute = manager.disputes.get(&contract.contract_id).unwrap(); + assert_eq!(dispute.status, DisputeStatus::ResolvedOwner); + } + + #[test] + fn test_record_recovery_attempt() { + let mut manager = RecoveryManager::default(); + let contract_id = [1u8; 32]; + + let attempt = RecoveryAttempt { + attempted_by: [2u8; 32], + attempted_at: 1000, + method: RecoveryMethod::ProgressiveUnlock, + success: false, + suspicious: false, + }; + + manager.record_recovery_attempt(contract_id, attempt); + + let attempts = manager.recovery_attempts.get(&contract_id).unwrap(); + assert_eq!(attempts.len(), 1); + } + + #[test] + fn test_suspicious_activity_detection() { + let mut manager = RecoveryManager::default(); + let contract_id = [1u8; 32]; + + // Record many failed attempts + for i in 0..10 { + let attempt = RecoveryAttempt { + attempted_by: [(i % 5) as u8; 32], // Different attemptors + attempted_at: 1000 + i * 100, + method: RecoveryMethod::EmergencyKey, + success: false, + suspicious: false, + }; + manager.record_recovery_attempt(contract_id, attempt); + } + + let report = manager.check_suspicious_activity(&contract_id); + + assert!(report.suspicious); + assert!(!report.indicators.is_empty()); + } + + #[test] + fn test_dispute_stats() { + let mut manager = RecoveryManager::default(); + let mut contract = create_test_contract(); + + manager.file_dispute(&mut contract, [0u8; 32], DisputeReason::OwnerAlive, 1000).unwrap(); + + let stats = manager.get_dispute_stats(&contract.contract_id); + + assert_eq!(stats.total_disputes, 1); + assert_eq!(stats.pending, 1); + assert_eq!(stats.resolved_owner, 0); + } +} diff --git a/inheritance/src/secret_sharing.rs b/inheritance/src/secret_sharing.rs new file mode 100644 index 0000000..a8a0290 --- /dev/null +++ b/inheritance/src/secret_sharing.rs @@ -0,0 +1,424 @@ +use serde::{Deserialize, Serialize}; +use sharks::{Sharks, Share}; + +/// Shamir Secret Sharing for OnionCoin inheritance +/// Allows splitting seed phrase into N shares, requiring M shares to recover +pub struct ShamirShares { + /// Threshold (M of N) + threshold: u8, + + /// Total shares (N) + total_shares: u8, + + /// Share distribution info + distribution: Vec<ShareInfo>, +} + +/// Information about a share +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShareInfo { + /// Share ID (1-indexed) + pub share_id: u8, + + /// Recipient of this share (encrypted) + pub recipient_pubkey: [u8; 32], + + /// Optional encrypted recipient info + pub encrypted_info: Option<Vec<u8>>, + + /// Has share been claimed? + pub claimed: bool, + + /// Claim timestamp + pub claim_time: Option<i64>, +} + +impl ShamirShares { + /// Create new Shamir secret sharing scheme + pub fn new(threshold: u8, total_shares: u8) -> Result<Self, ShamirError> { + if threshold > total_shares { + return Err(ShamirError::InvalidThreshold { + threshold, + total_shares, + }); + } + + if threshold == 0 || total_shares == 0 { + return Err(ShamirError::ZeroShares); + } + + // Note: u8 can't be > 255, but keeping check for clarity + #[allow(unused_comparisons)] + if total_shares > 255 { + return Err(ShamirError::TooManyShares(total_shares)); + } + + Ok(Self { + threshold, + total_shares, + distribution: Vec::new(), + }) + } + + /// Split a secret into shares + pub fn split_secret(&mut self, secret: &[u8], recipients: &[[u8; 32]]) -> Result<Vec<Vec<u8>>, ShamirError> { + if recipients.len() != self.total_shares as usize { + return Err(ShamirError::RecipientCountMismatch { + expected: self.total_shares, + got: recipients.len() as u8, + }); + } + + // Create Sharks instance + let sharks = Sharks(self.threshold); + + // Generate shares from secret + let dealer = sharks.dealer(secret); + let shares: Vec<Share> = dealer.take(self.total_shares as usize).collect(); + + // Convert shares to bytes + let share_bytes: Vec<Vec<u8>> = shares.iter() + .map(|s| s.y.iter().map(|gf| gf.0).collect()) + .collect(); + + // Record distribution + for (i, recipient) in recipients.iter().enumerate() { + self.distribution.push(ShareInfo { + share_id: (i + 1) as u8, + recipient_pubkey: *recipient, + encrypted_info: None, + claimed: false, + claim_time: None, + }); + } + + Ok(share_bytes) + } + + /// Recover secret from shares + pub fn recover_secret( + &self, + shares: &[Vec<u8>], + ) -> Result<Vec<u8>, ShamirError> { + if shares.len() < self.threshold as usize { + return Err(ShamirError::InsufficientShares { + required: self.threshold, + provided: shares.len() as u8, + }); + } + + // Convert bytes back to Share objects + let share_objects: Result<Vec<Share>, _> = shares + .iter() + .map(|s| Share::try_from(s.as_slice())) + .collect(); + + let share_objects = share_objects.map_err(|_| ShamirError::InvalidShare)?; + + // Create Sharks instance and recover + let sharks = Sharks(self.threshold); + + sharks + .recover(&share_objects) + .map_err(|_| ShamirError::RecoveryFailed) + } + + /// Mark a share as claimed + pub fn claim_share(&mut self, share_id: u8, current_time: i64) -> Result<(), ShamirError> { + let share_info = self + .distribution + .iter_mut() + .find(|s| s.share_id == share_id) + .ok_or(ShamirError::ShareNotFound(share_id))?; + + if share_info.claimed { + return Err(ShamirError::ShareAlreadyClaimed(share_id)); + } + + share_info.claimed = true; + share_info.claim_time = Some(current_time); + + Ok(()) + } + + /// Get number of claimed shares + pub fn claimed_count(&self) -> usize { + self.distribution.iter().filter(|s| s.claimed).count() + } + + /// Check if recovery is possible (enough shares claimed) + pub fn can_recover(&self) -> bool { + self.claimed_count() >= self.threshold as usize + } + + /// Get recovery status + pub fn recovery_status(&self) -> String { + let claimed = self.claimed_count(); + let required = self.threshold as usize; + + if claimed >= required { + format!("✓ Recovery possible: {}/{} shares claimed", claimed, required) + } else { + format!( + "⏳ Waiting for shares: {}/{} claimed ({} more needed)", + claimed, + required, + required - claimed + ) + } + } + + /// Get configuration + pub fn config(&self) -> (u8, u8) { + (self.threshold, self.total_shares) + } +} + +/// Distribution plan for shares +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShareDistribution { + /// Shamir configuration + pub threshold: u8, + pub total_shares: u8, + + /// Share holders (trusted guardians) + pub guardians: Vec<Guardian>, + + /// Instructions for recovery + pub recovery_instructions: String, +} + +/// A guardian who holds a share +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Guardian { + pub pubkey: [u8; 32], + pub name: Option<String>, // Optional encrypted name + pub contact: Option<String>, // Optional encrypted contact info + pub share_id: u8, +} + +impl ShareDistribution { + /// Create a standard 3-of-5 distribution + pub fn standard_3_of_5(guardians: Vec<[u8; 32]>) -> Result<Self, ShamirError> { + if guardians.len() != 5 { + return Err(ShamirError::RecipientCountMismatch { + expected: 5, + got: guardians.len() as u8, + }); + } + + let guardians = guardians + .into_iter() + .enumerate() + .map(|(i, pubkey)| Guardian { + pubkey, + name: None, + contact: None, + share_id: (i + 1) as u8, + }) + .collect(); + + Ok(Self { + threshold: 3, + total_shares: 5, + guardians, + recovery_instructions: "Contact any 3 of the 5 guardians to recover the secret.".to_string(), + }) + } + + /// Create a custom distribution + pub fn custom( + threshold: u8, + guardians: Vec<Guardian>, + instructions: String, + ) -> Result<Self, ShamirError> { + if threshold > guardians.len() as u8 { + return Err(ShamirError::InvalidThreshold { + threshold, + total_shares: guardians.len() as u8, + }); + } + + Ok(Self { + threshold, + total_shares: guardians.len() as u8, + guardians, + recovery_instructions: instructions, + }) + } + + /// Generate and distribute shares + pub fn generate_shares(&self, secret: &[u8]) -> Result<Vec<(Guardian, Vec<u8>)>, ShamirError> { + let mut shamir = ShamirShares::new(self.threshold, self.total_shares)?; + + let recipients: Vec<[u8; 32]> = self.guardians.iter().map(|g| g.pubkey).collect(); + + let shares = shamir.split_secret(secret, &recipients)?; + + Ok(self + .guardians + .iter() + .zip(shares) + .map(|(g, s)| (g.clone(), s)) + .collect()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ShamirError { + #[error("Invalid threshold: {threshold} of {total_shares} (threshold must be ≤ total)")] + InvalidThreshold { threshold: u8, total_shares: u8 }, + + #[error("Threshold and total shares cannot be zero")] + ZeroShares, + + #[error("Too many shares: {0} (maximum 255)")] + TooManyShares(u8), + + #[error("Recipient count mismatch: expected {expected}, got {got}")] + RecipientCountMismatch { expected: u8, got: u8 }, + + #[error("Insufficient shares for recovery: need {required}, got {provided}")] + InsufficientShares { required: u8, provided: u8 }, + + #[error("Invalid share data")] + InvalidShare, + + #[error("Secret recovery failed")] + RecoveryFailed, + + #[error("Share {0} not found")] + ShareNotFound(u8), + + #[error("Share {0} already claimed")] + ShareAlreadyClaimed(u8), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_shamir_creation() { + let shamir = ShamirShares::new(3, 5); + assert!(shamir.is_ok()); + + let invalid = ShamirShares::new(5, 3); // Threshold > total + assert!(invalid.is_err()); + } + + #[test] + fn test_secret_splitting_and_recovery() { + let secret = b"my super secret seed phrase here"; + let recipients = vec![[1u8; 32], [2u8; 32], [3u8; 32], [4u8; 32], [5u8; 32]]; + + let mut shamir = ShamirShares::new(3, 5).unwrap(); + + // Split secret + let shares = shamir.split_secret(secret, &recipients).unwrap(); + assert_eq!(shares.len(), 5); + + // Recover with 3 shares + let recovered = shamir.recover_secret(&shares[0..3]).unwrap(); + assert_eq!(recovered, secret); + + // Recover with different 3 shares + let recovered2 = shamir.recover_secret(&[shares[1].clone(), shares[3].clone(), shares[4].clone()]).unwrap(); + assert_eq!(recovered2, secret); + + // Recover with all 5 shares + let recovered3 = shamir.recover_secret(&shares).unwrap(); + assert_eq!(recovered3, secret); + } + + #[test] + fn test_insufficient_shares() { + let secret = b"my super secret seed phrase here"; + let recipients = vec![[1u8; 32], [2u8; 32], [3u8; 32], [4u8; 32], [5u8; 32]]; + + let mut shamir = ShamirShares::new(3, 5).unwrap(); + let shares = shamir.split_secret(secret, &recipients).unwrap(); + + // Try to recover with only 2 shares + let result = shamir.recover_secret(&shares[0..2]); + assert!(result.is_err()); + } + + #[test] + fn test_share_claiming() { + let secret = b"test secret"; + let recipients = vec![[1u8; 32], [2u8; 32], [3u8; 32]]; + + let mut shamir = ShamirShares::new(2, 3).unwrap(); + shamir.split_secret(secret, &recipients).unwrap(); + + assert_eq!(shamir.claimed_count(), 0); + assert!(!shamir.can_recover()); + + // Claim first share + shamir.claim_share(1, 1000).unwrap(); + assert_eq!(shamir.claimed_count(), 1); + assert!(!shamir.can_recover()); + + // Claim second share + shamir.claim_share(2, 2000).unwrap(); + assert_eq!(shamir.claimed_count(), 2); + assert!(shamir.can_recover()); + + // Try to claim already claimed share + let result = shamir.claim_share(1, 3000); + assert!(result.is_err()); + } + + #[test] + fn test_share_distribution() { + let guardians = vec![[1u8; 32], [2u8; 32], [3u8; 32], [4u8; 32], [5u8; 32]]; + + let distribution = ShareDistribution::standard_3_of_5(guardians).unwrap(); + + assert_eq!(distribution.threshold, 3); + assert_eq!(distribution.total_shares, 5); + assert_eq!(distribution.guardians.len(), 5); + } + + #[test] + fn test_distribution_share_generation() { + let guardians = vec![[1u8; 32], [2u8; 32], [3u8; 32], [4u8; 32], [5u8; 32]]; + + let distribution = ShareDistribution::standard_3_of_5(guardians).unwrap(); + let secret = b"my wallet seed phrase"; + + let shares = distribution.generate_shares(secret).unwrap(); + + assert_eq!(shares.len(), 5); + + // Extract just the share bytes + let share_bytes: Vec<Vec<u8>> = shares.iter().map(|(_, s)| s.clone()).collect(); + + // Test recovery with 3 shares + let shamir = ShamirShares::new(3, 5).unwrap(); + let recovered = shamir.recover_secret(&share_bytes[0..3]).unwrap(); + + assert_eq!(recovered, secret); + } + + #[test] + fn test_recovery_status() { + let secret = b"test"; + let recipients = vec![[1u8; 32], [2u8; 32], [3u8; 32]]; + + let mut shamir = ShamirShares::new(2, 3).unwrap(); + shamir.split_secret(secret, &recipients).unwrap(); + + let status = shamir.recovery_status(); + assert!(status.contains("0/2")); + + shamir.claim_share(1, 0).unwrap(); + let status = shamir.recovery_status(); + assert!(status.contains("1/2")); + + shamir.claim_share(2, 0).unwrap(); + let status = shamir.recovery_status(); + assert!(status.contains("Recovery possible")); + } +} 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<i64>, +} + +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<UnlockTier>, + + /// 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<UnlockTier>) -> Result<Self, UnlockError> { + // 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<UnlockEvent> { + 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<i64> { + 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<Vec<Distribution>, 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<UnlockTier>), +} + +/// 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); + } +} |
