Skip to main content

ic_memory/
validation.rs

1use crate::{
2    capability::ValidatedAllocations,
3    declaration::DeclarationSnapshot,
4    key::StableKey,
5    ledger::{AllocationLedger, ClaimConflict, RecoveredLedger, 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    /// Current declaration attempted to revive a retired allocation.
45    #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
46    RetiredAllocation {
47        /// Retired stable key.
48        stable_key: StableKey,
49        /// Retired allocation slot.
50        slot: MemoryManagerSlot,
51    },
52}
53
54/// Validate a committed ledger and current declarations before opening.
55///
56/// This produces a pre-commit [`ValidatedAllocations`] value: the historical
57/// ledger must pass current-format and committed-integrity checks before current
58/// declarations are checked against framework policy and ledger history. The
59/// result can be staged, but it cannot open storage. Open authority is granted
60/// only by [`crate::CommittedAllocations`] after persistence confirmation.
61pub fn validate_allocations<P: RuntimeBootstrapPolicy>(
62    recovered: &RecoveredLedger,
63    snapshot: DeclarationSnapshot,
64    policy: &P,
65) -> Result<ValidatedAllocations, AllocationValidationError<P::Error>> {
66    check_allocations(recovered, &snapshot, policy)?;
67    let declarations = snapshot.into_declarations();
68
69    Ok(ValidatedAllocations::new(
70        recovered.current_generation(),
71        declarations,
72    ))
73}
74
75// Doctor needs the same checks as bootstrap, but does not consume declarations
76// or mint a capability. Keep check ordering and error ownership in one place.
77pub fn check_allocations<P: RuntimeBootstrapPolicy>(
78    recovered: &RecoveredLedger,
79    snapshot: &DeclarationSnapshot,
80    policy: &P,
81) -> Result<(), AllocationValidationError<P::Error>> {
82    let ledger = recovered.ledger();
83
84    for declaration in snapshot.declarations() {
85        // The internal ledger declaration stays outside application callbacks.
86        // Historical ownership checks still apply to it below.
87        if declaration.stable_key.as_str() != crate::IC_MEMORY_LEDGER_STABLE_KEY {
88            policy
89                .validate_key(&declaration.stable_key)
90                .map_err(AllocationValidationError::Policy)?;
91            policy
92                .validate_slot(&declaration.stable_key, &declaration.slot)
93                .map_err(AllocationValidationError::Policy)?;
94        }
95
96        validate_declaration_history(ledger, declaration)?;
97    }
98
99    Ok(())
100}
101
102fn validate_declaration_history<P>(
103    ledger: &AllocationLedger,
104    declaration: &crate::declaration::AllocationDeclaration,
105) -> Result<(), AllocationValidationError<P>> {
106    validate_declaration_claim(ledger, declaration)
107        .map(|_| ())
108        .map_err(|conflict| map_validation_claim_conflict(declaration, conflict))
109}
110
111fn map_validation_claim_conflict<P>(
112    declaration: &crate::declaration::AllocationDeclaration,
113    conflict: ClaimConflict<'_>,
114) -> AllocationValidationError<P> {
115    match conflict {
116        ClaimConflict::StableKeyMoved { record } => {
117            AllocationValidationError::StableKeySlotConflict {
118                stable_key: declaration.stable_key.clone(),
119                historical_slot: record.slot.clone(),
120                declared_slot: declaration.slot.clone(),
121            }
122        }
123        ClaimConflict::SlotReused { record } => AllocationValidationError::SlotStableKeyConflict {
124            slot: declaration.slot.clone(),
125            historical_key: record.stable_key.clone(),
126            declared_key: declaration.stable_key.clone(),
127        },
128        ClaimConflict::Tombstoned { record } => AllocationValidationError::RetiredAllocation {
129            stable_key: declaration.stable_key.clone(),
130            slot: record.slot.clone(),
131        },
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::{
139        declaration::AllocationDeclaration,
140        ledger::{AllocationRecord, AllocationState},
141        schema::SchemaMetadata,
142        slot::MemoryManagerSlot,
143    };
144
145    #[derive(Debug, Eq, PartialEq)]
146    struct TestPolicy;
147
148    impl RuntimeBootstrapPolicy for TestPolicy {
149        type Error = &'static str;
150
151        fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
152            if key.as_str().starts_with("bad.") {
153                return Err("bad key");
154            }
155            Ok(())
156        }
157
158        fn validate_slot(
159            &self,
160            key: &StableKey,
161            _slot: &MemoryManagerSlot,
162        ) -> Result<(), Self::Error> {
163            assert!(!key.as_str().starts_with("bad."));
164            Ok(())
165        }
166
167        fn runtime_bootstrap_identity(
168            &self,
169        ) -> Result<crate::PolicyIdentity, crate::PolicyIdentityError> {
170            crate::PolicyIdentity::new("validation-test", 1)
171        }
172    }
173
174    fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
175        AllocationLedger {
176            current_generation: 7,
177            records,
178        }
179    }
180
181    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
182        AllocationDeclaration::new(
183            key,
184            MemoryManagerSlot::new(id).expect("usable slot"),
185            SchemaMetadata::default(),
186        )
187        .expect("declaration")
188    }
189
190    fn active_record(key: &str, id: u8) -> AllocationRecord {
191        AllocationRecord::active(&declaration(key, id))
192    }
193
194    fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
195        RecoveredLedger::from_trusted_ledger(ledger(records))
196    }
197
198    #[test]
199    fn accepts_matching_historical_owner() {
200        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 100)])
201            .expect("snapshot");
202
203        let validated = validate_allocations(
204            &recovered(vec![active_record("app.users.v1", 100)]),
205            snapshot,
206            &TestPolicy,
207        )
208        .expect("validated");
209
210        assert_eq!(validated.base_generation(), 7);
211    }
212
213    #[test]
214    fn omitted_historical_records_do_not_fail_validation() {
215        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 100)])
216            .expect("snapshot");
217
218        validate_allocations(
219            &recovered(vec![
220                active_record("app.users.v1", 100),
221                active_record("app.orders.v1", 101),
222            ]),
223            snapshot,
224            &TestPolicy,
225        )
226        .expect("omitted records are preserved, not retired");
227    }
228
229    #[test]
230    fn rejects_same_key_different_slot() {
231        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 101)])
232            .expect("snapshot");
233
234        let err = validate_allocations(
235            &recovered(vec![active_record("app.users.v1", 100)]),
236            snapshot,
237            &TestPolicy,
238        )
239        .expect_err("conflict");
240
241        assert!(matches!(
242            err,
243            AllocationValidationError::StableKeySlotConflict { .. }
244        ));
245    }
246
247    #[test]
248    fn rejects_same_slot_different_key() {
249        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.orders.v1", 100)])
250            .expect("snapshot");
251
252        let err = validate_allocations(
253            &recovered(vec![active_record("app.users.v1", 100)]),
254            snapshot,
255            &TestPolicy,
256        )
257        .expect_err("conflict");
258
259        assert!(matches!(
260            err,
261            AllocationValidationError::SlotStableKeyConflict { .. }
262        ));
263    }
264
265    #[test]
266    fn rejects_retired_redeclaration() {
267        let mut record = active_record("app.users.v1", 100);
268        record.state = AllocationState::Retired;
269        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 100)])
270            .expect("snapshot");
271
272        let err = validate_allocations(&recovered(vec![record]), snapshot, &TestPolicy)
273            .expect_err("retired");
274
275        assert!(matches!(
276            err,
277            AllocationValidationError::RetiredAllocation { .. }
278        ));
279    }
280
281    #[test]
282    fn policy_rejections_fail_before_validation_succeeds() {
283        let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("bad.users.v1", 100)])
284            .expect("snapshot");
285
286        let err = validate_allocations(&recovered(Vec::new()), snapshot, &TestPolicy)
287            .expect_err("policy failure");
288
289        assert_eq!(err, AllocationValidationError::Policy("bad key"));
290    }
291
292    #[test]
293    fn full_slot_domain_validates_stages_and_commits() {
294        let mut store = crate::LedgerCommitStore::default();
295        let recovered = store.recover_or_genesis().unwrap();
296        let snapshot = DeclarationSnapshot::canonicalize(
297            (0..=crate::MEMORY_MANAGER_MAX_ID)
298                .map(|id| declaration(&format!("app.store{id}.v1"), id))
299                .collect(),
300        )
301        .unwrap();
302        let validated = validate_allocations(&recovered, snapshot, &TestPolicy).unwrap();
303        let staged =
304            crate::ledger::stage_validated_generation(recovered.ledger().clone(), &validated)
305                .unwrap();
306        assert_eq!(staged.records().len(), 255);
307
308        let committed = store.commit(&staged).unwrap();
309        assert_eq!(committed.current_generation(), 1);
310        assert_eq!(store.recover().unwrap(), committed);
311    }
312}