Skip to main content

ic_memory/ledger/
record.rs

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