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)]
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/// This type is not serializable and has no public constructor. It is the
203/// provenance boundary required before declarations can mint pre-commit
204/// [`crate::ValidatedAllocations`].
205#[derive(Clone, Debug, Eq, PartialEq)]
206pub struct RecoveredLedger {
207    ledger: AllocationLedger,
208    physical_generation: u64,
209}
210
211impl RecoveredLedger {
212    pub(crate) const fn from_trusted_parts(
213        ledger: AllocationLedger,
214        physical_generation: u64,
215    ) -> Self {
216        Self {
217            ledger,
218            physical_generation,
219        }
220    }
221
222    /// Borrow the recovered canonical allocation ledger.
223    ///
224    /// The returned ledger is diagnostic/staging state. It is not itself an
225    /// authority token; callers must keep passing the `RecoveredLedger` proof
226    /// across validation boundaries.
227    #[must_use]
228    pub const fn ledger(&self) -> &AllocationLedger {
229        &self.ledger
230    }
231
232    /// Return the selected physical committed generation.
233    #[must_use]
234    pub const fn physical_generation(&self) -> u64 {
235        self.physical_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    fn from_declaration(
369        generation: u64,
370        declaration: AllocationDeclaration,
371        state: AllocationState,
372    ) -> Result<Self, SchemaMetadataError> {
373        Ok(Self {
374            stable_key: declaration.stable_key,
375            slot: declaration.slot,
376            state,
377            first_generation: generation,
378            last_seen_generation: generation,
379            schema_history: vec![SchemaMetadataRecord::new(generation, declaration.schema)?],
380        })
381    }
382
383    /// Create a new active allocation record from a declaration.
384    pub(crate) fn active(
385        generation: u64,
386        declaration: AllocationDeclaration,
387    ) -> Result<Self, SchemaMetadataError> {
388        Self::from_declaration(generation, declaration, AllocationState::Active)
389    }
390
391    /// Create a new reserved allocation record from a declaration.
392    pub(crate) fn reserved(
393        generation: u64,
394        declaration: AllocationDeclaration,
395    ) -> Result<Self, SchemaMetadataError> {
396        Self::from_declaration(generation, declaration, AllocationState::Reserved)
397    }
398
399    /// Return the stable key that owns this allocation record.
400    #[must_use]
401    pub const fn stable_key(&self) -> &StableKey {
402        &self.stable_key
403    }
404
405    /// Return the durable allocation slot owned by this record.
406    #[must_use]
407    pub const fn slot(&self) -> &AllocationSlotDescriptor {
408        &self.slot
409    }
410
411    /// Return the current allocation lifecycle state.
412    #[must_use]
413    pub const fn state(&self) -> AllocationState {
414        self.state
415    }
416
417    /// Return the first committed generation that recorded this allocation.
418    #[must_use]
419    pub const fn first_generation(&self) -> u64 {
420        self.first_generation
421    }
422
423    /// Return the latest committed generation that observed this allocation.
424    #[must_use]
425    pub const fn last_seen_generation(&self) -> u64 {
426        self.last_seen_generation
427    }
428
429    /// Return the per-generation schema metadata history.
430    #[must_use]
431    pub fn schema_history(&self) -> &[SchemaMetadataRecord] {
432        &self.schema_history
433    }
434
435    pub(crate) fn observe_declaration(
436        &mut self,
437        generation: u64,
438        declaration: &AllocationDeclaration,
439    ) -> Result<(), SchemaMetadataError> {
440        self.last_seen_generation = generation;
441        if self.state == AllocationState::Reserved {
442            self.state = AllocationState::Active;
443        }
444
445        let latest_schema = self.schema_history.last().map(|record| &record.schema);
446        if latest_schema != Some(&declaration.schema) {
447            self.schema_history.push(SchemaMetadataRecord::new(
448                generation,
449                declaration.schema.clone(),
450            )?);
451        }
452        Ok(())
453    }
454
455    pub(crate) fn observe_reservation(
456        &mut self,
457        generation: u64,
458        reservation: &AllocationDeclaration,
459    ) -> Result<(), SchemaMetadataError> {
460        self.last_seen_generation = generation;
461
462        let latest_schema = self.schema_history.last().map(|record| &record.schema);
463        if latest_schema != Some(&reservation.schema) {
464            self.schema_history.push(SchemaMetadataRecord::new(
465                generation,
466                reservation.schema.clone(),
467            )?);
468        }
469        Ok(())
470    }
471}
472
473impl AllocationLedger {
474    /// Build a ledger DTO and validate structural ledger invariants.
475    ///
476    /// This constructor validates duplicate records, lifecycle state, record
477    /// generation bounds, and schema metadata records. It does not require a
478    /// complete committed-generation chain. Use
479    /// [`AllocationLedger::new_committed`] when constructing an authoritative
480    /// committed ledger DTO.
481    pub fn new(
482        current_generation: u64,
483        allocation_history: AllocationHistory,
484    ) -> Result<Self, LedgerIntegrityError> {
485        let ledger = Self {
486            current_generation,
487            allocation_history,
488        };
489        ledger.validate_integrity()?;
490        Ok(ledger)
491    }
492
493    /// Build a committed ledger DTO and validate strict committed-history invariants.
494    ///
495    /// This constructor runs the same committed-integrity checks used by
496    /// recovery and commit. Use it when the value should be treated as an
497    /// authoritative committed ledger, not merely as a structurally valid DTO.
498    pub fn new_committed(
499        current_generation: u64,
500        allocation_history: AllocationHistory,
501    ) -> Result<Self, LedgerIntegrityError> {
502        let ledger = Self::new(current_generation, allocation_history)?;
503        ledger.validate_committed_integrity()?;
504        Ok(ledger)
505    }
506
507    /// Return the current committed generation selected by recovery.
508    #[must_use]
509    pub const fn current_generation(&self) -> u64 {
510        self.current_generation
511    }
512
513    /// Return the historical allocation facts.
514    #[must_use]
515    pub const fn allocation_history(&self) -> &AllocationHistory {
516        &self.allocation_history
517    }
518}