Skip to main content

ic_memory/
validation.rs

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