Skip to main content

ic_memory/ledger/
integrity.rs

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