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