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
163    fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
164        AllocationLedger {
165            current_generation: 7,
166            records,
167        }
168    }
169
170    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
171        AllocationDeclaration::new(
172            key,
173            MemoryManagerSlot::new(id).expect("usable slot"),
174            None,
175            SchemaMetadata::default(),
176        )
177        .expect("declaration")
178    }
179
180    fn active_record(key: &str, id: u8) -> AllocationRecord {
181        AllocationRecord::active(&declaration(key, id))
182    }
183
184    fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
185        RecoveredLedger::from_trusted_ledger(ledger(records))
186    }
187
188    #[test]
189    fn accepts_matching_historical_owner() {
190        let snapshot =
191            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
192
193        let validated = validate_allocations(
194            &recovered(vec![active_record("app.users.v1", 100)]),
195            snapshot,
196            &TestPolicy,
197        )
198        .expect("validated");
199
200        assert_eq!(validated.base_generation(), 7);
201    }
202
203    #[test]
204    fn omitted_historical_records_do_not_fail_validation() {
205        let snapshot =
206            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
207
208        validate_allocations(
209            &recovered(vec![
210                active_record("app.users.v1", 100),
211                active_record("app.orders.v1", 101),
212            ]),
213            snapshot,
214            &TestPolicy,
215        )
216        .expect("omitted records are preserved, not retired");
217    }
218
219    #[test]
220    fn rejects_same_key_different_slot() {
221        let snapshot =
222            DeclarationSnapshot::new(vec![declaration("app.users.v1", 101)]).expect("snapshot");
223
224        let err = validate_allocations(
225            &recovered(vec![active_record("app.users.v1", 100)]),
226            snapshot,
227            &TestPolicy,
228        )
229        .expect_err("conflict");
230
231        assert!(matches!(
232            err,
233            AllocationValidationError::StableKeySlotConflict { .. }
234        ));
235    }
236
237    #[test]
238    fn rejects_same_slot_different_key() {
239        let snapshot =
240            DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).expect("snapshot");
241
242        let err = validate_allocations(
243            &recovered(vec![active_record("app.users.v1", 100)]),
244            snapshot,
245            &TestPolicy,
246        )
247        .expect_err("conflict");
248
249        assert!(matches!(
250            err,
251            AllocationValidationError::SlotStableKeyConflict { .. }
252        ));
253    }
254
255    #[test]
256    fn rejects_retired_redeclaration() {
257        let mut record = active_record("app.users.v1", 100);
258        record.state = AllocationState::Retired;
259        let snapshot =
260            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
261
262        let err = validate_allocations(&recovered(vec![record]), snapshot, &TestPolicy)
263            .expect_err("retired");
264
265        assert!(matches!(
266            err,
267            AllocationValidationError::RetiredAllocation { .. }
268        ));
269    }
270
271    #[test]
272    fn policy_rejections_fail_before_validation_succeeds() {
273        let snapshot =
274            DeclarationSnapshot::new(vec![declaration("bad.users.v1", 100)]).expect("snapshot");
275
276        let err = validate_allocations(&recovered(Vec::new()), snapshot, &TestPolicy)
277            .expect_err("policy failure");
278
279        assert_eq!(err, AllocationValidationError::Policy("bad key"));
280    }
281
282    #[test]
283    fn full_slot_domain_validates_stages_and_commits_through_public_boundaries() {
284        let mut store = crate::LedgerCommitStore::default();
285        let genesis = AllocationLedger::new(0, Vec::new()).unwrap();
286        let recovered = store.recover_or_initialize(&genesis).unwrap();
287        let snapshot = DeclarationSnapshot::new(
288            (0..=crate::MEMORY_MANAGER_MAX_ID)
289                .map(|id| declaration(&format!("app.store{id}.v1"), id))
290                .collect(),
291        )
292        .unwrap();
293        let validated = validate_allocations(&recovered, snapshot, &TestPolicy).unwrap();
294        let staged = recovered
295            .ledger()
296            .stage_validated_generation(&validated)
297            .unwrap();
298        assert_eq!(staged.records().len(), 255);
299
300        let committed = store.commit(&staged).unwrap();
301        assert_eq!(committed.current_generation(), 1);
302        assert_eq!(store.recover().unwrap(), committed);
303    }
304}