Skip to main content

ic_memory/ledger/
mod.rs

1use crate::{
2    declaration::{AllocationDeclaration, DeclarationSnapshotError, validate_runtime_fingerprint},
3    key::{StableKey, StableKeyError},
4    physical::{CommitRecoveryError, DualCommitStore},
5    schema::SchemaMetadata,
6    session::ValidatedAllocations,
7    slot::AllocationSlotDescriptor,
8};
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeSet;
11
12/// Current allocation ledger schema version.
13pub const CURRENT_LEDGER_SCHEMA_VERSION: u32 = 1;
14
15/// Current protected physical ledger format identifier.
16pub const CURRENT_PHYSICAL_FORMAT_ID: u32 = 1;
17
18///
19/// AllocationLedger
20///
21/// Durable root of allocation history.
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23pub struct AllocationLedger {
24    /// Ledger schema version.
25    pub ledger_schema_version: u32,
26    /// Physical encoding format identifier.
27    pub physical_format_id: u32,
28    /// Current committed generation selected by recovery.
29    pub current_generation: u64,
30    /// Historical allocation facts.
31    pub allocation_history: AllocationHistory,
32}
33
34///
35/// AllocationHistory
36///
37/// Durable allocation records and generation history.
38#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
39pub struct AllocationHistory {
40    /// Stable-key allocation records.
41    pub records: Vec<AllocationRecord>,
42    /// Committed generation records.
43    pub generations: Vec<GenerationRecord>,
44}
45
46///
47/// AllocationRecord
48///
49/// Durable ownership record for one stable key.
50#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51pub struct AllocationRecord {
52    /// Stable key that owns the slot.
53    pub stable_key: StableKey,
54    /// Durable allocation slot owned by the key.
55    pub slot: AllocationSlotDescriptor,
56    /// Current allocation lifecycle state.
57    pub state: AllocationState,
58    /// First committed generation that recorded this allocation.
59    pub first_generation: u64,
60    /// Latest committed generation that observed this allocation declaration.
61    pub last_seen_generation: u64,
62    /// Generation that explicitly retired this allocation.
63    pub retired_generation: Option<u64>,
64    /// Per-generation schema metadata history.
65    pub schema_history: Vec<SchemaMetadataRecord>,
66}
67
68///
69/// AllocationRetirement
70///
71/// Explicit request to tombstone one historical allocation identity.
72#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
73pub struct AllocationRetirement {
74    /// Stable key being retired.
75    pub stable_key: StableKey,
76    /// Allocation slot historically owned by the stable key.
77    pub slot: AllocationSlotDescriptor,
78}
79
80impl AllocationRetirement {
81    /// Build an explicit retirement request from raw parts.
82    pub fn new(
83        stable_key: impl AsRef<str>,
84        slot: AllocationSlotDescriptor,
85    ) -> Result<Self, AllocationRetirementError> {
86        Ok(Self {
87            stable_key: StableKey::parse(stable_key).map_err(AllocationRetirementError::Key)?,
88            slot,
89        })
90    }
91}
92
93///
94/// AllocationState
95///
96/// Allocation lifecycle state.
97#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
98pub enum AllocationState {
99    /// Slot is reserved for a future allocation identity.
100    Reserved,
101    /// Slot is active and may be opened after validation.
102    Active,
103    /// Slot was explicitly retired and remains tombstoned.
104    Retired,
105}
106
107///
108/// SchemaMetadataRecord
109///
110/// Schema metadata observed in one committed generation.
111#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
112pub struct SchemaMetadataRecord {
113    /// Generation that declared this schema metadata.
114    pub generation: u64,
115    /// Schema metadata declared by that generation.
116    pub schema: SchemaMetadata,
117}
118
119///
120/// GenerationRecord
121///
122/// Diagnostic metadata for one committed ledger generation.
123#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
124pub struct GenerationRecord {
125    /// Committed generation number.
126    pub generation: u64,
127    /// Parent generation, if recorded.
128    pub parent_generation: Option<u64>,
129    /// Optional binary/runtime fingerprint.
130    pub runtime_fingerprint: Option<String>,
131    /// Number of declarations in the generation.
132    pub declaration_count: u32,
133    /// Optional commit timestamp supplied by the integration layer.
134    pub committed_at: Option<u64>,
135}
136
137///
138/// LedgerCompatibility
139///
140/// Supported logical and physical ledger format versions.
141#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
142pub struct LedgerCompatibility {
143    /// Minimum supported ledger schema version.
144    pub min_ledger_schema_version: u32,
145    /// Maximum supported ledger schema version.
146    pub max_ledger_schema_version: u32,
147    /// Required physical encoding format identifier.
148    pub physical_format_id: u32,
149}
150
151impl LedgerCompatibility {
152    /// Return the compatibility supported by this crate version.
153    #[must_use]
154    pub const fn current() -> Self {
155        Self {
156            min_ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
157            max_ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
158            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
159        }
160    }
161
162    /// Validate a decoded ledger before it is used as authoritative state.
163    pub const fn validate(
164        &self,
165        ledger: &AllocationLedger,
166    ) -> Result<(), LedgerCompatibilityError> {
167        if ledger.ledger_schema_version < self.min_ledger_schema_version {
168            return Err(LedgerCompatibilityError::UnsupportedLedgerSchemaVersion {
169                found: ledger.ledger_schema_version,
170                min_supported: self.min_ledger_schema_version,
171                max_supported: self.max_ledger_schema_version,
172            });
173        }
174        if ledger.ledger_schema_version > self.max_ledger_schema_version {
175            return Err(LedgerCompatibilityError::UnsupportedLedgerSchemaVersion {
176                found: ledger.ledger_schema_version,
177                min_supported: self.min_ledger_schema_version,
178                max_supported: self.max_ledger_schema_version,
179            });
180        }
181        if ledger.physical_format_id != self.physical_format_id {
182            return Err(LedgerCompatibilityError::UnsupportedPhysicalFormat {
183                found: ledger.physical_format_id,
184                supported: self.physical_format_id,
185            });
186        }
187        Ok(())
188    }
189}
190
191impl Default for LedgerCompatibility {
192    fn default() -> Self {
193        Self::current()
194    }
195}
196
197///
198/// LedgerCompatibilityError
199///
200/// Decoded ledger format is unsupported by this reader.
201#[derive(Clone, Copy, Debug, Eq, thiserror::Error, PartialEq)]
202pub enum LedgerCompatibilityError {
203    /// Ledger schema version is outside the supported range.
204    #[error(
205        "ledger_schema_version {found} is unsupported; supported range is {min_supported}-{max_supported}"
206    )]
207    UnsupportedLedgerSchemaVersion {
208        /// Version found in the ledger.
209        found: u32,
210        /// Minimum supported version.
211        min_supported: u32,
212        /// Maximum supported version.
213        max_supported: u32,
214    },
215    /// Physical format ID is not supported by this reader.
216    #[error("physical_format_id {found} is unsupported; supported format is {supported}")]
217    UnsupportedPhysicalFormat {
218        /// Format found in the ledger.
219        found: u32,
220        /// Supported format ID.
221        supported: u32,
222    },
223}
224
225///
226/// LedgerIntegrityError
227///
228/// Decoded ledger violates structural allocation-history invariants.
229#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
230pub enum LedgerIntegrityError {
231    /// Stable key appears in more than one allocation record.
232    #[error("stable key '{stable_key}' appears in more than one allocation record")]
233    DuplicateStableKey {
234        /// Duplicate stable key.
235        stable_key: StableKey,
236    },
237    /// Allocation slot appears in more than one allocation record.
238    #[error("allocation slot '{slot:?}' appears in more than one allocation record")]
239    DuplicateSlot {
240        /// Duplicate allocation slot.
241        slot: Box<AllocationSlotDescriptor>,
242    },
243    /// Allocation record generation ordering is invalid.
244    #[error("stable key '{stable_key}' has first_generation after last_seen_generation")]
245    InvalidRecordGenerationOrder {
246        /// Stable key whose record is invalid.
247        stable_key: StableKey,
248        /// First generation in the record.
249        first_generation: u64,
250        /// Last seen generation in the record.
251        last_seen_generation: u64,
252    },
253    /// Allocation record points past the current generation.
254    #[error(
255        "stable key '{stable_key}' references generation {generation} after current generation {current_generation}"
256    )]
257    FutureRecordGeneration {
258        /// Stable key whose record is invalid.
259        stable_key: StableKey,
260        /// Generation referenced by the record.
261        generation: u64,
262        /// Current ledger generation.
263        current_generation: u64,
264    },
265    /// Non-retired allocation carries retired metadata.
266    #[error("stable key '{stable_key}' is not retired but has retired_generation metadata")]
267    UnexpectedRetiredGeneration {
268        /// Stable key whose record is invalid.
269        stable_key: StableKey,
270    },
271    /// Retired allocation is missing retired metadata.
272    #[error("stable key '{stable_key}' is retired but retired_generation is missing")]
273    MissingRetiredGeneration {
274        /// Stable key whose record is invalid.
275        stable_key: StableKey,
276    },
277    /// Retired generation predates the allocation record.
278    #[error("stable key '{stable_key}' has retired_generation before first_generation")]
279    RetiredBeforeFirstGeneration {
280        /// Stable key whose record is invalid.
281        stable_key: StableKey,
282        /// First generation in the record.
283        first_generation: u64,
284        /// Retired generation in the record.
285        retired_generation: u64,
286    },
287    /// Allocation record has no schema metadata history.
288    #[error("stable key '{stable_key}' has empty schema metadata history")]
289    EmptySchemaHistory {
290        /// Stable key whose record is invalid.
291        stable_key: StableKey,
292    },
293    /// Schema metadata generation history is not strictly increasing.
294    #[error("stable key '{stable_key}' has non-increasing schema metadata generation history")]
295    NonIncreasingSchemaHistory {
296        /// Stable key whose record is invalid.
297        stable_key: StableKey,
298    },
299    /// Schema metadata generation is outside the allocation record lifetime.
300    #[error("stable key '{stable_key}' has schema metadata generation outside the ledger bounds")]
301    SchemaHistoryOutOfBounds {
302        /// Stable key whose record is invalid.
303        stable_key: StableKey,
304        /// Schema metadata generation.
305        generation: u64,
306    },
307    /// Generation record appears more than once.
308    #[error("generation {generation} appears more than once")]
309    DuplicateGeneration {
310        /// Duplicate generation.
311        generation: u64,
312    },
313    /// Generation record points past the current generation.
314    #[error("generation {generation} is after current generation {current_generation}")]
315    FutureGeneration {
316        /// Generation record value.
317        generation: u64,
318        /// Current ledger generation.
319        current_generation: u64,
320    },
321    /// Generation parent does not precede the child generation.
322    #[error("generation {generation} has invalid parent generation {parent_generation:?}")]
323    InvalidParentGeneration {
324        /// Generation record value.
325        generation: u64,
326        /// Invalid parent generation.
327        parent_generation: Option<u64>,
328    },
329    /// Current ledger generation has no committed generation record.
330    #[error("current generation {current_generation} has no committed generation record")]
331    MissingCurrentGenerationRecord {
332        /// Current ledger generation.
333        current_generation: u64,
334    },
335    /// Generation records are not strictly increasing in durable order.
336    #[error("generation records are not strictly increasing at generation {generation}")]
337    NonIncreasingGenerationRecords {
338        /// Non-increasing generation.
339        generation: u64,
340    },
341    /// Generation record parent does not match the previous committed generation.
342    #[error(
343        "generation {generation} does not link to previous committed generation {expected_parent:?}"
344    )]
345    BrokenGenerationChain {
346        /// Generation whose parent link is invalid.
347        generation: u64,
348        /// Expected parent generation.
349        expected_parent: Option<u64>,
350        /// Actual parent generation.
351        actual_parent: Option<u64>,
352    },
353    /// Allocation record refers to a generation absent from committed history.
354    #[error("stable key '{stable_key}' references unknown generation {generation}")]
355    UnknownRecordGeneration {
356        /// Stable key whose record is invalid.
357        stable_key: StableKey,
358        /// Unknown generation.
359        generation: u64,
360    },
361    /// Generation diagnostic metadata is invalid.
362    #[error(transparent)]
363    DiagnosticMetadata(DeclarationSnapshotError),
364}
365
366///
367/// LedgerCodec
368///
369/// Integration-supplied encoding for persisted allocation ledgers.
370pub trait LedgerCodec {
371    /// Encoding or decoding error type.
372    type Error;
373
374    /// Encode a logical allocation ledger into durable bytes.
375    fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error>;
376
377    /// Decode durable bytes into a logical allocation ledger.
378    fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error>;
379}
380
381///
382/// LedgerCommitStore
383///
384/// Generation-scoped allocation ledger commit store.
385///
386/// This type owns the generic commit lifecycle. It deliberately does not own
387/// serialization or stable-memory IO; those remain substrate/integration
388/// responsibilities.
389#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
390pub struct LedgerCommitStore {
391    /// Protected physical commit slots.
392    pub physical: DualCommitStore,
393}
394
395impl LedgerCommitStore {
396    /// Recover the authoritative allocation ledger using `codec`.
397    pub fn recover<C: LedgerCodec>(
398        &self,
399        codec: &C,
400    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
401        self.recover_with_compatibility(codec, LedgerCompatibility::current())
402    }
403
404    /// Recover the authoritative allocation ledger using explicit compatibility rules.
405    pub fn recover_with_compatibility<C: LedgerCodec>(
406        &self,
407        codec: &C,
408        compatibility: LedgerCompatibility,
409    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
410        let committed = self
411            .physical
412            .authoritative()
413            .map_err(LedgerCommitError::Recovery)?;
414        let ledger = codec
415            .decode(&committed.payload)
416            .map_err(LedgerCommitError::Codec)?;
417        if committed.generation != ledger.current_generation {
418            return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
419                physical_generation: committed.generation,
420                logical_generation: ledger.current_generation,
421            });
422        }
423        compatibility
424            .validate(&ledger)
425            .map_err(LedgerCommitError::Compatibility)?;
426        ledger
427            .validate_committed_integrity()
428            .map_err(LedgerCommitError::Integrity)?;
429        Ok(ledger)
430    }
431
432    /// Recover the authoritative ledger, or explicitly initialize an empty store.
433    ///
434    /// Initialization is allowed only when no physical commit slot has ever
435    /// been written. Corrupt or partially written stores fail closed even when
436    /// a genesis ledger is supplied.
437    pub fn recover_or_initialize<C: LedgerCodec>(
438        &mut self,
439        codec: &C,
440        genesis: &AllocationLedger,
441    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
442        self.recover_or_initialize_with_compatibility(
443            codec,
444            genesis,
445            LedgerCompatibility::current(),
446        )
447    }
448
449    /// Recover the authoritative ledger, or initialize an empty store with explicit compatibility.
450    pub fn recover_or_initialize_with_compatibility<C: LedgerCodec>(
451        &mut self,
452        codec: &C,
453        genesis: &AllocationLedger,
454        compatibility: LedgerCompatibility,
455    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
456        match self.recover_with_compatibility(codec, compatibility) {
457            Ok(ledger) => Ok(ledger),
458            Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration))
459                if self.physical.is_uninitialized() =>
460            {
461                self.commit_with_compatibility(genesis, codec, compatibility)
462            }
463            Err(err) => Err(err),
464        }
465    }
466
467    /// Commit one logical allocation ledger generation through `codec`.
468    pub fn commit<C: LedgerCodec>(
469        &mut self,
470        ledger: &AllocationLedger,
471        codec: &C,
472    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
473        self.commit_with_compatibility(ledger, codec, LedgerCompatibility::current())
474    }
475
476    /// Commit one logical allocation ledger generation through explicit compatibility.
477    pub fn commit_with_compatibility<C: LedgerCodec>(
478        &mut self,
479        ledger: &AllocationLedger,
480        codec: &C,
481        compatibility: LedgerCompatibility,
482    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
483        compatibility
484            .validate(ledger)
485            .map_err(LedgerCommitError::Compatibility)?;
486        ledger
487            .validate_committed_integrity()
488            .map_err(LedgerCommitError::Integrity)?;
489        let payload = codec.encode(ledger).map_err(LedgerCommitError::Codec)?;
490        self.physical
491            .commit_payload_at_generation(ledger.current_generation, payload)
492            .map_err(LedgerCommitError::Recovery)?;
493        self.recover_with_compatibility(codec, compatibility)
494    }
495
496    /// Simulate a torn write of a logical ledger payload into the inactive slot.
497    pub fn write_corrupt_inactive_ledger<C: LedgerCodec>(
498        &mut self,
499        ledger: &AllocationLedger,
500        codec: &C,
501    ) -> Result<(), LedgerCommitError<C::Error>> {
502        let payload = codec.encode(ledger).map_err(LedgerCommitError::Codec)?;
503        self.physical
504            .write_corrupt_inactive_slot(ledger.current_generation, payload);
505        Ok(())
506    }
507}
508
509///
510/// LedgerCommitError
511///
512/// Failure to recover or commit a logical allocation ledger.
513#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
514pub enum LedgerCommitError<E> {
515    /// Protected physical commit recovery failed.
516    #[error(transparent)]
517    Recovery(CommitRecoveryError),
518    /// Physical slot generation and decoded logical ledger generation disagree.
519    #[error(
520        "physical generation {physical_generation} does not match logical ledger generation {logical_generation}"
521    )]
522    PhysicalLogicalGenerationMismatch {
523        /// Generation encoded in the physical commit slot.
524        physical_generation: u64,
525        /// Generation decoded from the logical allocation ledger.
526        logical_generation: u64,
527    },
528    /// Integration-supplied codec failed.
529    #[error("allocation ledger codec failed")]
530    Codec(E),
531    /// Decoded ledger format is not compatible with this reader.
532    #[error(transparent)]
533    Compatibility(LedgerCompatibilityError),
534    /// Decoded ledger violates structural allocation-history invariants.
535    #[error(transparent)]
536    Integrity(LedgerIntegrityError),
537}
538
539///
540/// AllocationStageError
541///
542/// Failure to stage a validated allocation generation.
543#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
544pub enum AllocationStageError {
545    /// Validated declarations were produced against a different ledger generation.
546    #[error(
547        "validated allocations were produced at generation {validated_generation}, but ledger is at generation {ledger_generation}"
548    )]
549    StaleValidatedAllocations {
550        /// Generation carried by the validated allocation session.
551        validated_generation: u64,
552        /// Current ledger generation.
553        ledger_generation: u64,
554    },
555    /// Ledger generation cannot be advanced without overflow.
556    #[error("ledger generation {generation} cannot be advanced without overflow")]
557    GenerationOverflow {
558        /// Current ledger generation.
559        generation: u64,
560    },
561    /// Stable key was historically bound to a different slot.
562    #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
563    StableKeySlotConflict {
564        /// Stable key being declared.
565        stable_key: StableKey,
566        /// Historical slot for the stable key.
567        historical_slot: Box<AllocationSlotDescriptor>,
568        /// Slot claimed by the declaration.
569        declared_slot: Box<AllocationSlotDescriptor>,
570    },
571    /// Slot was historically bound to a different stable key.
572    #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
573    SlotStableKeyConflict {
574        /// Slot being declared.
575        slot: Box<AllocationSlotDescriptor>,
576        /// Historical stable key for the slot.
577        historical_key: StableKey,
578        /// Stable key claimed by the declaration.
579        declared_key: StableKey,
580    },
581    /// Current declaration attempted to revive a retired allocation.
582    #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
583    RetiredAllocation {
584        /// Retired stable key.
585        stable_key: StableKey,
586        /// Retired allocation slot.
587        slot: Box<AllocationSlotDescriptor>,
588    },
589}
590
591///
592/// AllocationReservationError
593///
594/// Failure to stage a reservation generation.
595#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
596pub enum AllocationReservationError {
597    /// Ledger generation cannot be advanced without overflow.
598    #[error("ledger generation {generation} cannot be advanced without overflow")]
599    GenerationOverflow {
600        /// Current ledger generation.
601        generation: u64,
602    },
603    /// Stable key was historically bound to a different slot.
604    #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
605    StableKeySlotConflict {
606        /// Stable key being reserved.
607        stable_key: StableKey,
608        /// Historical slot for the stable key.
609        historical_slot: Box<AllocationSlotDescriptor>,
610        /// Slot claimed by the reservation.
611        reserved_slot: Box<AllocationSlotDescriptor>,
612    },
613    /// Slot was historically bound to a different stable key.
614    #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
615    SlotStableKeyConflict {
616        /// Slot being reserved.
617        slot: Box<AllocationSlotDescriptor>,
618        /// Historical stable key for the slot.
619        historical_key: StableKey,
620        /// Stable key claimed by the reservation.
621        reserved_key: StableKey,
622    },
623    /// Allocation already exists as an active record.
624    #[error("stable key '{stable_key}' is already active and cannot be reserved")]
625    ActiveAllocation {
626        /// Active stable key.
627        stable_key: StableKey,
628        /// Active allocation slot.
629        slot: Box<AllocationSlotDescriptor>,
630    },
631    /// Allocation was already retired and cannot be reserved.
632    #[error("stable key '{stable_key}' was explicitly retired and cannot be reserved")]
633    RetiredAllocation {
634        /// Retired stable key.
635        stable_key: StableKey,
636        /// Retired allocation slot.
637        slot: Box<AllocationSlotDescriptor>,
638    },
639}
640
641///
642/// AllocationRetirementError
643///
644/// Failure to stage an explicit retirement generation.
645#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
646pub enum AllocationRetirementError {
647    /// Stable-key grammar failure.
648    #[error(transparent)]
649    Key(StableKeyError),
650    /// Ledger generation cannot be advanced without overflow.
651    #[error("ledger generation {generation} cannot be advanced without overflow")]
652    GenerationOverflow {
653        /// Current ledger generation.
654        generation: u64,
655    },
656    /// Stable key has no historical allocation record.
657    #[error("stable key '{0}' has no allocation record to retire")]
658    UnknownStableKey(StableKey),
659    /// Stable key was historically bound to a different slot.
660    #[error("stable key '{stable_key}' cannot be retired for a different allocation slot")]
661    SlotMismatch {
662        /// Stable key being retired.
663        stable_key: StableKey,
664        /// Historical slot for the stable key.
665        historical_slot: Box<AllocationSlotDescriptor>,
666        /// Slot named by the retirement request.
667        retired_slot: Box<AllocationSlotDescriptor>,
668    },
669    /// Allocation was already retired.
670    #[error("stable key '{stable_key}' was already retired")]
671    AlreadyRetired {
672        /// Retired stable key.
673        stable_key: StableKey,
674        /// Retired allocation slot.
675        slot: Box<AllocationSlotDescriptor>,
676    },
677}
678
679impl AllocationRecord {
680    /// Create a new allocation record from a declaration.
681    #[must_use]
682    pub fn from_declaration(
683        generation: u64,
684        declaration: AllocationDeclaration,
685        state: AllocationState,
686    ) -> Self {
687        Self {
688            stable_key: declaration.stable_key,
689            slot: declaration.slot,
690            state,
691            first_generation: generation,
692            last_seen_generation: generation,
693            retired_generation: None,
694            schema_history: vec![SchemaMetadataRecord {
695                generation,
696                schema: declaration.schema,
697            }],
698        }
699    }
700
701    /// Create a new reserved allocation record from a declaration.
702    #[must_use]
703    pub fn reserved(generation: u64, declaration: AllocationDeclaration) -> Self {
704        Self::from_declaration(generation, declaration, AllocationState::Reserved)
705    }
706
707    fn observe_declaration(&mut self, generation: u64, declaration: &AllocationDeclaration) {
708        self.last_seen_generation = generation;
709        if self.state == AllocationState::Reserved {
710            self.state = AllocationState::Active;
711        }
712
713        let latest_schema = self.schema_history.last().map(|record| &record.schema);
714        if latest_schema != Some(&declaration.schema) {
715            self.schema_history.push(SchemaMetadataRecord {
716                generation,
717                schema: declaration.schema.clone(),
718            });
719        }
720    }
721
722    fn observe_reservation(&mut self, generation: u64, reservation: &AllocationDeclaration) {
723        self.last_seen_generation = generation;
724
725        let latest_schema = self.schema_history.last().map(|record| &record.schema);
726        if latest_schema != Some(&reservation.schema) {
727            self.schema_history.push(SchemaMetadataRecord {
728                generation,
729                schema: reservation.schema.clone(),
730            });
731        }
732    }
733}
734
735impl AllocationLedger {
736    /// Validate structural ledger invariants before recovery or commit.
737    pub fn validate_integrity(&self) -> Result<(), LedgerIntegrityError> {
738        let mut stable_keys = BTreeSet::new();
739        let mut slots = BTreeSet::new();
740
741        for record in &self.allocation_history.records {
742            if !stable_keys.insert(record.stable_key.clone()) {
743                return Err(LedgerIntegrityError::DuplicateStableKey {
744                    stable_key: record.stable_key.clone(),
745                });
746            }
747            if !slots.insert(record.slot.clone()) {
748                return Err(LedgerIntegrityError::DuplicateSlot {
749                    slot: Box::new(record.slot.clone()),
750                });
751            }
752            validate_record_integrity(self.current_generation, record)?;
753        }
754
755        let mut generations = BTreeSet::new();
756        for generation in &self.allocation_history.generations {
757            if !generations.insert(generation.generation) {
758                return Err(LedgerIntegrityError::DuplicateGeneration {
759                    generation: generation.generation,
760                });
761            }
762            if generation.generation > self.current_generation {
763                return Err(LedgerIntegrityError::FutureGeneration {
764                    generation: generation.generation,
765                    current_generation: self.current_generation,
766                });
767            }
768            if generation
769                .parent_generation
770                .is_some_and(|parent| parent >= generation.generation)
771            {
772                return Err(LedgerIntegrityError::InvalidParentGeneration {
773                    generation: generation.generation,
774                    parent_generation: generation.parent_generation,
775                });
776            }
777        }
778
779        Ok(())
780    }
781
782    /// Validate strict committed-ledger invariants before recovery or commit.
783    ///
784    /// Public durable structs are DTOs: decoded or manually constructed values
785    /// are untrusted until this method succeeds.
786    pub fn validate_committed_integrity(&self) -> Result<(), LedgerIntegrityError> {
787        self.validate_integrity()?;
788
789        if self.current_generation != 0
790            && !self
791                .allocation_history
792                .generations
793                .iter()
794                .any(|record| record.generation == self.current_generation)
795        {
796            return Err(LedgerIntegrityError::MissingCurrentGenerationRecord {
797                current_generation: self.current_generation,
798            });
799        }
800
801        let mut previous = None;
802        let mut known_generations = BTreeSet::new();
803        for generation in &self.allocation_history.generations {
804            validate_runtime_fingerprint(generation.runtime_fingerprint.as_deref())
805                .map_err(LedgerIntegrityError::DiagnosticMetadata)?;
806
807            let expected_generation = previous.map_or(1, |previous| previous + 1);
808            if generation.generation != expected_generation {
809                return Err(LedgerIntegrityError::NonIncreasingGenerationRecords {
810                    generation: generation.generation,
811                });
812            }
813
814            let expected_parent =
815                previous.or_else(|| (generation.parent_generation == Some(0)).then_some(0));
816            if generation.parent_generation != expected_parent {
817                return Err(LedgerIntegrityError::BrokenGenerationChain {
818                    generation: generation.generation,
819                    expected_parent,
820                    actual_parent: generation.parent_generation,
821                });
822            }
823
824            known_generations.insert(generation.generation);
825            previous = Some(generation.generation);
826        }
827
828        for record in &self.allocation_history.records {
829            validate_known_record_generation(
830                &known_generations,
831                &record.stable_key,
832                record.first_generation,
833            )?;
834            validate_known_record_generation(
835                &known_generations,
836                &record.stable_key,
837                record.last_seen_generation,
838            )?;
839            if let Some(retired_generation) = record.retired_generation {
840                validate_known_record_generation(
841                    &known_generations,
842                    &record.stable_key,
843                    retired_generation,
844                )?;
845            }
846            for schema in &record.schema_history {
847                validate_known_record_generation(
848                    &known_generations,
849                    &record.stable_key,
850                    schema.generation,
851                )?;
852            }
853        }
854
855        Ok(())
856    }
857
858    /// Return a copy of the ledger with `validated` recorded as the next generation.
859    ///
860    /// This is a pure logical update. Physical atomicity is the responsibility of
861    /// the substrate commit protocol.
862    pub fn stage_validated_generation(
863        &self,
864        validated: &ValidatedAllocations,
865        committed_at: Option<u64>,
866    ) -> Result<Self, AllocationStageError> {
867        if validated.generation() != self.current_generation {
868            return Err(AllocationStageError::StaleValidatedAllocations {
869                validated_generation: validated.generation(),
870                ledger_generation: self.current_generation,
871            });
872        }
873        let next_generation = checked_next_generation(self.current_generation)
874            .map_err(|generation| AllocationStageError::GenerationOverflow { generation })?;
875        let mut next = self.clone();
876        next.current_generation = next_generation;
877        let declaration_count = u32::try_from(validated.declarations().len()).unwrap_or(u32::MAX);
878
879        for declaration in validated.declarations() {
880            record_declaration(&mut next, next_generation, declaration)?;
881        }
882
883        next.allocation_history.generations.push(GenerationRecord {
884            generation: next_generation,
885            parent_generation: Some(self.current_generation),
886            runtime_fingerprint: validated.runtime_fingerprint().map(str::to_string),
887            declaration_count,
888            committed_at,
889        });
890
891        Ok(next)
892    }
893
894    /// Return a copy of the ledger with `reservations` recorded as the next generation.
895    ///
896    /// This is a pure logical update. The caller is responsible for applying
897    /// framework policy before staging reservations.
898    pub fn stage_reservation_generation(
899        &self,
900        reservations: &[AllocationDeclaration],
901        committed_at: Option<u64>,
902    ) -> Result<Self, AllocationReservationError> {
903        let next_generation = checked_next_generation(self.current_generation)
904            .map_err(|generation| AllocationReservationError::GenerationOverflow { generation })?;
905        let mut next = self.clone();
906        next.current_generation = next_generation;
907
908        for reservation in reservations {
909            record_reservation(&mut next, next_generation, reservation)?;
910        }
911
912        next.allocation_history.generations.push(GenerationRecord {
913            generation: next_generation,
914            parent_generation: Some(self.current_generation),
915            runtime_fingerprint: None,
916            declaration_count: u32::try_from(reservations.len()).unwrap_or(u32::MAX),
917            committed_at,
918        });
919
920        Ok(next)
921    }
922
923    /// Return a copy of the ledger with one explicit retirement committed.
924    pub fn stage_retirement_generation(
925        &self,
926        retirement: &AllocationRetirement,
927        committed_at: Option<u64>,
928    ) -> Result<Self, AllocationRetirementError> {
929        let next_generation = checked_next_generation(self.current_generation)
930            .map_err(|generation| AllocationRetirementError::GenerationOverflow { generation })?;
931        let mut next = self.clone();
932        let record = next
933            .allocation_history
934            .records
935            .iter_mut()
936            .find(|record| record.stable_key == retirement.stable_key)
937            .ok_or_else(|| {
938                AllocationRetirementError::UnknownStableKey(retirement.stable_key.clone())
939            })?;
940
941        if record.slot != retirement.slot {
942            return Err(AllocationRetirementError::SlotMismatch {
943                stable_key: retirement.stable_key.clone(),
944                historical_slot: Box::new(record.slot.clone()),
945                retired_slot: Box::new(retirement.slot.clone()),
946            });
947        }
948        if record.state == AllocationState::Retired {
949            return Err(AllocationRetirementError::AlreadyRetired {
950                stable_key: retirement.stable_key.clone(),
951                slot: Box::new(record.slot.clone()),
952            });
953        }
954
955        record.state = AllocationState::Retired;
956        record.retired_generation = Some(next_generation);
957        next.current_generation = next_generation;
958        next.allocation_history.generations.push(GenerationRecord {
959            generation: next_generation,
960            parent_generation: Some(self.current_generation),
961            runtime_fingerprint: None,
962            declaration_count: 0,
963            committed_at,
964        });
965
966        Ok(next)
967    }
968}
969
970fn record_declaration(
971    ledger: &mut AllocationLedger,
972    generation: u64,
973    declaration: &AllocationDeclaration,
974) -> Result<(), AllocationStageError> {
975    if let Some(record) = ledger
976        .allocation_history
977        .records
978        .iter_mut()
979        .find(|record| record.stable_key == declaration.stable_key)
980    {
981        if record.state == AllocationState::Retired {
982            return Err(AllocationStageError::RetiredAllocation {
983                stable_key: declaration.stable_key.clone(),
984                slot: Box::new(record.slot.clone()),
985            });
986        }
987        if record.slot != declaration.slot {
988            return Err(AllocationStageError::StableKeySlotConflict {
989                stable_key: declaration.stable_key.clone(),
990                historical_slot: Box::new(record.slot.clone()),
991                declared_slot: Box::new(declaration.slot.clone()),
992            });
993        }
994        record.observe_declaration(generation, declaration);
995        return Ok(());
996    }
997
998    if let Some(record) = ledger
999        .allocation_history
1000        .records
1001        .iter()
1002        .find(|record| record.slot == declaration.slot)
1003    {
1004        return Err(AllocationStageError::SlotStableKeyConflict {
1005            slot: Box::new(declaration.slot.clone()),
1006            historical_key: record.stable_key.clone(),
1007            declared_key: declaration.stable_key.clone(),
1008        });
1009    }
1010
1011    ledger
1012        .allocation_history
1013        .records
1014        .push(AllocationRecord::from_declaration(
1015            generation,
1016            declaration.clone(),
1017            AllocationState::Active,
1018        ));
1019    Ok(())
1020}
1021
1022const fn checked_next_generation(current_generation: u64) -> Result<u64, u64> {
1023    match current_generation.checked_add(1) {
1024        Some(next_generation) => Ok(next_generation),
1025        None => Err(current_generation),
1026    }
1027}
1028
1029fn record_reservation(
1030    ledger: &mut AllocationLedger,
1031    generation: u64,
1032    reservation: &AllocationDeclaration,
1033) -> Result<(), AllocationReservationError> {
1034    if let Some(record) = ledger
1035        .allocation_history
1036        .records
1037        .iter_mut()
1038        .find(|record| record.stable_key == reservation.stable_key)
1039    {
1040        if record.slot != reservation.slot {
1041            return Err(AllocationReservationError::StableKeySlotConflict {
1042                stable_key: reservation.stable_key.clone(),
1043                historical_slot: Box::new(record.slot.clone()),
1044                reserved_slot: Box::new(reservation.slot.clone()),
1045            });
1046        }
1047
1048        return match record.state {
1049            AllocationState::Reserved => {
1050                record.observe_reservation(generation, reservation);
1051                Ok(())
1052            }
1053            AllocationState::Active => Err(AllocationReservationError::ActiveAllocation {
1054                stable_key: reservation.stable_key.clone(),
1055                slot: Box::new(record.slot.clone()),
1056            }),
1057            AllocationState::Retired => Err(AllocationReservationError::RetiredAllocation {
1058                stable_key: reservation.stable_key.clone(),
1059                slot: Box::new(record.slot.clone()),
1060            }),
1061        };
1062    }
1063
1064    if let Some(record) = ledger
1065        .allocation_history
1066        .records
1067        .iter()
1068        .find(|record| record.slot == reservation.slot)
1069    {
1070        return Err(AllocationReservationError::SlotStableKeyConflict {
1071            slot: Box::new(reservation.slot.clone()),
1072            historical_key: record.stable_key.clone(),
1073            reserved_key: reservation.stable_key.clone(),
1074        });
1075    }
1076
1077    ledger
1078        .allocation_history
1079        .records
1080        .push(AllocationRecord::reserved(generation, reservation.clone()));
1081    Ok(())
1082}
1083
1084fn validate_record_integrity(
1085    current_generation: u64,
1086    record: &AllocationRecord,
1087) -> Result<(), LedgerIntegrityError> {
1088    if record.first_generation > record.last_seen_generation {
1089        return Err(LedgerIntegrityError::InvalidRecordGenerationOrder {
1090            stable_key: record.stable_key.clone(),
1091            first_generation: record.first_generation,
1092            last_seen_generation: record.last_seen_generation,
1093        });
1094    }
1095    if record.last_seen_generation > current_generation {
1096        return Err(LedgerIntegrityError::FutureRecordGeneration {
1097            stable_key: record.stable_key.clone(),
1098            generation: record.last_seen_generation,
1099            current_generation,
1100        });
1101    }
1102
1103    match (record.state, record.retired_generation) {
1104        (AllocationState::Retired, Some(retired_generation)) => {
1105            if retired_generation < record.first_generation {
1106                return Err(LedgerIntegrityError::RetiredBeforeFirstGeneration {
1107                    stable_key: record.stable_key.clone(),
1108                    first_generation: record.first_generation,
1109                    retired_generation,
1110                });
1111            }
1112            if retired_generation > current_generation {
1113                return Err(LedgerIntegrityError::FutureRecordGeneration {
1114                    stable_key: record.stable_key.clone(),
1115                    generation: retired_generation,
1116                    current_generation,
1117                });
1118            }
1119        }
1120        (AllocationState::Retired, None) => {
1121            return Err(LedgerIntegrityError::MissingRetiredGeneration {
1122                stable_key: record.stable_key.clone(),
1123            });
1124        }
1125        (AllocationState::Reserved | AllocationState::Active, Some(_)) => {
1126            return Err(LedgerIntegrityError::UnexpectedRetiredGeneration {
1127                stable_key: record.stable_key.clone(),
1128            });
1129        }
1130        (AllocationState::Reserved | AllocationState::Active, None) => {}
1131    }
1132
1133    validate_schema_history_integrity(current_generation, record)
1134}
1135
1136fn validate_known_record_generation(
1137    known_generations: &BTreeSet<u64>,
1138    stable_key: &StableKey,
1139    generation: u64,
1140) -> Result<(), LedgerIntegrityError> {
1141    if known_generations.contains(&generation) {
1142        return Ok(());
1143    }
1144    Err(LedgerIntegrityError::UnknownRecordGeneration {
1145        stable_key: stable_key.clone(),
1146        generation,
1147    })
1148}
1149
1150fn validate_schema_history_integrity(
1151    current_generation: u64,
1152    record: &AllocationRecord,
1153) -> Result<(), LedgerIntegrityError> {
1154    if record.schema_history.is_empty() {
1155        return Err(LedgerIntegrityError::EmptySchemaHistory {
1156            stable_key: record.stable_key.clone(),
1157        });
1158    }
1159
1160    let mut previous = None;
1161    for schema in &record.schema_history {
1162        if previous.is_some_and(|generation| schema.generation <= generation) {
1163            return Err(LedgerIntegrityError::NonIncreasingSchemaHistory {
1164                stable_key: record.stable_key.clone(),
1165            });
1166        }
1167        if schema.generation < record.first_generation || schema.generation > current_generation {
1168            return Err(LedgerIntegrityError::SchemaHistoryOutOfBounds {
1169                stable_key: record.stable_key.clone(),
1170                generation: schema.generation,
1171            });
1172        }
1173        previous = Some(schema.generation);
1174    }
1175
1176    Ok(())
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181    use super::*;
1182    use crate::{
1183        declaration::DeclarationSnapshot, physical::CommittedGenerationBytes,
1184        schema::SchemaMetadata,
1185    };
1186    use std::cell::RefCell;
1187
1188    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1189    struct TestCodec;
1190
1191    impl LedgerCodec for TestCodec {
1192        type Error = &'static str;
1193
1194        fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
1195            let mut bytes = Vec::with_capacity(16);
1196            bytes.extend_from_slice(&ledger.ledger_schema_version.to_le_bytes());
1197            bytes.extend_from_slice(&ledger.physical_format_id.to_le_bytes());
1198            bytes.extend_from_slice(&ledger.current_generation.to_le_bytes());
1199            Ok(bytes)
1200        }
1201
1202        fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
1203            let bytes = <[u8; 16]>::try_from(bytes).map_err(|_| "invalid ledger")?;
1204            let ledger_schema_version =
1205                u32::from_le_bytes(bytes[0..4].try_into().map_err(|_| "invalid schema")?);
1206            let physical_format_id =
1207                u32::from_le_bytes(bytes[4..8].try_into().map_err(|_| "invalid format")?);
1208            let current_generation =
1209                u64::from_le_bytes(bytes[8..16].try_into().map_err(|_| "invalid generation")?);
1210            let mut ledger = committed_ledger(current_generation);
1211            ledger.ledger_schema_version = ledger_schema_version;
1212            ledger.physical_format_id = physical_format_id;
1213            Ok(ledger)
1214        }
1215    }
1216
1217    #[derive(Debug, Default)]
1218    struct FullLedgerCodec {
1219        ledgers: RefCell<Vec<AllocationLedger>>,
1220    }
1221
1222    impl LedgerCodec for FullLedgerCodec {
1223        type Error = &'static str;
1224
1225        fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
1226            let mut ledgers = self.ledgers.borrow_mut();
1227            let index = u64::try_from(ledgers.len()).map_err(|_| "too many ledgers")?;
1228            ledgers.push(ledger.clone());
1229            Ok(index.to_le_bytes().to_vec())
1230        }
1231
1232        fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
1233            let bytes = <[u8; 8]>::try_from(bytes).map_err(|_| "invalid ledger index")?;
1234            let index =
1235                usize::try_from(u64::from_le_bytes(bytes)).map_err(|_| "invalid ledger index")?;
1236            self.ledgers
1237                .borrow()
1238                .get(index)
1239                .cloned()
1240                .ok_or("unknown ledger index")
1241        }
1242    }
1243
1244    fn declaration(key: &str, id: u8, schema_version: Option<u32>) -> AllocationDeclaration {
1245        AllocationDeclaration::new(
1246            key,
1247            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
1248            None,
1249            SchemaMetadata {
1250                schema_version,
1251                schema_fingerprint: None,
1252            },
1253        )
1254        .expect("declaration")
1255    }
1256
1257    fn ledger() -> AllocationLedger {
1258        AllocationLedger {
1259            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
1260            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
1261            current_generation: 3,
1262            allocation_history: AllocationHistory::default(),
1263        }
1264    }
1265
1266    fn committed_ledger(current_generation: u64) -> AllocationLedger {
1267        AllocationLedger {
1268            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
1269            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
1270            current_generation,
1271            allocation_history: AllocationHistory {
1272                records: Vec::new(),
1273                generations: (1..=current_generation)
1274                    .map(|generation| GenerationRecord {
1275                        generation,
1276                        parent_generation: if generation == 1 {
1277                            Some(0)
1278                        } else {
1279                            Some(generation - 1)
1280                        },
1281                        runtime_fingerprint: None,
1282                        declaration_count: 0,
1283                        committed_at: None,
1284                    })
1285                    .collect(),
1286            },
1287        }
1288    }
1289
1290    fn active_record(key: &str, id: u8) -> AllocationRecord {
1291        AllocationRecord::from_declaration(1, declaration(key, id, None), AllocationState::Active)
1292    }
1293
1294    fn validated(
1295        generation: u64,
1296        declarations: Vec<AllocationDeclaration>,
1297    ) -> crate::session::ValidatedAllocations {
1298        crate::session::ValidatedAllocations::new(generation, declarations, None)
1299    }
1300
1301    fn record<'ledger>(ledger: &'ledger AllocationLedger, key: &str) -> &'ledger AllocationRecord {
1302        ledger
1303            .allocation_history
1304            .records
1305            .iter()
1306            .find(|record| record.stable_key.as_str() == key)
1307            .expect("allocation record")
1308    }
1309
1310    #[test]
1311    fn stage_validated_generation_records_new_allocations() {
1312        let declarations = vec![declaration("app.users.v1", 100, Some(1))];
1313        let validated = validated(3, declarations);
1314
1315        let staged = ledger()
1316            .stage_validated_generation(&validated, Some(42))
1317            .expect("staged generation");
1318
1319        assert_eq!(staged.current_generation, 4);
1320        assert_eq!(staged.allocation_history.records.len(), 1);
1321        assert_eq!(staged.allocation_history.records[0].first_generation, 4);
1322        assert_eq!(staged.allocation_history.generations[0].generation, 4);
1323        assert_eq!(
1324            staged.allocation_history.generations[0].committed_at,
1325            Some(42)
1326        );
1327    }
1328
1329    #[test]
1330    fn stage_validated_generation_rejects_stale_validated_allocations() {
1331        let validated = validated(2, vec![declaration("app.users.v1", 100, Some(1))]);
1332
1333        let err = ledger()
1334            .stage_validated_generation(&validated, None)
1335            .expect_err("stale validated allocations");
1336
1337        assert_eq!(
1338            err,
1339            AllocationStageError::StaleValidatedAllocations {
1340                validated_generation: 2,
1341                ledger_generation: 3
1342            }
1343        );
1344    }
1345
1346    #[test]
1347    fn stage_validated_generation_rejects_generation_overflow() {
1348        let ledger = AllocationLedger {
1349            current_generation: u64::MAX,
1350            ..ledger()
1351        };
1352        let validated = validated(u64::MAX, vec![declaration("app.users.v1", 100, Some(1))]);
1353
1354        let err = ledger
1355            .stage_validated_generation(&validated, None)
1356            .expect_err("overflow must fail");
1357
1358        assert_eq!(
1359            err,
1360            AllocationStageError::GenerationOverflow {
1361                generation: u64::MAX
1362            }
1363        );
1364    }
1365
1366    #[test]
1367    fn stage_validated_generation_preserves_omitted_records() {
1368        let first = validated(
1369            3,
1370            vec![
1371                declaration("app.users.v1", 100, Some(1)),
1372                declaration("app.orders.v1", 101, Some(1)),
1373            ],
1374        );
1375        let second = validated(4, vec![declaration("app.users.v1", 100, Some(1))]);
1376
1377        let staged = ledger()
1378            .stage_validated_generation(&first, None)
1379            .expect("first generation");
1380        let staged = staged
1381            .stage_validated_generation(&second, None)
1382            .expect("second generation");
1383
1384        assert_eq!(staged.current_generation, 5);
1385        assert_eq!(staged.allocation_history.records.len(), 2);
1386        let omitted = staged
1387            .allocation_history
1388            .records
1389            .iter()
1390            .find(|record| record.stable_key.as_str() == "app.orders.v1")
1391            .expect("omitted record");
1392        assert_eq!(omitted.state, AllocationState::Active);
1393        assert_eq!(omitted.last_seen_generation, 4);
1394    }
1395
1396    #[test]
1397    fn stage_validated_generation_records_schema_metadata_history() {
1398        let first = validated(3, vec![declaration("app.users.v1", 100, Some(1))]);
1399        let second = validated(4, vec![declaration("app.users.v1", 100, Some(2))]);
1400
1401        let staged = ledger()
1402            .stage_validated_generation(&first, None)
1403            .expect("first generation");
1404        let staged = staged
1405            .stage_validated_generation(&second, None)
1406            .expect("second generation");
1407        let record = &staged.allocation_history.records[0];
1408
1409        assert_eq!(record.schema_history.len(), 2);
1410        assert_eq!(record.schema_history[0].generation, 4);
1411        assert_eq!(record.schema_history[1].generation, 5);
1412    }
1413
1414    #[test]
1415    fn stage_reservation_generation_records_reserved_allocations() {
1416        let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
1417
1418        let staged = ledger()
1419            .stage_reservation_generation(&reservations, Some(42))
1420            .expect("reserved generation");
1421
1422        assert_eq!(staged.current_generation, 4);
1423        assert_eq!(staged.allocation_history.records.len(), 1);
1424        assert_eq!(
1425            staged.allocation_history.records[0].state,
1426            AllocationState::Reserved
1427        );
1428        assert_eq!(
1429            staged.allocation_history.generations[0].declaration_count,
1430            1
1431        );
1432    }
1433
1434    #[test]
1435    fn stage_reservation_generation_rejects_generation_overflow() {
1436        let ledger = AllocationLedger {
1437            current_generation: u64::MAX,
1438            ..ledger()
1439        };
1440        let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
1441
1442        let err = ledger
1443            .stage_reservation_generation(&reservations, None)
1444            .expect_err("overflow must fail");
1445
1446        assert_eq!(
1447            err,
1448            AllocationReservationError::GenerationOverflow {
1449                generation: u64::MAX
1450            }
1451        );
1452    }
1453
1454    #[test]
1455    fn stage_reservation_generation_rejects_active_allocation() {
1456        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1457        let staged = ledger()
1458            .stage_validated_generation(&active, None)
1459            .expect("active generation");
1460        let reservations = vec![declaration("app.users.v1", 100, None)];
1461
1462        let err = staged
1463            .stage_reservation_generation(&reservations, None)
1464            .expect_err("active cannot become reserved");
1465
1466        assert!(matches!(
1467            err,
1468            AllocationReservationError::ActiveAllocation { .. }
1469        ));
1470    }
1471
1472    #[test]
1473    fn stage_reservation_generation_rejects_retired_allocation() {
1474        let mut ledger = ledger();
1475        let mut record = active_record("app.users.v1", 100);
1476        record.state = AllocationState::Retired;
1477        record.retired_generation = Some(3);
1478        ledger.allocation_history.records = vec![record];
1479        let reservations = vec![declaration("app.users.v1", 100, None)];
1480
1481        let err = ledger
1482            .stage_reservation_generation(&reservations, None)
1483            .expect_err("retired cannot revive");
1484
1485        assert!(matches!(
1486            err,
1487            AllocationReservationError::RetiredAllocation { .. }
1488        ));
1489    }
1490
1491    #[test]
1492    fn stage_validated_generation_activates_reserved_record() {
1493        let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1494        let staged = ledger()
1495            .stage_reservation_generation(&reservations, None)
1496            .expect("reserved generation");
1497        let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
1498
1499        let staged = staged
1500            .stage_validated_generation(&active, None)
1501            .expect("active generation");
1502        let record = &staged.allocation_history.records[0];
1503
1504        assert_eq!(record.state, AllocationState::Active);
1505        assert_eq!(record.first_generation, 4);
1506        assert_eq!(record.last_seen_generation, 5);
1507        assert_eq!(record.schema_history.len(), 2);
1508    }
1509
1510    #[test]
1511    fn stage_retirement_generation_tombstones_named_allocation() {
1512        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1513        let staged = ledger()
1514            .stage_validated_generation(&active, None)
1515            .expect("active generation");
1516        let retirement = AllocationRetirement::new(
1517            "app.users.v1",
1518            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1519        )
1520        .expect("retirement");
1521
1522        let staged = staged
1523            .stage_retirement_generation(&retirement, Some(42))
1524            .expect("retired generation");
1525        let record = &staged.allocation_history.records[0];
1526
1527        assert_eq!(staged.current_generation, 5);
1528        assert_eq!(record.state, AllocationState::Retired);
1529        assert_eq!(record.retired_generation, Some(5));
1530        assert_eq!(
1531            staged.allocation_history.generations[1].declaration_count,
1532            0
1533        );
1534    }
1535
1536    #[test]
1537    fn stage_retirement_generation_rejects_generation_overflow() {
1538        let mut ledger = ledger();
1539        ledger.current_generation = u64::MAX;
1540        ledger.allocation_history.records = vec![active_record("app.users.v1", 100)];
1541        let retirement = AllocationRetirement::new(
1542            "app.users.v1",
1543            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1544        )
1545        .expect("retirement");
1546
1547        let err = ledger
1548            .stage_retirement_generation(&retirement, None)
1549            .expect_err("overflow must fail");
1550
1551        assert_eq!(
1552            err,
1553            AllocationRetirementError::GenerationOverflow {
1554                generation: u64::MAX
1555            }
1556        );
1557    }
1558
1559    #[test]
1560    fn stage_retirement_generation_requires_matching_slot() {
1561        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1562        let staged = ledger()
1563            .stage_validated_generation(&active, None)
1564            .expect("active generation");
1565        let retirement = AllocationRetirement::new(
1566            "app.users.v1",
1567            AllocationSlotDescriptor::memory_manager(101).expect("usable slot"),
1568        )
1569        .expect("retirement");
1570
1571        let err = staged
1572            .stage_retirement_generation(&retirement, None)
1573            .expect_err("slot mismatch");
1574
1575        assert!(matches!(
1576            err,
1577            AllocationRetirementError::SlotMismatch { .. }
1578        ));
1579    }
1580
1581    #[test]
1582    fn snapshot_can_feed_validated_generation() {
1583        let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1584            .expect("snapshot");
1585        let (declarations, runtime_fingerprint) = snapshot.into_parts();
1586        let validated =
1587            crate::session::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1588
1589        let staged = ledger()
1590            .stage_validated_generation(&validated, None)
1591            .expect("validated generation");
1592
1593        assert_eq!(staged.allocation_history.records.len(), 1);
1594    }
1595
1596    #[test]
1597    fn stage_validated_generation_records_runtime_fingerprint() {
1598        let validated = crate::session::ValidatedAllocations::new(
1599            3,
1600            vec![declaration("app.users.v1", 100, None)],
1601            Some("wasm:abc123".to_string()),
1602        );
1603
1604        let staged = ledger()
1605            .stage_validated_generation(&validated, None)
1606            .expect("validated generation");
1607
1608        assert_eq!(
1609            staged.allocation_history.generations[0].runtime_fingerprint,
1610            Some("wasm:abc123".to_string())
1611        );
1612    }
1613
1614    #[test]
1615    fn strict_committed_integrity_accepts_full_lifecycle() {
1616        let mut ledger = committed_ledger(0);
1617        ledger
1618            .validate_committed_integrity()
1619            .expect("genesis ledger with no history");
1620
1621        ledger = ledger
1622            .stage_validated_generation(
1623                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1624                Some(1),
1625            )
1626            .expect("first real commit after genesis");
1627        ledger
1628            .validate_committed_integrity()
1629            .expect("first real commit");
1630
1631        ledger = ledger
1632            .stage_validated_generation(
1633                &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1634                Some(2),
1635            )
1636            .expect("repeated active declaration");
1637        ledger
1638            .validate_committed_integrity()
1639            .expect("repeated active declaration");
1640        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1641
1642        ledger = ledger
1643            .stage_validated_generation(
1644                &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1645                Some(3),
1646            )
1647            .expect("schema drift");
1648        ledger
1649            .validate_committed_integrity()
1650            .expect("schema metadata drift");
1651        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1652
1653        ledger = ledger
1654            .stage_reservation_generation(
1655                &[declaration("app.future_store.v1", 101, Some(1))],
1656                Some(4),
1657            )
1658            .expect("reservation-only generation");
1659        ledger
1660            .validate_committed_integrity()
1661            .expect("reservation-only generation");
1662        assert_eq!(
1663            record(&ledger, "app.future_store.v1").state,
1664            AllocationState::Reserved
1665        );
1666
1667        ledger = ledger
1668            .stage_validated_generation(
1669                &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1670                Some(5),
1671            )
1672            .expect("reservation activation");
1673        ledger
1674            .validate_committed_integrity()
1675            .expect("reservation activation");
1676        assert_eq!(
1677            record(&ledger, "app.future_store.v1").state,
1678            AllocationState::Active
1679        );
1680
1681        let retirement = AllocationRetirement::new(
1682            "app.users.v1",
1683            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1684        )
1685        .expect("retirement");
1686        ledger = ledger
1687            .stage_retirement_generation(&retirement, Some(6))
1688            .expect("retirement generation");
1689        ledger
1690            .validate_committed_integrity()
1691            .expect("retirement generation");
1692        assert_eq!(ledger.current_generation, 6);
1693        assert_eq!(
1694            record(&ledger, "app.users.v1").state,
1695            AllocationState::Retired
1696        );
1697        assert_eq!(
1698            record(&ledger, "app.future_store.v1").last_seen_generation,
1699            5
1700        );
1701    }
1702
1703    #[test]
1704    fn validate_integrity_rejects_duplicate_stable_keys() {
1705        let mut ledger = ledger();
1706        ledger.allocation_history.records = vec![
1707            active_record("app.users.v1", 100),
1708            active_record("app.users.v1", 101),
1709        ];
1710
1711        let err = ledger.validate_integrity().expect_err("duplicate key");
1712
1713        assert!(matches!(
1714            err,
1715            LedgerIntegrityError::DuplicateStableKey { .. }
1716        ));
1717    }
1718
1719    #[test]
1720    fn validate_integrity_rejects_duplicate_slots() {
1721        let mut ledger = ledger();
1722        ledger.allocation_history.records = vec![
1723            active_record("app.users.v1", 100),
1724            active_record("app.orders.v1", 100),
1725        ];
1726
1727        let err = ledger.validate_integrity().expect_err("duplicate slot");
1728
1729        assert!(matches!(err, LedgerIntegrityError::DuplicateSlot { .. }));
1730    }
1731
1732    #[test]
1733    fn validate_integrity_rejects_retired_record_without_retired_generation() {
1734        let mut ledger = ledger();
1735        let mut record = active_record("app.users.v1", 100);
1736        record.state = AllocationState::Retired;
1737        ledger.allocation_history.records = vec![record];
1738
1739        let err = ledger
1740            .validate_integrity()
1741            .expect_err("missing retired generation");
1742
1743        assert!(matches!(
1744            err,
1745            LedgerIntegrityError::MissingRetiredGeneration { .. }
1746        ));
1747    }
1748
1749    #[test]
1750    fn validate_integrity_rejects_non_retired_record_with_retired_generation() {
1751        let mut ledger = ledger();
1752        let mut record = active_record("app.users.v1", 100);
1753        record.retired_generation = Some(2);
1754        ledger.allocation_history.records = vec![record];
1755
1756        let err = ledger
1757            .validate_integrity()
1758            .expect_err("unexpected retired generation");
1759
1760        assert!(matches!(
1761            err,
1762            LedgerIntegrityError::UnexpectedRetiredGeneration { .. }
1763        ));
1764    }
1765
1766    #[test]
1767    fn validate_integrity_rejects_non_increasing_schema_history() {
1768        let mut ledger = ledger();
1769        let mut record = active_record("app.users.v1", 100);
1770        record.schema_history.push(SchemaMetadataRecord {
1771            generation: 1,
1772            schema: SchemaMetadata::default(),
1773        });
1774        ledger.allocation_history.records = vec![record];
1775
1776        let err = ledger
1777            .validate_integrity()
1778            .expect_err("non-increasing schema history");
1779
1780        assert!(matches!(
1781            err,
1782            LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1783        ));
1784    }
1785
1786    #[test]
1787    fn validate_committed_integrity_requires_current_generation_record() {
1788        let err = ledger()
1789            .validate_committed_integrity()
1790            .expect_err("missing current generation");
1791
1792        assert_eq!(
1793            err,
1794            LedgerIntegrityError::MissingCurrentGenerationRecord {
1795                current_generation: 3
1796            }
1797        );
1798    }
1799
1800    #[test]
1801    fn validate_committed_integrity_rejects_generation_history_gaps() {
1802        let mut ledger = committed_ledger(3);
1803        ledger.allocation_history.generations.remove(1);
1804
1805        let err = ledger
1806            .validate_committed_integrity()
1807            .expect_err("generation history gap");
1808
1809        assert!(matches!(
1810            err,
1811            LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1812        ));
1813    }
1814
1815    #[test]
1816    fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1817        let mut store = LedgerCommitStore::default();
1818        let codec = TestCodec;
1819        let mut invalid = ledger();
1820        invalid.allocation_history.records = vec![
1821            active_record("app.users.v1", 100),
1822            active_record("app.orders.v1", 100),
1823        ];
1824
1825        let err = store.commit(&invalid, &codec).expect_err("invalid ledger");
1826
1827        assert!(matches!(
1828            err,
1829            LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1830        ));
1831        assert!(store.physical.is_uninitialized());
1832    }
1833
1834    #[test]
1835    fn ledger_commit_store_recovers_latest_committed_ledger() {
1836        let mut store = LedgerCommitStore::default();
1837        let codec = TestCodec;
1838        let first = committed_ledger(1);
1839        let second = committed_ledger(2);
1840
1841        store.commit(&first, &codec).expect("first commit");
1842        store.commit(&second, &codec).expect("second commit");
1843        let recovered = store.recover(&codec).expect("recovered ledger");
1844
1845        assert_eq!(recovered.current_generation, 2);
1846    }
1847
1848    #[test]
1849    fn ledger_commit_store_recovers_compatible_genesis_and_first_real_commit() {
1850        let mut store = LedgerCommitStore::default();
1851        let codec = FullLedgerCodec::default();
1852        let genesis = committed_ledger(0);
1853
1854        let recovered = store
1855            .recover_or_initialize(&codec, &genesis)
1856            .expect("compatible genesis ledger");
1857        assert_eq!(recovered.current_generation, 0);
1858        assert!(recovered.allocation_history.generations.is_empty());
1859
1860        let first = recovered
1861            .stage_validated_generation(
1862                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1863                None,
1864            )
1865            .expect("first real generation");
1866        let recovered = store.commit(&first, &codec).expect("first commit");
1867
1868        assert_eq!(recovered.current_generation, 1);
1869        assert_eq!(recovered.allocation_history.generations[0].generation, 1);
1870        assert_eq!(record(&recovered, "app.users.v1").first_generation, 1);
1871    }
1872
1873    #[test]
1874    fn ledger_commit_store_recovers_full_payload_after_corrupt_latest_slot() {
1875        let mut store = LedgerCommitStore::default();
1876        let codec = FullLedgerCodec::default();
1877        let genesis = committed_ledger(0);
1878        store.commit(&genesis, &codec).expect("genesis commit");
1879        let first = genesis
1880            .stage_validated_generation(
1881                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1882                None,
1883            )
1884            .expect("first generation");
1885        let first = store.commit(&first, &codec).expect("first commit");
1886        let second = first
1887            .stage_validated_generation(
1888                &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
1889                None,
1890            )
1891            .expect("second generation");
1892
1893        store
1894            .write_corrupt_inactive_ledger(&second, &codec)
1895            .expect("corrupt latest");
1896        let recovered = store.recover(&codec).expect("recover prior generation");
1897
1898        assert_eq!(recovered.current_generation, 1);
1899        assert_eq!(record(&recovered, "app.users.v1").schema_history.len(), 1);
1900    }
1901
1902    #[test]
1903    fn ledger_commit_store_recovers_identical_duplicate_slots() {
1904        let codec = FullLedgerCodec::default();
1905        let ledger = committed_ledger(0)
1906            .stage_validated_generation(
1907                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1908                None,
1909            )
1910            .expect("first generation");
1911        let payload = codec.encode(&ledger).expect("payload");
1912        let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
1913        let store = LedgerCommitStore {
1914            physical: DualCommitStore {
1915                slot0: Some(committed.clone()),
1916                slot1: Some(committed),
1917            },
1918        };
1919
1920        let recovered = store.recover(&codec).expect("recovered");
1921
1922        assert_eq!(recovered, ledger);
1923    }
1924
1925    #[test]
1926    fn ledger_commit_store_ignores_corrupt_inactive_ledger() {
1927        let mut store = LedgerCommitStore::default();
1928        let codec = TestCodec;
1929        let first = committed_ledger(1);
1930        let second = committed_ledger(2);
1931
1932        store.commit(&first, &codec).expect("first commit");
1933        store
1934            .write_corrupt_inactive_ledger(&second, &codec)
1935            .expect("corrupt write");
1936        let recovered = store.recover(&codec).expect("recovered ledger");
1937
1938        assert_eq!(recovered.current_generation, 1);
1939    }
1940
1941    #[test]
1942    fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
1943        let store = LedgerCommitStore {
1944            physical: DualCommitStore {
1945                slot0: Some(CommittedGenerationBytes::new(
1946                    7,
1947                    TestCodec.encode(&committed_ledger(6)).expect("payload"),
1948                )),
1949                slot1: None,
1950            },
1951        };
1952        let codec = TestCodec;
1953
1954        let err = store.recover(&codec).expect_err("mismatch");
1955
1956        assert_eq!(
1957            err,
1958            LedgerCommitError::PhysicalLogicalGenerationMismatch {
1959                physical_generation: 7,
1960                logical_generation: 6
1961            }
1962        );
1963    }
1964
1965    #[test]
1966    fn ledger_commit_store_rejects_non_next_logical_generation() {
1967        let mut store = LedgerCommitStore::default();
1968        let codec = TestCodec;
1969        store
1970            .commit(&committed_ledger(1), &codec)
1971            .expect("first commit");
1972
1973        let err = store
1974            .commit(&committed_ledger(3), &codec)
1975            .expect_err("skipped generation");
1976
1977        assert_eq!(
1978            err,
1979            LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
1980                expected: 2,
1981                actual: 3
1982            })
1983        );
1984    }
1985
1986    #[test]
1987    fn ledger_commit_store_initializes_empty_store_explicitly() {
1988        let mut store = LedgerCommitStore::default();
1989        let codec = TestCodec;
1990        let genesis = committed_ledger(3);
1991
1992        let recovered = store
1993            .recover_or_initialize(&codec, &genesis)
1994            .expect("initialized ledger");
1995
1996        assert_eq!(recovered.current_generation, 3);
1997        assert!(!store.physical.is_uninitialized());
1998    }
1999
2000    #[test]
2001    fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2002        let mut store = LedgerCommitStore::default();
2003        let codec = TestCodec;
2004        store
2005            .write_corrupt_inactive_ledger(&ledger(), &codec)
2006            .expect("corrupt write");
2007
2008        let err = store
2009            .recover_or_initialize(&codec, &ledger())
2010            .expect_err("corrupt state");
2011
2012        assert!(matches!(
2013            err,
2014            LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)
2015        ));
2016    }
2017
2018    #[test]
2019    fn ledger_commit_store_rejects_incompatible_schema_before_write() {
2020        let mut store = LedgerCommitStore::default();
2021        let codec = TestCodec;
2022        let incompatible = AllocationLedger {
2023            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
2024            ..committed_ledger(0)
2025        };
2026
2027        let err = store
2028            .commit(&incompatible, &codec)
2029            .expect_err("incompatible schema");
2030
2031        assert!(matches!(
2032            err,
2033            LedgerCommitError::Compatibility(
2034                LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
2035            )
2036        ));
2037        assert!(store.physical.is_uninitialized());
2038    }
2039
2040    #[test]
2041    fn ledger_commit_store_rejects_incompatible_schema_on_recovery() {
2042        let mut store = LedgerCommitStore::default();
2043        let codec = TestCodec;
2044        let incompatible = AllocationLedger {
2045            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
2046            ..committed_ledger(3)
2047        };
2048        let payload = codec.encode(&incompatible).expect("payload");
2049        store
2050            .physical
2051            .commit_payload_at_generation(incompatible.current_generation, payload)
2052            .expect("physical commit");
2053
2054        let err = store.recover(&codec).expect_err("incompatible schema");
2055
2056        assert!(matches!(
2057            err,
2058            LedgerCommitError::Compatibility(
2059                LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
2060            )
2061        ));
2062    }
2063
2064    #[test]
2065    fn ledger_commit_store_rejects_incompatible_physical_format() {
2066        let mut store = LedgerCommitStore::default();
2067        let codec = TestCodec;
2068        let incompatible = AllocationLedger {
2069            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID + 1,
2070            ..committed_ledger(0)
2071        };
2072
2073        let err = store
2074            .recover_or_initialize(&codec, &incompatible)
2075            .expect_err("incompatible format");
2076
2077        assert!(matches!(
2078            err,
2079            LedgerCommitError::Compatibility(
2080                LedgerCompatibilityError::UnsupportedPhysicalFormat { .. }
2081            )
2082        ));
2083        assert!(store.physical.is_uninitialized());
2084    }
2085}