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