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/// Public staging APIs clone this DTO before applying a logical generation;
23/// bootstrap transfers its owned ledger into the same staging implementation.
24/// The ledger contains allocation metadata only, bounded by the number of
25/// stable allocation identities and committed bootstrap generations, not user
26/// collection contents.
27///
28
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30#[serde(deny_unknown_fields)]
31pub struct AllocationLedger {
32    /// Current committed generation selected by recovery.
33    pub(crate) current_generation: u64,
34    /// Historical allocation facts.
35    pub(crate) allocation_history: AllocationHistory,
36}
37
38///
39/// AllocationHistory
40///
41/// Durable allocation records and generation history.
42///
43/// This is the durable DTO embedded in an [`AllocationLedger`]. It records
44/// allocation facts and generation diagnostics; callers should prefer ledger
45/// staging/validation methods over mutating histories directly.
46#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
47#[serde(deny_unknown_fields)]
48pub struct AllocationHistory {
49    /// Stable-key allocation records.
50    #[serde(deserialize_with = "crate::cbor::deserialize_records")]
51    pub(crate) records: Vec<AllocationRecord>,
52    /// Committed generation records.
53    #[serde(deserialize_with = "crate::cbor::deserialize_history")]
54    pub(crate) generations: Vec<GenerationRecord>,
55}
56
57///
58/// AllocationRecord
59///
60/// Durable ownership record for one stable key.
61///
62/// Records are historical facts, not live handles. Fields are private so stale
63/// or invalid ownership state cannot be assembled through public struct
64/// literals; use accessors for diagnostics and ledger methods for mutation.
65///
66
67#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
68#[serde(deny_unknown_fields)]
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    /// Per-generation schema metadata history.
81    #[serde(deserialize_with = "crate::cbor::deserialize_history")]
82    pub(crate) schema_history: Vec<SchemaMetadataRecord>,
83}
84
85///
86/// AllocationRetirement
87///
88/// Explicit request to tombstone one historical allocation identity.
89///
90/// Retirement prevents a stable key from being redeclared. It does not make the
91/// physical slot safe for another active stable key.
92#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
93#[serde(deny_unknown_fields)]
94pub struct AllocationRetirement {
95    /// Stable key being retired.
96    pub(crate) stable_key: StableKey,
97    /// Allocation slot historically owned by the stable key.
98    pub(crate) 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        let stable_key = StableKey::parse(stable_key).map_err(AllocationRetirementError::Key)?;
108        slot.validate()
109            .map_err(AllocationRetirementError::MemoryManagerSlot)?;
110        Ok(Self { stable_key, slot })
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/// Recovery checks that physical and logical generations agree before
204/// constructing this proof; both generation accessors report that one value.
205///
206/// This type is not serializable and has no public constructor. It is the
207/// provenance boundary required before declarations can mint pre-commit
208/// [`crate::ValidatedAllocations`].
209///
210
211#[derive(Clone, Debug, Eq, PartialEq)]
212pub struct RecoveredLedger {
213    ledger: AllocationLedger,
214}
215
216impl RecoveredLedger {
217    pub(crate) const fn from_trusted_ledger(ledger: AllocationLedger) -> Self {
218        Self { ledger }
219    }
220
221    /// Borrow the recovered canonical allocation ledger.
222    ///
223    /// The returned ledger is diagnostic/staging state. It is not itself an
224    /// authority token; callers must keep passing the `RecoveredLedger` proof
225    /// across validation boundaries.
226    #[must_use]
227    pub const fn ledger(&self) -> &AllocationLedger {
228        &self.ledger
229    }
230
231    /// Return the selected physical committed generation.
232    /// Recovery has established that it equals the current logical generation.
233    #[must_use]
234    pub const fn physical_generation(&self) -> u64 {
235        self.ledger.current_generation
236    }
237
238    /// Return the recovered ledger's current logical generation.
239    #[must_use]
240    pub const fn current_generation(&self) -> u64 {
241        self.ledger.current_generation
242    }
243
244    pub(crate) fn into_ledger(self) -> AllocationLedger {
245        self.ledger
246    }
247}
248
249impl AllocationHistory {
250    #[cfg(test)]
251    pub(crate) const fn from_parts(
252        records: Vec<AllocationRecord>,
253        generations: Vec<GenerationRecord>,
254    ) -> Self {
255        Self {
256            records,
257            generations,
258        }
259    }
260
261    /// Borrow stable-key allocation records in durable order.
262    #[must_use]
263    pub fn records(&self) -> &[AllocationRecord] {
264        &self.records
265    }
266
267    /// Borrow committed generation records in durable order.
268    #[must_use]
269    pub fn generations(&self) -> &[GenerationRecord] {
270        &self.generations
271    }
272
273    /// Return true when the history has no allocation records and no generation records.
274    #[must_use]
275    pub const fn is_empty(&self) -> bool {
276        self.records.is_empty() && self.generations.is_empty()
277    }
278}
279
280impl SchemaMetadataRecord {
281    /// Build a schema metadata history record after validating the metadata.
282    pub fn new(generation: u64, schema: SchemaMetadata) -> Result<Self, SchemaMetadataError> {
283        schema.validate()?;
284        Ok(Self { generation, schema })
285    }
286
287    /// Return the generation that declared this schema metadata.
288    #[must_use]
289    pub const fn generation(&self) -> u64 {
290        self.generation
291    }
292
293    /// Return the schema metadata declared by that generation.
294    #[must_use]
295    pub const fn schema(&self) -> &SchemaMetadata {
296        &self.schema
297    }
298}
299
300impl GenerationRecord {
301    /// Build a committed generation diagnostic record after validating metadata.
302    pub fn new(
303        generation: u64,
304        parent_generation: u64,
305        runtime_fingerprint: Option<String>,
306        declaration_count: u32,
307        committed_at: Option<u64>,
308    ) -> Result<Self, DeclarationSnapshotError> {
309        validate_runtime_fingerprint(runtime_fingerprint.as_deref())?;
310        Ok(Self {
311            generation,
312            parent_generation,
313            runtime_fingerprint,
314            declaration_count,
315            committed_at,
316        })
317    }
318
319    /// Return the committed generation number.
320    #[must_use]
321    pub const fn generation(&self) -> u64 {
322        self.generation
323    }
324
325    /// Return the parent generation.
326    #[must_use]
327    pub const fn parent_generation(&self) -> u64 {
328        self.parent_generation
329    }
330
331    /// Borrow the optional binary/runtime fingerprint.
332    #[must_use]
333    pub fn runtime_fingerprint(&self) -> Option<&str> {
334        self.runtime_fingerprint.as_deref()
335    }
336
337    /// Return the number of declarations in the generation.
338    #[must_use]
339    pub const fn declaration_count(&self) -> u32 {
340        self.declaration_count
341    }
342
343    /// Return the optional commit timestamp supplied by the integration layer.
344    #[must_use]
345    pub const fn committed_at(&self) -> Option<u64> {
346        self.committed_at
347    }
348}
349
350impl AllocationRecord {
351    // Staging supplies checked schema metadata: active declarations come from
352    // ValidatedAllocations, and raw reservations are validated before mutation.
353    // Copy only persisted fields; declaration labels are not ledger history.
354    fn from_declaration(
355        generation: u64,
356        declaration: &AllocationDeclaration,
357        state: AllocationState,
358    ) -> Self {
359        Self {
360            stable_key: declaration.stable_key.clone(),
361            slot: declaration.slot.clone(),
362            state,
363            first_generation: generation,
364            last_seen_generation: generation,
365            schema_history: vec![SchemaMetadataRecord {
366                generation,
367                schema: declaration.schema.clone(),
368            }],
369        }
370    }
371
372    /// Create an active record from a declaration with validated schema metadata.
373    pub(crate) fn active(generation: u64, declaration: &AllocationDeclaration) -> Self {
374        Self::from_declaration(generation, declaration, AllocationState::Active)
375    }
376
377    /// Create a reserved record from a declaration with validated schema metadata.
378    pub(crate) fn reserved(generation: u64, declaration: &AllocationDeclaration) -> Self {
379        Self::from_declaration(generation, declaration, AllocationState::Reserved)
380    }
381
382    /// Return the stable key that owns this allocation record.
383    #[must_use]
384    pub const fn stable_key(&self) -> &StableKey {
385        &self.stable_key
386    }
387
388    /// Return the durable allocation slot owned by this record.
389    #[must_use]
390    pub const fn slot(&self) -> &AllocationSlotDescriptor {
391        &self.slot
392    }
393
394    /// Return the current allocation lifecycle state.
395    #[must_use]
396    pub const fn state(&self) -> AllocationState {
397        self.state
398    }
399
400    /// Return the first committed generation that recorded this allocation.
401    #[must_use]
402    pub const fn first_generation(&self) -> u64 {
403        self.first_generation
404    }
405
406    /// Return the latest committed generation that observed this allocation.
407    #[must_use]
408    pub const fn last_seen_generation(&self) -> u64 {
409        self.last_seen_generation
410    }
411
412    /// Return the per-generation schema metadata history.
413    #[must_use]
414    pub fn schema_history(&self) -> &[SchemaMetadataRecord] {
415        &self.schema_history
416    }
417
418    pub(super) fn observe_schema(&mut self, generation: u64, schema: &SchemaMetadata) {
419        self.last_seen_generation = generation;
420
421        let latest_schema = self.schema_history.last().map(|record| &record.schema);
422        if latest_schema != Some(schema) {
423            self.schema_history.push(SchemaMetadataRecord {
424                generation,
425                schema: schema.clone(),
426            });
427        }
428    }
429}
430
431impl AllocationLedger {
432    /// Build the known empty genesis used by runtime bootstrap and diagnostics.
433    pub(crate) fn empty_genesis() -> Self {
434        // An empty history at generation zero satisfies committed integrity
435        // without decoded input or declaration references to validate.
436        Self {
437            current_generation: 0,
438            allocation_history: AllocationHistory::default(),
439        }
440    }
441
442    /// Build a ledger DTO and validate structural ledger invariants.
443    ///
444    /// This constructor validates duplicate records, lifecycle state, record
445    /// generation bounds, and schema metadata records. It does not require a
446    /// complete committed-generation chain. Use
447    /// [`AllocationLedger::new_committed`] when constructing an authoritative
448    /// committed ledger DTO.
449    pub fn new(
450        current_generation: u64,
451        allocation_history: AllocationHistory,
452    ) -> Result<Self, LedgerIntegrityError> {
453        let ledger = Self {
454            current_generation,
455            allocation_history,
456        };
457        ledger.validate_integrity()?;
458        Ok(ledger)
459    }
460
461    /// Build a committed ledger DTO and validate strict committed-history invariants.
462    ///
463    /// This constructor runs the same committed-integrity checks used by
464    /// recovery and commit. Use it when the value should be treated as an
465    /// authoritative committed ledger, not merely as a structurally valid DTO.
466    pub fn new_committed(
467        current_generation: u64,
468        allocation_history: AllocationHistory,
469    ) -> Result<Self, LedgerIntegrityError> {
470        let ledger = Self {
471            current_generation,
472            allocation_history,
473        };
474        ledger.validate_committed_integrity()?;
475        Ok(ledger)
476    }
477
478    /// Return the current committed generation selected by recovery.
479    #[must_use]
480    pub const fn current_generation(&self) -> u64 {
481        self.current_generation
482    }
483
484    /// Return the historical allocation facts.
485    #[must_use]
486    pub const fn allocation_history(&self) -> &AllocationHistory {
487        &self.allocation_history
488    }
489}