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::from_parts(records, Vec::new()),
174        }
175    }
176
177    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
178        AllocationDeclaration::new(
179            key,
180            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
181            None,
182            SchemaMetadata::default(),
183        )
184        .expect("declaration")
185    }
186
187    fn active_record(key: &str, id: u8) -> AllocationRecord {
188        AllocationRecord::from_declaration(1, declaration(key, id), AllocationState::Active)
189    }
190
191    #[test]
192    fn accepts_matching_historical_owner() {
193        let snapshot =
194            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
195
196        let validated = validate_allocations(
197            &ledger(vec![active_record("app.users.v1", 100)]),
198            snapshot,
199            &TestPolicy,
200        )
201        .expect("validated");
202
203        assert_eq!(validated.generation(), 7);
204    }
205
206    #[test]
207    fn omitted_historical_records_do_not_fail_validation() {
208        let snapshot =
209            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
210
211        validate_allocations(
212            &ledger(vec![
213                active_record("app.users.v1", 100),
214                active_record("app.orders.v1", 101),
215            ]),
216            snapshot,
217            &TestPolicy,
218        )
219        .expect("omitted records are preserved, not retired");
220    }
221
222    #[test]
223    fn rejects_same_key_different_slot() {
224        let snapshot =
225            DeclarationSnapshot::new(vec![declaration("app.users.v1", 101)]).expect("snapshot");
226
227        let err = validate_allocations(
228            &ledger(vec![active_record("app.users.v1", 100)]),
229            snapshot,
230            &TestPolicy,
231        )
232        .expect_err("conflict");
233
234        assert!(matches!(
235            err,
236            AllocationValidationError::StableKeySlotConflict { .. }
237        ));
238    }
239
240    #[test]
241    fn rejects_same_slot_different_key() {
242        let snapshot =
243            DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).expect("snapshot");
244
245        let err = validate_allocations(
246            &ledger(vec![active_record("app.users.v1", 100)]),
247            snapshot,
248            &TestPolicy,
249        )
250        .expect_err("conflict");
251
252        assert!(matches!(
253            err,
254            AllocationValidationError::SlotStableKeyConflict { .. }
255        ));
256    }
257
258    #[test]
259    fn rejects_retired_redeclaration() {
260        let mut record = active_record("app.users.v1", 100);
261        record.state = AllocationState::Retired;
262        record.retired_generation = Some(3);
263        let snapshot =
264            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
265
266        let err = validate_allocations(&ledger(vec![record]), snapshot, &TestPolicy)
267            .expect_err("retired");
268
269        assert!(matches!(
270            err,
271            AllocationValidationError::RetiredAllocation { .. }
272        ));
273    }
274
275    #[test]
276    fn policy_rejections_fail_before_validation_succeeds() {
277        let snapshot =
278            DeclarationSnapshot::new(vec![declaration("bad.users.v1", 100)]).expect("snapshot");
279
280        let err = validate_allocations(&ledger(Vec::new()), snapshot, &TestPolicy)
281            .expect_err("policy failure");
282
283        assert_eq!(err, AllocationValidationError::Policy("bad key"));
284    }
285}