Skip to main content

clawft_kernel/licence/
store_accept.rs

1//! Accepting bindings and grants into a [`CheckoutGrantStore`]
2//! (ADR-106 sections 3 and 4). Every change is built on a copy, saved, and
3//! only then made the state; records that only restrict (an unbind, a
4//! withdrawal, a conflict) are applied even if the save fails.
5
6use super::store::{CheckoutGrantStore, Held};
7use super::{
8    AdmissionPosture, BindState, BindingExtraCheck, CheckoutGrant, GRANT_SKEW_SECS, LicenceError,
9    LicenceEvent, MAX_GRANT_SLOTS, Outcome, SignedBinding, SignedGrant, verify_binding_member,
10    verify_grant,
11};
12use crate::workload_pkg::codec::hex_decode_exact;
13
14impl CheckoutGrantStore {
15    /// Accept a binding. `posture` is the node's admission state; `extra` is
16    /// the steward profile hook (use [`super::NoExtraChecks`] for members).
17    pub fn accept_binding(
18        &self,
19        signed: &SignedBinding,
20        posture: AdmissionPosture,
21        extra: &dyn BindingExtraCheck,
22    ) -> Result<Outcome, LicenceError> {
23        self.run(|inner, ev| {
24            if let Some(p) = &inner.poisoned {
25                return Err(LicenceError::Poisoned(p.clone()));
26            }
27            if let Err(e) = posture.check() {
28                ev.push(LicenceEvent::BindingRefused(e.to_string()));
29                return Err(e);
30            }
31            let local = self.local.get().ok_or(LicenceError::NoLocalMesh)?;
32            let rec = verify_binding_member(signed, &self.anchors, &local)?;
33            extra.check(&rec)?;
34            let mut next = inner.clone();
35            if let Some(cur) = &inner.binding {
36                if rec.seq < cur.body.seq {
37                    return Ok(Outcome::Ignored);
38                }
39                if rec.seq == cur.body.seq {
40                    if cur.signed.payload == signed.payload {
41                        return Ok(Outcome::Duplicate);
42                    }
43                    ev.push(LicenceEvent::BindingConflict(rec.seq));
44                    return Err(LicenceError::Conflict(rec.seq));
45                }
46                if cur.body.grant_pubkey != rec.grant_pubkey {
47                    next.slots.clear(); // grants under the old key are void
48                }
49            }
50            let key = rec.grant_pubkey.clone();
51            next.floors.retain(|k, _| *k == key); // floors of old keys are dead weight
52            let restrictive = rec.state == BindState::Unbound;
53            next.binding = Some(Held { signed: signed.clone(), body: rec });
54            next.orphan_reported = false;
55            if restrictive {
56                return Ok(self.commit_restrictive(inner, next));
57            }
58            self.commit(inner, next)?;
59            self.note_bound();
60            Ok(Outcome::Applied)
61        })
62    }
63
64    /// Accept a grant under the bound key (see ADR-106 section 4).
65    pub fn accept_grant(&self, signed: &SignedGrant) -> Result<Outcome, LicenceError> {
66        self.run(|inner, ev| {
67            let b = self.binding_in_effect(inner, ev)?;
68            let pk = hex_decode_exact::<32>(&b.grant_pubkey)
69                .ok_or_else(|| LicenceError::Malformed("bound grant key".into()))?;
70            if self.key_revoked(&b.grant_pubkey) {
71                return Err(LicenceError::KeyRevoked);
72            }
73            let local = self.local.get().ok_or(LicenceError::NoLocalMesh)?;
74            let g = verify_grant(signed, &pk, &local)?;
75            let eff = self.eff_now(inner, &b.grant_pubkey);
76            if g.issued_at > eff.saturating_add(GRANT_SKEW_SECS) {
77                return Err(LicenceError::NotYetValid);
78            }
79            let key = (g.cog_id.clone(), g.version.clone());
80            if !inner.slots.contains_key(&key) && inner.slots.len() >= MAX_GRANT_SLOTS {
81                return Err(LicenceError::Full);
82            }
83            let held = Held { signed: signed.clone(), body: g };
84            let seq = held.body.seq;
85            let (cur, conflicted) = match inner.slots.get(&key) {
86                Some(s) => (s.current.clone(), s.conflicted.contains(&seq)),
87                None => (None, false),
88            };
89            if conflicted {
90                return Err(LicenceError::Conflict(seq));
91            }
92            let mut next = inner.clone();
93            if let Some(cur) = &cur {
94                if seq < cur.body.seq {
95                    return Ok(Outcome::Ignored);
96                }
97                if seq == cur.body.seq {
98                    if cur.signed.payload == held.signed.payload {
99                        return Ok(Outcome::Duplicate);
100                    }
101                    // Two payloads at one seq: refuse both. If either is a
102                    // withdrawal, fail closed and keep it as the tombstone;
103                    // otherwise fall back to the newest earlier grant.
104                    let wd = [cur, &held].into_iter().find(|h| h.body.is_withdrawal()).cloned();
105                    let slot = next.slots.entry(key).or_default();
106                    slot.conflicted.push(seq);
107                    match wd {
108                        Some(w) => {
109                            slot.current = Some(w);
110                            slot.previous = None;
111                        }
112                        None => slot.current = slot.previous.take(),
113                    }
114                    ev.push(LicenceEvent::GrantConflict {
115                        cog_id: held.body.cog_id,
116                        version: held.body.version,
117                        seq,
118                    });
119                    let _ = self.commit_restrictive(inner, next);
120                    return Err(LicenceError::Conflict(seq));
121                }
122                check_union(&cur.body, &held.body)?;
123            }
124            let issued = held.body.issued_at;
125            let restrictive = held.body.is_withdrawal();
126            let slot = next.slots.entry(key).or_default();
127            slot.previous = slot.current.take();
128            slot.current = Some(held);
129            next.floors.entry(b.grant_pubkey).or_default().note_issued(issued);
130            if restrictive {
131                return Ok(self.commit_restrictive(inner, next));
132            }
133            self.commit(inner, next)?;
134            Ok(Outcome::Applied)
135        })
136    }
137}
138
139/// A newer grant carries the union of arches and never changes a held one.
140fn check_union(cur: &CheckoutGrant, new: &CheckoutGrant) -> Result<(), LicenceError> {
141    for a in &cur.artifacts {
142        match new.artifact(&a.arch) {
143            None => return Err(LicenceError::DropsArch(a.arch.clone())),
144            Some(n) if n != a => return Err(LicenceError::ChangesArtifact(a.arch.clone())),
145            Some(_) => {}
146        }
147    }
148    Ok(())
149}