Skip to main content

ic_memory/ledger/
record.rs

1use super::{AllocationRetirementError, LedgerCompatibilityError, LedgerIntegrityError};
2use crate::{
3    declaration::{AllocationDeclaration, DeclarationSnapshotError, validate_runtime_fingerprint},
4    key::StableKey,
5    schema::{SchemaMetadata, SchemaMetadataError},
6    slot::AllocationSlotDescriptor,
7};
8use serde::{Deserialize, Serialize};
9
10/// Current allocation ledger schema version.
11pub const CURRENT_LEDGER_SCHEMA_VERSION: u32 = 1;
12
13/// Current protected physical ledger format identifier.
14pub const CURRENT_PHYSICAL_FORMAT_ID: u32 = 1;
15
16///
17/// AllocationLedger
18///
19/// Durable root of allocation history.
20///
21/// Decoded ledgers are input from persistent storage and should be treated as
22/// untrusted until compatibility and integrity validation pass. Public
23/// construction goes through [`AllocationLedger::new`], which validates
24/// structural history invariants before returning a value. Use
25/// [`AllocationLedger::new_committed`] when the value should also satisfy the
26/// strict committed-generation chain required by recovery and commit.
27///
28/// Staging APIs clone this DTO before applying a logical generation. The ledger
29/// is expected to contain allocation metadata only, bounded by the number of
30/// stable allocation identities and committed bootstrap generations, not user
31/// collection contents.
32#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
33pub struct AllocationLedger {
34    /// Ledger schema version.
35    pub(crate) ledger_schema_version: u32,
36    /// Physical encoding format identifier.
37    pub(crate) physical_format_id: u32,
38    /// Current committed generation selected by recovery.
39    pub(crate) current_generation: u64,
40    /// Historical allocation facts.
41    pub(crate) allocation_history: AllocationHistory,
42}
43
44///
45/// AllocationHistory
46///
47/// Durable allocation records and generation history.
48///
49/// This is the durable DTO embedded in an [`AllocationLedger`]. It records
50/// allocation facts and generation diagnostics; callers should prefer ledger
51/// staging/validation methods over mutating histories directly.
52#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
53pub struct AllocationHistory {
54    /// Stable-key allocation records.
55    pub(crate) records: Vec<AllocationRecord>,
56    /// Committed generation records.
57    pub(crate) generations: Vec<GenerationRecord>,
58}
59
60///
61/// AllocationRecord
62///
63/// Durable ownership record for one stable key.
64///
65/// Records are historical facts, not live handles. Fields are private so stale
66/// or invalid ownership state cannot be assembled through public struct
67/// literals; use accessors for diagnostics and ledger methods for mutation.
68#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
69pub struct AllocationRecord {
70    /// Stable key that owns the slot.
71    pub(crate) stable_key: StableKey,
72    /// Durable allocation slot owned by the key.
73    pub(crate) slot: AllocationSlotDescriptor,
74    /// Current allocation lifecycle state.
75    pub(crate) state: AllocationState,
76    /// First committed generation that recorded this allocation.
77    pub(crate) first_generation: u64,
78    /// Latest committed generation that observed this allocation declaration.
79    pub(crate) last_seen_generation: u64,
80    /// Generation that explicitly retired this allocation.
81    pub(crate) retired_generation: Option<u64>,
82    /// Per-generation schema metadata history.
83    pub(crate) schema_history: Vec<SchemaMetadataRecord>,
84}
85
86///
87/// AllocationRetirement
88///
89/// Explicit request to tombstone one historical allocation identity.
90///
91/// Retirement prevents a stable key from being redeclared. It does not make the
92/// physical slot safe for another active stable key.
93#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
94pub struct AllocationRetirement {
95    /// Stable key being retired.
96    pub stable_key: StableKey,
97    /// Allocation slot historically owned by the stable key.
98    pub slot: AllocationSlotDescriptor,
99}
100
101impl AllocationRetirement {
102    /// Build an explicit retirement request from raw parts.
103    pub fn new(
104        stable_key: impl AsRef<str>,
105        slot: AllocationSlotDescriptor,
106    ) -> Result<Self, AllocationRetirementError> {
107        Ok(Self {
108            stable_key: StableKey::parse(stable_key).map_err(AllocationRetirementError::Key)?,
109            slot,
110        })
111    }
112}
113
114///
115/// AllocationState
116///
117/// Allocation lifecycle state.
118#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
119pub enum AllocationState {
120    /// Slot is reserved for a future allocation identity.
121    Reserved,
122    /// Slot is active and may be opened after validation.
123    Active,
124    /// Slot was explicitly retired and remains tombstoned.
125    Retired,
126}
127
128///
129/// SchemaMetadataRecord
130///
131/// Schema metadata observed in one committed generation.
132///
133/// Schema metadata is diagnostic ledger history. It is validated for bounded
134/// durable encoding, but `ic-memory` does not prove application schema
135/// compatibility or data migration correctness.
136#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
137pub struct SchemaMetadataRecord {
138    /// Generation that declared this schema metadata.
139    pub(crate) generation: u64,
140    /// Schema metadata declared by that generation.
141    pub(crate) schema: SchemaMetadata,
142}
143
144///
145/// GenerationRecord
146///
147/// Diagnostic metadata for one committed ledger generation.
148#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
149pub struct GenerationRecord {
150    /// Committed generation number.
151    pub(crate) generation: u64,
152    /// Parent generation, if recorded.
153    pub(crate) parent_generation: Option<u64>,
154    /// Optional binary/runtime fingerprint.
155    pub(crate) runtime_fingerprint: Option<String>,
156    /// Number of declarations in the generation.
157    pub(crate) declaration_count: u32,
158    /// Optional commit timestamp supplied by the integration layer.
159    pub(crate) committed_at: Option<u64>,
160}
161
162///
163/// LedgerCompatibility
164///
165/// Supported logical and physical ledger format versions.
166///
167/// Run this check on recovered ledgers before treating them as authoritative
168/// state. Integrity validation then checks allocation history invariants.
169#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
170pub struct LedgerCompatibility {
171    /// Minimum supported ledger schema version.
172    pub min_ledger_schema_version: u32,
173    /// Maximum supported ledger schema version.
174    pub max_ledger_schema_version: u32,
175    /// Required physical encoding format identifier.
176    pub physical_format_id: u32,
177}
178
179impl LedgerCompatibility {
180    /// Return the compatibility supported by this crate version.
181    #[must_use]
182    pub const fn current() -> Self {
183        Self {
184            min_ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
185            max_ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
186            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
187        }
188    }
189
190    /// Validate a decoded ledger before it is used as authoritative state.
191    pub const fn validate(
192        &self,
193        ledger: &AllocationLedger,
194    ) -> Result<(), LedgerCompatibilityError> {
195        if ledger.ledger_schema_version < self.min_ledger_schema_version {
196            return Err(LedgerCompatibilityError::UnsupportedLedgerSchemaVersion {
197                found: ledger.ledger_schema_version,
198                min_supported: self.min_ledger_schema_version,
199                max_supported: self.max_ledger_schema_version,
200            });
201        }
202        if ledger.ledger_schema_version > self.max_ledger_schema_version {
203            return Err(LedgerCompatibilityError::UnsupportedLedgerSchemaVersion {
204                found: ledger.ledger_schema_version,
205                min_supported: self.min_ledger_schema_version,
206                max_supported: self.max_ledger_schema_version,
207            });
208        }
209        if ledger.physical_format_id != self.physical_format_id {
210            return Err(LedgerCompatibilityError::UnsupportedPhysicalFormat {
211                found: ledger.physical_format_id,
212                supported: self.physical_format_id,
213            });
214        }
215        Ok(())
216    }
217}
218
219impl Default for LedgerCompatibility {
220    fn default() -> Self {
221        Self::current()
222    }
223}
224
225impl AllocationHistory {
226    #[cfg(test)]
227    pub(crate) const fn from_parts(
228        records: Vec<AllocationRecord>,
229        generations: Vec<GenerationRecord>,
230    ) -> Self {
231        Self {
232            records,
233            generations,
234        }
235    }
236
237    /// Borrow stable-key allocation records in durable order.
238    #[must_use]
239    pub fn records(&self) -> &[AllocationRecord] {
240        &self.records
241    }
242
243    /// Borrow committed generation records in durable order.
244    #[must_use]
245    pub fn generations(&self) -> &[GenerationRecord] {
246        &self.generations
247    }
248
249    /// Return true when the history has no allocation records and no generation records.
250    #[must_use]
251    pub fn is_empty(&self) -> bool {
252        self.records.is_empty() && self.generations.is_empty()
253    }
254
255    pub(crate) const fn records_mut(&mut self) -> &mut Vec<AllocationRecord> {
256        &mut self.records
257    }
258
259    #[cfg(test)]
260    pub(crate) const fn generations_mut(&mut self) -> &mut Vec<GenerationRecord> {
261        &mut self.generations
262    }
263
264    pub(crate) fn push_record(&mut self, record: AllocationRecord) {
265        self.records.push(record);
266    }
267
268    pub(crate) fn push_generation(&mut self, generation: GenerationRecord) {
269        self.generations.push(generation);
270    }
271}
272
273impl SchemaMetadataRecord {
274    /// Build a schema metadata history record after validating the metadata.
275    pub fn new(generation: u64, schema: SchemaMetadata) -> Result<Self, SchemaMetadataError> {
276        schema.validate()?;
277        Ok(Self { generation, schema })
278    }
279
280    /// Return the generation that declared this schema metadata.
281    #[must_use]
282    pub const fn generation(&self) -> u64 {
283        self.generation
284    }
285
286    /// Return the schema metadata declared by that generation.
287    #[must_use]
288    pub const fn schema(&self) -> &SchemaMetadata {
289        &self.schema
290    }
291}
292
293impl GenerationRecord {
294    /// Build a committed generation diagnostic record after validating metadata.
295    pub fn new(
296        generation: u64,
297        parent_generation: Option<u64>,
298        runtime_fingerprint: Option<String>,
299        declaration_count: u32,
300        committed_at: Option<u64>,
301    ) -> Result<Self, DeclarationSnapshotError> {
302        validate_runtime_fingerprint(runtime_fingerprint.as_deref())?;
303        Ok(Self {
304            generation,
305            parent_generation,
306            runtime_fingerprint,
307            declaration_count,
308            committed_at,
309        })
310    }
311
312    /// Return the committed generation number.
313    #[must_use]
314    pub const fn generation(&self) -> u64 {
315        self.generation
316    }
317
318    /// Return the parent generation, if recorded.
319    #[must_use]
320    pub const fn parent_generation(&self) -> Option<u64> {
321        self.parent_generation
322    }
323
324    /// Borrow the optional binary/runtime fingerprint.
325    #[must_use]
326    pub fn runtime_fingerprint(&self) -> Option<&str> {
327        self.runtime_fingerprint.as_deref()
328    }
329
330    /// Return the number of declarations in the generation.
331    #[must_use]
332    pub const fn declaration_count(&self) -> u32 {
333        self.declaration_count
334    }
335
336    /// Return the optional commit timestamp supplied by the integration layer.
337    #[must_use]
338    pub const fn committed_at(&self) -> Option<u64> {
339        self.committed_at
340    }
341}
342
343impl AllocationRecord {
344    /// Create a new allocation record from a declaration.
345    #[must_use]
346    pub(crate) fn from_declaration(
347        generation: u64,
348        declaration: AllocationDeclaration,
349        state: AllocationState,
350    ) -> Self {
351        Self {
352            stable_key: declaration.stable_key,
353            slot: declaration.slot,
354            state,
355            first_generation: generation,
356            last_seen_generation: generation,
357            retired_generation: None,
358            schema_history: vec![
359                SchemaMetadataRecord::new(generation, declaration.schema)
360                    .expect("declarations validate schema metadata"),
361            ],
362        }
363    }
364
365    /// Create a new reserved allocation record from a declaration.
366    #[must_use]
367    pub(crate) fn reserved(generation: u64, declaration: AllocationDeclaration) -> Self {
368        Self::from_declaration(generation, declaration, AllocationState::Reserved)
369    }
370
371    /// Return the stable key that owns this allocation record.
372    #[must_use]
373    pub const fn stable_key(&self) -> &StableKey {
374        &self.stable_key
375    }
376
377    /// Return the durable allocation slot owned by this record.
378    #[must_use]
379    pub const fn slot(&self) -> &AllocationSlotDescriptor {
380        &self.slot
381    }
382
383    /// Return the current allocation lifecycle state.
384    #[must_use]
385    pub const fn state(&self) -> AllocationState {
386        self.state
387    }
388
389    /// Return the first committed generation that recorded this allocation.
390    #[must_use]
391    pub const fn first_generation(&self) -> u64 {
392        self.first_generation
393    }
394
395    /// Return the latest committed generation that observed this allocation.
396    #[must_use]
397    pub const fn last_seen_generation(&self) -> u64 {
398        self.last_seen_generation
399    }
400
401    /// Return the generation that explicitly retired this allocation, if any.
402    #[must_use]
403    pub const fn retired_generation(&self) -> Option<u64> {
404        self.retired_generation
405    }
406
407    /// Return the per-generation schema metadata history.
408    #[must_use]
409    pub fn schema_history(&self) -> &[SchemaMetadataRecord] {
410        &self.schema_history
411    }
412
413    pub(crate) fn observe_declaration(
414        &mut self,
415        generation: u64,
416        declaration: &AllocationDeclaration,
417    ) {
418        self.last_seen_generation = generation;
419        if self.state == AllocationState::Reserved {
420            self.state = AllocationState::Active;
421        }
422
423        let latest_schema = self.schema_history.last().map(|record| &record.schema);
424        if latest_schema != Some(&declaration.schema) {
425            self.schema_history.push(
426                SchemaMetadataRecord::new(generation, declaration.schema.clone())
427                    .expect("declarations validate schema metadata"),
428            );
429        }
430    }
431
432    pub(crate) fn observe_reservation(
433        &mut self,
434        generation: u64,
435        reservation: &AllocationDeclaration,
436    ) {
437        self.last_seen_generation = generation;
438
439        let latest_schema = self.schema_history.last().map(|record| &record.schema);
440        if latest_schema != Some(&reservation.schema) {
441            self.schema_history.push(
442                SchemaMetadataRecord::new(generation, reservation.schema.clone())
443                    .expect("reservations validate schema metadata"),
444            );
445        }
446    }
447}
448
449impl AllocationLedger {
450    /// Build a ledger DTO and validate structural ledger invariants.
451    ///
452    /// This constructor validates duplicate records, lifecycle state, record
453    /// generation bounds, and schema metadata records. It does not require a
454    /// complete committed-generation chain. Use
455    /// [`AllocationLedger::new_committed`] when constructing an authoritative
456    /// committed ledger DTO.
457    pub fn new(
458        ledger_schema_version: u32,
459        physical_format_id: u32,
460        current_generation: u64,
461        allocation_history: AllocationHistory,
462    ) -> Result<Self, LedgerIntegrityError> {
463        let ledger = Self {
464            ledger_schema_version,
465            physical_format_id,
466            current_generation,
467            allocation_history,
468        };
469        ledger.validate_integrity()?;
470        Ok(ledger)
471    }
472
473    /// Build a committed ledger DTO and validate strict committed-history invariants.
474    ///
475    /// This constructor runs the same committed-integrity checks used by
476    /// recovery and commit. Use it when the value should be treated as an
477    /// authoritative committed ledger, not merely as a structurally valid DTO.
478    pub fn new_committed(
479        ledger_schema_version: u32,
480        physical_format_id: u32,
481        current_generation: u64,
482        allocation_history: AllocationHistory,
483    ) -> Result<Self, LedgerIntegrityError> {
484        let ledger = Self::new(
485            ledger_schema_version,
486            physical_format_id,
487            current_generation,
488            allocation_history,
489        )?;
490        ledger.validate_committed_integrity()?;
491        Ok(ledger)
492    }
493
494    /// Return the ledger schema version.
495    #[must_use]
496    pub const fn ledger_schema_version(&self) -> u32 {
497        self.ledger_schema_version
498    }
499
500    /// Return the protected physical format identifier.
501    #[must_use]
502    pub const fn physical_format_id(&self) -> u32 {
503        self.physical_format_id
504    }
505
506    /// Return the current committed generation selected by recovery.
507    #[must_use]
508    pub const fn current_generation(&self) -> u64 {
509        self.current_generation
510    }
511
512    /// Return the historical allocation facts.
513    #[must_use]
514    pub const fn allocation_history(&self) -> &AllocationHistory {
515        &self.allocation_history
516    }
517}