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