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