Skip to main content

confium_patterns/revocation/
revocation_submission.rs

1//! Submission lifecycle.
2
3use crate::revocation::revocation_blob::RevocationBlob;
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6
7/// A revocation submission.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Submission {
10    /// Unique submission ID.
11    pub id: String,
12    /// The blob being processed.
13    pub blob: RevocationBlob,
14    /// When the submission was first received.
15    pub submitted_at: DateTime<Utc>,
16    /// Current state.
17    pub state: SubmissionState,
18    /// When the first confirmation was sent (if any).
19    pub first_confirmation_at: Option<DateTime<Utc>>,
20    /// 24-hour delay end time (if first confirmation received).
21    pub delay_until: Option<DateTime<Utc>>,
22}
23
24/// Submission state machine.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum SubmissionState {
28    /// Submission received; awaiting email verification.
29    AwaitingEmailVerification,
30    /// Email verified; first confirmation sent; awaiting 24h delay.
31    FirstConfirmedDelay,
32    /// Second confirmation received; ready to process.
33    SecondConfirmed,
34    /// Threshold decryption ceremony completed.
35    Decrypted,
36    /// Revocation signature published to keyservers.
37    Published,
38    /// Cancelled by user.
39    Cancelled,
40    /// Expired (delay elapsed without confirmation).
41    Expired,
42}
43
44impl Submission {
45    /// Construct a new submission.
46    pub fn new(id: impl Into<String>, blob: RevocationBlob) -> Self {
47        Self {
48            id: id.into(),
49            blob,
50            submitted_at: Utc::now(),
51            state: SubmissionState::AwaitingEmailVerification,
52            first_confirmation_at: None,
53            delay_until: None,
54        }
55    }
56
57    /// Move to FirstConfirmedDelay (after email verification).
58    pub fn confirm_first(&mut self) -> Result<(), String> {
59        if self.state != SubmissionState::AwaitingEmailVerification {
60            return Err(format!("invalid state: {:?}", self.state));
61        }
62        let now = Utc::now();
63        self.first_confirmation_at = Some(now);
64        self.delay_until = Some(now + Duration::hours(24));
65        self.state = SubmissionState::FirstConfirmedDelay;
66        Ok(())
67    }
68
69    /// Move to SecondConfirmed (after 24h delay).
70    pub fn confirm_second(&mut self) -> Result<(), String> {
71        if self.state != SubmissionState::FirstConfirmedDelay {
72            return Err(format!("invalid state: {:?}", self.state));
73        }
74        if let Some(until) = self.delay_until {
75            if Utc::now() < until {
76                return Err("24h delay not yet elapsed".into());
77            }
78        }
79        self.state = SubmissionState::SecondConfirmed;
80        Ok(())
81    }
82
83    /// Mark threshold decryption completed.
84    pub fn mark_decrypted(&mut self) -> Result<(), String> {
85        if self.state != SubmissionState::SecondConfirmed {
86            return Err(format!("invalid state: {:?}", self.state));
87        }
88        self.state = SubmissionState::Decrypted;
89        Ok(())
90    }
91
92    /// Mark published to keyservers.
93    pub fn mark_published(&mut self) -> Result<(), String> {
94        if self.state != SubmissionState::Decrypted {
95            return Err(format!("invalid state: {:?}", self.state));
96        }
97        self.state = SubmissionState::Published;
98        Ok(())
99    }
100
101    /// Cancel.
102    pub fn cancel(&mut self) -> Result<(), String> {
103        if matches!(
104            self.state,
105            SubmissionState::Published | SubmissionState::Cancelled
106        ) {
107            return Err("cannot cancel terminal submission".into());
108        }
109        self.state = SubmissionState::Cancelled;
110        Ok(())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::revocation::revocation_blob::RevocationBlob;
118
119    fn sample_blob() -> RevocationBlob {
120        RevocationBlob {
121            user_email: "alice@example.com".into(),
122            key_fingerprint: "ABCDEF0123456789".into(),
123            encapsulated_key: vec![0u8; 32],
124            ciphertext: vec![0u8; 100],
125            nonce: vec![0u8; 12],
126        }
127    }
128
129    #[test]
130    fn full_submission_lifecycle() {
131        let mut sub = Submission::new("sub-1", sample_blob());
132        assert_eq!(sub.state, SubmissionState::AwaitingEmailVerification);
133
134        sub.confirm_first().unwrap();
135        assert_eq!(sub.state, SubmissionState::FirstConfirmedDelay);
136
137        // Mock delay elapsed
138        sub.delay_until = Some(Utc::now() - Duration::hours(1));
139        sub.confirm_second().unwrap();
140        assert_eq!(sub.state, SubmissionState::SecondConfirmed);
141
142        sub.mark_decrypted().unwrap();
143        assert_eq!(sub.state, SubmissionState::Decrypted);
144
145        sub.mark_published().unwrap();
146        assert_eq!(sub.state, SubmissionState::Published);
147    }
148
149    #[test]
150    fn confirm_second_before_delay_fails() {
151        let mut sub = Submission::new("sub-2", sample_blob());
152        sub.confirm_first().unwrap();
153        // Set delay to future
154        sub.delay_until = Some(Utc::now() + Duration::hours(24));
155        let result = sub.confirm_second();
156        assert!(result.is_err());
157    }
158
159    #[test]
160    fn cancel_works_from_any_nonterminal_state() {
161        let mut sub = Submission::new("sub-3", sample_blob());
162        sub.cancel().unwrap();
163        assert_eq!(sub.state, SubmissionState::Cancelled);
164    }
165}