Skip to main content

ic_memory/
validation.rs

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