Skip to main content

ic_memory/ledger/
record.rs

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