Skip to main content

ic_memory/
validation.rs

1use crate::{
2    key::StableKey,
3    ledger::{AllocationLedger, ClaimConflict, RecoveredLedger, validate_declaration_claim},
4    policy::RuntimeBootstrapPolicy,
5    requests::DeclarationSnapshot,
6    slot::MemoryManagerSlot,
7};
8
9///
10/// AllocationValidationError
11///
12/// Failure to validate declarations against policy and historical ledger facts.
13/// Resolution establishes snapshot invariants; recovery establishes
14/// ledger integrity before this boundary.
15///
16
17#[non_exhaustive]
18#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
19pub enum AllocationValidationError<P> {
20    /// Policy adapter rejected the declaration.
21    #[error("allocation policy rejected a declaration")]
22    Policy(P),
23    /// Stable key was historically bound to a different slot.
24    #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
25    StableKeySlotConflict {
26        /// Stable key that was redeclared.
27        stable_key: StableKey,
28        /// Historical slot for the stable key.
29        historical_slot: MemoryManagerSlot,
30        /// Slot claimed by the current declaration.
31        declared_slot: MemoryManagerSlot,
32    },
33    /// Slot was historically bound to a different stable key.
34    #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
35    SlotStableKeyConflict {
36        /// Slot claimed by the current declaration.
37        slot: MemoryManagerSlot,
38        /// Historical stable key for the slot.
39        historical_key: StableKey,
40        /// Stable key claimed by the current declaration.
41        declared_key: StableKey,
42    },
43    /// Current declaration attempted to revive a retired allocation.
44    #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
45    RetiredAllocation {
46        /// Retired stable key.
47        stable_key: StableKey,
48        /// Retired allocation slot.
49        slot: MemoryManagerSlot,
50    },
51}
52
53// Doctor needs the same checks as bootstrap, but does not consume declarations
54// or mint a capability. Keep check ordering and error ownership in one place.
55pub fn check_allocations<P: RuntimeBootstrapPolicy>(
56    recovered: &RecoveredLedger,
57    snapshot: &DeclarationSnapshot,
58    policy: &P,
59) -> Result<(), AllocationValidationError<P::Error>> {
60    for declaration in snapshot.declarations() {
61        check_declaration(recovered.ledger(), declaration, policy)?;
62    }
63
64    Ok(())
65}
66
67pub fn check_declaration<P: RuntimeBootstrapPolicy>(
68    ledger: &AllocationLedger,
69    declaration: &crate::declaration::AllocationDeclaration,
70    policy: &P,
71) -> Result<crate::ledger::ClaimOutcome, AllocationValidationError<P::Error>> {
72    // Keep callback and history error precedence identical in doctor and staging.
73    // Governance is checked against history without invoking application policy.
74    if declaration.stable_key.as_str() != crate::IC_MEMORY_LEDGER_STABLE_KEY {
75        policy
76            .validate_allocation(&declaration.stable_key, &declaration.slot)
77            .map_err(AllocationValidationError::Policy)?;
78    }
79    validate_declaration_claim(ledger, declaration)
80        .map_err(|conflict| map_validation_claim_conflict(declaration, conflict))
81}
82
83fn map_validation_claim_conflict<P>(
84    declaration: &crate::declaration::AllocationDeclaration,
85    conflict: ClaimConflict<'_>,
86) -> AllocationValidationError<P> {
87    match conflict {
88        ClaimConflict::StableKeyMoved { record } => {
89            AllocationValidationError::StableKeySlotConflict {
90                stable_key: declaration.stable_key.clone(),
91                historical_slot: record.slot.clone(),
92                declared_slot: declaration.slot.clone(),
93            }
94        }
95        ClaimConflict::SlotReused { record } => AllocationValidationError::SlotStableKeyConflict {
96            slot: declaration.slot.clone(),
97            historical_key: record.stable_key.clone(),
98            declared_key: declaration.stable_key.clone(),
99        },
100        ClaimConflict::Tombstoned { record } => AllocationValidationError::RetiredAllocation {
101            stable_key: declaration.stable_key.clone(),
102            slot: record.slot.clone(),
103        },
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::{
111        declaration::AllocationDeclaration,
112        ledger::{AllocationRecord, AllocationState},
113        slot::MemoryManagerSlot,
114    };
115
116    #[derive(Debug, Eq, PartialEq)]
117    struct TestPolicy;
118
119    impl RuntimeBootstrapPolicy for TestPolicy {
120        type Error = &'static str;
121
122        fn validate_allocation(
123            &self,
124            key: &StableKey,
125            _slot: &MemoryManagerSlot,
126        ) -> Result<(), Self::Error> {
127            if key.as_str().starts_with("bad.") {
128                return Err("bad key");
129            }
130            assert!(!key.as_str().starts_with("bad."));
131            Ok(())
132        }
133
134        fn runtime_bootstrap_identity(
135            &self,
136        ) -> Result<crate::PolicyIdentity, crate::PolicyIdentityError> {
137            crate::PolicyIdentity::new("validation-test", 1)
138        }
139    }
140
141    fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
142        AllocationLedger {
143            current_generation: 7,
144            records,
145        }
146    }
147
148    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
149        AllocationDeclaration::new(key, MemoryManagerSlot::new(id).expect("usable slot"))
150            .expect("declaration")
151    }
152
153    fn active_record(key: &str, id: u8) -> AllocationRecord {
154        AllocationRecord::active(&declaration(key, id))
155    }
156
157    fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
158        RecoveredLedger::from_trusted_ledger(ledger(records))
159    }
160
161    #[test]
162    fn accepts_matching_historical_owner() {
163        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
164
165        check_allocations(
166            &recovered(vec![active_record("app.users.v1", 100)]),
167            &snapshot,
168            &TestPolicy,
169        )
170        .expect("validated");
171    }
172
173    #[test]
174    fn omitted_historical_records_do_not_fail_validation() {
175        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
176
177        check_allocations(
178            &recovered(vec![
179                active_record("app.users.v1", 100),
180                active_record("app.orders.v1", 101),
181            ]),
182            &snapshot,
183            &TestPolicy,
184        )
185        .expect("omitted records are preserved, not retired");
186    }
187
188    #[test]
189    fn rejects_same_key_different_slot() {
190        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 101)]);
191
192        let err = check_allocations(
193            &recovered(vec![active_record("app.users.v1", 100)]),
194            &snapshot,
195            &TestPolicy,
196        )
197        .expect_err("conflict");
198
199        assert!(matches!(
200            err,
201            AllocationValidationError::StableKeySlotConflict { .. }
202        ));
203    }
204
205    #[test]
206    fn rejects_same_slot_different_key() {
207        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.orders.v1", 100)]);
208
209        let err = check_allocations(
210            &recovered(vec![active_record("app.users.v1", 100)]),
211            &snapshot,
212            &TestPolicy,
213        )
214        .expect_err("conflict");
215
216        assert!(matches!(
217            err,
218            AllocationValidationError::SlotStableKeyConflict { .. }
219        ));
220    }
221
222    #[test]
223    fn rejects_retired_redeclaration() {
224        let mut record = active_record("app.users.v1", 100);
225        record.state = AllocationState::Retired;
226        let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
227
228        let err = check_allocations(&recovered(vec![record]), &snapshot, &TestPolicy)
229            .expect_err("retired");
230
231        assert!(matches!(
232            err,
233            AllocationValidationError::RetiredAllocation { .. }
234        ));
235    }
236
237    #[test]
238    fn policy_rejections_fail_before_validation_succeeds() {
239        let snapshot = DeclarationSnapshot::for_test(vec![declaration("bad.users.v1", 100)]);
240
241        let err = check_allocations(&recovered(Vec::new()), &snapshot, &TestPolicy)
242            .expect_err("policy failure");
243
244        assert_eq!(err, AllocationValidationError::Policy("bad key"));
245    }
246
247    #[test]
248    fn full_slot_domain_validates_stages_and_commits() {
249        let mut store = crate::LedgerCommitStore::default();
250        let recovered = store.recover_or_genesis().unwrap();
251        let snapshot = DeclarationSnapshot::for_test(
252            (0..=crate::MEMORY_MANAGER_MAX_ID)
253                .map(|id| declaration(&format!("app.store{id}.v1"), id))
254                .collect(),
255        );
256        let (staged, _) =
257            crate::ledger::stage_generation(recovered, snapshot, &TestPolicy).unwrap();
258        assert_eq!(staged.records().len(), 255);
259
260        let committed = store.commit(&staged).unwrap();
261        assert_eq!(committed.current_generation(), 1);
262        assert_eq!(store.recover().unwrap(), committed);
263    }
264}