Skip to main content

ic_memory/
validation.rs

1use crate::{
2    declaration::{DeclarationSnapshot, DeclarationSnapshotError},
3    key::StableKey,
4    ledger::{
5        AllocationLedger, ClaimConflict, LedgerIntegrityError, RecoveredLedger,
6        claim_conflict_record, validate_declaration_claim,
7    },
8    policy::AllocationPolicy,
9    session::ValidatedAllocations,
10    slot::AllocationSlotDescriptor,
11};
12
13///
14/// Validate
15///
16/// Re-check constructor invariants on decoded DTOs before they become
17/// authoritative.
18pub trait Validate {
19    /// Validation error for this DTO.
20    type Error;
21
22    /// Validate this value's domain invariants.
23    fn validate(&self) -> Result<(), Self::Error>;
24}
25
26///
27/// AllocationValidationError
28///
29/// Failure to validate declarations against policy and historical ledger facts.
30#[non_exhaustive]
31#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
32pub enum AllocationValidationError<P> {
33    /// Historical ledger was decoded or assembled with invalid committed state.
34    #[error(transparent)]
35    LedgerIntegrity(LedgerIntegrityError),
36    /// Declaration snapshot was decoded or assembled with invalid DTOs.
37    #[error(transparent)]
38    Snapshot(DeclarationSnapshotError),
39    /// Policy adapter rejected the declaration.
40    #[error("allocation policy rejected a declaration")]
41    Policy(P),
42    /// Stable key was historically bound to a different slot.
43    #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
44    StableKeySlotConflict {
45        /// Stable key that was redeclared.
46        stable_key: StableKey,
47        /// Historical slot for the stable key.
48        historical_slot: Box<AllocationSlotDescriptor>,
49        /// Slot claimed by the current declaration.
50        declared_slot: Box<AllocationSlotDescriptor>,
51    },
52    /// Slot was historically bound to a different stable key.
53    #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
54    SlotStableKeyConflict {
55        /// Slot claimed by the current declaration.
56        slot: Box<AllocationSlotDescriptor>,
57        /// Historical stable key for the slot.
58        historical_key: StableKey,
59        /// Stable key claimed by the current declaration.
60        declared_key: StableKey,
61    },
62    /// Current declaration attempted to revive a retired allocation.
63    #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
64    RetiredAllocation {
65        /// Retired stable key.
66        stable_key: StableKey,
67        /// Retired allocation slot.
68        slot: Box<AllocationSlotDescriptor>,
69    },
70    /// Internal claim validation reported an active-allocation conflict where
71    /// declaration validation expected only move, reuse, or tombstone conflicts.
72    #[error("stable key '{stable_key}' produced an unexpected active-allocation conflict")]
73    UnexpectedActiveAllocationConflict {
74        /// Active stable key.
75        stable_key: StableKey,
76        /// Active allocation slot.
77        slot: Box<AllocationSlotDescriptor>,
78    },
79}
80
81/// Validate a committed ledger and current declarations before opening.
82///
83/// This is the authority boundary for [`ValidatedAllocations`]: the historical
84/// ledger must pass current-format and committed-integrity checks before current
85/// declarations are checked against framework policy and ledger history. This
86/// proves allocation ABI safety only. It does not prove store-level schema
87/// support.
88pub fn validate_allocations<P: AllocationPolicy>(
89    recovered: &RecoveredLedger,
90    snapshot: DeclarationSnapshot,
91    policy: &P,
92) -> Result<ValidatedAllocations, AllocationValidationError<P::Error>> {
93    let ledger = recovered.ledger();
94
95    snapshot
96        .validate()
97        .map_err(AllocationValidationError::Snapshot)?;
98
99    for declaration in snapshot.declarations() {
100        policy
101            .validate_key(&declaration.stable_key)
102            .map_err(AllocationValidationError::Policy)?;
103        policy
104            .validate_slot(&declaration.stable_key, &declaration.slot)
105            .map_err(AllocationValidationError::Policy)?;
106
107        validate_declaration_history(ledger, declaration)?;
108    }
109
110    let (declarations, runtime_fingerprint) = snapshot.into_parts();
111
112    Ok(ValidatedAllocations::new(
113        ledger.current_generation,
114        declarations,
115        runtime_fingerprint,
116    ))
117}
118
119fn validate_declaration_history<P>(
120    ledger: &AllocationLedger,
121    declaration: &crate::declaration::AllocationDeclaration,
122) -> Result<(), AllocationValidationError<P>> {
123    validate_declaration_claim(ledger, declaration)
124        .map(|_| ())
125        .map_err(|conflict| map_validation_claim_conflict(ledger, declaration, conflict))
126}
127
128fn map_validation_claim_conflict<P>(
129    ledger: &AllocationLedger,
130    declaration: &crate::declaration::AllocationDeclaration,
131    conflict: ClaimConflict,
132) -> AllocationValidationError<P> {
133    let record = claim_conflict_record(ledger, conflict);
134    match conflict {
135        ClaimConflict::StableKeyMoved { .. } => AllocationValidationError::StableKeySlotConflict {
136            stable_key: declaration.stable_key.clone(),
137            historical_slot: Box::new(record.slot.clone()),
138            declared_slot: Box::new(declaration.slot.clone()),
139        },
140        ClaimConflict::SlotReused { .. } => AllocationValidationError::SlotStableKeyConflict {
141            slot: Box::new(declaration.slot.clone()),
142            historical_key: record.stable_key.clone(),
143            declared_key: declaration.stable_key.clone(),
144        },
145        ClaimConflict::Tombstoned { .. } => AllocationValidationError::RetiredAllocation {
146            stable_key: declaration.stable_key.clone(),
147            slot: Box::new(record.slot.clone()),
148        },
149        ClaimConflict::ActiveAllocation { .. } => {
150            AllocationValidationError::UnexpectedActiveAllocationConflict {
151                stable_key: record.stable_key.clone(),
152                slot: Box::new(record.slot.clone()),
153            }
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::{
162        declaration::AllocationDeclaration,
163        ledger::{AllocationHistory, AllocationRecord, AllocationState, GenerationRecord},
164        schema::SchemaMetadata,
165        slot::AllocationSlotDescriptor,
166    };
167
168    #[derive(Debug, Eq, PartialEq)]
169    struct TestPolicy;
170
171    impl AllocationPolicy for TestPolicy {
172        type Error = &'static str;
173
174        fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
175            if key.as_str().starts_with("bad.") {
176                return Err("bad key");
177            }
178            Ok(())
179        }
180
181        fn validate_slot(
182            &self,
183            _key: &StableKey,
184            slot: &AllocationSlotDescriptor,
185        ) -> Result<(), Self::Error> {
186            if slot
187                == &AllocationSlotDescriptor::memory_manager_unchecked(
188                    crate::MEMORY_MANAGER_INVALID_ID,
189                )
190            {
191                return Err("bad slot");
192            }
193            Ok(())
194        }
195
196        fn validate_reserved_slot(
197            &self,
198            _key: &StableKey,
199            _slot: &AllocationSlotDescriptor,
200        ) -> Result<(), Self::Error> {
201            Ok(())
202        }
203    }
204
205    fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
206        let generations = (1..=7)
207            .map(|generation| {
208                GenerationRecord::new(
209                    generation,
210                    if generation == 1 { 0 } else { generation - 1 },
211                    None,
212                    0,
213                    None,
214                )
215                .expect("generation record")
216            })
217            .collect();
218
219        AllocationLedger {
220            current_generation: 7,
221            allocation_history: AllocationHistory::from_parts(records, generations),
222        }
223    }
224
225    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
226        AllocationDeclaration::new(
227            key,
228            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
229            None,
230            SchemaMetadata::default(),
231        )
232        .expect("declaration")
233    }
234
235    fn active_record(key: &str, id: u8) -> AllocationRecord {
236        AllocationRecord::from_declaration(1, declaration(key, id), AllocationState::Active)
237            .expect("valid schema metadata")
238    }
239
240    fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
241        RecoveredLedger::from_trusted_parts(ledger(records), 7)
242    }
243
244    #[test]
245    fn accepts_matching_historical_owner() {
246        let snapshot =
247            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
248
249        let validated = validate_allocations(
250            &recovered(vec![active_record("app.users.v1", 100)]),
251            snapshot,
252            &TestPolicy,
253        )
254        .expect("validated");
255
256        assert_eq!(validated.generation(), 7);
257    }
258
259    #[test]
260    fn omitted_historical_records_do_not_fail_validation() {
261        let snapshot =
262            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
263
264        validate_allocations(
265            &recovered(vec![
266                active_record("app.users.v1", 100),
267                active_record("app.orders.v1", 101),
268            ]),
269            snapshot,
270            &TestPolicy,
271        )
272        .expect("omitted records are preserved, not retired");
273    }
274
275    #[test]
276    fn rejects_same_key_different_slot() {
277        let snapshot =
278            DeclarationSnapshot::new(vec![declaration("app.users.v1", 101)]).expect("snapshot");
279
280        let err = validate_allocations(
281            &recovered(vec![active_record("app.users.v1", 100)]),
282            snapshot,
283            &TestPolicy,
284        )
285        .expect_err("conflict");
286
287        assert!(matches!(
288            err,
289            AllocationValidationError::StableKeySlotConflict { .. }
290        ));
291    }
292
293    #[test]
294    fn rejects_same_slot_different_key() {
295        let snapshot =
296            DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).expect("snapshot");
297
298        let err = validate_allocations(
299            &recovered(vec![active_record("app.users.v1", 100)]),
300            snapshot,
301            &TestPolicy,
302        )
303        .expect_err("conflict");
304
305        assert!(matches!(
306            err,
307            AllocationValidationError::SlotStableKeyConflict { .. }
308        ));
309    }
310
311    #[test]
312    fn rejects_retired_redeclaration() {
313        let mut record = active_record("app.users.v1", 100);
314        record.state = AllocationState::Retired;
315        record.retired_generation = Some(3);
316        let snapshot =
317            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
318
319        let err = validate_allocations(&recovered(vec![record]), snapshot, &TestPolicy)
320            .expect_err("retired");
321
322        assert!(matches!(
323            err,
324            AllocationValidationError::RetiredAllocation { .. }
325        ));
326    }
327
328    #[test]
329    fn policy_rejections_fail_before_validation_succeeds() {
330        let snapshot =
331            DeclarationSnapshot::new(vec![declaration("bad.users.v1", 100)]).expect("snapshot");
332
333        let err = validate_allocations(&recovered(Vec::new()), snapshot, &TestPolicy)
334            .expect_err("policy failure");
335
336        assert_eq!(err, AllocationValidationError::Policy("bad key"));
337    }
338}