Skip to main content

qualia_core_db/domains/geospatial/
steward.rs

1//! Governed steward unlock contracts using deontic norm patterns.
2
3use crate::modalities::logic::deontic::{extract_deontic_opcode, OP_PERMIT};
4use crate::q_hash;
5use crate::NQuin;
6
7use super::canvas_rights;
8
9/// A steward-issued unlock contract for a scoped asset.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct StewardContract {
12    pub steward_did_hash: u64,
13    pub asset_hash: u64,
14    pub scope: u64,
15    pub expiry_unix: u32,
16    pub quorum: u8,
17}
18
19/// Outcome of steward unlock validation.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum StewardVerdict {
22    Granted,
23    Denied(&'static str),
24    Expired,
25}
26
27/// Validate whether `requester_hash` may unlock `contract.asset_hash` within `contract.scope`.
28///
29/// Uses deontic permit norms (`OP_PERMIT` + `q42:stewardUnlock`), steward quorum
30/// endorsements (`q42:stewards`), explicit-denial guard, and contract expiry.
31pub fn validate_steward_unlock(
32    requester_hash: u64,
33    contract: &StewardContract,
34    arena_quins: &[NQuin],
35    now_unix: u32,
36) -> StewardVerdict {
37    if now_unix > contract.expiry_unix {
38        return StewardVerdict::Expired;
39    }
40
41    if canvas_rights::explicit_denial_guard(
42        contract.scope,
43        requester_hash,
44        contract.asset_hash,
45        arena_quins,
46    ) {
47        return StewardVerdict::Denied("explicit denial guard");
48    }
49
50    let unlock_path = q_hash("q42:stewardUnlock");
51    let stewards_pred = q_hash("q42:stewards");
52
53    // `compile_norm_quin` stores the property-path in predicate bits [8..62]
54    // via `(path << 8) & !DEFEATER_BIT` (opcode byte low, defeater bit high).
55    // Compare within that same window — the old `(predicate >> 8)` readback
56    // dropped the high bits and compared against the full 64-bit hash, so a
57    // real `q_hash` path never matched and unlock could NEVER be granted.
58    const PATH_WINDOW: u64 = 0x7FFF_FFFF_FFFF_FF00;
59    let expected_path_bits = (unlock_path << 8) & PATH_WINDOW;
60
61    let mut has_permit = false;
62    for quin in arena_quins {
63        if extract_deontic_opcode(quin.predicate) != OP_PERMIT {
64            continue;
65        }
66        let path_bits = quin.predicate & PATH_WINDOW;
67        if quin.subject == contract.steward_did_hash
68            && path_bits == expected_path_bits
69            && quin.object == contract.asset_hash
70            && quin.context == contract.scope
71        {
72            let expiry = (quin.metadata & 0xFFFF_FFFF) as u32;
73            if now_unix <= expiry {
74                has_permit = true;
75                break;
76            }
77        }
78    }
79
80    if !has_permit {
81        return StewardVerdict::Denied("no active steward unlock permit");
82    }
83
84    let mut endorsements = 0u8;
85    for quin in arena_quins {
86        if quin.predicate == stewards_pred && quin.subject == contract.steward_did_hash {
87            if quin.object == requester_hash || quin.object == contract.asset_hash {
88                endorsements = endorsements.saturating_add(1);
89            }
90        }
91    }
92
93    if endorsements < contract.quorum {
94        return StewardVerdict::Denied("steward quorum not met");
95    }
96
97    if requester_hash != contract.steward_did_hash && endorsements == 0 {
98        return StewardVerdict::Denied("requester not authorized");
99    }
100
101    StewardVerdict::Granted
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::modalities::logic::deontic::compile_norm_quin;
108
109    fn contract() -> StewardContract {
110        StewardContract {
111            steward_did_hash: 0xAA,
112            asset_hash: 0xBB,
113            scope: 0xCC,
114            expiry_unix: 2_000_000_000,
115            quorum: 2,
116        }
117    }
118
119    fn permit_quin(contract: &StewardContract) -> NQuin {
120        compile_norm_quin(
121            contract.steward_did_hash,
122            OP_PERMIT,
123            q_hash("q42:stewardUnlock"),
124            contract.asset_hash,
125            contract.scope,
126            contract.expiry_unix,
127            false,
128        )
129    }
130
131    fn steward_endorsement(steward: u64, object: u64) -> NQuin {
132        let mut q = NQuin {
133            subject: steward,
134            predicate: q_hash("q42:stewards"),
135            object,
136            context: 0,
137            metadata: 0,
138            parity: 0,
139        };
140        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
141        q
142    }
143
144    #[test]
145    fn steward_unlock_granted() {
146        let c = contract();
147        let requester = 0xDD;
148        let quins = vec![
149            permit_quin(&c),
150            steward_endorsement(c.steward_did_hash, requester),
151            steward_endorsement(c.steward_did_hash, c.asset_hash),
152        ];
153
154        assert_eq!(
155            validate_steward_unlock(requester, &c, &quins, 1_900_000_000),
156            StewardVerdict::Granted
157        );
158    }
159
160    #[test]
161    fn steward_unlock_denied_no_permit() {
162        let c = contract();
163        let requester = 0xDD;
164        let quins = vec![
165            steward_endorsement(c.steward_did_hash, requester),
166            steward_endorsement(c.steward_did_hash, c.asset_hash),
167        ];
168
169        assert_eq!(
170            validate_steward_unlock(requester, &c, &quins, 1_900_000_000),
171            StewardVerdict::Denied("no active steward unlock permit")
172        );
173    }
174
175    #[test]
176    fn steward_unlock_expired() {
177        let c = contract();
178        let requester = 0xDD;
179        let quins = vec![
180            permit_quin(&c),
181            steward_endorsement(c.steward_did_hash, requester),
182            steward_endorsement(c.steward_did_hash, c.asset_hash),
183        ];
184
185        assert_eq!(
186            validate_steward_unlock(requester, &c, &quins, 2_100_000_000),
187            StewardVerdict::Expired
188        );
189    }
190}