Skip to main content

saorsa_core/attestation/
sunset.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: david@saorsalabs.com
9
10//! Sunset timestamp management for binary version expiry.
11
12use serde::{Deserialize, Serialize};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15/// A sunset timestamp representing when a binary version expires.
16///
17/// After the sunset timestamp, nodes running this binary version
18/// may be rejected (depending on enforcement mode and grace period).
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub struct SunsetTimestamp {
21    /// Unix timestamp (seconds since epoch) when this version sunsets.
22    timestamp: u64,
23}
24
25impl SunsetTimestamp {
26    /// Create a new sunset timestamp.
27    #[must_use]
28    pub fn new(timestamp: u64) -> Self {
29        Self { timestamp }
30    }
31
32    /// Create a sunset timestamp for N days from now.
33    #[must_use]
34    pub fn days_from_now(days: u32) -> Self {
35        let now = SystemTime::now()
36            .duration_since(UNIX_EPOCH)
37            .map(|d| d.as_secs())
38            .unwrap_or(0);
39        let seconds_per_day = 86400u64;
40        Self {
41            timestamp: now + (u64::from(days) * seconds_per_day),
42        }
43    }
44
45    /// Get the raw timestamp value.
46    #[must_use]
47    pub fn timestamp(&self) -> u64 {
48        self.timestamp
49    }
50
51    /// Check if this sunset timestamp has passed.
52    #[must_use]
53    pub fn is_expired(&self) -> bool {
54        let now = SystemTime::now()
55            .duration_since(UNIX_EPOCH)
56            .map(|d| d.as_secs())
57            .unwrap_or(0);
58        now > self.timestamp
59    }
60
61    /// Check if within the grace period after expiry.
62    ///
63    /// Returns true if:
64    /// - Not expired yet, OR
65    /// - Expired but within `grace_days` of the sunset timestamp
66    #[must_use]
67    pub fn is_within_grace_period(&self, grace_days: u32) -> bool {
68        let now = SystemTime::now()
69            .duration_since(UNIX_EPOCH)
70            .map(|d| d.as_secs())
71            .unwrap_or(0);
72
73        if now <= self.timestamp {
74            // Not expired yet
75            return true;
76        }
77
78        let seconds_per_day = 86400u64;
79        let grace_seconds = u64::from(grace_days) * seconds_per_day;
80        let grace_deadline = self.timestamp.saturating_add(grace_seconds);
81
82        now <= grace_deadline
83    }
84
85    /// Get the number of days until sunset (or 0 if already passed).
86    #[must_use]
87    pub fn days_until_sunset(&self) -> u32 {
88        let now = SystemTime::now()
89            .duration_since(UNIX_EPOCH)
90            .map(|d| d.as_secs())
91            .unwrap_or(0);
92
93        if now >= self.timestamp {
94            return 0;
95        }
96
97        let seconds_remaining = self.timestamp - now;
98        let seconds_per_day = 86400u64;
99        (seconds_remaining / seconds_per_day) as u32
100    }
101
102    /// Get the number of days since sunset (or 0 if not yet passed).
103    #[must_use]
104    pub fn days_since_sunset(&self) -> u32 {
105        let now = SystemTime::now()
106            .duration_since(UNIX_EPOCH)
107            .map(|d| d.as_secs())
108            .unwrap_or(0);
109
110        if now <= self.timestamp {
111            return 0;
112        }
113
114        let seconds_elapsed = now - self.timestamp;
115        let seconds_per_day = 86400u64;
116        (seconds_elapsed / seconds_per_day) as u32
117    }
118}
119
120impl Default for SunsetTimestamp {
121    fn default() -> Self {
122        // Default to 90 days from now
123        Self::days_from_now(90)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_sunset_not_expired() {
133        let sunset = SunsetTimestamp::days_from_now(30);
134        assert!(!sunset.is_expired());
135        assert!(sunset.days_until_sunset() > 0);
136        assert_eq!(sunset.days_since_sunset(), 0);
137    }
138
139    #[test]
140    fn test_sunset_expired() {
141        // Create a timestamp 1 day in the past
142        let now = SystemTime::now()
143            .duration_since(UNIX_EPOCH)
144            .unwrap()
145            .as_secs();
146        let sunset = SunsetTimestamp::new(now - 86400);
147        assert!(sunset.is_expired());
148        assert_eq!(sunset.days_until_sunset(), 0);
149        assert!(sunset.days_since_sunset() >= 1);
150    }
151
152    #[test]
153    fn test_grace_period() {
154        // Create a timestamp 1 hour in the past
155        let now = SystemTime::now()
156            .duration_since(UNIX_EPOCH)
157            .unwrap()
158            .as_secs();
159        let sunset = SunsetTimestamp::new(now - 3600);
160
161        // Expired
162        assert!(sunset.is_expired());
163        // But within 1 day grace period
164        assert!(sunset.is_within_grace_period(1));
165        // Not within 0 day grace period
166        assert!(!sunset.is_within_grace_period(0));
167    }
168
169    #[test]
170    fn test_beyond_grace_period() {
171        // Create a timestamp 7 days in the past
172        let now = SystemTime::now()
173            .duration_since(UNIX_EPOCH)
174            .unwrap()
175            .as_secs();
176        let sunset = SunsetTimestamp::new(now - (7 * 86400));
177
178        assert!(sunset.is_expired());
179        assert!(!sunset.is_within_grace_period(1));
180        assert!(sunset.is_within_grace_period(10));
181    }
182
183    #[test]
184    fn test_days_calculation() {
185        let sunset = SunsetTimestamp::days_from_now(30);
186        // Should be approximately 30 days (allowing for test execution time)
187        let days = sunset.days_until_sunset();
188        assert!((29..=30).contains(&days));
189    }
190}