Skip to main content

ic_memory/
validation.rs

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