1use crate::{
2 capability::ValidatedAllocations,
3 declaration::{DeclarationSnapshot, DeclarationSnapshotError},
4 key::StableKey,
5 ledger::{AllocationLedger, ClaimConflict, RecoveredLedger, validate_declaration_claim},
6 policy::AllocationPolicy,
7 slot::MemoryManagerSlot,
8};
9
10#[non_exhaustive]
18#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
19pub enum AllocationValidationError<P> {
20 #[error(transparent)]
22 Snapshot(DeclarationSnapshotError),
23 #[error("allocation policy rejected a declaration")]
25 Policy(P),
26 #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
28 StableKeySlotConflict {
29 stable_key: StableKey,
31 historical_slot: MemoryManagerSlot,
33 declared_slot: MemoryManagerSlot,
35 },
36 #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
38 SlotStableKeyConflict {
39 slot: MemoryManagerSlot,
41 historical_key: StableKey,
43 declared_key: StableKey,
45 },
46 #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
48 RetiredAllocation {
49 stable_key: StableKey,
51 slot: MemoryManagerSlot,
53 },
54}
55
56pub fn validate_allocations<P: AllocationPolicy>(
64 recovered: &RecoveredLedger,
65 snapshot: DeclarationSnapshot,
66 policy: &P,
67) -> Result<ValidatedAllocations, AllocationValidationError<P::Error>> {
68 check_allocations(recovered, &snapshot, policy)?;
69 let (declarations, runtime_fingerprint) = snapshot.into_parts();
70
71 Ok(ValidatedAllocations::new(
72 recovered.current_generation(),
73 declarations,
74 runtime_fingerprint,
75 ))
76}
77
78pub fn check_allocations<P: AllocationPolicy>(
81 recovered: &RecoveredLedger,
82 snapshot: &DeclarationSnapshot,
83 policy: &P,
84) -> Result<(), AllocationValidationError<P::Error>> {
85 let ledger = recovered.ledger();
86
87 snapshot
88 .validate()
89 .map_err(AllocationValidationError::Snapshot)?;
90
91 for declaration in snapshot.declarations() {
92 policy
93 .validate_key(&declaration.stable_key)
94 .map_err(AllocationValidationError::Policy)?;
95 policy
96 .validate_slot(&declaration.stable_key, &declaration.slot)
97 .map_err(AllocationValidationError::Policy)?;
98
99 validate_declaration_history(ledger, declaration)?;
100 }
101
102 Ok(())
103}
104
105fn validate_declaration_history<P>(
106 ledger: &AllocationLedger,
107 declaration: &crate::declaration::AllocationDeclaration,
108) -> Result<(), AllocationValidationError<P>> {
109 validate_declaration_claim(ledger, declaration)
110 .map(|_| ())
111 .map_err(|conflict| map_validation_claim_conflict(declaration, conflict))
112}
113
114fn map_validation_claim_conflict<P>(
115 declaration: &crate::declaration::AllocationDeclaration,
116 conflict: ClaimConflict<'_>,
117) -> AllocationValidationError<P> {
118 match conflict {
119 ClaimConflict::StableKeyMoved { record } => {
120 AllocationValidationError::StableKeySlotConflict {
121 stable_key: declaration.stable_key.clone(),
122 historical_slot: record.slot.clone(),
123 declared_slot: declaration.slot.clone(),
124 }
125 }
126 ClaimConflict::SlotReused { record } => AllocationValidationError::SlotStableKeyConflict {
127 slot: declaration.slot.clone(),
128 historical_key: record.stable_key.clone(),
129 declared_key: declaration.stable_key.clone(),
130 },
131 ClaimConflict::Tombstoned { record } => AllocationValidationError::RetiredAllocation {
132 stable_key: declaration.stable_key.clone(),
133 slot: record.slot.clone(),
134 },
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::{
142 declaration::AllocationDeclaration,
143 ledger::{AllocationHistory, AllocationRecord, AllocationState, GenerationRecord},
144 schema::SchemaMetadata,
145 slot::MemoryManagerSlot,
146 };
147
148 #[derive(Debug, Eq, PartialEq)]
149 struct TestPolicy;
150
151 impl AllocationPolicy for TestPolicy {
152 type Error = &'static str;
153
154 fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
155 if key.as_str().starts_with("bad.") {
156 return Err("bad key");
157 }
158 Ok(())
159 }
160
161 fn validate_slot(
162 &self,
163 _key: &StableKey,
164 _slot: &MemoryManagerSlot,
165 ) -> Result<(), Self::Error> {
166 Ok(())
167 }
168
169 fn validate_reserved_slot(
170 &self,
171 _key: &StableKey,
172 _slot: &MemoryManagerSlot,
173 ) -> Result<(), Self::Error> {
174 Ok(())
175 }
176 }
177
178 fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
179 let generations = (1..=7)
180 .map(|generation| {
181 GenerationRecord::new(
182 generation,
183 if generation == 1 { 0 } else { generation - 1 },
184 None,
185 0,
186 None,
187 )
188 .expect("generation record")
189 })
190 .collect();
191
192 AllocationLedger {
193 current_generation: 7,
194 allocation_history: AllocationHistory::from_parts(records, generations),
195 }
196 }
197
198 fn declaration(key: &str, id: u8) -> AllocationDeclaration {
199 AllocationDeclaration::new(
200 key,
201 MemoryManagerSlot::new(id).expect("usable slot"),
202 None,
203 SchemaMetadata::default(),
204 )
205 .expect("declaration")
206 }
207
208 fn active_record(key: &str, id: u8) -> AllocationRecord {
209 AllocationRecord::active(1, &declaration(key, id))
210 }
211
212 fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
213 RecoveredLedger::from_trusted_ledger(ledger(records))
214 }
215
216 #[test]
217 fn accepts_matching_historical_owner() {
218 let snapshot =
219 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
220
221 let validated = validate_allocations(
222 &recovered(vec![active_record("app.users.v1", 100)]),
223 snapshot,
224 &TestPolicy,
225 )
226 .expect("validated");
227
228 assert_eq!(validated.base_generation(), 7);
229 }
230
231 #[test]
232 fn omitted_historical_records_do_not_fail_validation() {
233 let snapshot =
234 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
235
236 validate_allocations(
237 &recovered(vec![
238 active_record("app.users.v1", 100),
239 active_record("app.orders.v1", 101),
240 ]),
241 snapshot,
242 &TestPolicy,
243 )
244 .expect("omitted records are preserved, not retired");
245 }
246
247 #[test]
248 fn rejects_same_key_different_slot() {
249 let snapshot =
250 DeclarationSnapshot::new(vec![declaration("app.users.v1", 101)]).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::StableKeySlotConflict { .. }
262 ));
263 }
264
265 #[test]
266 fn rejects_same_slot_different_key() {
267 let snapshot =
268 DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).expect("snapshot");
269
270 let err = validate_allocations(
271 &recovered(vec![active_record("app.users.v1", 100)]),
272 snapshot,
273 &TestPolicy,
274 )
275 .expect_err("conflict");
276
277 assert!(matches!(
278 err,
279 AllocationValidationError::SlotStableKeyConflict { .. }
280 ));
281 }
282
283 #[test]
284 fn rejects_retired_redeclaration() {
285 let mut record = active_record("app.users.v1", 100);
286 record.state = AllocationState::Retired { generation: 3 };
287 let snapshot =
288 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
289
290 let err = validate_allocations(&recovered(vec![record]), snapshot, &TestPolicy)
291 .expect_err("retired");
292
293 assert!(matches!(
294 err,
295 AllocationValidationError::RetiredAllocation { .. }
296 ));
297 }
298
299 #[test]
300 fn policy_rejections_fail_before_validation_succeeds() {
301 let snapshot =
302 DeclarationSnapshot::new(vec![declaration("bad.users.v1", 100)]).expect("snapshot");
303
304 let err = validate_allocations(&recovered(Vec::new()), snapshot, &TestPolicy)
305 .expect_err("policy failure");
306
307 assert_eq!(err, AllocationValidationError::Policy("bad key"));
308 }
309
310 #[test]
311 fn decoded_snapshot_rejects_invalid_label_and_count_before_minting_authority() {
312 let recovered = recovered(Vec::new());
313 let source = serde_json::to_value(
314 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).unwrap(),
315 )
316 .unwrap();
317 let mut invalid_label = source.clone();
318 invalid_label["declarations"][0]["label"] = "".into();
319 let mut oversized = source;
320 oversized["declarations"] =
321 serde_json::Value::Array(vec![oversized["declarations"][0].clone(); 256]);
322
323 for (value, expected) in [
324 (invalid_label, DeclarationSnapshotError::EmptyLabel),
325 (oversized, DeclarationSnapshotError::TooManyDeclarations),
326 ] {
327 let snapshot: DeclarationSnapshot = serde_json::from_value(value).unwrap();
328 assert_eq!(
329 validate_allocations(&recovered, snapshot, &TestPolicy),
330 Err(AllocationValidationError::Snapshot(expected))
331 );
332 }
333 }
334
335 #[test]
336 fn full_slot_domain_validates_stages_and_commits_through_public_boundaries() {
337 let mut store = crate::LedgerCommitStore::default();
338 let genesis = AllocationLedger::new(0, AllocationHistory::default()).unwrap();
339 let recovered = store.recover_or_initialize(&genesis).unwrap();
340 let snapshot = DeclarationSnapshot::new(
341 (0..=crate::MEMORY_MANAGER_MAX_ID)
342 .map(|id| declaration(&format!("app.store{id}.v1"), id))
343 .collect(),
344 )
345 .unwrap();
346 let validated = validate_allocations(&recovered, snapshot, &TestPolicy).unwrap();
347 let staged = recovered
348 .ledger()
349 .stage_validated_generation(&validated, None)
350 .unwrap();
351 assert_eq!(staged.allocation_history().records().len(), 255);
352 assert_eq!(
353 staged.allocation_history().generations()[0].declaration_count(),
354 255
355 );
356 let committed = store.commit(&staged).unwrap();
357 assert_eq!(committed.current_generation(), 1);
358 assert_eq!(store.recover().unwrap(), committed);
359 }
360}