Skip to main content

ic_memory/ledger/
record.rs

1use super::{AllocationRetirementError, LedgerCompatibilityError, 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/// Current allocation ledger schema version.
11pub const CURRENT_LEDGER_SCHEMA_VERSION: u32 = 1;
12
13/// Current protected physical ledger format identifier.
14pub const CURRENT_PHYSICAL_FORMAT_ID: u32 = 1;
15
16///
17/// AllocationLedger
18///
19/// Durable root of allocation history.
20///
21/// Decoded ledgers are input from persistent storage and should be treated as
22/// untrusted until compatibility and integrity validation pass. Public
23/// construction goes through [`AllocationLedger::new`], which validates
24/// structural history invariants before returning a value. Use
25/// [`AllocationLedger::new_committed`] when the value should also satisfy the
26/// strict committed-generation chain required by recovery and commit.
27#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
28pub struct AllocationLedger {
29    /// Ledger schema version.
30    pub(crate) ledger_schema_version: u32,
31    /// Physical encoding format identifier.
32    pub(crate) physical_format_id: u32,
33    /// Current committed generation selected by recovery.
34    pub(crate) current_generation: u64,
35    /// Historical allocation facts.
36    pub(crate) allocation_history: AllocationHistory,
37}
38
39///
40/// AllocationHistory
41///
42/// Durable allocation records and generation history.
43///
44/// This is the durable DTO embedded in an [`AllocationLedger`]. It records
45/// allocation facts and generation diagnostics; callers should prefer ledger
46/// staging/validation methods over mutating histories directly.
47#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
48pub struct AllocationHistory {
49    /// Stable-key allocation records.
50    pub(crate) records: Vec<AllocationRecord>,
51    /// Committed generation records.
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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64pub struct AllocationRecord {
65    /// Stable key that owns the slot.
66    pub(crate) stable_key: StableKey,
67    /// Durable allocation slot owned by the key.
68    pub(crate) slot: AllocationSlotDescriptor,
69    /// Current allocation lifecycle state.
70    pub(crate) state: AllocationState,
71    /// First committed generation that recorded this allocation.
72    pub(crate) first_generation: u64,
73    /// Latest committed generation that observed this allocation declaration.
74    pub(crate) last_seen_generation: u64,
75    /// Generation that explicitly retired this allocation.
76    pub(crate) retired_generation: Option<u64>,
77    /// Per-generation schema metadata history.
78    pub(crate) schema_history: Vec<SchemaMetadataRecord>,
79}
80
81///
82/// AllocationRetirement
83///
84/// Explicit request to tombstone one historical allocation identity.
85///
86/// Retirement prevents a stable key from being redeclared. It does not make the
87/// physical slot safe for another active stable key.
88#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
89pub struct AllocationRetirement {
90    /// Stable key being retired.
91    pub stable_key: StableKey,
92    /// Allocation slot historically owned by the stable key.
93    pub 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        Ok(Self {
103            stable_key: StableKey::parse(stable_key).map_err(AllocationRetirementError::Key)?,
104            slot,
105        })
106    }
107}
108
109///
110/// AllocationState
111///
112/// Allocation lifecycle state.
113#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
114pub enum AllocationState {
115    /// Slot is reserved for a future allocation identity.
116    Reserved,
117    /// Slot is active and may be opened after validation.
118    Active,
119    /// Slot was explicitly retired and remains tombstoned.
120    Retired,
121}
122
123///
124/// SchemaMetadataRecord
125///
126/// Schema metadata observed in one committed generation.
127///
128/// Schema metadata is diagnostic ledger history. It is validated for bounded
129/// durable encoding, but `ic-memory` does not prove application schema
130/// compatibility or data migration correctness.
131#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
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)]
144pub struct GenerationRecord {
145    /// Committed generation number.
146    pub(crate) generation: u64,
147    /// Parent generation, if recorded.
148    pub(crate) parent_generation: Option<u64>,
149    /// Optional binary/runtime fingerprint.
150    pub(crate) runtime_fingerprint: Option<String>,
151    /// Number of declarations in the generation.
152    pub(crate) declaration_count: u32,
153    /// Optional commit timestamp supplied by the integration layer.
154    pub(crate) committed_at: Option<u64>,
155}
156
157///
158/// LedgerCompatibility
159///
160/// Supported logical and physical ledger format versions.
161///
162/// Run this check on recovered ledgers before treating them as authoritative
163/// state. Integrity validation then checks allocation history invariants.
164#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
165pub struct LedgerCompatibility {
166    /// Minimum supported ledger schema version.
167    pub min_ledger_schema_version: u32,
168    /// Maximum supported ledger schema version.
169    pub max_ledger_schema_version: u32,
170    /// Required physical encoding format identifier.
171    pub physical_format_id: u32,
172}
173
174impl LedgerCompatibility {
175    /// Return the compatibility supported by this crate version.
176    #[must_use]
177    pub const fn current() -> Self {
178        Self {
179            min_ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
180            max_ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
181            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
182        }
183    }
184
185    /// Validate a decoded ledger before it is used as authoritative state.
186    pub const fn validate(
187        &self,
188        ledger: &AllocationLedger,
189    ) -> Result<(), LedgerCompatibilityError> {
190        if ledger.ledger_schema_version < self.min_ledger_schema_version {
191            return Err(LedgerCompatibilityError::UnsupportedLedgerSchemaVersion {
192                found: ledger.ledger_schema_version,
193                min_supported: self.min_ledger_schema_version,
194                max_supported: self.max_ledger_schema_version,
195            });
196        }
197        if ledger.ledger_schema_version > self.max_ledger_schema_version {
198            return Err(LedgerCompatibilityError::UnsupportedLedgerSchemaVersion {
199                found: ledger.ledger_schema_version,
200                min_supported: self.min_ledger_schema_version,
201                max_supported: self.max_ledger_schema_version,
202            });
203        }
204        if ledger.physical_format_id != self.physical_format_id {
205            return Err(LedgerCompatibilityError::UnsupportedPhysicalFormat {
206                found: ledger.physical_format_id,
207                supported: self.physical_format_id,
208            });
209        }
210        Ok(())
211    }
212}
213
214impl Default for LedgerCompatibility {
215    fn default() -> Self {
216        Self::current()
217    }
218}
219
220impl AllocationHistory {
221    #[cfg(test)]
222    pub(crate) const fn from_parts(
223        records: Vec<AllocationRecord>,
224        generations: Vec<GenerationRecord>,
225    ) -> Self {
226        Self {
227            records,
228            generations,
229        }
230    }
231
232    /// Borrow stable-key allocation records in durable order.
233    #[must_use]
234    pub fn records(&self) -> &[AllocationRecord] {
235        &self.records
236    }
237
238    /// Borrow committed generation records in durable order.
239    #[must_use]
240    pub fn generations(&self) -> &[GenerationRecord] {
241        &self.generations
242    }
243
244    /// Return true when the history has no allocation records and no generation records.
245    #[must_use]
246    pub fn is_empty(&self) -> bool {
247        self.records.is_empty() && self.generations.is_empty()
248    }
249
250    pub(crate) const fn records_mut(&mut self) -> &mut Vec<AllocationRecord> {
251        &mut self.records
252    }
253
254    #[cfg(test)]
255    pub(crate) const fn generations_mut(&mut self) -> &mut Vec<GenerationRecord> {
256        &mut self.generations
257    }
258
259    pub(crate) fn push_record(&mut self, record: AllocationRecord) {
260        self.records.push(record);
261    }
262
263    pub(crate) fn push_generation(&mut self, generation: GenerationRecord) {
264        self.generations.push(generation);
265    }
266}
267
268impl SchemaMetadataRecord {
269    /// Build a schema metadata history record after validating the metadata.
270    pub fn new(generation: u64, schema: SchemaMetadata) -> Result<Self, SchemaMetadataError> {
271        schema.validate()?;
272        Ok(Self { generation, schema })
273    }
274
275    /// Return the generation that declared this schema metadata.
276    #[must_use]
277    pub const fn generation(&self) -> u64 {
278        self.generation
279    }
280
281    /// Return the schema metadata declared by that generation.
282    #[must_use]
283    pub const fn schema(&self) -> &SchemaMetadata {
284        &self.schema
285    }
286}
287
288impl GenerationRecord {
289    /// Build a committed generation diagnostic record after validating metadata.
290    pub fn new(
291        generation: u64,
292        parent_generation: Option<u64>,
293        runtime_fingerprint: Option<String>,
294        declaration_count: u32,
295        committed_at: Option<u64>,
296    ) -> Result<Self, DeclarationSnapshotError> {
297        validate_runtime_fingerprint(runtime_fingerprint.as_deref())?;
298        Ok(Self {
299            generation,
300            parent_generation,
301            runtime_fingerprint,
302            declaration_count,
303            committed_at,
304        })
305    }
306
307    /// Return the committed generation number.
308    #[must_use]
309    pub const fn generation(&self) -> u64 {
310        self.generation
311    }
312
313    /// Return the parent generation, if recorded.
314    #[must_use]
315    pub const fn parent_generation(&self) -> Option<u64> {
316        self.parent_generation
317    }
318
319    /// Borrow the optional binary/runtime fingerprint.
320    #[must_use]
321    pub fn runtime_fingerprint(&self) -> Option<&str> {
322        self.runtime_fingerprint.as_deref()
323    }
324
325    /// Return the number of declarations in the generation.
326    #[must_use]
327    pub const fn declaration_count(&self) -> u32 {
328        self.declaration_count
329    }
330
331    /// Return the optional commit timestamp supplied by the integration layer.
332    #[must_use]
333    pub const fn committed_at(&self) -> Option<u64> {
334        self.committed_at
335    }
336}
337
338impl AllocationRecord {
339    /// Create a new allocation record from a declaration.
340    #[must_use]
341    pub(crate) fn from_declaration(
342        generation: u64,
343        declaration: AllocationDeclaration,
344        state: AllocationState,
345    ) -> Self {
346        Self {
347            stable_key: declaration.stable_key,
348            slot: declaration.slot,
349            state,
350            first_generation: generation,
351            last_seen_generation: generation,
352            retired_generation: None,
353            schema_history: vec![
354                SchemaMetadataRecord::new(generation, declaration.schema)
355                    .expect("declarations validate schema metadata"),
356            ],
357        }
358    }
359
360    /// Create a new reserved allocation record from a declaration.
361    #[must_use]
362    pub(crate) fn reserved(generation: u64, declaration: AllocationDeclaration) -> Self {
363        Self::from_declaration(generation, declaration, AllocationState::Reserved)
364    }
365
366    /// Return the stable key that owns this allocation record.
367    #[must_use]
368    pub const fn stable_key(&self) -> &StableKey {
369        &self.stable_key
370    }
371
372    /// Return the durable allocation slot owned by this record.
373    #[must_use]
374    pub const fn slot(&self) -> &AllocationSlotDescriptor {
375        &self.slot
376    }
377
378    /// Return the current allocation lifecycle state.
379    #[must_use]
380    pub const fn state(&self) -> AllocationState {
381        self.state
382    }
383
384    /// Return the first committed generation that recorded this allocation.
385    #[must_use]
386    pub const fn first_generation(&self) -> u64 {
387        self.first_generation
388    }
389
390    /// Return the latest committed generation that observed this allocation.
391    #[must_use]
392    pub const fn last_seen_generation(&self) -> u64 {
393        self.last_seen_generation
394    }
395
396    /// Return the generation that explicitly retired this allocation, if any.
397    #[must_use]
398    pub const fn retired_generation(&self) -> Option<u64> {
399        self.retired_generation
400    }
401
402    /// Return the per-generation schema metadata history.
403    #[must_use]
404    pub fn schema_history(&self) -> &[SchemaMetadataRecord] {
405        &self.schema_history
406    }
407
408    pub(crate) fn observe_declaration(
409        &mut self,
410        generation: u64,
411        declaration: &AllocationDeclaration,
412    ) {
413        self.last_seen_generation = generation;
414        if self.state == AllocationState::Reserved {
415            self.state = AllocationState::Active;
416        }
417
418        let latest_schema = self.schema_history.last().map(|record| &record.schema);
419        if latest_schema != Some(&declaration.schema) {
420            self.schema_history.push(
421                SchemaMetadataRecord::new(generation, declaration.schema.clone())
422                    .expect("declarations validate schema metadata"),
423            );
424        }
425    }
426
427    pub(crate) fn observe_reservation(
428        &mut self,
429        generation: u64,
430        reservation: &AllocationDeclaration,
431    ) {
432        self.last_seen_generation = generation;
433
434        let latest_schema = self.schema_history.last().map(|record| &record.schema);
435        if latest_schema != Some(&reservation.schema) {
436            self.schema_history.push(
437                SchemaMetadataRecord::new(generation, reservation.schema.clone())
438                    .expect("reservations validate schema metadata"),
439            );
440        }
441    }
442}
443
444impl AllocationLedger {
445    /// Build a ledger DTO and validate structural ledger invariants.
446    ///
447    /// This constructor validates duplicate records, lifecycle state, record
448    /// generation bounds, and schema metadata records. It does not require a
449    /// complete committed-generation chain. Use
450    /// [`AllocationLedger::new_committed`] when constructing an authoritative
451    /// committed ledger DTO.
452    pub fn new(
453        ledger_schema_version: u32,
454        physical_format_id: u32,
455        current_generation: u64,
456        allocation_history: AllocationHistory,
457    ) -> Result<Self, LedgerIntegrityError> {
458        let ledger = Self {
459            ledger_schema_version,
460            physical_format_id,
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        ledger_schema_version: u32,
475        physical_format_id: u32,
476        current_generation: u64,
477        allocation_history: AllocationHistory,
478    ) -> Result<Self, LedgerIntegrityError> {
479        let ledger = Self::new(
480            ledger_schema_version,
481            physical_format_id,
482            current_generation,
483            allocation_history,
484        )?;
485        ledger.validate_committed_integrity()?;
486        Ok(ledger)
487    }
488
489    /// Return the ledger schema version.
490    #[must_use]
491    pub const fn ledger_schema_version(&self) -> u32 {
492        self.ledger_schema_version
493    }
494
495    /// Return the protected physical format identifier.
496    #[must_use]
497    pub const fn physical_format_id(&self) -> u32 {
498        self.physical_format_id
499    }
500
501    /// Return the current committed generation selected by recovery.
502    #[must_use]
503    pub const fn current_generation(&self) -> u64 {
504        self.current_generation
505    }
506
507    /// Return the historical allocation facts.
508    #[must_use]
509    pub const fn allocation_history(&self) -> &AllocationHistory {
510        &self.allocation_history
511    }
512}