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::AllocationSlotDescriptor,
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: AllocationSlotDescriptor,
33 declared_slot: AllocationSlotDescriptor,
35 },
36 #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
38 SlotStableKeyConflict {
39 slot: AllocationSlotDescriptor,
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: AllocationSlotDescriptor,
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::AllocationSlotDescriptor,
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: &AllocationSlotDescriptor,
165 ) -> Result<(), Self::Error> {
166 if slot
167 == &AllocationSlotDescriptor::memory_manager_unchecked(
168 crate::MEMORY_MANAGER_INVALID_ID,
169 )
170 {
171 return Err("bad slot");
172 }
173 Ok(())
174 }
175
176 fn validate_reserved_slot(
177 &self,
178 _key: &StableKey,
179 _slot: &AllocationSlotDescriptor,
180 ) -> Result<(), Self::Error> {
181 Ok(())
182 }
183 }
184
185 fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
186 let generations = (1..=7)
187 .map(|generation| {
188 GenerationRecord::new(
189 generation,
190 if generation == 1 { 0 } else { generation - 1 },
191 None,
192 0,
193 None,
194 )
195 .expect("generation record")
196 })
197 .collect();
198
199 AllocationLedger {
200 current_generation: 7,
201 allocation_history: AllocationHistory::from_parts(records, generations),
202 }
203 }
204
205 fn declaration(key: &str, id: u8) -> AllocationDeclaration {
206 AllocationDeclaration::new(
207 key,
208 AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
209 None,
210 SchemaMetadata::default(),
211 )
212 .expect("declaration")
213 }
214
215 fn active_record(key: &str, id: u8) -> AllocationRecord {
216 AllocationRecord::active(1, &declaration(key, id))
217 }
218
219 fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
220 RecoveredLedger::from_trusted_ledger(ledger(records))
221 }
222
223 #[test]
224 fn accepts_matching_historical_owner() {
225 let snapshot =
226 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
227
228 let validated = validate_allocations(
229 &recovered(vec![active_record("app.users.v1", 100)]),
230 snapshot,
231 &TestPolicy,
232 )
233 .expect("validated");
234
235 assert_eq!(validated.base_generation(), 7);
236 }
237
238 #[test]
239 fn omitted_historical_records_do_not_fail_validation() {
240 let snapshot =
241 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
242
243 validate_allocations(
244 &recovered(vec![
245 active_record("app.users.v1", 100),
246 active_record("app.orders.v1", 101),
247 ]),
248 snapshot,
249 &TestPolicy,
250 )
251 .expect("omitted records are preserved, not retired");
252 }
253
254 #[test]
255 fn rejects_same_key_different_slot() {
256 let snapshot =
257 DeclarationSnapshot::new(vec![declaration("app.users.v1", 101)]).expect("snapshot");
258
259 let err = validate_allocations(
260 &recovered(vec![active_record("app.users.v1", 100)]),
261 snapshot,
262 &TestPolicy,
263 )
264 .expect_err("conflict");
265
266 assert!(matches!(
267 err,
268 AllocationValidationError::StableKeySlotConflict { .. }
269 ));
270 }
271
272 #[test]
273 fn rejects_same_slot_different_key() {
274 let snapshot =
275 DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).expect("snapshot");
276
277 let err = validate_allocations(
278 &recovered(vec![active_record("app.users.v1", 100)]),
279 snapshot,
280 &TestPolicy,
281 )
282 .expect_err("conflict");
283
284 assert!(matches!(
285 err,
286 AllocationValidationError::SlotStableKeyConflict { .. }
287 ));
288 }
289
290 #[test]
291 fn rejects_retired_redeclaration() {
292 let mut record = active_record("app.users.v1", 100);
293 record.state = AllocationState::Retired { generation: 3 };
294 let snapshot =
295 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
296
297 let err = validate_allocations(&recovered(vec![record]), snapshot, &TestPolicy)
298 .expect_err("retired");
299
300 assert!(matches!(
301 err,
302 AllocationValidationError::RetiredAllocation { .. }
303 ));
304 }
305
306 #[test]
307 fn policy_rejections_fail_before_validation_succeeds() {
308 let snapshot =
309 DeclarationSnapshot::new(vec![declaration("bad.users.v1", 100)]).expect("snapshot");
310
311 let err = validate_allocations(&recovered(Vec::new()), snapshot, &TestPolicy)
312 .expect_err("policy failure");
313
314 assert_eq!(err, AllocationValidationError::Policy("bad key"));
315 }
316
317 #[test]
318 fn decoded_snapshot_rejects_invalid_schema_and_count_before_minting_authority() {
319 let recovered = recovered(Vec::new());
320 let source = serde_json::to_value(
321 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).unwrap(),
322 )
323 .unwrap();
324 let mut invalid_schema = source.clone();
325 invalid_schema["declarations"][0]["schema"]["schema_version"] = 0.into();
326 let mut oversized = source;
327 oversized["declarations"] =
328 serde_json::Value::Array(vec![oversized["declarations"][0].clone(); 256]);
329
330 for (value, expected) in [
331 (
332 invalid_schema,
333 DeclarationSnapshotError::SchemaMetadata(
334 crate::SchemaMetadataError::InvalidVersion,
335 ),
336 ),
337 (oversized, DeclarationSnapshotError::TooManyDeclarations),
338 ] {
339 let snapshot: DeclarationSnapshot = serde_json::from_value(value).unwrap();
340 assert_eq!(
341 validate_allocations(&recovered, snapshot, &TestPolicy),
342 Err(AllocationValidationError::Snapshot(expected))
343 );
344 }
345 }
346
347 #[test]
348 fn full_slot_domain_validates_stages_and_commits_through_public_boundaries() {
349 let mut store = crate::LedgerCommitStore::default();
350 let genesis = AllocationLedger::new(0, AllocationHistory::default()).unwrap();
351 let recovered = store.recover_or_initialize(&genesis).unwrap();
352 let snapshot = DeclarationSnapshot::new(
353 (0..=crate::MEMORY_MANAGER_MAX_ID)
354 .map(|id| declaration(&format!("app.store{id}.v1"), id))
355 .collect(),
356 )
357 .unwrap();
358 let validated = validate_allocations(&recovered, snapshot, &TestPolicy).unwrap();
359 let staged = recovered
360 .ledger()
361 .stage_validated_generation(&validated, None)
362 .unwrap();
363 assert_eq!(staged.allocation_history().records().len(), 255);
364 assert_eq!(
365 staged.allocation_history().generations()[0].declaration_count(),
366 255
367 );
368 let committed = store.commit(&staged).unwrap();
369 assert_eq!(committed.current_generation(), 1);
370 assert_eq!(store.recover().unwrap(), committed);
371 }
372}