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                255,
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 previous = None;
114        for generation in self.allocation_history.generations() {
115            validate_runtime_fingerprint(generation.runtime_fingerprint.as_deref())
116                .map_err(LedgerIntegrityError::DiagnosticMetadata)?;
117
118            let expected_generation = previous.map_or(1, |previous| previous + 1);
119            if generation.generation != expected_generation {
120                return Err(LedgerIntegrityError::NonIncreasingGenerationRecords {
121                    generation: generation.generation,
122                });
123            }
124
125            let expected_parent = previous.unwrap_or(0);
126            if generation.parent_generation != expected_parent {
127                return Err(LedgerIntegrityError::BrokenGenerationChain {
128                    generation: generation.generation,
129                    expected_parent,
130                    actual_parent: generation.parent_generation,
131                });
132            }
133
134            previous = Some(generation.generation);
135        }
136
137        // The checked chain contains exactly generations 1..=current_generation.
138        // Structural validation bounds every record reference by its first
139        // generation and current_generation, so only genesis exclusion remains.
140        for record in self.allocation_history.records() {
141            if record.first_generation == 0 {
142                return Err(LedgerIntegrityError::UnknownRecordGeneration {
143                    stable_key: record.stable_key.clone(),
144                    generation: 0,
145                });
146            }
147        }
148
149        Ok(())
150    }
151}
152
153fn validate_record_integrity(
154    current_generation: u64,
155    record: &AllocationRecord,
156) -> Result<(), LedgerIntegrityError> {
157    record
158        .stable_key
159        .validate()
160        .map_err(LedgerIntegrityError::InvalidStableKey)?;
161    record
162        .slot
163        .validate()
164        .map_err(LedgerIntegrityError::InvalidSlotDescriptor)?;
165
166    if record.first_generation > record.last_seen_generation {
167        return Err(LedgerIntegrityError::InvalidRecordGenerationOrder {
168            stable_key: record.stable_key.clone(),
169            first_generation: record.first_generation,
170            last_seen_generation: record.last_seen_generation,
171        });
172    }
173    if record.last_seen_generation > current_generation {
174        return Err(LedgerIntegrityError::FutureRecordGeneration {
175            stable_key: record.stable_key.clone(),
176            generation: record.last_seen_generation,
177            current_generation,
178        });
179    }
180
181    match record.state {
182        AllocationState::Retired {
183            generation: retired_generation,
184        } => {
185            if retired_generation < record.first_generation {
186                return Err(LedgerIntegrityError::RetiredBeforeFirstGeneration {
187                    stable_key: record.stable_key.clone(),
188                    first_generation: record.first_generation,
189                    retired_generation,
190                });
191            }
192            if retired_generation > current_generation {
193                return Err(LedgerIntegrityError::FutureRecordGeneration {
194                    stable_key: record.stable_key.clone(),
195                    generation: retired_generation,
196                    current_generation,
197                });
198            }
199            if retired_generation <= record.last_seen_generation {
200                return Err(LedgerIntegrityError::RetirementNotAfterLastSeen {
201                    stable_key: record.stable_key.clone(),
202                    last_seen_generation: record.last_seen_generation,
203                    retired_generation,
204                });
205            }
206        }
207        AllocationState::Reserved | AllocationState::Active => {}
208    }
209
210    validate_schema_history_integrity(current_generation, record)
211}
212
213fn validate_schema_history_integrity(
214    current_generation: u64,
215    record: &AllocationRecord,
216) -> Result<(), LedgerIntegrityError> {
217    if record.schema_history.is_empty() {
218        return Err(LedgerIntegrityError::EmptySchemaHistory {
219            stable_key: record.stable_key.clone(),
220        });
221    }
222
223    let first_schema_generation = record.schema_history[0].generation;
224    if first_schema_generation != record.first_generation {
225        return Err(LedgerIntegrityError::SchemaHistoryStartMismatch {
226            stable_key: record.stable_key.clone(),
227            first_generation: record.first_generation,
228            schema_generation: first_schema_generation,
229        });
230    }
231
232    let mut previous = None;
233    for schema in &record.schema_history {
234        schema
235            .schema
236            .validate()
237            .map_err(|error| LedgerIntegrityError::InvalidSchemaMetadata {
238                stable_key: record.stable_key.clone(),
239                generation: schema.generation,
240                error,
241            })?;
242        if previous.is_some_and(|generation| schema.generation <= generation) {
243            return Err(LedgerIntegrityError::NonIncreasingSchemaHistory {
244                stable_key: record.stable_key.clone(),
245            });
246        }
247        if schema.generation < record.first_generation || schema.generation > current_generation {
248            return Err(LedgerIntegrityError::SchemaHistoryOutOfBounds {
249                stable_key: record.stable_key.clone(),
250                generation: schema.generation,
251            });
252        }
253        if schema.generation > record.last_seen_generation {
254            return Err(LedgerIntegrityError::SchemaHistoryAfterLastSeen {
255                stable_key: record.stable_key.clone(),
256                generation: schema.generation,
257                last_seen_generation: record.last_seen_generation,
258            });
259        }
260        previous = Some(schema.generation);
261    }
262
263    Ok(())
264}