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::MemoryManagerSlot,
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: MemoryManagerSlot,
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: MemoryManagerSlot,
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: MemoryManagerSlot,
106    ) -> Result<Self, AllocationRetirementError> {
107        let stable_key = StableKey::parse(stable_key).map_err(AllocationRetirementError::Key)?;
108        Ok(Self { stable_key, slot })
109    }
110
111    /// Return the stable key being retired.
112    #[must_use]
113    pub const fn stable_key(&self) -> &StableKey {
114        &self.stable_key
115    }
116
117    /// Return the allocation slot historically owned by the stable key.
118    #[must_use]
119    pub const fn slot(&self) -> &MemoryManagerSlot {
120        &self.slot
121    }
122
123    /// Validate constructor invariants after decode or manual assembly.
124    pub fn validate(&self) -> Result<(), AllocationRetirementError> {
125        self.stable_key
126            .validate()
127            .map_err(AllocationRetirementError::Key)
128    }
129}
130
131///
132/// AllocationState
133///
134/// Allocation lifecycle state.
135///
136
137#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
138#[serde(deny_unknown_fields)]
139pub enum AllocationState {
140    /// Slot is reserved for a future allocation identity.
141    Reserved,
142    /// Slot is active and may be opened after validation.
143    Active,
144    /// Slot was explicitly retired and remains tombstoned.
145    Retired {
146        /// Committed generation that retired the allocation.
147        generation: u64,
148    },
149}
150
151///
152/// SchemaMetadataRecord
153///
154/// Schema metadata observed in one committed generation.
155///
156/// Schema metadata is diagnostic ledger history. It is validated for bounded
157/// durable encoding, but `ic-memory` does not prove application schema
158/// support or data migration correctness.
159#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
160#[serde(deny_unknown_fields)]
161pub struct SchemaMetadataRecord {
162    /// Generation that declared this schema metadata.
163    pub(crate) generation: u64,
164    /// Schema metadata declared by that generation.
165    pub(crate) schema: SchemaMetadata,
166}
167
168///
169/// GenerationRecord
170///
171/// Diagnostic metadata for one committed ledger generation.
172///
173
174#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
175#[serde(deny_unknown_fields)]
176pub struct GenerationRecord {
177    /// Committed generation number.
178    pub(crate) generation: u64,
179    /// Parent generation.
180    pub(crate) parent_generation: u64,
181    /// Optional binary/runtime fingerprint.
182    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
183    pub(crate) runtime_fingerprint: Option<String>,
184    /// Number of declarations in the generation.
185    pub(crate) declaration_count: u32,
186    /// Optional commit timestamp supplied by the integration layer.
187    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
188    pub(crate) committed_at: Option<u64>,
189}
190
191///
192/// RecoveredLedger
193///
194/// Proof object for an allocation ledger that has crossed physical recovery,
195/// logical payload-envelope routing, current-format checks, and committed
196/// integrity validation.
197///
198/// Recovery checks that physical and logical generations agree before
199/// constructing this proof; both generation accessors report that one value.
200///
201/// This type is not serializable and has no public constructor. It is the
202/// provenance boundary required before declarations can mint pre-commit
203/// [`crate::ValidatedAllocations`].
204///
205
206#[derive(Clone, Debug, Eq, PartialEq)]
207pub struct RecoveredLedger {
208    ledger: AllocationLedger,
209}
210
211impl RecoveredLedger {
212    pub(crate) const fn from_trusted_ledger(ledger: AllocationLedger) -> Self {
213        Self { ledger }
214    }
215
216    /// Borrow the recovered canonical allocation ledger.
217    ///
218    /// The returned ledger is diagnostic/staging state. It is not itself an
219    /// authority token; callers must keep passing the `RecoveredLedger` proof
220    /// across validation boundaries.
221    #[must_use]
222    pub const fn ledger(&self) -> &AllocationLedger {
223        &self.ledger
224    }
225
226    /// Return the selected physical committed generation.
227    /// Recovery has established that it equals the current logical generation.
228    #[must_use]
229    pub const fn physical_generation(&self) -> u64 {
230        self.ledger.current_generation
231    }
232
233    /// Return the recovered ledger's current logical generation.
234    #[must_use]
235    pub const fn current_generation(&self) -> u64 {
236        self.ledger.current_generation
237    }
238
239    pub(crate) fn into_ledger(self) -> AllocationLedger {
240        self.ledger
241    }
242}
243
244impl AllocationHistory {
245    #[cfg(test)]
246    pub(crate) const fn from_parts(
247        records: Vec<AllocationRecord>,
248        generations: Vec<GenerationRecord>,
249    ) -> Self {
250        Self {
251            records,
252            generations,
253        }
254    }
255
256    /// Borrow stable-key allocation records in durable order.
257    #[must_use]
258    pub fn records(&self) -> &[AllocationRecord] {
259        &self.records
260    }
261
262    /// Borrow committed generation records in durable order.
263    #[must_use]
264    pub fn generations(&self) -> &[GenerationRecord] {
265        &self.generations
266    }
267
268    /// Return true when the history has no allocation records and no generation records.
269    #[must_use]
270    pub const fn is_empty(&self) -> bool {
271        self.records.is_empty() && self.generations.is_empty()
272    }
273}
274
275impl SchemaMetadataRecord {
276    /// Build a schema metadata history record after validating the metadata.
277    pub fn new(generation: u64, schema: SchemaMetadata) -> Result<Self, SchemaMetadataError> {
278        schema.validate()?;
279        Ok(Self { generation, schema })
280    }
281
282    /// Return the generation that declared this schema metadata.
283    #[must_use]
284    pub const fn generation(&self) -> u64 {
285        self.generation
286    }
287
288    /// Return the schema metadata declared by that generation.
289    #[must_use]
290    pub const fn schema(&self) -> &SchemaMetadata {
291        &self.schema
292    }
293}
294
295impl GenerationRecord {
296    /// Build a committed generation diagnostic record after validating metadata.
297    pub fn new(
298        generation: u64,
299        parent_generation: u64,
300        runtime_fingerprint: Option<String>,
301        declaration_count: u32,
302        committed_at: Option<u64>,
303    ) -> Result<Self, DeclarationSnapshotError> {
304        validate_runtime_fingerprint(runtime_fingerprint.as_deref())?;
305        Ok(Self {
306            generation,
307            parent_generation,
308            runtime_fingerprint,
309            declaration_count,
310            committed_at,
311        })
312    }
313
314    /// Return the committed generation number.
315    #[must_use]
316    pub const fn generation(&self) -> u64 {
317        self.generation
318    }
319
320    /// Return the parent generation.
321    #[must_use]
322    pub const fn parent_generation(&self) -> u64 {
323        self.parent_generation
324    }
325
326    /// Borrow the optional binary/runtime fingerprint.
327    #[must_use]
328    pub fn runtime_fingerprint(&self) -> Option<&str> {
329        self.runtime_fingerprint.as_deref()
330    }
331
332    /// Return the number of declarations in the generation.
333    #[must_use]
334    pub const fn declaration_count(&self) -> u32 {
335        self.declaration_count
336    }
337
338    /// Return the optional commit timestamp supplied by the integration layer.
339    #[must_use]
340    pub const fn committed_at(&self) -> Option<u64> {
341        self.committed_at
342    }
343}
344
345impl AllocationRecord {
346    // Staging supplies checked schema metadata: active declarations come from
347    // ValidatedAllocations, and raw reservations are validated before mutation.
348    // Copy only persisted fields; declaration labels are not ledger history.
349    fn from_declaration(
350        generation: u64,
351        declaration: &AllocationDeclaration,
352        state: AllocationState,
353    ) -> Self {
354        Self {
355            stable_key: declaration.stable_key.clone(),
356            slot: declaration.slot.clone(),
357            state,
358            first_generation: generation,
359            last_seen_generation: generation,
360            schema_history: vec![SchemaMetadataRecord {
361                generation,
362                schema: declaration.schema.clone(),
363            }],
364        }
365    }
366
367    /// Create an active record from a declaration with validated schema metadata.
368    pub(crate) fn active(generation: u64, declaration: &AllocationDeclaration) -> Self {
369        Self::from_declaration(generation, declaration, AllocationState::Active)
370    }
371
372    /// Create a reserved record from a declaration with validated schema metadata.
373    pub(crate) fn reserved(generation: u64, declaration: &AllocationDeclaration) -> Self {
374        Self::from_declaration(generation, declaration, AllocationState::Reserved)
375    }
376
377    /// Return the stable key that owns this allocation record.
378    #[must_use]
379    pub const fn stable_key(&self) -> &StableKey {
380        &self.stable_key
381    }
382
383    /// Return the durable allocation slot owned by this record.
384    #[must_use]
385    pub const fn slot(&self) -> &MemoryManagerSlot {
386        &self.slot
387    }
388
389    /// Return the current allocation lifecycle state.
390    #[must_use]
391    pub const fn state(&self) -> AllocationState {
392        self.state
393    }
394
395    /// Return the first committed generation that recorded this allocation.
396    #[must_use]
397    pub const fn first_generation(&self) -> u64 {
398        self.first_generation
399    }
400
401    /// Return the latest committed generation that observed this allocation.
402    #[must_use]
403    pub const fn last_seen_generation(&self) -> u64 {
404        self.last_seen_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(super) fn observe_schema(&mut self, generation: u64, schema: &SchemaMetadata) {
414        self.last_seen_generation = generation;
415
416        let latest_schema = self.schema_history.last().map(|record| &record.schema);
417        if latest_schema != Some(schema) {
418            self.schema_history.push(SchemaMetadataRecord {
419                generation,
420                schema: schema.clone(),
421            });
422        }
423    }
424}
425
426impl AllocationLedger {
427    /// Build the known empty genesis used by runtime bootstrap and diagnostics.
428    pub(crate) fn empty_genesis() -> Self {
429        // An empty history at generation zero satisfies committed integrity
430        // without decoded input or declaration references to validate.
431        Self {
432            current_generation: 0,
433            allocation_history: AllocationHistory::default(),
434        }
435    }
436
437    /// Build a ledger DTO and validate structural ledger invariants.
438    ///
439    /// This constructor validates duplicate records, lifecycle state, record
440    /// generation bounds, and schema metadata records. It does not require a
441    /// complete committed-generation chain. Use
442    /// [`AllocationLedger::new_committed`] when constructing an authoritative
443    /// committed ledger DTO.
444    pub fn new(
445        current_generation: u64,
446        allocation_history: AllocationHistory,
447    ) -> Result<Self, LedgerIntegrityError> {
448        let ledger = Self {
449            current_generation,
450            allocation_history,
451        };
452        ledger.validate_integrity()?;
453        Ok(ledger)
454    }
455
456    /// Build a committed ledger DTO and validate strict committed-history invariants.
457    ///
458    /// This constructor runs the same committed-integrity checks used by
459    /// recovery and commit. Use it when the value should be treated as an
460    /// authoritative committed ledger, not merely as a structurally valid DTO.
461    pub fn new_committed(
462        current_generation: u64,
463        allocation_history: AllocationHistory,
464    ) -> Result<Self, LedgerIntegrityError> {
465        let ledger = Self {
466            current_generation,
467            allocation_history,
468        };
469        ledger.validate_committed_integrity()?;
470        Ok(ledger)
471    }
472
473    /// Return the current committed generation selected by recovery.
474    #[must_use]
475    pub const fn current_generation(&self) -> u64 {
476        self.current_generation
477    }
478
479    /// Return the historical allocation facts.
480    #[must_use]
481    pub const fn allocation_history(&self) -> &AllocationHistory {
482        &self.allocation_history
483    }
484}