Skip to main content

ic_memory/ledger/
record.rs

1use super::{AllocationRetirementError, LedgerCompatibilityError, LedgerIntegrityError};
2use crate::{
3    declaration::AllocationDeclaration, key::StableKey, schema::SchemaMetadata,
4    slot::AllocationSlotDescriptor,
5};
6use serde::{Deserialize, Serialize};
7
8/// Current allocation ledger schema version.
9pub const CURRENT_LEDGER_SCHEMA_VERSION: u32 = 1;
10
11/// Current protected physical ledger format identifier.
12pub const CURRENT_PHYSICAL_FORMAT_ID: u32 = 1;
13
14///
15/// AllocationLedger
16///
17/// Durable root of allocation history.
18///
19/// Decoded ledgers are input from persistent storage and should be treated as
20/// untrusted until compatibility and integrity validation pass. Public
21/// construction goes through [`AllocationLedger::new`], which validates the
22/// invariant-bearing history before returning a value.
23#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
24pub struct AllocationLedger {
25    /// Ledger schema version.
26    pub(crate) ledger_schema_version: u32,
27    /// Physical encoding format identifier.
28    pub(crate) physical_format_id: u32,
29    /// Current committed generation selected by recovery.
30    pub(crate) current_generation: u64,
31    /// Historical allocation facts.
32    pub(crate) allocation_history: AllocationHistory,
33}
34
35///
36/// AllocationHistory
37///
38/// Durable allocation records and generation history.
39///
40/// This is the durable DTO embedded in an [`AllocationLedger`]. It records
41/// allocation facts and generation diagnostics; callers should prefer ledger
42/// staging/validation methods over mutating histories directly.
43#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
44pub struct AllocationHistory {
45    /// Stable-key allocation records.
46    pub records: Vec<AllocationRecord>,
47    /// Committed generation records.
48    pub generations: Vec<GenerationRecord>,
49}
50
51///
52/// AllocationRecord
53///
54/// Durable ownership record for one stable key.
55///
56/// Records are historical facts, not live handles. Fields are private so stale
57/// or invalid ownership state cannot be assembled through public struct
58/// literals; use accessors for diagnostics and ledger methods for mutation.
59#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
60pub struct AllocationRecord {
61    /// Stable key that owns the slot.
62    pub(crate) stable_key: StableKey,
63    /// Durable allocation slot owned by the key.
64    pub(crate) slot: AllocationSlotDescriptor,
65    /// Current allocation lifecycle state.
66    pub(crate) state: AllocationState,
67    /// First committed generation that recorded this allocation.
68    pub(crate) first_generation: u64,
69    /// Latest committed generation that observed this allocation declaration.
70    pub(crate) last_seen_generation: u64,
71    /// Generation that explicitly retired this allocation.
72    pub(crate) retired_generation: Option<u64>,
73    /// Per-generation schema metadata history.
74    pub(crate) schema_history: Vec<SchemaMetadataRecord>,
75}
76
77///
78/// AllocationRetirement
79///
80/// Explicit request to tombstone one historical allocation identity.
81///
82/// Retirement prevents a stable key from being redeclared. It does not make the
83/// physical slot safe for another active stable key.
84#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
85pub struct AllocationRetirement {
86    /// Stable key being retired.
87    pub stable_key: StableKey,
88    /// Allocation slot historically owned by the stable key.
89    pub slot: AllocationSlotDescriptor,
90}
91
92impl AllocationRetirement {
93    /// Build an explicit retirement request from raw parts.
94    pub fn new(
95        stable_key: impl AsRef<str>,
96        slot: AllocationSlotDescriptor,
97    ) -> Result<Self, AllocationRetirementError> {
98        Ok(Self {
99            stable_key: StableKey::parse(stable_key).map_err(AllocationRetirementError::Key)?,
100            slot,
101        })
102    }
103}
104
105///
106/// AllocationState
107///
108/// Allocation lifecycle state.
109#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
110pub enum AllocationState {
111    /// Slot is reserved for a future allocation identity.
112    Reserved,
113    /// Slot is active and may be opened after validation.
114    Active,
115    /// Slot was explicitly retired and remains tombstoned.
116    Retired,
117}
118
119///
120/// SchemaMetadataRecord
121///
122/// Schema metadata observed in one committed generation.
123///
124/// Schema metadata is diagnostic ledger history. It is validated for bounded
125/// durable encoding, but `ic-memory` does not prove application schema
126/// compatibility or data migration correctness.
127#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
128pub struct SchemaMetadataRecord {
129    /// Generation that declared this schema metadata.
130    pub generation: u64,
131    /// Schema metadata declared by that generation.
132    pub schema: SchemaMetadata,
133}
134
135///
136/// GenerationRecord
137///
138/// Diagnostic metadata for one committed ledger generation.
139#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
140pub struct GenerationRecord {
141    /// Committed generation number.
142    pub generation: u64,
143    /// Parent generation, if recorded.
144    pub parent_generation: Option<u64>,
145    /// Optional binary/runtime fingerprint.
146    pub runtime_fingerprint: Option<String>,
147    /// Number of declarations in the generation.
148    pub declaration_count: u32,
149    /// Optional commit timestamp supplied by the integration layer.
150    pub committed_at: Option<u64>,
151}
152
153///
154/// LedgerCompatibility
155///
156/// Supported logical and physical ledger format versions.
157///
158/// Run this check on recovered ledgers before treating them as authoritative
159/// state. Integrity validation then checks allocation history invariants.
160#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
161pub struct LedgerCompatibility {
162    /// Minimum supported ledger schema version.
163    pub min_ledger_schema_version: u32,
164    /// Maximum supported ledger schema version.
165    pub max_ledger_schema_version: u32,
166    /// Required physical encoding format identifier.
167    pub physical_format_id: u32,
168}
169
170impl LedgerCompatibility {
171    /// Return the compatibility supported by this crate version.
172    #[must_use]
173    pub const fn current() -> Self {
174        Self {
175            min_ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
176            max_ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
177            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
178        }
179    }
180
181    /// Validate a decoded ledger before it is used as authoritative state.
182    pub const fn validate(
183        &self,
184        ledger: &AllocationLedger,
185    ) -> Result<(), LedgerCompatibilityError> {
186        if ledger.ledger_schema_version < self.min_ledger_schema_version {
187            return Err(LedgerCompatibilityError::UnsupportedLedgerSchemaVersion {
188                found: ledger.ledger_schema_version,
189                min_supported: self.min_ledger_schema_version,
190                max_supported: self.max_ledger_schema_version,
191            });
192        }
193        if ledger.ledger_schema_version > self.max_ledger_schema_version {
194            return Err(LedgerCompatibilityError::UnsupportedLedgerSchemaVersion {
195                found: ledger.ledger_schema_version,
196                min_supported: self.min_ledger_schema_version,
197                max_supported: self.max_ledger_schema_version,
198            });
199        }
200        if ledger.physical_format_id != self.physical_format_id {
201            return Err(LedgerCompatibilityError::UnsupportedPhysicalFormat {
202                found: ledger.physical_format_id,
203                supported: self.physical_format_id,
204            });
205        }
206        Ok(())
207    }
208}
209
210impl Default for LedgerCompatibility {
211    fn default() -> Self {
212        Self::current()
213    }
214}
215
216impl AllocationRecord {
217    /// Create a new allocation record from a declaration.
218    #[must_use]
219    pub(crate) fn from_declaration(
220        generation: u64,
221        declaration: AllocationDeclaration,
222        state: AllocationState,
223    ) -> Self {
224        Self {
225            stable_key: declaration.stable_key,
226            slot: declaration.slot,
227            state,
228            first_generation: generation,
229            last_seen_generation: generation,
230            retired_generation: None,
231            schema_history: vec![SchemaMetadataRecord {
232                generation,
233                schema: declaration.schema,
234            }],
235        }
236    }
237
238    /// Create a new reserved allocation record from a declaration.
239    #[must_use]
240    pub(crate) fn reserved(generation: u64, declaration: AllocationDeclaration) -> Self {
241        Self::from_declaration(generation, declaration, AllocationState::Reserved)
242    }
243
244    /// Return the stable key that owns this allocation record.
245    #[must_use]
246    pub const fn stable_key(&self) -> &StableKey {
247        &self.stable_key
248    }
249
250    /// Return the durable allocation slot owned by this record.
251    #[must_use]
252    pub const fn slot(&self) -> &AllocationSlotDescriptor {
253        &self.slot
254    }
255
256    /// Return the current allocation lifecycle state.
257    #[must_use]
258    pub const fn state(&self) -> AllocationState {
259        self.state
260    }
261
262    /// Return the first committed generation that recorded this allocation.
263    #[must_use]
264    pub const fn first_generation(&self) -> u64 {
265        self.first_generation
266    }
267
268    /// Return the latest committed generation that observed this allocation.
269    #[must_use]
270    pub const fn last_seen_generation(&self) -> u64 {
271        self.last_seen_generation
272    }
273
274    /// Return the generation that explicitly retired this allocation, if any.
275    #[must_use]
276    pub const fn retired_generation(&self) -> Option<u64> {
277        self.retired_generation
278    }
279
280    /// Return the per-generation schema metadata history.
281    #[must_use]
282    pub fn schema_history(&self) -> &[SchemaMetadataRecord] {
283        &self.schema_history
284    }
285
286    pub(crate) fn observe_declaration(
287        &mut self,
288        generation: u64,
289        declaration: &AllocationDeclaration,
290    ) {
291        self.last_seen_generation = generation;
292        if self.state == AllocationState::Reserved {
293            self.state = AllocationState::Active;
294        }
295
296        let latest_schema = self.schema_history.last().map(|record| &record.schema);
297        if latest_schema != Some(&declaration.schema) {
298            self.schema_history.push(SchemaMetadataRecord {
299                generation,
300                schema: declaration.schema.clone(),
301            });
302        }
303    }
304
305    pub(crate) fn observe_reservation(
306        &mut self,
307        generation: u64,
308        reservation: &AllocationDeclaration,
309    ) {
310        self.last_seen_generation = generation;
311
312        let latest_schema = self.schema_history.last().map(|record| &record.schema);
313        if latest_schema != Some(&reservation.schema) {
314            self.schema_history.push(SchemaMetadataRecord {
315                generation,
316                schema: reservation.schema.clone(),
317            });
318        }
319    }
320}
321
322impl AllocationLedger {
323    /// Build a ledger DTO and validate structural ledger invariants.
324    ///
325    /// This constructor is intended for recovered durable records and tests. It
326    /// validates committed allocation history, including schema metadata
327    /// records, but it does not open storage or allocate slots.
328    pub fn new(
329        ledger_schema_version: u32,
330        physical_format_id: u32,
331        current_generation: u64,
332        allocation_history: AllocationHistory,
333    ) -> Result<Self, LedgerIntegrityError> {
334        let ledger = Self {
335            ledger_schema_version,
336            physical_format_id,
337            current_generation,
338            allocation_history,
339        };
340        ledger.validate_integrity()?;
341        Ok(ledger)
342    }
343
344    /// Return the ledger schema version.
345    #[must_use]
346    pub const fn ledger_schema_version(&self) -> u32 {
347        self.ledger_schema_version
348    }
349
350    /// Return the protected physical format identifier.
351    #[must_use]
352    pub const fn physical_format_id(&self) -> u32 {
353        self.physical_format_id
354    }
355
356    /// Return the current committed generation selected by recovery.
357    #[must_use]
358    pub const fn current_generation(&self) -> u64 {
359        self.current_generation
360    }
361
362    /// Return the historical allocation facts.
363    #[must_use]
364    pub const fn allocation_history(&self) -> &AllocationHistory {
365        &self.allocation_history
366    }
367}