summaryrefslogtreecommitdiffstats
path: root/inheritance/src
diff options
context:
space:
mode:
authorgabrix73 <gabriel1@frozenstar.info>2026-06-01 18:41:36 +0200
committergabrix73 <gabriel1@frozenstar.info>2026-06-01 18:41:36 +0200
commit9f5d864d533ce86459e654f5d78212933c0269ea (patch)
tree4b71e8c7ab4e78da93e5f3164803da4de68c7031 /inheritance/src
downloadonioncoin-9e960fa6d453b29aef970ba3cd81e9cbbfd94bcc.tar.gz
onioncoin-9e960fa6d453b29aef970ba3cd81e9cbbfd94bcc.tar.xz
onioncoin-9e960fa6d453b29aef970ba3cd81e9cbbfd94bcc.zip
Initial commit: OnionCoin prototype with Proof-of-Relay consensusHEADv0.1.0main
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
Diffstat (limited to 'inheritance/src')
-rw-r--r--inheritance/src/contract.rs561
-rw-r--r--inheritance/src/heartbeat.rs447
-rw-r--r--inheritance/src/lib.rs11
-rw-r--r--inheritance/src/recovery.rs683
-rw-r--r--inheritance/src/secret_sharing.rs424
-rw-r--r--inheritance/src/unlock.rs469
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);
+ }
+}