1use 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 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}