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