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