Skip to main content

ic_memory/ledger/
integrity.rs

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