Skip to main content

ic_memory/
validation.rs

1use crate::{
2    declaration::DeclarationSnapshot,
3    key::StableKey,
4    ledger::{AllocationLedger, ClaimConflict, RecoveredLedger, validate_declaration_claim},
5    policy::RuntimeBootstrapPolicy,
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_key(&declaration.stable_key)
77            .map_err(AllocationValidationError::Policy)?;
78        policy
79            .validate_slot(&declaration.stable_key, &declaration.slot)
80            .map_err(AllocationValidationError::Policy)?;
81    }
82    validate_declaration_claim(ledger, declaration)
83        .map_err(|conflict| map_validation_claim_conflict(declaration, conflict))
84}
85
86fn map_validation_claim_conflict<P>(
87    declaration: &crate::declaration::AllocationDeclaration,
88    conflict: ClaimConflict<'_>,
89) -> AllocationValidationError<P> {
90    match conflict {
91        ClaimConflict::StableKeyMoved { record } => {
92            AllocationValidationError::StableKeySlotConflict {
93                stable_key: declaration.stable_key.clone(),
94                historical_slot: record.slot.clone(),
95                declared_slot: declaration.slot.clone(),
96            }
97        }
98        ClaimConflict::SlotReused { record } => AllocationValidationError::SlotStableKeyConflict {
99            slot: declaration.slot.clone(),
100            historical_key: record.stable_key.clone(),
101            declared_key: declaration.stable_key.clone(),
102        },
103        ClaimConflict::Tombstoned { record } => AllocationValidationError::RetiredAllocation {
104            stable_key: declaration.stable_key.clone(),
105            slot: record.slot.clone(),
106        },
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::{
114        declaration::AllocationDeclaration,
115        ledger::{AllocationRecord, AllocationState},
116        schema::SchemaMetadata,
117        slot::MemoryManagerSlot,
118    };
119
120    #[derive(Debug, Eq, PartialEq)]
121    struct TestPolicy;
122
123    impl RuntimeBootstrapPolicy for TestPolicy {
124        type Error = &'static str;
125
126        fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
127            if key.as_str().starts_with("bad.") {
128                return Err("bad key");
129            }
130            Ok(())
131        }
132
133        fn validate_slot(
134            &self,
135            key: &StableKey,
136            _slot: &MemoryManagerSlot,
137        ) -> Result<(), Self::Error> {
138            assert!(!key.as_str().starts_with("bad."));
139            Ok(())
140        }
141
142        fn runtime_bootstrap_identity(
143            &self,
144        ) -> Result<crate::PolicyIdentity, crate::PolicyIdentityError> {
145            crate::PolicyIdentity::new("validation-test", 1)
146        }
147    }
148
149    fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
150        AllocationLedger {
151            current_generation: 7,
152            records,
153        }
154    }
155
156    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
157        AllocationDeclaration::new(
158            key,
159            MemoryManagerSlot::new(id).expect("usable slot"),
160            SchemaMetadata::default(),
161        )
162        .expect("declaration")
163    }
164
165    fn active_record(key: &str, id: u8) -> AllocationRecord {
166        AllocationRecord::active(&declaration(key, id))
167    }
168
169    fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
170        RecoveredLedger::from_trusted_ledger(ledger(records))
171    }
172
173    #[test]
174    fn accepts_matching_historical_owner() {
175        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 100)])
176            .expect("snapshot");
177
178        check_allocations(
179            &recovered(vec![active_record("app.users.v1", 100)]),
180            &snapshot,
181            &TestPolicy,
182        )
183        .expect("validated");
184    }
185
186    #[test]
187    fn omitted_historical_records_do_not_fail_validation() {
188        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 100)])
189            .expect("snapshot");
190
191        check_allocations(
192            &recovered(vec![
193                active_record("app.users.v1", 100),
194                active_record("app.orders.v1", 101),
195            ]),
196            &snapshot,
197            &TestPolicy,
198        )
199        .expect("omitted records are preserved, not retired");
200    }
201
202    #[test]
203    fn rejects_same_key_different_slot() {
204        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 101)])
205            .expect("snapshot");
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::StableKeySlotConflict { .. }
217        ));
218    }
219
220    #[test]
221    fn rejects_same_slot_different_key() {
222        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.orders.v1", 100)])
223            .expect("snapshot");
224
225        let err = check_allocations(
226            &recovered(vec![active_record("app.users.v1", 100)]),
227            &snapshot,
228            &TestPolicy,
229        )
230        .expect_err("conflict");
231
232        assert!(matches!(
233            err,
234            AllocationValidationError::SlotStableKeyConflict { .. }
235        ));
236    }
237
238    #[test]
239    fn rejects_retired_redeclaration() {
240        let mut record = active_record("app.users.v1", 100);
241        record.state = AllocationState::Retired;
242        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 100)])
243            .expect("snapshot");
244
245        let err = check_allocations(&recovered(vec![record]), &snapshot, &TestPolicy)
246            .expect_err("retired");
247
248        assert!(matches!(
249            err,
250            AllocationValidationError::RetiredAllocation { .. }
251        ));
252    }
253
254    #[test]
255    fn policy_rejections_fail_before_validation_succeeds() {
256        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("bad.users.v1", 100)])
257            .expect("snapshot");
258
259        let err = check_allocations(&recovered(Vec::new()), &snapshot, &TestPolicy)
260            .expect_err("policy failure");
261
262        assert_eq!(err, AllocationValidationError::Policy("bad key"));
263    }
264
265    #[test]
266    fn full_slot_domain_validates_stages_and_commits() {
267        let mut store = crate::LedgerCommitStore::default();
268        let recovered = store.recover_or_genesis().unwrap();
269        let snapshot = DeclarationSnapshot::canonicalize(
270            (0..=crate::MEMORY_MANAGER_MAX_ID)
271                .map(|id| declaration(&format!("app.store{id}.v1"), id))
272                .collect(),
273        )
274        .unwrap();
275        let (staged, _) =
276            crate::ledger::stage_generation(recovered, snapshot, &TestPolicy).unwrap();
277        assert_eq!(staged.records().len(), 255);
278
279        let committed = store.commit(&staged).unwrap();
280        assert_eq!(committed.current_generation(), 1);
281        assert_eq!(store.recover().unwrap(), committed);
282    }
283}