1use super::{AllocationLedger, AllocationRecord, AllocationState, LedgerIntegrityError};
2use std::collections::BTreeSet;
3
4impl AllocationLedger {
5 pub(crate) fn validate_bounds(&self) -> Result<(), LedgerIntegrityError> {
6 for (resource, count, limit) in [
7 (
8 "allocation records",
9 self.allocation_history.records().len(),
10 crate::constants::MAX_ALLOCATIONS,
11 ),
12 (
13 "generation history",
14 self.allocation_history.generations().len(),
15 crate::constants::MAX_LEDGER_GENERATIONS,
16 ),
17 ] {
18 if count > limit {
19 return Err(LedgerIntegrityError::LimitExceeded { resource, limit });
20 }
21 }
22 let mut schema_headroom = crate::constants::MAX_LEDGER_GENERATIONS;
26 for record in self.allocation_history.records() {
27 schema_headroom = schema_headroom
28 .checked_sub(record.schema_history.len())
29 .ok_or(LedgerIntegrityError::LimitExceeded {
30 resource: "schema history",
31 limit: crate::constants::MAX_LEDGER_GENERATIONS,
32 })?;
33 }
34 Ok(())
35 }
36
37 pub(crate) fn validate_staging_bounds(&self) -> Result<(), LedgerIntegrityError> {
38 self.validate_bounds()?;
39 if self.allocation_history.generations().len() >= crate::constants::MAX_LEDGER_GENERATIONS {
40 return Err(LedgerIntegrityError::LimitExceeded {
41 resource: "generation history",
42 limit: crate::constants::MAX_LEDGER_GENERATIONS,
43 });
44 }
45 Ok(())
46 }
47
48 pub fn validate_integrity(&self) -> Result<(), LedgerIntegrityError> {
50 self.validate_bounds()?;
51 let mut stable_keys = BTreeSet::new();
52 let mut slots = [false; crate::constants::MAX_ALLOCATIONS];
54
55 for record in self.allocation_history.records() {
56 if !stable_keys.insert(&record.stable_key) {
57 return Err(LedgerIntegrityError::DuplicateStableKey {
58 stable_key: record.stable_key.clone(),
59 });
60 }
61 let occupied = &mut slots[usize::from(record.slot.id())];
62 if *occupied {
63 return Err(LedgerIntegrityError::DuplicateSlot {
64 slot: record.slot.clone(),
65 });
66 }
67 *occupied = true;
68 validate_record_integrity(self.current_generation, record)?;
69 }
70
71 let ordered = self
74 .allocation_history
75 .generations()
76 .windows(2)
77 .all(|pair| pair[0].generation < pair[1].generation);
78 let mut generations = BTreeSet::new();
79 for generation in self.allocation_history.generations() {
80 if !ordered && !generations.insert(generation.generation) {
81 return Err(LedgerIntegrityError::DuplicateGeneration {
82 generation: generation.generation,
83 });
84 }
85 if generation.generation > self.current_generation {
86 return Err(LedgerIntegrityError::FutureGeneration {
87 generation: generation.generation,
88 current_generation: self.current_generation,
89 });
90 }
91 if generation.parent_generation >= generation.generation {
92 return Err(LedgerIntegrityError::InvalidParentGeneration {
93 generation: generation.generation,
94 parent_generation: generation.parent_generation,
95 });
96 }
97 }
98
99 Ok(())
100 }
101
102 pub fn validate_committed_integrity(&self) -> Result<(), LedgerIntegrityError> {
107 self.validate_integrity()?;
108
109 if self.current_generation != 0
110 && !self
111 .allocation_history
112 .generations()
113 .iter()
114 .any(|record| record.generation == self.current_generation)
115 {
116 return Err(LedgerIntegrityError::MissingCurrentGenerationRecord {
117 current_generation: self.current_generation,
118 });
119 }
120
121 let mut expected_parent = 0;
122 for generation in self.allocation_history.generations() {
123 if generation.generation != expected_parent + 1 {
124 return Err(LedgerIntegrityError::NonIncreasingGenerationRecords {
125 generation: generation.generation,
126 });
127 }
128
129 if generation.parent_generation != expected_parent {
130 return Err(LedgerIntegrityError::BrokenGenerationChain {
131 generation: generation.generation,
132 expected_parent,
133 actual_parent: generation.parent_generation,
134 });
135 }
136
137 expected_parent = generation.generation;
138 }
139
140 for record in self.allocation_history.records() {
144 if record.first_generation == 0 {
145 return Err(LedgerIntegrityError::UnknownRecordGeneration {
146 stable_key: record.stable_key.clone(),
147 generation: 0,
148 });
149 }
150 }
151
152 Ok(())
153 }
154}
155
156fn validate_record_integrity(
157 current_generation: u64,
158 record: &AllocationRecord,
159) -> Result<(), LedgerIntegrityError> {
160 if record.first_generation > record.last_seen_generation {
161 return Err(LedgerIntegrityError::InvalidRecordGenerationOrder {
162 stable_key: record.stable_key.clone(),
163 first_generation: record.first_generation,
164 last_seen_generation: record.last_seen_generation,
165 });
166 }
167 if record.last_seen_generation > current_generation {
168 return Err(LedgerIntegrityError::FutureRecordGeneration {
169 stable_key: record.stable_key.clone(),
170 generation: record.last_seen_generation,
171 current_generation,
172 });
173 }
174
175 match record.state {
176 AllocationState::Retired {
177 generation: retired_generation,
178 } => {
179 if retired_generation < record.first_generation {
180 return Err(LedgerIntegrityError::RetiredBeforeFirstGeneration {
181 stable_key: record.stable_key.clone(),
182 first_generation: record.first_generation,
183 retired_generation,
184 });
185 }
186 if retired_generation > current_generation {
187 return Err(LedgerIntegrityError::FutureRecordGeneration {
188 stable_key: record.stable_key.clone(),
189 generation: retired_generation,
190 current_generation,
191 });
192 }
193 if retired_generation <= record.last_seen_generation {
194 return Err(LedgerIntegrityError::RetirementNotAfterLastSeen {
195 stable_key: record.stable_key.clone(),
196 last_seen_generation: record.last_seen_generation,
197 retired_generation,
198 });
199 }
200 }
201 AllocationState::Reserved | AllocationState::Active => {}
202 }
203
204 validate_schema_history_integrity(current_generation, record)
205}
206
207fn validate_schema_history_integrity(
208 current_generation: u64,
209 record: &AllocationRecord,
210) -> Result<(), LedgerIntegrityError> {
211 if record.schema_history.is_empty() {
212 return Err(LedgerIntegrityError::EmptySchemaHistory {
213 stable_key: record.stable_key.clone(),
214 });
215 }
216
217 let first_schema_generation = record.schema_history[0].generation;
218 if first_schema_generation != record.first_generation {
219 return Err(LedgerIntegrityError::SchemaHistoryStartMismatch {
220 stable_key: record.stable_key.clone(),
221 first_generation: record.first_generation,
222 schema_generation: first_schema_generation,
223 });
224 }
225
226 let mut previous = None;
227 for schema in &record.schema_history {
228 if previous.is_some_and(|generation| schema.generation <= generation) {
229 return Err(LedgerIntegrityError::NonIncreasingSchemaHistory {
230 stable_key: record.stable_key.clone(),
231 });
232 }
233 if schema.generation > current_generation {
235 return Err(LedgerIntegrityError::SchemaHistoryOutOfBounds {
236 stable_key: record.stable_key.clone(),
237 generation: schema.generation,
238 });
239 }
240 if schema.generation > record.last_seen_generation {
241 return Err(LedgerIntegrityError::SchemaHistoryAfterLastSeen {
242 stable_key: record.stable_key.clone(),
243 generation: schema.generation,
244 last_seen_generation: record.last_seen_generation,
245 });
246 }
247 previous = Some(schema.generation);
248 }
249
250 Ok(())
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use crate::{AllocationHistory, GenerationRecord};
257
258 fn reference(records: &[GenerationRecord], current: u64) -> Result<(), LedgerIntegrityError> {
259 let mut seen = Vec::new();
260 for record in records {
261 let generation = record.generation();
262 if seen.contains(&generation) {
263 return Err(LedgerIntegrityError::DuplicateGeneration { generation });
264 }
265 seen.push(generation);
266 if generation > current {
267 return Err(LedgerIntegrityError::FutureGeneration {
268 generation,
269 current_generation: current,
270 });
271 }
272 if record.parent_generation() >= generation {
273 return Err(LedgerIntegrityError::InvalidParentGeneration {
274 generation,
275 parent_generation: record.parent_generation(),
276 });
277 }
278 }
279 Ok(())
280 }
281
282 #[test]
283 fn generation_validation_matches_reference_for_orderings_and_overlapping_failures() {
284 for len in 0..=4 {
285 for mut code in 0..12_usize.pow(len) {
286 let mut records = Vec::new();
287 for _ in 0..len {
288 let choice = u8::try_from(code % 12).unwrap();
289 code /= 12;
290 let generation = u64::from(choice / 2);
291 let parent = if choice % 2 == 0 {
292 generation.saturating_sub(1)
293 } else {
294 generation
295 };
296 records.push(GenerationRecord::new(generation, parent, None, 0, None).unwrap());
297 }
298 let expected = reference(&records, 4);
299 let ledger = AllocationLedger {
300 current_generation: 4,
301 allocation_history: AllocationHistory::from_parts(Vec::new(), records),
302 };
303 assert_eq!(ledger.validate_integrity(), expected);
304 }
305 }
306 }
307}