Skip to main content

confium_operator/
controller.rs

1//! Controller loop — watches for ConfiumSigningCeremony CRs and
2//! reconciles them.
3//!
4//! The controller is a simplified reconciliation loop. In production
5//! this would use `kube-rs` to watch the Kubernetes API; the scaffold
6//! demonstrates the logic without the Kubernetes client dependency.
7
8use std::time::Duration;
9
10pub struct CeremonyController {
11    namespace: Option<String>,
12}
13
14impl CeremonyController {
15    pub fn new(namespace: Option<String>) -> Self {
16        Self { namespace }
17    }
18
19    /// Run the reconciliation loop indefinitely.
20    pub async fn run(&self) -> anyhow::Result<()> {
21        loop {
22            self.reconcile_once().await?;
23            tokio::time::sleep(Duration::from_secs(10)).await;
24        }
25    }
26
27    /// One reconciliation pass. In production: list all
28    /// ConfiumSigningCeremony CRs in the namespace, find those in
29    /// Pending phase, and drive them through the lifecycle.
30    pub async fn reconcile_once(&self) -> anyhow::Result<()> {
31        tracing::info!(namespace = ?self.namespace, "reconciliation pass");
32        // Scaffold: no actual Kubernetes client. The real loop would:
33        // 1. List ConfiumSigningCeremony CRs via kube-rs.
34        // 2. Filter for phase == Pending.
35        // 3. For each: read messageRef ConfigMap, run DKG + sign,
36        //    write signature to outputRef Secret, update status.
37        Ok(())
38    }
39
40    /// Execute a threshold signing ceremony (the core logic, separate
41    /// from Kubernetes plumbing so it's testable in isolation).
42    pub fn execute_ceremony(
43        scheme: &str,
44        threshold: u32,
45        party_count: u32,
46        message: &[u8],
47    ) -> anyhow::Result<Vec<u8>> {
48        tracing::info!(
49            scheme,
50            threshold,
51            party_count,
52            msg_len = message.len(),
53            "executing ceremony"
54        );
55
56        let (public_key, shares) = match scheme {
57            "cmp20" => {
58                let kg = confium_tc_cmp20::inprocess::keygen(threshold, party_count as usize)?;
59                (kg.public_key, kg.shares)
60            }
61            "gg18" => {
62                let kg = confium_tc_gg18::inprocess::keygen(threshold, party_count as usize)?;
63                (kg.public_key, kg.shares)
64            }
65            other => anyhow::bail!("unknown scheme: {other}"),
66        };
67
68        let sig = match scheme {
69            "cmp20" => confium_tc_cmp20::inprocess::sign(&shares, threshold, message)?,
70            "gg18" => confium_tc_gg18::inprocess::sign(&shares, threshold, message)?,
71            _ => unreachable!(),
72        };
73
74        tracing::info!(
75            pk_len = public_key.len(),
76            sig_len = sig.len(),
77            "ceremony completed"
78        );
79
80        Ok(sig)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn execute_ceremony_cmp20() {
90        let sig = CeremonyController::execute_ceremony("cmp20", 2, 3, b"release artifact").unwrap();
91        assert_eq!(sig.len(), 64);
92    }
93
94    #[test]
95    fn execute_ceremony_gg18() {
96        let sig = CeremonyController::execute_ceremony("gg18", 2, 3, b"release artifact").unwrap();
97        assert_eq!(sig.len(), 64);
98    }
99
100    #[test]
101    fn execute_ceremony_rejects_unknown_scheme() {
102        assert!(CeremonyController::execute_ceremony("bogus", 2, 3, b"msg").is_err());
103    }
104}