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#[non_exhaustive]
19#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
20pub enum AllocationValidationError<P> {
21 #[error("allocation policy rejected a declaration")]
23 Policy(P),
24 #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
26 StableKeySlotConflict {
27 stable_key: StableKey,
29 historical_slot: MemoryManagerSlot,
31 declared_slot: MemoryManagerSlot,
33 },
34 #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
36 SlotStableKeyConflict {
37 slot: MemoryManagerSlot,
39 historical_key: StableKey,
41 declared_key: StableKey,
43 },
44 #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
46 RetiredAllocation {
47 stable_key: StableKey,
49 slot: MemoryManagerSlot,
51 },
52}
53
54pub 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
75pub 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 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}