Skip to main content

ic_memory/ledger/
record.rs

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