Skip to main content

ic_memory/
validation.rs

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