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                "schema history",
19                self.allocation_history
20                    .records()
21                    .iter()
22                    .map(|r| r.schema_history.len())
23                    .sum(),
24                crate::constants::MAX_LEDGER_GENERATIONS,
25            ),
26        ] {
27            if count > limit {
28                return Err(LedgerIntegrityError::LimitExceeded { resource, limit });
29            }
30        }
31        Ok(())
32    }
33
34    pub(crate) fn validate_staging_bounds(&self) -> Result<(), LedgerIntegrityError> {
35        self.validate_bounds()?;
36        if self.allocation_history.generations().len() >= crate::constants::MAX_LEDGER_GENERATIONS {
37            return Err(LedgerIntegrityError::LimitExceeded {
38                resource: "generation history",
39                limit: crate::constants::MAX_LEDGER_GENERATIONS,
40            });
41        }
42        Ok(())
43    }
44
45    /// Validate structural ledger invariants before recovery or commit.
46    pub fn validate_integrity(&self) -> Result<(), LedgerIntegrityError> {
47        self.validate_bounds()?;
48        let mut stable_keys = BTreeSet::new();
49        // Slot construction and decoding have already excluded the sentinel.
50        let mut slots = [false; crate::constants::MAX_ALLOCATIONS];
51
52        for record in self.allocation_history.records() {
53            if !stable_keys.insert(&record.stable_key) {
54                return Err(LedgerIntegrityError::DuplicateStableKey {
55                    stable_key: record.stable_key.clone(),
56                });
57            }
58            let occupied = &mut slots[usize::from(record.slot.id())];
59            if *occupied {
60                return Err(LedgerIntegrityError::DuplicateSlot {
61                    slot: record.slot.clone(),
62                });
63            }
64            *occupied = true;
65            validate_record_integrity(self.current_generation, record)?;
66        }
67
68        let mut generations = BTreeSet::new();
69        for generation in self.allocation_history.generations() {
70            if !generations.insert(generation.generation) {
71                return Err(LedgerIntegrityError::DuplicateGeneration {
72                    generation: generation.generation,
73                });
74            }
75            if generation.generation > self.current_generation {
76                return Err(LedgerIntegrityError::FutureGeneration {
77                    generation: generation.generation,
78                    current_generation: self.current_generation,
79                });
80            }
81            if generation.parent_generation >= generation.generation {
82                return Err(LedgerIntegrityError::InvalidParentGeneration {
83                    generation: generation.generation,
84                    parent_generation: generation.parent_generation,
85                });
86            }
87        }
88
89        Ok(())
90    }
91
92    /// Validate strict committed-ledger invariants before recovery or commit.
93    ///
94    /// Public durable structs are DTOs: decoded or manually constructed values
95    /// are untrusted until this method succeeds.
96    pub fn validate_committed_integrity(&self) -> Result<(), LedgerIntegrityError> {
97        self.validate_integrity()?;
98
99        if self.current_generation != 0
100            && !self
101                .allocation_history
102                .generations()
103                .iter()
104                .any(|record| record.generation == self.current_generation)
105        {
106            return Err(LedgerIntegrityError::MissingCurrentGenerationRecord {
107                current_generation: self.current_generation,
108            });
109        }
110
111        let mut expected_parent = 0;
112        for generation in self.allocation_history.generations() {
113            if generation.generation != expected_parent + 1 {
114                return Err(LedgerIntegrityError::NonIncreasingGenerationRecords {
115                    generation: generation.generation,
116                });
117            }
118
119            if generation.parent_generation != expected_parent {
120                return Err(LedgerIntegrityError::BrokenGenerationChain {
121                    generation: generation.generation,
122                    expected_parent,
123                    actual_parent: generation.parent_generation,
124                });
125            }
126
127            expected_parent = generation.generation;
128        }
129
130        // The checked chain contains exactly generations 1..=current_generation.
131        // Structural validation bounds every record reference by its first
132        // generation and current_generation, so only genesis exclusion remains.
133        for record in self.allocation_history.records() {
134            if record.first_generation == 0 {
135                return Err(LedgerIntegrityError::UnknownRecordGeneration {
136                    stable_key: record.stable_key.clone(),
137                    generation: 0,
138                });
139            }
140        }
141
142        Ok(())
143    }
144}
145
146fn validate_record_integrity(
147    current_generation: u64,
148    record: &AllocationRecord,
149) -> Result<(), LedgerIntegrityError> {
150    if record.first_generation > record.last_seen_generation {
151        return Err(LedgerIntegrityError::InvalidRecordGenerationOrder {
152            stable_key: record.stable_key.clone(),
153            first_generation: record.first_generation,
154            last_seen_generation: record.last_seen_generation,
155        });
156    }
157    if record.last_seen_generation > current_generation {
158        return Err(LedgerIntegrityError::FutureRecordGeneration {
159            stable_key: record.stable_key.clone(),
160            generation: record.last_seen_generation,
161            current_generation,
162        });
163    }
164
165    match record.state {
166        AllocationState::Retired {
167            generation: retired_generation,
168        } => {
169            if retired_generation < record.first_generation {
170                return Err(LedgerIntegrityError::RetiredBeforeFirstGeneration {
171                    stable_key: record.stable_key.clone(),
172                    first_generation: record.first_generation,
173                    retired_generation,
174                });
175            }
176            if retired_generation > current_generation {
177                return Err(LedgerIntegrityError::FutureRecordGeneration {
178                    stable_key: record.stable_key.clone(),
179                    generation: retired_generation,
180                    current_generation,
181                });
182            }
183            if retired_generation <= record.last_seen_generation {
184                return Err(LedgerIntegrityError::RetirementNotAfterLastSeen {
185                    stable_key: record.stable_key.clone(),
186                    last_seen_generation: record.last_seen_generation,
187                    retired_generation,
188                });
189            }
190        }
191        AllocationState::Reserved | AllocationState::Active => {}
192    }
193
194    validate_schema_history_integrity(current_generation, record)
195}
196
197fn validate_schema_history_integrity(
198    current_generation: u64,
199    record: &AllocationRecord,
200) -> Result<(), LedgerIntegrityError> {
201    if record.schema_history.is_empty() {
202        return Err(LedgerIntegrityError::EmptySchemaHistory {
203            stable_key: record.stable_key.clone(),
204        });
205    }
206
207    let first_schema_generation = record.schema_history[0].generation;
208    if first_schema_generation != record.first_generation {
209        return Err(LedgerIntegrityError::SchemaHistoryStartMismatch {
210            stable_key: record.stable_key.clone(),
211            first_generation: record.first_generation,
212            schema_generation: first_schema_generation,
213        });
214    }
215
216    let mut previous = None;
217    for schema in &record.schema_history {
218        if previous.is_some_and(|generation| schema.generation <= generation) {
219            return Err(LedgerIntegrityError::NonIncreasingSchemaHistory {
220                stable_key: record.stable_key.clone(),
221            });
222        }
223        // The matching first entry and strict ordering establish the lower bound.
224        if schema.generation > current_generation {
225            return Err(LedgerIntegrityError::SchemaHistoryOutOfBounds {
226                stable_key: record.stable_key.clone(),
227                generation: schema.generation,
228            });
229        }
230        if schema.generation > record.last_seen_generation {
231            return Err(LedgerIntegrityError::SchemaHistoryAfterLastSeen {
232                stable_key: record.stable_key.clone(),
233                generation: schema.generation,
234                last_seen_generation: record.last_seen_generation,
235            });
236        }
237        previous = Some(schema.generation);
238    }
239
240    Ok(())
241}