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        let mut generations = BTreeSet::new();
72        for generation in self.allocation_history.generations() {
73            if !generations.insert(generation.generation) {
74                return Err(LedgerIntegrityError::DuplicateGeneration {
75                    generation: generation.generation,
76                });
77            }
78            if generation.generation > self.current_generation {
79                return Err(LedgerIntegrityError::FutureGeneration {
80                    generation: generation.generation,
81                    current_generation: self.current_generation,
82                });
83            }
84            if generation.parent_generation >= generation.generation {
85                return Err(LedgerIntegrityError::InvalidParentGeneration {
86                    generation: generation.generation,
87                    parent_generation: generation.parent_generation,
88                });
89            }
90        }
91
92        Ok(())
93    }
94
95    /// Validate strict committed-ledger invariants before recovery or commit.
96    ///
97    /// Public durable structs are DTOs: decoded or manually constructed values
98    /// are untrusted until this method succeeds.
99    pub fn validate_committed_integrity(&self) -> Result<(), LedgerIntegrityError> {
100        self.validate_integrity()?;
101
102        if self.current_generation != 0
103            && !self
104                .allocation_history
105                .generations()
106                .iter()
107                .any(|record| record.generation == self.current_generation)
108        {
109            return Err(LedgerIntegrityError::MissingCurrentGenerationRecord {
110                current_generation: self.current_generation,
111            });
112        }
113
114        let mut expected_parent = 0;
115        for generation in self.allocation_history.generations() {
116            if generation.generation != expected_parent + 1 {
117                return Err(LedgerIntegrityError::NonIncreasingGenerationRecords {
118                    generation: generation.generation,
119                });
120            }
121
122            if generation.parent_generation != expected_parent {
123                return Err(LedgerIntegrityError::BrokenGenerationChain {
124                    generation: generation.generation,
125                    expected_parent,
126                    actual_parent: generation.parent_generation,
127                });
128            }
129
130            expected_parent = generation.generation;
131        }
132
133        // The checked chain contains exactly generations 1..=current_generation.
134        // Structural validation bounds every record reference by its first
135        // generation and current_generation, so only genesis exclusion remains.
136        for record in self.allocation_history.records() {
137            if record.first_generation == 0 {
138                return Err(LedgerIntegrityError::UnknownRecordGeneration {
139                    stable_key: record.stable_key.clone(),
140                    generation: 0,
141                });
142            }
143        }
144
145        Ok(())
146    }
147}
148
149fn validate_record_integrity(
150    current_generation: u64,
151    record: &AllocationRecord,
152) -> Result<(), LedgerIntegrityError> {
153    if record.first_generation > record.last_seen_generation {
154        return Err(LedgerIntegrityError::InvalidRecordGenerationOrder {
155            stable_key: record.stable_key.clone(),
156            first_generation: record.first_generation,
157            last_seen_generation: record.last_seen_generation,
158        });
159    }
160    if record.last_seen_generation > current_generation {
161        return Err(LedgerIntegrityError::FutureRecordGeneration {
162            stable_key: record.stable_key.clone(),
163            generation: record.last_seen_generation,
164            current_generation,
165        });
166    }
167
168    match record.state {
169        AllocationState::Retired {
170            generation: retired_generation,
171        } => {
172            if retired_generation < record.first_generation {
173                return Err(LedgerIntegrityError::RetiredBeforeFirstGeneration {
174                    stable_key: record.stable_key.clone(),
175                    first_generation: record.first_generation,
176                    retired_generation,
177                });
178            }
179            if retired_generation > current_generation {
180                return Err(LedgerIntegrityError::FutureRecordGeneration {
181                    stable_key: record.stable_key.clone(),
182                    generation: retired_generation,
183                    current_generation,
184                });
185            }
186            if retired_generation <= record.last_seen_generation {
187                return Err(LedgerIntegrityError::RetirementNotAfterLastSeen {
188                    stable_key: record.stable_key.clone(),
189                    last_seen_generation: record.last_seen_generation,
190                    retired_generation,
191                });
192            }
193        }
194        AllocationState::Reserved | AllocationState::Active => {}
195    }
196
197    validate_schema_history_integrity(current_generation, record)
198}
199
200fn validate_schema_history_integrity(
201    current_generation: u64,
202    record: &AllocationRecord,
203) -> Result<(), LedgerIntegrityError> {
204    if record.schema_history.is_empty() {
205        return Err(LedgerIntegrityError::EmptySchemaHistory {
206            stable_key: record.stable_key.clone(),
207        });
208    }
209
210    let first_schema_generation = record.schema_history[0].generation;
211    if first_schema_generation != record.first_generation {
212        return Err(LedgerIntegrityError::SchemaHistoryStartMismatch {
213            stable_key: record.stable_key.clone(),
214            first_generation: record.first_generation,
215            schema_generation: first_schema_generation,
216        });
217    }
218
219    let mut previous = None;
220    for schema in &record.schema_history {
221        if previous.is_some_and(|generation| schema.generation <= generation) {
222            return Err(LedgerIntegrityError::NonIncreasingSchemaHistory {
223                stable_key: record.stable_key.clone(),
224            });
225        }
226        // The matching first entry and strict ordering establish the lower bound.
227        if schema.generation > current_generation {
228            return Err(LedgerIntegrityError::SchemaHistoryOutOfBounds {
229                stable_key: record.stable_key.clone(),
230                generation: schema.generation,
231            });
232        }
233        if schema.generation > record.last_seen_generation {
234            return Err(LedgerIntegrityError::SchemaHistoryAfterLastSeen {
235                stable_key: record.stable_key.clone(),
236                generation: schema.generation,
237                last_seen_generation: record.last_seen_generation,
238            });
239        }
240        previous = Some(schema.generation);
241    }
242
243    Ok(())
244}