Skip to main content

ic_memory/ledger/
integrity.rs

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        // Refuse oversized outer collections before walking their histories.
23        // Subtract each history from remaining capacity and stop at the first
24        // excess, without constructing an unbounded aggregate count.
25        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    /// Validate structural ledger invariants before recovery or commit.
49    pub fn validate_integrity(&self) -> Result<(), LedgerIntegrityError> {
50        self.validate_bounds()?;
51        let mut stable_keys = BTreeSet::new();
52        // Slot construction and decoding have already excluded the sentinel.
53        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        // Increasing generation numbers are already unique. Retain the general
72        // set path for unordered DTOs, including its existing refusal order.
73        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    /// Validate strict committed-ledger invariants before recovery or commit.
103    ///
104    /// Public durable structs are DTOs: decoded or manually constructed values
105    /// are untrusted until this method succeeds.
106    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        // The checked chain contains exactly generations 1..=current_generation.
141        // Structural validation bounds every record reference by its first
142        // generation and current_generation, so only genesis exclusion remains.
143        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        // The matching first entry and strict ordering establish the lower bound.
234        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}