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 validation proofs 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    fn from_declaration(declaration: &AllocationDeclaration, state: AllocationState) -> Self {
110        Self {
111            stable_key: declaration.stable_key.clone(),
112            slot: declaration.slot.clone(),
113            state,
114            schema: declaration.schema.clone(),
115        }
116    }
117
118    pub(crate) fn active(declaration: &AllocationDeclaration) -> Self {
119        Self::from_declaration(declaration, AllocationState::Active)
120    }
121
122    #[cfg(any(test, feature = "test-support"))]
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    /// Borrow the latest declared schema metadata; no schema history is kept.
146    #[must_use]
147    pub const fn schema(&self) -> &SchemaMetadata {
148        &self.schema
149    }
150}
151
152impl AllocationLedger {
153    pub(crate) const fn empty_genesis() -> Self {
154        Self {
155            current_generation: 0,
156            records: Vec::new(),
157        }
158    }
159
160    /// Build a ledger DTO after validating count, ownership and genesis rules.
161    /// Recovery must still establish the persisted format and physical binding.
162    pub fn new(
163        current_generation: u64,
164        records: Vec<AllocationRecord>,
165    ) -> Result<Self, LedgerIntegrityError> {
166        let ledger = Self {
167            current_generation,
168            records,
169        };
170        ledger.validate_integrity()?;
171        Ok(ledger)
172    }
173
174    /// Return the current commit counter; this is not retained upgrade history.
175    #[must_use]
176    pub const fn current_generation(&self) -> u64 {
177        self.current_generation
178    }
179
180    /// Borrow retained ownership records, including omitted and retired stores.
181    #[must_use]
182    pub fn records(&self) -> &[AllocationRecord] {
183        &self.records
184    }
185
186    // Every retained lifecycle state owns its ID, including omitted/retired keys.
187    pub(crate) fn occupied_slots(&self) -> [bool; crate::constants::MAX_ALLOCATIONS] {
188        let mut occupied = [false; crate::constants::MAX_ALLOCATIONS];
189        for record in &self.records {
190            occupied[usize::from(record.slot.id())] = true;
191        }
192        occupied
193    }
194}