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    record
155        .stable_key
156        .validate()
157        .map_err(LedgerIntegrityError::InvalidStableKey)?;
158
159    if record.first_generation > record.last_seen_generation {
160        return Err(LedgerIntegrityError::InvalidRecordGenerationOrder {
161            stable_key: record.stable_key.clone(),
162            first_generation: record.first_generation,
163            last_seen_generation: record.last_seen_generation,
164        });
165    }
166    if record.last_seen_generation > current_generation {
167        return Err(LedgerIntegrityError::FutureRecordGeneration {
168            stable_key: record.stable_key.clone(),
169            generation: record.last_seen_generation,
170            current_generation,
171        });
172    }
173
174    match record.state {
175        AllocationState::Retired {
176            generation: retired_generation,
177        } => {
178            if retired_generation < record.first_generation {
179                return Err(LedgerIntegrityError::RetiredBeforeFirstGeneration {
180                    stable_key: record.stable_key.clone(),
181                    first_generation: record.first_generation,
182                    retired_generation,
183                });
184            }
185            if retired_generation > current_generation {
186                return Err(LedgerIntegrityError::FutureRecordGeneration {
187                    stable_key: record.stable_key.clone(),
188                    generation: retired_generation,
189                    current_generation,
190                });
191            }
192            if retired_generation <= record.last_seen_generation {
193                return Err(LedgerIntegrityError::RetirementNotAfterLastSeen {
194                    stable_key: record.stable_key.clone(),
195                    last_seen_generation: record.last_seen_generation,
196                    retired_generation,
197                });
198            }
199        }
200        AllocationState::Reserved | AllocationState::Active => {}
201    }
202
203    validate_schema_history_integrity(current_generation, record)
204}
205
206fn validate_schema_history_integrity(
207    current_generation: u64,
208    record: &AllocationRecord,
209) -> Result<(), LedgerIntegrityError> {
210    if record.schema_history.is_empty() {
211        return Err(LedgerIntegrityError::EmptySchemaHistory {
212            stable_key: record.stable_key.clone(),
213        });
214    }
215
216    let first_schema_generation = record.schema_history[0].generation;
217    if first_schema_generation != record.first_generation {
218        return Err(LedgerIntegrityError::SchemaHistoryStartMismatch {
219            stable_key: record.stable_key.clone(),
220            first_generation: record.first_generation,
221            schema_generation: first_schema_generation,
222        });
223    }
224
225    let mut previous = None;
226    for schema in &record.schema_history {
227        schema
228            .schema
229            .validate()
230            .map_err(|error| LedgerIntegrityError::InvalidSchemaMetadata {
231                stable_key: record.stable_key.clone(),
232                generation: schema.generation,
233                error,
234            })?;
235        if previous.is_some_and(|generation| schema.generation <= generation) {
236            return Err(LedgerIntegrityError::NonIncreasingSchemaHistory {
237                stable_key: record.stable_key.clone(),
238            });
239        }
240        // The matching first entry and strict ordering establish the lower bound.
241        if schema.generation > current_generation {
242            return Err(LedgerIntegrityError::SchemaHistoryOutOfBounds {
243                stable_key: record.stable_key.clone(),
244                generation: schema.generation,
245            });
246        }
247        if schema.generation > record.last_seen_generation {
248            return Err(LedgerIntegrityError::SchemaHistoryAfterLastSeen {
249                stable_key: record.stable_key.clone(),
250                generation: schema.generation,
251                last_seen_generation: record.last_seen_generation,
252            });
253        }
254        previous = Some(schema.generation);
255    }
256
257    Ok(())
258}