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