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