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