Skip to main content

qualia_client_core/
guardianship.rs

1//! Bilateral guardianship — global suspended-transaction queue for M:N co-signature.
2
3use std::sync::{Mutex, OnceLock};
4
5use qualia_core_db::crdt::SuspendedTransactionQueue;
6use qualia_core_db::wal::WriteAheadLog;
7use qualia_core_db::{q_hash, NQuin};
8use serde::{Deserialize, Serialize};
9
10const MAX_RATIFIED: usize = 32;
11
12static SUSPENDED_QUEUE: OnceLock<Mutex<SuspendedTransactionQueue>> = OnceLock::new();
13static RATIFIED_IDS: OnceLock<Mutex<[Option<u64>; MAX_RATIFIED]>> = OnceLock::new();
14
15pub fn suspended_queue() -> &'static Mutex<SuspendedTransactionQueue> {
16    SUSPENDED_QUEUE.get_or_init(|| Mutex::new(SuspendedTransactionQueue::new()))
17}
18
19fn ratified_ids() -> &'static Mutex<[Option<u64>; MAX_RATIFIED]> {
20    RATIFIED_IDS.get_or_init(|| Mutex::new([None; MAX_RATIFIED]))
21}
22
23fn mark_ratified(agreement_id: u64) {
24    let mut slots = ratified_ids().lock().expect("ratified_ids");
25    if slots.iter().any(|s| *s == Some(agreement_id)) {
26        return;
27    }
28    for slot in slots.iter_mut() {
29        if slot.is_none() {
30            *slot = Some(agreement_id);
31            return;
32        }
33    }
34}
35
36/// View of a suspended guardianship transaction for UI trays.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SuspendedTxView {
39    pub agreement_id: u64,
40    pub threshold: u8,
41    pub collected_signatures: u8,
42    pub subject: u64,
43    pub predicate: u64,
44    pub object: u64,
45    pub context: u64,
46    pub metadata: u64,
47    pub label: String,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum GuardianTokenOutcome {
53    Ratified,
54    Pending,
55    Denied,
56    NotFound,
57}
58
59pub fn list_pending_affirmations() -> Vec<SuspendedTxView> {
60    let guard = suspended_queue().lock().expect("suspended_queue");
61    let mut out = Vec::new();
62    for slot in guard.queue.iter() {
63        if let Some(tx) = slot {
64            let q = tx.suspended_quin;
65            out.push(SuspendedTxView {
66                agreement_id: tx.agreement_id,
67                threshold: tx.threshold,
68                collected_signatures: tx.collected_signatures,
69                subject: q.subject,
70                predicate: q.predicate,
71                object: q.object,
72                context: q.context,
73                metadata: q.metadata,
74                label: format!(
75                    "Guardianship Proposal — agreement 0x{:016x}",
76                    tx.agreement_id
77                ),
78            });
79        }
80    }
81    out
82}
83
84pub fn pending_affirmation_count() -> usize {
85    list_pending_affirmations().len()
86}
87
88pub fn is_agreement_ratified(agreement_id: u64) -> bool {
89    ratified_ids()
90        .lock()
91        .expect("ratified_ids")
92        .iter()
93        .any(|s| *s == Some(agreement_id))
94}
95
96/// Apply a guardian consent token (`q42:issuesConsentToken`) for the given agreement.
97pub fn apply_guardian_token(agreement_id: u64, token_fields: [u64; 6]) -> GuardianTokenOutcome {
98    let token = NQuin {
99        subject: token_fields[0],
100        predicate: token_fields[1],
101        object: token_fields[2],
102        context: token_fields[3],
103        metadata: token_fields[4],
104        parity: token_fields[5],
105    };
106    if token.context != agreement_id {
107        return GuardianTokenOutcome::NotFound;
108    }
109
110    let mut guard = suspended_queue().lock().expect("suspended_queue");
111    if let Some(tx) = guard.apply_consensus_token(&token) {
112        mark_ratified(agreement_id);
113        #[cfg(not(target_arch = "wasm32"))]
114        {
115            if let Ok(mut wal) = WriteAheadLog::open(".qualia_graph_mutations.wal") {
116                let mut quin = tx.suspended_quin;
117                let _ = wal.append_mutation_volatile(&mut quin);
118            }
119        }
120        GuardianTokenOutcome::Ratified
121    } else if guard.queue.iter().any(|s| {
122        s.as_ref()
123            .map(|tx| tx.agreement_id == agreement_id)
124            .unwrap_or(false)
125    }) {
126        GuardianTokenOutcome::Pending
127    } else {
128        GuardianTokenOutcome::NotFound
129    }
130}
131
132/// Build a consent token for the local principal co-signing an agreement.
133pub fn build_consent_token(agreement_id: u64, principal_hash: u64) -> [u64; 6] {
134    let q = NQuin {
135        subject: principal_hash,
136        predicate: q_hash("q42:issuesConsentToken"),
137        object: agreement_id,
138        context: agreement_id,
139        metadata: 0,
140        parity: 0,
141    };
142    [
143        q.subject,
144        q.predicate,
145        q.object,
146        q.context,
147        q.metadata,
148        q.parity,
149    ]
150}
151
152/// Remove a suspended transaction without committing to the WAL.
153pub fn deny_guardian_affirmation(agreement_id: u64) -> bool {
154    let mut guard = suspended_queue().lock().expect("suspended_queue");
155    for slot in guard.queue.iter_mut() {
156        if let Some(tx) = slot {
157            if tx.agreement_id == agreement_id {
158                *slot = None;
159                return true;
160            }
161        }
162    }
163    false
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use qualia_core_db::crdt::SuspendedTransaction;
170
171    #[test]
172    fn list_and_deny_pending() {
173        let queue = suspended_queue();
174        let mut guard = queue.lock().unwrap();
175        *guard = SuspendedTransactionQueue::new();
176        drop(guard);
177
178        let tx = SuspendedTransaction {
179            agreement_id: 0xABCD,
180            threshold: 2,
181            collected_signatures: 1,
182            registers: [None; 16],
183            bytecode_buffer: [None; 64],
184            yielded_op: None,
185            suspended_quin: NQuin::default(),
186        };
187        suspended_queue().lock().unwrap().push(tx).unwrap();
188
189        assert_eq!(pending_affirmation_count(), 1);
190        assert!(deny_guardian_affirmation(0xABCD));
191        assert_eq!(pending_affirmation_count(), 0);
192    }
193}