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, runtime_fingerprint) = snapshot.into_parts();
68
69    Ok(ValidatedAllocations::new(
70        recovered.current_generation(),
71        declarations,
72        runtime_fingerprint,
73    ))
74}
75
76// Doctor needs the same checks as bootstrap, but does not consume declarations
77// or mint a capability. Keep check ordering and error ownership in one place.
78pub fn check_allocations<P: AllocationPolicy>(
79    recovered: &RecoveredLedger,
80    snapshot: &DeclarationSnapshot,
81    policy: &P,
82) -> Result<(), AllocationValidationError<P::Error>> {
83    let ledger = recovered.ledger();
84
85    for declaration in snapshot.declarations() {
86        policy
87            .validate_key(&declaration.stable_key)
88            .map_err(AllocationValidationError::Policy)?;
89        policy
90            .validate_slot(&declaration.stable_key, &declaration.slot)
91            .map_err(AllocationValidationError::Policy)?;
92
93        validate_declaration_history(ledger, declaration)?;
94    }
95
96    Ok(())
97}
98
99fn validate_declaration_history<P>(
100    ledger: &AllocationLedger,
101    declaration: &crate::declaration::AllocationDeclaration,
102) -> Result<(), AllocationValidationError<P>> {
103    validate_declaration_claim(ledger, declaration)
104        .map(|_| ())
105        .map_err(|conflict| map_validation_claim_conflict(declaration, conflict))
106}
107
108fn map_validation_claim_conflict<P>(
109    declaration: &crate::declaration::AllocationDeclaration,
110    conflict: ClaimConflict<'_>,
111) -> AllocationValidationError<P> {
112    match conflict {
113        ClaimConflict::StableKeyMoved { record } => {
114            AllocationValidationError::StableKeySlotConflict {
115                stable_key: declaration.stable_key.clone(),
116                historical_slot: record.slot.clone(),
117                declared_slot: declaration.slot.clone(),
118            }
119        }
120        ClaimConflict::SlotReused { record } => AllocationValidationError::SlotStableKeyConflict {
121            slot: declaration.slot.clone(),
122            historical_key: record.stable_key.clone(),
123            declared_key: declaration.stable_key.clone(),
124        },
125        ClaimConflict::Tombstoned { record } => AllocationValidationError::RetiredAllocation {
126            stable_key: declaration.stable_key.clone(),
127            slot: record.slot.clone(),
128        },
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::{
136        declaration::AllocationDeclaration,
137        ledger::{AllocationHistory, AllocationRecord, AllocationState, GenerationRecord},
138        schema::SchemaMetadata,
139        slot::MemoryManagerSlot,
140    };
141
142    #[derive(Debug, Eq, PartialEq)]
143    struct TestPolicy;
144
145    impl AllocationPolicy for TestPolicy {
146        type Error = &'static str;
147
148        fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
149            if key.as_str().starts_with("bad.") {
150                return Err("bad key");
151            }
152            Ok(())
153        }
154
155        fn validate_slot(
156            &self,
157            _key: &StableKey,
158            _slot: &MemoryManagerSlot,
159        ) -> Result<(), Self::Error> {
160            Ok(())
161        }
162
163        fn validate_reserved_slot(
164            &self,
165            _key: &StableKey,
166            _slot: &MemoryManagerSlot,
167        ) -> Result<(), Self::Error> {
168            Ok(())
169        }
170    }
171
172    fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
173        let generations = (1..=7)
174            .map(|generation| {
175                GenerationRecord::new(
176                    generation,
177                    if generation == 1 { 0 } else { generation - 1 },
178                    None,
179                    0,
180                    None,
181                )
182                .expect("generation record")
183            })
184            .collect();
185
186        AllocationLedger {
187            current_generation: 7,
188            allocation_history: AllocationHistory::from_parts(records, generations),
189        }
190    }
191
192    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
193        AllocationDeclaration::new(
194            key,
195            MemoryManagerSlot::new(id).expect("usable slot"),
196            None,
197            SchemaMetadata::default(),
198        )
199        .expect("declaration")
200    }
201
202    fn active_record(key: &str, id: u8) -> AllocationRecord {
203        AllocationRecord::active(1, &declaration(key, id))
204    }
205
206    fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
207        RecoveredLedger::from_trusted_ledger(ledger(records))
208    }
209
210    #[test]
211    fn accepts_matching_historical_owner() {
212        let snapshot =
213            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
214
215        let validated = validate_allocations(
216            &recovered(vec![active_record("app.users.v1", 100)]),
217            snapshot,
218            &TestPolicy,
219        )
220        .expect("validated");
221
222        assert_eq!(validated.base_generation(), 7);
223    }
224
225    #[test]
226    fn omitted_historical_records_do_not_fail_validation() {
227        let snapshot =
228            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
229
230        validate_allocations(
231            &recovered(vec![
232                active_record("app.users.v1", 100),
233                active_record("app.orders.v1", 101),
234            ]),
235            snapshot,
236            &TestPolicy,
237        )
238        .expect("omitted records are preserved, not retired");
239    }
240
241    #[test]
242    fn rejects_same_key_different_slot() {
243        let snapshot =
244            DeclarationSnapshot::new(vec![declaration("app.users.v1", 101)]).expect("snapshot");
245
246        let err = validate_allocations(
247            &recovered(vec![active_record("app.users.v1", 100)]),
248            snapshot,
249            &TestPolicy,
250        )
251        .expect_err("conflict");
252
253        assert!(matches!(
254            err,
255            AllocationValidationError::StableKeySlotConflict { .. }
256        ));
257    }
258
259    #[test]
260    fn rejects_same_slot_different_key() {
261        let snapshot =
262            DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).expect("snapshot");
263
264        let err = validate_allocations(
265            &recovered(vec![active_record("app.users.v1", 100)]),
266            snapshot,
267            &TestPolicy,
268        )
269        .expect_err("conflict");
270
271        assert!(matches!(
272            err,
273            AllocationValidationError::SlotStableKeyConflict { .. }
274        ));
275    }
276
277    #[test]
278    fn rejects_retired_redeclaration() {
279        let mut record = active_record("app.users.v1", 100);
280        record.state = AllocationState::Retired { generation: 3 };
281        let snapshot =
282            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
283
284        let err = validate_allocations(&recovered(vec![record]), snapshot, &TestPolicy)
285            .expect_err("retired");
286
287        assert!(matches!(
288            err,
289            AllocationValidationError::RetiredAllocation { .. }
290        ));
291    }
292
293    #[test]
294    fn policy_rejections_fail_before_validation_succeeds() {
295        let snapshot =
296            DeclarationSnapshot::new(vec![declaration("bad.users.v1", 100)]).expect("snapshot");
297
298        let err = validate_allocations(&recovered(Vec::new()), snapshot, &TestPolicy)
299            .expect_err("policy failure");
300
301        assert_eq!(err, AllocationValidationError::Policy("bad key"));
302    }
303
304    #[test]
305    fn full_slot_domain_validates_stages_and_commits_through_public_boundaries() {
306        let mut store = crate::LedgerCommitStore::default();
307        let genesis = AllocationLedger::new(0, AllocationHistory::default()).unwrap();
308        let recovered = store.recover_or_initialize(&genesis).unwrap();
309        let snapshot = DeclarationSnapshot::new(
310            (0..=crate::MEMORY_MANAGER_MAX_ID)
311                .map(|id| declaration(&format!("app.store{id}.v1"), id))
312                .collect(),
313        )
314        .unwrap();
315        let validated = validate_allocations(&recovered, snapshot, &TestPolicy).unwrap();
316        let staged = recovered
317            .ledger()
318            .stage_validated_generation(&validated, None)
319            .unwrap();
320        assert_eq!(staged.allocation_history().records().len(), 255);
321        assert_eq!(
322            staged.allocation_history().generations()[0].declaration_count(),
323            255
324        );
325        let committed = store.commit(&staged).unwrap();
326        assert_eq!(committed.current_generation(), 1);
327        assert_eq!(store.recover().unwrap(), committed);
328    }
329}