Skip to main content

tf_types/
quorum.rs

1//! Quorum approval collector — Rust mirror of
2//! `tools/tf-types-ts/src/core/quorum.ts`.
3
4use std::sync::{Arc, Mutex};
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct QuorumConfig {
10    pub min_approvers: u32,
11    pub of: Vec<String>,
12}
13
14#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
15pub struct QuorumOutcome {
16    pub decision: String,
17    pub approvers: Vec<String>,
18    pub deniers: Vec<String>,
19    pub ceremony: QuorumCeremony,
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
23pub struct QuorumCeremony {
24    pub ceremony_version: String,
25    pub ceremony_id: String,
26    pub kind: String,
27    pub request_id: String,
28    pub started_at: String,
29    pub completed_at: Option<String>,
30    pub min_approvers: u32,
31    pub of: Vec<String>,
32    pub approvers: Vec<String>,
33    pub signatures: Vec<QuorumSignature>,
34}
35
36#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
37pub struct QuorumSignature {
38    pub algorithm: String,
39    pub signer: String,
40    pub signature: String,
41}
42
43#[derive(Debug)]
44pub struct QuorumApprovalCollector {
45    cfg: QuorumConfig,
46}
47
48#[derive(Debug)]
49pub struct QuorumHandle {
50    cfg: QuorumConfig,
51    request_id: String,
52    started_at: String,
53    state: Arc<Mutex<QuorumState>>,
54}
55
56#[derive(Debug, Default)]
57struct QuorumState {
58    approvers: Vec<String>,
59    deniers: Vec<String>,
60    signatures: Vec<QuorumSignature>,
61    outcome: Option<QuorumOutcome>,
62}
63
64impl QuorumApprovalCollector {
65    pub fn new(cfg: QuorumConfig) -> Result<Self, String> {
66        if cfg.min_approvers < 1 {
67            return Err("quorum.min_approvers must be ≥ 1".into());
68        }
69        if (cfg.of.len() as u32) < cfg.min_approvers {
70            return Err(format!(
71                "quorum.of ({}) must contain at least min_approvers ({}) actors",
72                cfg.of.len(),
73                cfg.min_approvers
74            ));
75        }
76        Ok(QuorumApprovalCollector { cfg })
77    }
78
79    pub fn push(&self, request_id: &str, started_at: &str) -> QuorumHandle {
80        QuorumHandle {
81            cfg: self.cfg.clone(),
82            request_id: request_id.into(),
83            started_at: started_at.into(),
84            state: Arc::new(Mutex::new(QuorumState::default())),
85        }
86    }
87}
88
89impl QuorumHandle {
90    /// Record one approver's vote. Returns true when the vote was accepted
91    /// (approver is in the eligible set and hasn't already voted), false
92    /// otherwise.
93    pub fn respond_as(&self, approver: &str, decision: &str, signature: QuorumSignature) -> bool {
94        if !self.cfg.of.iter().any(|a| a == approver) {
95            return false;
96        }
97        let mut state = self.state.lock().unwrap();
98        if state.approvers.iter().any(|a| a == approver)
99            || state.deniers.iter().any(|a| a == approver)
100        {
101            return false;
102        }
103        if decision == "approve" {
104            state.approvers.push(approver.to_string());
105            state.signatures.push(signature);
106        } else {
107            state.deniers.push(approver.to_string());
108        }
109        if state.approvers.len() as u32 >= self.cfg.min_approvers && state.outcome.is_none() {
110            state.outcome = Some(self.materialise(&state, "approve"));
111        } else if state.approvers.len() + state.deniers.len() >= self.cfg.of.len()
112            && state.outcome.is_none()
113        {
114            state.outcome = Some(self.materialise(&state, "deny"));
115        }
116        true
117    }
118
119    pub fn outcome(&self) -> Option<QuorumOutcome> {
120        self.state.lock().unwrap().outcome.clone()
121    }
122
123    fn materialise(&self, state: &QuorumState, decision: &str) -> QuorumOutcome {
124        QuorumOutcome {
125            decision: decision.into(),
126            approvers: state.approvers.clone(),
127            deniers: state.deniers.clone(),
128            ceremony: QuorumCeremony {
129                ceremony_version: "1".into(),
130                ceremony_id: format!("cer-{}-quorum", self.request_id),
131                kind: "quorum".into(),
132                request_id: self.request_id.clone(),
133                started_at: self.started_at.clone(),
134                completed_at: Some(now_iso8601()),
135                min_approvers: self.cfg.min_approvers,
136                of: self.cfg.of.clone(),
137                approvers: state.approvers.clone(),
138                signatures: state.signatures.clone(),
139            },
140        }
141    }
142}
143
144fn now_iso8601() -> String {
145    let secs = std::time::SystemTime::now()
146        .duration_since(std::time::UNIX_EPOCH)
147        .unwrap_or_default()
148        .as_secs() as i64;
149    let (year, month, day, hour, minute, second) = secs_to_ymdhms(secs);
150    format!(
151        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
152        year, month, day, hour, minute, second
153    )
154}
155
156fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
157    let days = secs.div_euclid(86_400);
158    let time = secs.rem_euclid(86_400);
159    let hour = (time / 3600) as u32;
160    let minute = ((time % 3600) / 60) as u32;
161    let second = (time % 60) as u32;
162    let z = days + 719_468;
163    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
164    let doe = (z - era * 146_097) as u64;
165    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
166    let y = yoe as i64 + era * 400;
167    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
168    let mp = (5 * doy + 2) / 153;
169    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
170    let m = if mp < 10 {
171        (mp + 3) as u32
172    } else {
173        (mp - 9) as u32
174    };
175    let year = if m <= 2 { y + 1 } else { y };
176    (year as i32, m, d, hour, minute, second)
177}