Skip to main content

ic_memory/ledger/
record.rs

1use super::LedgerIntegrityError;
2use crate::{declaration::AllocationDeclaration, key::StableKey, slot::MemoryManagerSlot};
3use serde::{Deserialize, Serialize};
4
5///
6/// AllocationLedger
7///
8/// Durable ownership, bounded by the usable memory-ID
9/// domain. Omitted and retired identities retain their slots; no per-upgrade or
10/// application schema state is stored.
11///
12/// The counter binds logical generations to physical commits. Decoded DTOs remain
13/// untrusted until integrity validation and protected recovery succeed.
14///
15
16#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
17#[serde(deny_unknown_fields)]
18pub struct AllocationLedger {
19    pub(crate) current_generation: u64,
20    #[serde(deserialize_with = "crate::cbor::deserialize_records")]
21    pub(crate) records: Vec<AllocationRecord>,
22}
23
24///
25/// AllocationRecord
26///
27/// Durable ownership and lifecycle state for one stable
28/// key. Retaining this record prevents slot reuse after omission or retirement.
29/// It is metadata, not a live memory handle or an upgrade audit log.
30///
31
32#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
33#[serde(deny_unknown_fields)]
34pub struct AllocationRecord {
35    pub(crate) stable_key: StableKey,
36    pub(crate) slot: MemoryManagerSlot,
37    pub(crate) state: AllocationState,
38}
39
40///
41/// AllocationState
42///
43/// Current allocation lifecycle state. Retirement permanently retains the
44/// identity and slot rather than adding a historical event or freeing the ID.
45///
46
47#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
48#[serde(deny_unknown_fields)]
49pub enum AllocationState {
50    /// Slot is reserved for a future allocation identity.
51    Reserved,
52    /// Slot is active and may be opened after validation.
53    Active,
54    /// Identity and slot are permanently tombstoned.
55    Retired,
56}
57
58///
59/// RecoveredLedger
60///
61/// Validated base for declaration checks: either a recovered ledger that passed
62/// current-format decoding, integrity checks and physical/logical counter binding,
63/// or empty logical genesis admitted only by explicit snapshot absence. Genesis
64/// is not a persisted generation. A decoded ledger alone is a passive DTO.
65///
66
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct RecoveredLedger {
69    ledger: AllocationLedger,
70}
71
72impl RecoveredLedger {
73    // Protected recovery alone establishes absence. All present-snapshot
74    // failures remain failures for bootstrap and diagnostic validation alike.
75    pub(crate) fn genesis_for_absent_snapshot(error: &super::LedgerCommitError) -> Option<Self> {
76        matches!(
77            error,
78            super::LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
79        )
80        .then(|| Self::from_trusted_ledger(AllocationLedger::empty_genesis()))
81    }
82
83    pub(crate) const fn from_trusted_ledger(ledger: AllocationLedger) -> Self {
84        Self { ledger }
85    }
86
87    /// Borrow the recovered metadata, without granting open authority.
88    #[must_use]
89    pub const fn ledger(&self) -> &AllocationLedger {
90        &self.ledger
91    }
92
93    /// Return the bound recovered counter, or zero for uncommitted logical genesis.
94    #[must_use]
95    pub const fn current_generation(&self) -> u64 {
96        self.ledger.current_generation
97    }
98
99    pub(crate) fn into_ledger(self) -> AllocationLedger {
100        self.ledger
101    }
102}
103
104impl AllocationRecord {
105    pub(crate) fn from_declaration(
106        declaration: &AllocationDeclaration,
107        state: AllocationState,
108    ) -> Self {
109        Self {
110            stable_key: declaration.stable_key.clone(),
111            slot: declaration.slot.clone(),
112            state,
113        }
114    }
115
116    pub(crate) fn active(declaration: &AllocationDeclaration) -> Self {
117        Self::from_declaration(declaration, AllocationState::Active)
118    }
119
120    #[cfg(test)]
121    pub(crate) fn reserved(declaration: &AllocationDeclaration) -> Self {
122        Self::from_declaration(declaration, AllocationState::Reserved)
123    }
124
125    /// Return the permanent store identity.
126    #[must_use]
127    pub const fn stable_key(&self) -> &StableKey {
128        &self.stable_key
129    }
130
131    /// Return its permanently claimed memory ID.
132    #[must_use]
133    pub const fn slot(&self) -> &MemoryManagerSlot {
134        &self.slot
135    }
136
137    /// Return the current lifecycle state.
138    #[must_use]
139    pub const fn state(&self) -> AllocationState {
140        self.state
141    }
142}
143
144impl AllocationLedger {
145    pub(crate) const fn empty_genesis() -> Self {
146        Self {
147            current_generation: 0,
148            records: Vec::new(),
149        }
150    }
151
152    /// Build a ledger DTO after validating count, ownership and genesis rules.
153    /// Recovery must still establish the persisted format and physical binding.
154    pub fn new(
155        current_generation: u64,
156        records: Vec<AllocationRecord>,
157    ) -> Result<Self, LedgerIntegrityError> {
158        let ledger = Self {
159            current_generation,
160            records,
161        };
162        ledger.validate_integrity()?;
163        Ok(ledger)
164    }
165
166    /// Return the current commit counter; this is not retained upgrade history.
167    #[must_use]
168    pub const fn current_generation(&self) -> u64 {
169        self.current_generation
170    }
171
172    /// Borrow retained ownership records, including omitted and retired stores.
173    #[must_use]
174    pub fn records(&self) -> &[AllocationRecord] {
175        &self.records
176    }
177
178    // Every retained lifecycle state owns its ID, including omitted/retired keys.
179    pub(crate) fn occupied_slots(&self) -> [bool; crate::constants::MAX_ALLOCATIONS] {
180        let mut occupied = [false; crate::constants::MAX_ALLOCATIONS];
181        for record in &self.records {
182            occupied[usize::from(record.slot.id())] = true;
183        }
184        occupied
185    }
186}