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                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        let mut slots = BTreeSet::new();
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            if !slots.insert(&record.slot) {
59                return Err(LedgerIntegrityError::DuplicateSlot {
60                    slot: record.slot.clone(),
61                });
62            }
63            validate_record_integrity(self.current_generation, record)?;
64        }
65
66        let mut generations = BTreeSet::new();
67        for generation in self.allocation_history.generations() {
68            if !generations.insert(generation.generation) {
69                return Err(LedgerIntegrityError::DuplicateGeneration {
70                    generation: generation.generation,
71                });
72            }
73            if generation.generation > self.current_generation {
74                return Err(LedgerIntegrityError::FutureGeneration {
75                    generation: generation.generation,
76                    current_generation: self.current_generation,
77                });
78            }
79            if generation.parent_generation >= generation.generation {
80                return Err(LedgerIntegrityError::InvalidParentGeneration {
81                    generation: generation.generation,
82                    parent_generation: generation.parent_generation,
83                });
84            }
85        }
86
87        Ok(())
88    }
89
90    /// Validate strict committed-ledger invariants before recovery or commit.
91    ///
92    /// Public durable structs are DTOs: decoded or manually constructed values
93    /// are untrusted until this method succeeds.
94    pub fn validate_committed_integrity(&self) -> Result<(), LedgerIntegrityError> {
95        self.validate_integrity()?;
96
97        if self.current_generation != 0
98            && !self
99                .allocation_history
100                .generations()
101                .iter()
102                .any(|record| record.generation == self.current_generation)
103        {
104            return Err(LedgerIntegrityError::MissingCurrentGenerationRecord {
105                current_generation: self.current_generation,
106            });
107        }
108
109        let mut previous = None;
110        for generation in self.allocation_history.generations() {
111            validate_runtime_fingerprint(generation.runtime_fingerprint.as_deref())
112                .map_err(LedgerIntegrityError::DiagnosticMetadata)?;
113
114            let expected_generation = previous.map_or(1, |previous| previous + 1);
115            if generation.generation != expected_generation {
116                return Err(LedgerIntegrityError::NonIncreasingGenerationRecords {
117                    generation: generation.generation,
118                });
119            }
120
121            let expected_parent = previous.unwrap_or(0);
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            previous = Some(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    record
154        .stable_key
155        .validate()
156        .map_err(LedgerIntegrityError::InvalidStableKey)?;
157    record
158        .slot
159        .validate()
160        .map_err(LedgerIntegrityError::InvalidSlotDescriptor)?;
161
162    if record.first_generation > record.last_seen_generation {
163        return Err(LedgerIntegrityError::InvalidRecordGenerationOrder {
164            stable_key: record.stable_key.clone(),
165            first_generation: record.first_generation,
166            last_seen_generation: record.last_seen_generation,
167        });
168    }
169    if record.last_seen_generation > current_generation {
170        return Err(LedgerIntegrityError::FutureRecordGeneration {
171            stable_key: record.stable_key.clone(),
172            generation: record.last_seen_generation,
173            current_generation,
174        });
175    }
176
177    match record.state {
178        AllocationState::Retired {
179            generation: retired_generation,
180        } => {
181            if retired_generation < record.first_generation {
182                return Err(LedgerIntegrityError::RetiredBeforeFirstGeneration {
183                    stable_key: record.stable_key.clone(),
184                    first_generation: record.first_generation,
185                    retired_generation,
186                });
187            }
188            if retired_generation > current_generation {
189                return Err(LedgerIntegrityError::FutureRecordGeneration {
190                    stable_key: record.stable_key.clone(),
191                    generation: retired_generation,
192                    current_generation,
193                });
194            }
195            if retired_generation <= record.last_seen_generation {
196                return Err(LedgerIntegrityError::RetirementNotAfterLastSeen {
197                    stable_key: record.stable_key.clone(),
198                    last_seen_generation: record.last_seen_generation,
199                    retired_generation,
200                });
201            }
202        }
203        AllocationState::Reserved | AllocationState::Active => {}
204    }
205
206    validate_schema_history_integrity(current_generation, record)
207}
208
209fn validate_schema_history_integrity(
210    current_generation: u64,
211    record: &AllocationRecord,
212) -> Result<(), LedgerIntegrityError> {
213    if record.schema_history.is_empty() {
214        return Err(LedgerIntegrityError::EmptySchemaHistory {
215            stable_key: record.stable_key.clone(),
216        });
217    }
218
219    let first_schema_generation = record.schema_history[0].generation;
220    if first_schema_generation != record.first_generation {
221        return Err(LedgerIntegrityError::SchemaHistoryStartMismatch {
222            stable_key: record.stable_key.clone(),
223            first_generation: record.first_generation,
224            schema_generation: first_schema_generation,
225        });
226    }
227
228    let mut previous = None;
229    for schema in &record.schema_history {
230        schema
231            .schema
232            .validate()
233            .map_err(|error| LedgerIntegrityError::InvalidSchemaMetadata {
234                stable_key: record.stable_key.clone(),
235                generation: schema.generation,
236                error,
237            })?;
238        if previous.is_some_and(|generation| schema.generation <= generation) {
239            return Err(LedgerIntegrityError::NonIncreasingSchemaHistory {
240                stable_key: record.stable_key.clone(),
241            });
242        }
243        if schema.generation < record.first_generation || schema.generation > current_generation {
244            return Err(LedgerIntegrityError::SchemaHistoryOutOfBounds {
245                stable_key: record.stable_key.clone(),
246                generation: schema.generation,
247            });
248        }
249        if schema.generation > record.last_seen_generation {
250            return Err(LedgerIntegrityError::SchemaHistoryAfterLastSeen {
251                stable_key: record.stable_key.clone(),
252                generation: schema.generation,
253                last_seen_generation: record.last_seen_generation,
254            });
255        }
256        previous = Some(schema.generation);
257    }
258
259    Ok(())
260}