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/// 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    pub(crate) const fn records_mut(&mut self) -> &mut Vec<AllocationRecord> {
280        &mut self.records
281    }
282
283    #[cfg(test)]
284    pub(crate) const fn generations_mut(&mut self) -> &mut Vec<GenerationRecord> {
285        &mut self.generations
286    }
287
288    pub(crate) fn push_record(&mut self, record: AllocationRecord) {
289        self.records.push(record);
290    }
291
292    pub(crate) fn push_generation(&mut self, generation: GenerationRecord) {
293        self.generations.push(generation);
294    }
295}
296
297impl SchemaMetadataRecord {
298    /// Build a schema metadata history record after validating the metadata.
299    pub fn new(generation: u64, schema: SchemaMetadata) -> Result<Self, SchemaMetadataError> {
300        schema.validate()?;
301        Ok(Self { generation, schema })
302    }
303
304    /// Return the generation that declared this schema metadata.
305    #[must_use]
306    pub const fn generation(&self) -> u64 {
307        self.generation
308    }
309
310    /// Return the schema metadata declared by that generation.
311    #[must_use]
312    pub const fn schema(&self) -> &SchemaMetadata {
313        &self.schema
314    }
315}
316
317impl GenerationRecord {
318    /// Build a committed generation diagnostic record after validating metadata.
319    pub fn new(
320        generation: u64,
321        parent_generation: u64,
322        runtime_fingerprint: Option<String>,
323        declaration_count: u32,
324        committed_at: Option<u64>,
325    ) -> Result<Self, DeclarationSnapshotError> {
326        validate_runtime_fingerprint(runtime_fingerprint.as_deref())?;
327        Ok(Self {
328            generation,
329            parent_generation,
330            runtime_fingerprint,
331            declaration_count,
332            committed_at,
333        })
334    }
335
336    /// Return the committed generation number.
337    #[must_use]
338    pub const fn generation(&self) -> u64 {
339        self.generation
340    }
341
342    /// Return the parent generation.
343    #[must_use]
344    pub const fn parent_generation(&self) -> u64 {
345        self.parent_generation
346    }
347
348    /// Borrow the optional binary/runtime fingerprint.
349    #[must_use]
350    pub fn runtime_fingerprint(&self) -> Option<&str> {
351        self.runtime_fingerprint.as_deref()
352    }
353
354    /// Return the number of declarations in the generation.
355    #[must_use]
356    pub const fn declaration_count(&self) -> u32 {
357        self.declaration_count
358    }
359
360    /// Return the optional commit timestamp supplied by the integration layer.
361    #[must_use]
362    pub const fn committed_at(&self) -> Option<u64> {
363        self.committed_at
364    }
365}
366
367impl AllocationRecord {
368    // Staging supplies checked schema metadata: active declarations come from
369    // ValidatedAllocations, and raw reservations are validated before mutation.
370    fn from_declaration(
371        generation: u64,
372        declaration: AllocationDeclaration,
373        state: AllocationState,
374    ) -> Self {
375        Self {
376            stable_key: declaration.stable_key,
377            slot: declaration.slot,
378            state,
379            first_generation: generation,
380            last_seen_generation: generation,
381            schema_history: vec![SchemaMetadataRecord {
382                generation,
383                schema: declaration.schema,
384            }],
385        }
386    }
387
388    /// Create an active record from a declaration with validated schema metadata.
389    pub(crate) fn active(generation: u64, declaration: AllocationDeclaration) -> Self {
390        Self::from_declaration(generation, declaration, AllocationState::Active)
391    }
392
393    /// Create a reserved record from a declaration with validated schema metadata.
394    pub(crate) fn reserved(generation: u64, declaration: AllocationDeclaration) -> Self {
395        Self::from_declaration(generation, declaration, AllocationState::Reserved)
396    }
397
398    /// Return the stable key that owns this allocation record.
399    #[must_use]
400    pub const fn stable_key(&self) -> &StableKey {
401        &self.stable_key
402    }
403
404    /// Return the durable allocation slot owned by this record.
405    #[must_use]
406    pub const fn slot(&self) -> &AllocationSlotDescriptor {
407        &self.slot
408    }
409
410    /// Return the current allocation lifecycle state.
411    #[must_use]
412    pub const fn state(&self) -> AllocationState {
413        self.state
414    }
415
416    /// Return the first committed generation that recorded this allocation.
417    #[must_use]
418    pub const fn first_generation(&self) -> u64 {
419        self.first_generation
420    }
421
422    /// Return the latest committed generation that observed this allocation.
423    #[must_use]
424    pub const fn last_seen_generation(&self) -> u64 {
425        self.last_seen_generation
426    }
427
428    /// Return the per-generation schema metadata history.
429    #[must_use]
430    pub fn schema_history(&self) -> &[SchemaMetadataRecord] {
431        &self.schema_history
432    }
433
434    pub(crate) fn observe_declaration(
435        &mut self,
436        generation: u64,
437        declaration: &AllocationDeclaration,
438    ) {
439        if self.state == AllocationState::Reserved {
440            self.state = AllocationState::Active;
441        }
442        self.observe_schema(generation, &declaration.schema);
443    }
444
445    pub(super) fn observe_schema(&mut self, generation: u64, schema: &SchemaMetadata) {
446        self.last_seen_generation = generation;
447
448        let latest_schema = self.schema_history.last().map(|record| &record.schema);
449        if latest_schema != Some(schema) {
450            self.schema_history.push(SchemaMetadataRecord {
451                generation,
452                schema: schema.clone(),
453            });
454        }
455    }
456}
457
458impl AllocationLedger {
459    /// Build the known empty genesis used by runtime bootstrap and diagnostics.
460    pub(crate) fn empty_genesis() -> Self {
461        // An empty history at generation zero satisfies committed integrity
462        // without decoded input or declaration references to validate.
463        Self {
464            current_generation: 0,
465            allocation_history: AllocationHistory::default(),
466        }
467    }
468
469    /// Build a ledger DTO and validate structural ledger invariants.
470    ///
471    /// This constructor validates duplicate records, lifecycle state, record
472    /// generation bounds, and schema metadata records. It does not require a
473    /// complete committed-generation chain. Use
474    /// [`AllocationLedger::new_committed`] when constructing an authoritative
475    /// committed ledger DTO.
476    pub fn new(
477        current_generation: u64,
478        allocation_history: AllocationHistory,
479    ) -> Result<Self, LedgerIntegrityError> {
480        let ledger = Self {
481            current_generation,
482            allocation_history,
483        };
484        ledger.validate_integrity()?;
485        Ok(ledger)
486    }
487
488    /// Build a committed ledger DTO and validate strict committed-history invariants.
489    ///
490    /// This constructor runs the same committed-integrity checks used by
491    /// recovery and commit. Use it when the value should be treated as an
492    /// authoritative committed ledger, not merely as a structurally valid DTO.
493    pub fn new_committed(
494        current_generation: u64,
495        allocation_history: AllocationHistory,
496    ) -> Result<Self, LedgerIntegrityError> {
497        let ledger = Self {
498            current_generation,
499            allocation_history,
500        };
501        ledger.validate_committed_integrity()?;
502        Ok(ledger)
503    }
504
505    /// Return the current committed generation selected by recovery.
506    #[must_use]
507    pub const fn current_generation(&self) -> u64 {
508        self.current_generation
509    }
510
511    /// Return the historical allocation facts.
512    #[must_use]
513    pub const fn allocation_history(&self) -> &AllocationHistory {
514        &self.allocation_history
515    }
516}