ic_memory/ledger/record.rs
1use super::{AllocationRetirementError, LedgerIntegrityError};
2use crate::{
3 declaration::{AllocationDeclaration, DeclarationSnapshotError, validate_runtime_fingerprint},
4 key::StableKey,
5 schema::{SchemaMetadata, SchemaMetadataError},
6 slot::MemoryManagerSlot,
7};
8use serde::{Deserialize, Serialize};
9
10///
11/// AllocationLedger
12///
13/// Durable root of allocation history.
14///
15/// Decoded ledgers are input from persistent storage and should be treated as
16/// untrusted until current-format and integrity validation pass. Public
17/// construction goes through [`AllocationLedger::new`], which validates
18/// structural history invariants before returning a value. Use
19/// [`AllocationLedger::new_committed`] when the value should also satisfy the
20/// strict committed-generation chain required by recovery and commit.
21///
22/// Public staging APIs clone this DTO before applying a logical generation;
23/// bootstrap transfers its owned ledger into the same staging implementation.
24/// The ledger contains allocation metadata only, bounded by the number of
25/// stable allocation identities and committed bootstrap generations, not user
26/// collection contents.
27///
28
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30#[serde(deny_unknown_fields)]
31pub struct AllocationLedger {
32 /// Current committed generation selected by recovery.
33 pub(crate) current_generation: u64,
34 /// Historical allocation facts.
35 pub(crate) allocation_history: AllocationHistory,
36}
37
38///
39/// AllocationHistory
40///
41/// Durable allocation records and generation history.
42///
43/// This is the durable DTO embedded in an [`AllocationLedger`]. It records
44/// allocation facts and generation diagnostics; callers should prefer ledger
45/// staging/validation methods over mutating histories directly.
46#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
47#[serde(deny_unknown_fields)]
48pub struct AllocationHistory {
49 /// Stable-key allocation records.
50 #[serde(deserialize_with = "crate::cbor::deserialize_records")]
51 pub(crate) records: Vec<AllocationRecord>,
52 /// Committed generation records.
53 #[serde(deserialize_with = "crate::cbor::deserialize_history")]
54 pub(crate) generations: Vec<GenerationRecord>,
55}
56
57///
58/// AllocationRecord
59///
60/// Durable ownership record for one stable key.
61///
62/// Records are historical facts, not live handles. Fields are private so stale
63/// or invalid ownership state cannot be assembled through public struct
64/// literals; use accessors for diagnostics and ledger methods for mutation.
65///
66
67#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
68#[serde(deny_unknown_fields)]
69pub struct AllocationRecord {
70 /// Stable key that owns the slot.
71 pub(crate) stable_key: StableKey,
72 /// Durable allocation slot owned by the key.
73 pub(crate) slot: MemoryManagerSlot,
74 /// Current allocation lifecycle state.
75 pub(crate) state: AllocationState,
76 /// First committed generation that recorded this allocation.
77 pub(crate) first_generation: u64,
78 /// Latest committed generation that observed this allocation declaration.
79 pub(crate) last_seen_generation: u64,
80 /// Per-generation schema metadata history.
81 #[serde(deserialize_with = "crate::cbor::deserialize_history")]
82 pub(crate) schema_history: Vec<SchemaMetadataRecord>,
83}
84
85///
86/// AllocationRetirement
87///
88/// Explicit request to tombstone one historical allocation identity.
89///
90/// Retirement prevents a stable key from being redeclared. It does not make the
91/// physical slot safe for another active stable key.
92/// Its key and slot are checked during construction and decoding; staging still
93/// verifies the historical assignment and retirement eligibility.
94///
95
96#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
97#[serde(deny_unknown_fields)]
98pub struct AllocationRetirement {
99 /// Stable key being retired.
100 pub(crate) stable_key: StableKey,
101 /// Allocation slot historically owned by the stable key.
102 pub(crate) slot: MemoryManagerSlot,
103}
104
105impl AllocationRetirement {
106 /// Build an explicit retirement request from raw parts.
107 pub fn new(
108 stable_key: impl AsRef<str>,
109 slot: MemoryManagerSlot,
110 ) -> Result<Self, AllocationRetirementError> {
111 let stable_key = StableKey::parse(stable_key).map_err(AllocationRetirementError::Key)?;
112 Ok(Self { stable_key, slot })
113 }
114
115 /// Return the stable key being retired.
116 #[must_use]
117 pub const fn stable_key(&self) -> &StableKey {
118 &self.stable_key
119 }
120
121 /// Return the allocation slot historically owned by the stable key.
122 #[must_use]
123 pub const fn slot(&self) -> &MemoryManagerSlot {
124 &self.slot
125 }
126}
127
128///
129/// AllocationState
130///
131/// Allocation lifecycle state.
132///
133
134#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
135#[serde(deny_unknown_fields)]
136pub enum AllocationState {
137 /// Slot is reserved for a future allocation identity.
138 Reserved,
139 /// Slot is active and may be opened after validation.
140 Active,
141 /// Slot was explicitly retired and remains tombstoned.
142 Retired {
143 /// Committed generation that retired the allocation.
144 generation: u64,
145 },
146}
147
148///
149/// SchemaMetadataRecord
150///
151/// Schema metadata observed in one committed generation.
152///
153/// Schema metadata is diagnostic ledger history. It is validated for bounded
154/// durable encoding, but `ic-memory` does not prove application schema
155/// support or data migration correctness.
156#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
157#[serde(deny_unknown_fields)]
158pub struct SchemaMetadataRecord {
159 /// Generation that declared this schema metadata.
160 pub(crate) generation: u64,
161 /// Schema metadata declared by that generation.
162 pub(crate) schema: SchemaMetadata,
163}
164
165///
166/// GenerationRecord
167///
168/// Diagnostic metadata for one committed ledger generation.
169///
170
171#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
172#[serde(deny_unknown_fields)]
173pub struct GenerationRecord {
174 /// Committed generation number.
175 pub(crate) generation: u64,
176 /// Parent generation.
177 pub(crate) parent_generation: u64,
178 /// Optional binary/runtime fingerprint.
179 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
180 pub(crate) runtime_fingerprint: Option<String>,
181 /// Number of declarations in the generation.
182 pub(crate) declaration_count: u32,
183 /// Optional commit timestamp supplied by the integration layer.
184 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
185 pub(crate) committed_at: Option<u64>,
186}
187
188///
189/// RecoveredLedger
190///
191/// Proof object for an allocation ledger that has crossed physical recovery,
192/// logical payload-envelope routing, current-format checks, and committed
193/// integrity validation.
194///
195/// Recovery checks that physical and logical generations agree before
196/// constructing this proof; both generation accessors report that one value.
197///
198/// This type is not serializable and has no public constructor. It is the
199/// provenance boundary required before declarations can mint pre-commit
200/// [`crate::ValidatedAllocations`].
201///
202
203#[derive(Clone, Debug, Eq, PartialEq)]
204pub struct RecoveredLedger {
205 ledger: AllocationLedger,
206}
207
208impl RecoveredLedger {
209 pub(crate) const fn from_trusted_ledger(ledger: AllocationLedger) -> Self {
210 Self { ledger }
211 }
212
213 /// Borrow the recovered canonical allocation ledger.
214 ///
215 /// The returned ledger is diagnostic/staging state. It is not itself an
216 /// authority token; callers must keep passing the `RecoveredLedger` proof
217 /// across validation boundaries.
218 #[must_use]
219 pub const fn ledger(&self) -> &AllocationLedger {
220 &self.ledger
221 }
222
223 /// Return the selected physical committed generation.
224 /// Recovery has established that it equals the current logical generation.
225 #[must_use]
226 pub const fn physical_generation(&self) -> u64 {
227 self.ledger.current_generation
228 }
229
230 /// Return the recovered ledger's current logical generation.
231 #[must_use]
232 pub const fn current_generation(&self) -> u64 {
233 self.ledger.current_generation
234 }
235
236 pub(crate) fn into_ledger(self) -> AllocationLedger {
237 self.ledger
238 }
239}
240
241impl AllocationHistory {
242 #[cfg(test)]
243 pub(crate) const fn from_parts(
244 records: Vec<AllocationRecord>,
245 generations: Vec<GenerationRecord>,
246 ) -> Self {
247 Self {
248 records,
249 generations,
250 }
251 }
252
253 /// Borrow stable-key allocation records in durable order.
254 #[must_use]
255 pub fn records(&self) -> &[AllocationRecord] {
256 &self.records
257 }
258
259 /// Borrow committed generation records in durable order.
260 #[must_use]
261 pub fn generations(&self) -> &[GenerationRecord] {
262 &self.generations
263 }
264
265 /// Return true when the history has no allocation records and no generation records.
266 #[must_use]
267 pub const fn is_empty(&self) -> bool {
268 self.records.is_empty() && self.generations.is_empty()
269 }
270}
271
272impl SchemaMetadataRecord {
273 /// Build a schema metadata history record after validating the metadata.
274 pub fn new(generation: u64, schema: SchemaMetadata) -> Result<Self, SchemaMetadataError> {
275 schema.validate()?;
276 Ok(Self { generation, schema })
277 }
278
279 /// Return the generation that declared this schema metadata.
280 #[must_use]
281 pub const fn generation(&self) -> u64 {
282 self.generation
283 }
284
285 /// Return the schema metadata declared by that generation.
286 #[must_use]
287 pub const fn schema(&self) -> &SchemaMetadata {
288 &self.schema
289 }
290}
291
292impl GenerationRecord {
293 /// Build a committed generation diagnostic record after validating metadata.
294 pub fn new(
295 generation: u64,
296 parent_generation: u64,
297 runtime_fingerprint: Option<String>,
298 declaration_count: u32,
299 committed_at: Option<u64>,
300 ) -> Result<Self, DeclarationSnapshotError> {
301 validate_runtime_fingerprint(runtime_fingerprint.as_deref())?;
302 Ok(Self {
303 generation,
304 parent_generation,
305 runtime_fingerprint,
306 declaration_count,
307 committed_at,
308 })
309 }
310
311 /// Return the committed generation number.
312 #[must_use]
313 pub const fn generation(&self) -> u64 {
314 self.generation
315 }
316
317 /// Return the parent generation.
318 #[must_use]
319 pub const fn parent_generation(&self) -> u64 {
320 self.parent_generation
321 }
322
323 /// Borrow the optional binary/runtime fingerprint.
324 #[must_use]
325 pub fn runtime_fingerprint(&self) -> Option<&str> {
326 self.runtime_fingerprint.as_deref()
327 }
328
329 /// Return the number of declarations in the generation.
330 #[must_use]
331 pub const fn declaration_count(&self) -> u32 {
332 self.declaration_count
333 }
334
335 /// Return the optional commit timestamp supplied by the integration layer.
336 #[must_use]
337 pub const fn committed_at(&self) -> Option<u64> {
338 self.committed_at
339 }
340}
341
342impl AllocationRecord {
343 // Staging supplies checked schema metadata: active declarations come from
344 // ValidatedAllocations, and raw reservations are validated before mutation.
345 // Copy only persisted fields; declaration labels are not ledger history.
346 fn from_declaration(
347 generation: u64,
348 declaration: &AllocationDeclaration,
349 state: AllocationState,
350 ) -> Self {
351 Self {
352 stable_key: declaration.stable_key.clone(),
353 slot: declaration.slot.clone(),
354 state,
355 first_generation: generation,
356 last_seen_generation: generation,
357 schema_history: vec![SchemaMetadataRecord {
358 generation,
359 schema: declaration.schema.clone(),
360 }],
361 }
362 }
363
364 /// Create an active record from a declaration with validated schema metadata.
365 pub(crate) fn active(generation: u64, declaration: &AllocationDeclaration) -> Self {
366 Self::from_declaration(generation, declaration, AllocationState::Active)
367 }
368
369 /// Create a reserved record from a declaration with validated schema metadata.
370 pub(crate) fn reserved(generation: u64, declaration: &AllocationDeclaration) -> Self {
371 Self::from_declaration(generation, declaration, AllocationState::Reserved)
372 }
373
374 /// Return the stable key that owns this allocation record.
375 #[must_use]
376 pub const fn stable_key(&self) -> &StableKey {
377 &self.stable_key
378 }
379
380 /// Return the durable allocation slot owned by this record.
381 #[must_use]
382 pub const fn slot(&self) -> &MemoryManagerSlot {
383 &self.slot
384 }
385
386 /// Return the current allocation lifecycle state.
387 #[must_use]
388 pub const fn state(&self) -> AllocationState {
389 self.state
390 }
391
392 /// Return the first committed generation that recorded this allocation.
393 #[must_use]
394 pub const fn first_generation(&self) -> u64 {
395 self.first_generation
396 }
397
398 /// Return the latest committed generation that observed this allocation.
399 #[must_use]
400 pub const fn last_seen_generation(&self) -> u64 {
401 self.last_seen_generation
402 }
403
404 /// Return the per-generation schema metadata history.
405 #[must_use]
406 pub fn schema_history(&self) -> &[SchemaMetadataRecord] {
407 &self.schema_history
408 }
409
410 pub(super) fn observe_schema(&mut self, generation: u64, schema: &SchemaMetadata) {
411 self.last_seen_generation = generation;
412
413 let latest_schema = self.schema_history.last().map(|record| &record.schema);
414 if latest_schema != Some(schema) {
415 self.schema_history.push(SchemaMetadataRecord {
416 generation,
417 schema: schema.clone(),
418 });
419 }
420 }
421}
422
423impl AllocationLedger {
424 /// Build the known empty genesis used by runtime bootstrap and diagnostics.
425 pub(crate) fn empty_genesis() -> Self {
426 // An empty history at generation zero satisfies committed integrity
427 // without decoded input or declaration references to validate.
428 Self {
429 current_generation: 0,
430 allocation_history: AllocationHistory::default(),
431 }
432 }
433
434 /// Build a ledger DTO and validate structural ledger invariants.
435 ///
436 /// This constructor validates duplicate records, lifecycle state, record
437 /// generation bounds, and schema metadata records. It does not require a
438 /// complete committed-generation chain. Use
439 /// [`AllocationLedger::new_committed`] when constructing an authoritative
440 /// committed ledger DTO.
441 pub fn new(
442 current_generation: u64,
443 allocation_history: AllocationHistory,
444 ) -> Result<Self, LedgerIntegrityError> {
445 let ledger = Self {
446 current_generation,
447 allocation_history,
448 };
449 ledger.validate_integrity()?;
450 Ok(ledger)
451 }
452
453 /// Build a committed ledger DTO and validate strict committed-history invariants.
454 ///
455 /// This constructor runs the same committed-integrity checks used by
456 /// recovery and commit. Use it when the value should be treated as an
457 /// authoritative committed ledger, not merely as a structurally valid DTO.
458 pub fn new_committed(
459 current_generation: u64,
460 allocation_history: AllocationHistory,
461 ) -> Result<Self, LedgerIntegrityError> {
462 let ledger = Self {
463 current_generation,
464 allocation_history,
465 };
466 ledger.validate_committed_integrity()?;
467 Ok(ledger)
468 }
469
470 /// Return the current committed generation selected by recovery.
471 #[must_use]
472 pub const fn current_generation(&self) -> u64 {
473 self.current_generation
474 }
475
476 /// Return the historical allocation facts.
477 #[must_use]
478 pub const fn allocation_history(&self) -> &AllocationHistory {
479 &self.allocation_history
480 }
481}