Skip to main content

ic_memory/
validation.rs

1use crate::{
2    key::StableKey,
3    ledger::{AllocationLedger, ClaimConflict, RecoveredLedger, validate_declaration_claim},
4    policy::RuntimeBootstrapPolicy,
5    requests::DeclarationSnapshot,
6    slot::MemoryManagerSlot,
7};
8
9///
10/// AllocationValidationError
11///
12/// Failure to validate declarations against policy and historical ledger facts.
13/// Resolution establishes snapshot invariants; recovery establishes
14/// ledger integrity before this boundary.
15///
16
17#[non_exhaustive]
18#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
19pub enum AllocationValidationError<P> {
20    /// Policy adapter rejected the declaration.
21    #[error("allocation policy rejected a declaration")]
22    Policy(P),
23    /// Stable key was historically bound to a different slot.
24    #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
25    StableKeySlotConflict {
26        /// Stable key that was redeclared.
27        stable_key: StableKey,
28        /// Historical slot for the stable key.
29        historical_slot: MemoryManagerSlot,
30        /// Slot claimed by the current declaration.
31        declared_slot: MemoryManagerSlot,
32    },
33    /// Slot was historically bound to a different stable key.
34    #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
35    SlotStableKeyConflict {
36        /// Slot claimed by the current declaration.
37        slot: MemoryManagerSlot,
38        /// Historical stable key for the slot.
39        historical_key: StableKey,
40        /// Stable key claimed by the current declaration.
41        declared_key: StableKey,
42    },
43    /// Current declaration attempted to revive a retired allocation.
44    #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
45    RetiredAllocation {
46        /// Retired stable key.
47        stable_key: StableKey,
48        /// Retired allocation slot.
49        slot: MemoryManagerSlot,
50    },
51}
52
53// Doctor needs the same checks as bootstrap, but does not consume declarations
54// or mint a capability. Keep check ordering and error ownership in one place.
55pub fn check_allocations<P: RuntimeBootstrapPolicy>(
56    recovered: &RecoveredLedger,
57    snapshot: &DeclarationSnapshot,
58    policy: &P,
59) -> Result<(), AllocationValidationError<P::Error>> {
60    for declaration in snapshot.declarations() {
61        check_declaration(recovered.ledger(), declaration, policy)?;
62    }
63
64    Ok(())
65}
66
67pub fn check_declaration<P: RuntimeBootstrapPolicy>(
68    ledger: &AllocationLedger,
69    declaration: &crate::declaration::AllocationDeclaration,
70    policy: &P,
71) -> Result<crate::ledger::ClaimOutcome, AllocationValidationError<P::Error>> {
72    // Keep callback and history error precedence identical in doctor and staging.
73    // Governance is checked against history without invoking application policy.
74    if declaration.stable_key.as_str() != crate::IC_MEMORY_LEDGER_STABLE_KEY {
75        policy
76            .validate_allocation(&declaration.stable_key, &declaration.slot)
77            .map_err(AllocationValidationError::Policy)?;
78    }
79    validate_declaration_claim(ledger, declaration)
80        .map_err(|conflict| map_validation_claim_conflict(declaration, conflict))
81}
82
83fn map_validation_claim_conflict<P>(
84    declaration: &crate::declaration::AllocationDeclaration,
85    conflict: ClaimConflict<'_>,
86) -> AllocationValidationError<P> {
87    match conflict {
88        ClaimConflict::StableKeyMoved { record } => {
89            AllocationValidationError::StableKeySlotConflict {
90                stable_key: declaration.stable_key.clone(),
91                historical_slot: record.slot.clone(),
92                declared_slot: declaration.slot.clone(),
93            }
94        }
95        ClaimConflict::SlotReused { record } => AllocationValidationError::SlotStableKeyConflict {
96            slot: declaration.slot.clone(),
97            historical_key: record.stable_key.clone(),
98            declared_key: declaration.stable_key.clone(),
99        },
100        ClaimConflict::Tombstoned { record } => AllocationValidationError::RetiredAllocation {
101            stable_key: declaration.stable_key.clone(),
102            slot: record.slot.clone(),
103        },
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::{
111        declaration::AllocationDeclaration,
112        ledger::{AllocationRecord, AllocationState},
113        slot::MemoryManagerSlot,
114    };
115
116    #[derive(Debug, Eq, PartialEq)]
117    struct TestPolicy;
118
119    impl RuntimeBootstrapPolicy for TestPolicy {
120        type Error = &'static str;
121
122        fn validate_allocation(
123            &self,
124            key: &StableKey,
125            _slot: &MemoryManagerSlot,
126        ) -> Result<(), Self::Error> {
127            if key.as_str().starts_with("bad.") {
128                return Err("bad key");
129            }
130            assert!(!key.as_str().starts_with("bad."));
131            Ok(())
132        }
133
134        fn runtime_bootstrap_identity(&self) -> crate::PolicyIdentity {
135            crate::PolicyIdentity::new(*b"validation-test.................")
136        }
137    }
138
139    fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
140        AllocationLedger {
141            current_generation: 7,
142            records,
143        }
144    }
145
146    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
147        AllocationDeclaration::new(key, MemoryManagerSlot::new(id).expect("usable slot"))
148            .expect("declaration")
149    }
150
151    fn active_record(key: &str, id: u8) -> AllocationRecord {
152        AllocationRecord::active(&declaration(key, id))
153    }
154
155    fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
156        RecoveredLedger::from_trusted_ledger(ledger(records))
157    }
158
159    #[test]
160    fn accepts_matching_historical_owner() {
161        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
162
163        check_allocations(
164            &recovered(vec![active_record("app.users.v1", 100)]),
165            &snapshot,
166            &TestPolicy,
167        )
168        .expect("validated");
169    }
170
171    #[test]
172    fn omitted_historical_records_do_not_fail_validation() {
173        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
174
175        check_allocations(
176            &recovered(vec![
177                active_record("app.users.v1", 100),
178                active_record("app.orders.v1", 101),
179            ]),
180            &snapshot,
181            &TestPolicy,
182        )
183        .expect("omitted records are preserved, not retired");
184    }
185
186    #[test]
187    fn rejects_same_key_different_slot() {
188        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 101)]);
189
190        let err = check_allocations(
191            &recovered(vec![active_record("app.users.v1", 100)]),
192            &snapshot,
193            &TestPolicy,
194        )
195        .expect_err("conflict");
196
197        assert!(matches!(
198            err,
199            AllocationValidationError::StableKeySlotConflict { .. }
200        ));
201    }
202
203    #[test]
204    fn rejects_same_slot_different_key() {
205        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.orders.v1", 100)]);
206
207        let err = check_allocations(
208            &recovered(vec![active_record("app.users.v1", 100)]),
209            &snapshot,
210            &TestPolicy,
211        )
212        .expect_err("conflict");
213
214        assert!(matches!(
215            err,
216            AllocationValidationError::SlotStableKeyConflict { .. }
217        ));
218    }
219
220    #[test]
221    fn rejects_retired_redeclaration() {
222        let mut record = active_record("app.users.v1", 100);
223        record.state = AllocationState::Retired;
224        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
225
226        let err = check_allocations(&recovered(vec![record]), &snapshot, &TestPolicy)
227            .expect_err("retired");
228
229        assert!(matches!(
230            err,
231            AllocationValidationError::RetiredAllocation { .. }
232        ));
233    }
234
235    #[test]
236    fn policy_rejections_fail_before_validation_succeeds() {
237        let snapshot = DeclarationSnapshot::for_test(vec![declaration("bad.users.v1", 100)]);
238
239        let err = check_allocations(&recovered(Vec::new()), &snapshot, &TestPolicy)
240            .expect_err("policy failure");
241
242        assert_eq!(err, AllocationValidationError::Policy("bad key"));
243    }
244
245    #[test]
246    fn full_slot_domain_validates_stages_and_commits() {
247        let mut store = crate::LedgerCommitStore::default();
248        let recovered = store.recover_or_genesis().unwrap();
249        let snapshot = DeclarationSnapshot::for_test(
250            (0..=crate::MEMORY_MANAGER_MAX_ID)
251                .map(|id| declaration(&format!("app.store{id}.v1"), id))
252                .collect(),
253        );
254        let (staged, _) =
255            crate::ledger::stage_generation(recovered, snapshot, &TestPolicy).unwrap();
256        assert_eq!(staged.records().len(), 255);
257
258        let committed = store.commit(&staged).unwrap();
259        assert_eq!(committed.current_generation(), 1);
260        assert_eq!(store.recover().unwrap(), committed);
261    }
262}