Skip to main content

ic_memory/ledger/
mod.rs

1pub(crate) mod claim;
2mod error;
3mod integrity;
4mod record;
5mod stage;
6
7use crate::physical::{CommitRecoveryError, DualCommitStore};
8use serde::{Deserialize, Serialize};
9
10pub use error::{
11    AllocationReservationError, AllocationRetirementError, AllocationStageError, LedgerCommitError,
12    LedgerCompatibilityError, LedgerIntegrityError,
13};
14pub use record::{
15    AllocationHistory, AllocationLedger, AllocationRecord, AllocationRetirement, AllocationState,
16    CURRENT_LEDGER_SCHEMA_VERSION, CURRENT_PHYSICAL_FORMAT_ID, GenerationRecord,
17    LedgerCompatibility, SchemaMetadataRecord,
18};
19
20///
21/// LedgerCodec
22///
23/// Integration-supplied encoding for persisted allocation ledgers.
24///
25/// Decoding returns an untrusted durable DTO. Callers should recover ledgers
26/// through [`LedgerCommitStore`], which checks physical/logical generation,
27/// compatibility, and committed ledger integrity before returning authoritative
28/// state.
29///
30
31pub trait LedgerCodec {
32    /// Encoding or decoding error type.
33    type Error;
34
35    /// Encode a logical allocation ledger into durable bytes.
36    fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error>;
37
38    /// Decode durable bytes into a logical allocation ledger.
39    fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error>;
40}
41
42///
43/// CborLedgerCodec
44///
45/// Native CBOR ledger codec for persisted [`AllocationLedger`] payloads.
46///
47/// This is the default codec for the native IC stack:
48/// `MemoryManager` ID 0 stores an `ic-stable-structures::Cell` containing a
49/// [`crate::StableCellLedgerRecord`], whose [`LedgerCommitStore`] contains
50/// dual protected CBOR-encoded ledger generations.
51///
52
53#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
54pub struct CborLedgerCodec;
55
56impl LedgerCodec for CborLedgerCodec {
57    type Error = serde_cbor::Error;
58
59    fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
60        serde_cbor::to_vec(ledger)
61    }
62
63    fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
64        serde_cbor::from_slice(bytes)
65    }
66}
67
68///
69/// LedgerCommitStore
70///
71/// Generation-scoped allocation ledger commit store.
72///
73/// This type owns the generic commit lifecycle. It deliberately does not own
74/// serialization or stable-memory IO; those remain substrate/integration
75/// responsibilities.
76///
77/// This store commits allocation ledger generations. It does not open
78/// stable-memory handles and does not allocate application slots.
79///
80/// TODO: Move commit/recovery behavior to `ledger::commit` once the staging
81/// split is also mechanical, so the commit tests can move with their fixtures.
82///
83
84#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
85pub struct LedgerCommitStore {
86    /// Protected physical commit slots.
87    physical: DualCommitStore,
88}
89
90impl LedgerCommitStore {
91    /// Borrow the protected physical commit store for diagnostics.
92    #[must_use]
93    pub const fn physical(&self) -> &DualCommitStore {
94        &self.physical
95    }
96
97    #[cfg(test)]
98    pub(crate) const fn physical_mut(&mut self) -> &mut DualCommitStore {
99        &mut self.physical
100    }
101
102    /// Recover the authoritative allocation ledger using `codec`.
103    pub fn recover<C: LedgerCodec>(
104        &self,
105        codec: &C,
106    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
107        self.recover_with_compatibility(codec, LedgerCompatibility::current())
108    }
109
110    /// Recover the authoritative allocation ledger using explicit compatibility rules.
111    pub fn recover_with_compatibility<C: LedgerCodec>(
112        &self,
113        codec: &C,
114        compatibility: LedgerCompatibility,
115    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
116        let committed = self
117            .physical
118            .authoritative()
119            .map_err(LedgerCommitError::Recovery)?;
120        let ledger = codec
121            .decode(committed.payload())
122            .map_err(LedgerCommitError::Codec)?;
123        if committed.generation() != ledger.current_generation {
124            return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
125                physical_generation: committed.generation(),
126                logical_generation: ledger.current_generation,
127            });
128        }
129        compatibility
130            .validate(&ledger)
131            .map_err(LedgerCommitError::Compatibility)?;
132        ledger
133            .validate_committed_integrity()
134            .map_err(LedgerCommitError::Integrity)?;
135        Ok(ledger)
136    }
137
138    /// Recover the authoritative ledger, or explicitly initialize an empty store.
139    ///
140    /// Initialization is allowed only when no physical commit slot has ever
141    /// been written. Corrupt or partially written stores fail closed even when
142    /// a genesis ledger is supplied.
143    pub fn recover_or_initialize<C: LedgerCodec>(
144        &mut self,
145        codec: &C,
146        genesis: &AllocationLedger,
147    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
148        self.recover_or_initialize_with_compatibility(
149            codec,
150            genesis,
151            LedgerCompatibility::current(),
152        )
153    }
154
155    /// Recover the authoritative ledger, or initialize an empty store with explicit compatibility.
156    pub fn recover_or_initialize_with_compatibility<C: LedgerCodec>(
157        &mut self,
158        codec: &C,
159        genesis: &AllocationLedger,
160        compatibility: LedgerCompatibility,
161    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
162        match self.recover_with_compatibility(codec, compatibility) {
163            Ok(ledger) => Ok(ledger),
164            Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration))
165                if self.physical.is_uninitialized() =>
166            {
167                self.commit_with_compatibility(genesis, codec, compatibility)
168            }
169            Err(err) => Err(err),
170        }
171    }
172
173    /// Commit one logical allocation ledger generation through `codec`.
174    pub fn commit<C: LedgerCodec>(
175        &mut self,
176        ledger: &AllocationLedger,
177        codec: &C,
178    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
179        self.commit_with_compatibility(ledger, codec, LedgerCompatibility::current())
180    }
181
182    /// Commit one logical allocation ledger generation through explicit compatibility.
183    pub fn commit_with_compatibility<C: LedgerCodec>(
184        &mut self,
185        ledger: &AllocationLedger,
186        codec: &C,
187        compatibility: LedgerCompatibility,
188    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
189        compatibility
190            .validate(ledger)
191            .map_err(LedgerCommitError::Compatibility)?;
192        ledger
193            .validate_committed_integrity()
194            .map_err(LedgerCommitError::Integrity)?;
195        let payload = codec.encode(ledger).map_err(LedgerCommitError::Codec)?;
196        self.physical
197            .commit_payload_at_generation(ledger.current_generation, payload)
198            .map_err(LedgerCommitError::Recovery)?;
199        self.recover_with_compatibility(codec, compatibility)
200    }
201
202    /// Simulate a torn write of a logical ledger payload into the inactive slot.
203    #[cfg(test)]
204    pub fn write_corrupt_inactive_ledger<C: LedgerCodec>(
205        &mut self,
206        ledger: &AllocationLedger,
207        codec: &C,
208    ) -> Result<(), LedgerCommitError<C::Error>> {
209        let payload = codec.encode(ledger).map_err(LedgerCommitError::Codec)?;
210        self.physical
211            .write_corrupt_inactive_slot(ledger.current_generation, payload);
212        Ok(())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::{
220        declaration::{AllocationDeclaration, DeclarationSnapshot, DeclarationSnapshotError},
221        key::StableKey,
222        physical::CommittedGenerationBytes,
223        schema::{SchemaMetadata, SchemaMetadataError},
224        slot::{AllocationSlotDescriptor, MEMORY_MANAGER_INVALID_ID, MemoryManagerSlotError},
225    };
226    use std::cell::RefCell;
227
228    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
229    struct TestCodec;
230
231    impl LedgerCodec for TestCodec {
232        type Error = &'static str;
233
234        fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
235            let mut bytes = Vec::with_capacity(16);
236            bytes.extend_from_slice(&ledger.ledger_schema_version.to_le_bytes());
237            bytes.extend_from_slice(&ledger.physical_format_id.to_le_bytes());
238            bytes.extend_from_slice(&ledger.current_generation.to_le_bytes());
239            Ok(bytes)
240        }
241
242        fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
243            let bytes = <[u8; 16]>::try_from(bytes).map_err(|_| "invalid ledger")?;
244            let ledger_schema_version =
245                u32::from_le_bytes(bytes[0..4].try_into().map_err(|_| "invalid schema")?);
246            let physical_format_id =
247                u32::from_le_bytes(bytes[4..8].try_into().map_err(|_| "invalid format")?);
248            let current_generation =
249                u64::from_le_bytes(bytes[8..16].try_into().map_err(|_| "invalid generation")?);
250            let mut ledger = committed_ledger(current_generation);
251            ledger.ledger_schema_version = ledger_schema_version;
252            ledger.physical_format_id = physical_format_id;
253            Ok(ledger)
254        }
255    }
256
257    #[derive(Debug, Default)]
258    struct FullLedgerCodec {
259        ledgers: RefCell<Vec<AllocationLedger>>,
260    }
261
262    impl LedgerCodec for FullLedgerCodec {
263        type Error = &'static str;
264
265        fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
266            let mut ledgers = self.ledgers.borrow_mut();
267            let index = u64::try_from(ledgers.len()).map_err(|_| "too many ledgers")?;
268            ledgers.push(ledger.clone());
269            Ok(index.to_le_bytes().to_vec())
270        }
271
272        fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
273            let bytes = <[u8; 8]>::try_from(bytes).map_err(|_| "invalid ledger index")?;
274            let index =
275                usize::try_from(u64::from_le_bytes(bytes)).map_err(|_| "invalid ledger index")?;
276            self.ledgers
277                .borrow()
278                .get(index)
279                .cloned()
280                .ok_or("unknown ledger index")
281        }
282    }
283
284    fn declaration(key: &str, id: u8, schema_version: Option<u32>) -> AllocationDeclaration {
285        AllocationDeclaration::new(
286            key,
287            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
288            None,
289            SchemaMetadata {
290                schema_version,
291                schema_fingerprint: None,
292            },
293        )
294        .expect("declaration")
295    }
296
297    fn invalid_schema_metadata() -> SchemaMetadata {
298        SchemaMetadata {
299            schema_version: Some(0),
300            schema_fingerprint: None,
301        }
302    }
303
304    fn declaration_with_invalid_schema(key: &str, id: u8) -> AllocationDeclaration {
305        let mut declaration = declaration(key, id, Some(1));
306        declaration.schema = invalid_schema_metadata();
307        declaration
308    }
309
310    fn ledger() -> AllocationLedger {
311        AllocationLedger {
312            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
313            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
314            current_generation: 3,
315            allocation_history: AllocationHistory::default(),
316        }
317    }
318
319    fn committed_ledger(current_generation: u64) -> AllocationLedger {
320        AllocationLedger {
321            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
322            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
323            current_generation,
324            allocation_history: AllocationHistory::from_parts(
325                Vec::new(),
326                (1..=current_generation)
327                    .map(|generation| GenerationRecord {
328                        generation,
329                        parent_generation: if generation == 1 {
330                            Some(0)
331                        } else {
332                            Some(generation - 1)
333                        },
334                        runtime_fingerprint: None,
335                        declaration_count: 0,
336                        committed_at: None,
337                    })
338                    .collect(),
339            ),
340        }
341    }
342
343    fn active_record(key: &str, id: u8) -> AllocationRecord {
344        AllocationRecord::from_declaration(1, declaration(key, id, None), AllocationState::Active)
345    }
346
347    fn validated(
348        generation: u64,
349        declarations: Vec<AllocationDeclaration>,
350    ) -> crate::session::ValidatedAllocations {
351        crate::session::ValidatedAllocations::new(generation, declarations, None)
352    }
353
354    fn record<'ledger>(ledger: &'ledger AllocationLedger, key: &str) -> &'ledger AllocationRecord {
355        ledger
356            .allocation_history
357            .records()
358            .iter()
359            .find(|record| record.stable_key.as_str() == key)
360            .expect("allocation record")
361    }
362
363    #[test]
364    fn allocation_history_accessors_expose_read_only_views() {
365        let history = AllocationHistory::from_parts(
366            vec![active_record("app.users.v1", 100)],
367            vec![GenerationRecord::new(1, Some(0), None, 1, Some(42)).expect("generation record")],
368        );
369
370        assert!(!history.is_empty());
371        assert_eq!(history.records().len(), 1);
372        assert_eq!(history.generations().len(), 1);
373        assert_eq!(history.generations()[0].committed_at(), Some(42));
374    }
375
376    #[test]
377    fn record_constructors_validate_metadata() {
378        let schema_err = SchemaMetadataRecord::new(1, invalid_schema_metadata())
379            .expect_err("invalid schema must fail");
380        assert_eq!(schema_err, SchemaMetadataError::InvalidVersion);
381
382        let generation_err = GenerationRecord::new(1, Some(0), Some(String::new()), 0, None)
383            .expect_err("empty fingerprint must fail");
384        assert_eq!(
385            generation_err,
386            DeclarationSnapshotError::EmptyRuntimeFingerprint
387        );
388    }
389
390    #[test]
391    fn cbor_ledger_codec_round_trips_allocation_ledger() {
392        let ledger = committed_ledger(2);
393        let codec = CborLedgerCodec;
394
395        let encoded = codec.encode(&ledger).expect("encode ledger");
396        let decoded = codec.decode(&encoded).expect("decode ledger");
397
398        assert_eq!(decoded, ledger);
399    }
400
401    #[test]
402    fn stage_validated_generation_records_new_allocations() {
403        let declarations = vec![declaration("app.users.v1", 100, Some(1))];
404        let validated = validated(3, declarations);
405
406        let staged = ledger()
407            .stage_validated_generation(&validated, Some(42))
408            .expect("staged generation");
409
410        assert_eq!(staged.current_generation, 4);
411        assert_eq!(staged.allocation_history.records().len(), 1);
412        assert_eq!(staged.allocation_history.records()[0].first_generation, 4);
413        assert_eq!(staged.allocation_history.generations()[0].generation, 4);
414        assert_eq!(
415            staged.allocation_history.generations()[0].committed_at,
416            Some(42)
417        );
418    }
419
420    #[test]
421    fn stage_validated_generation_allows_empty_generation_boundary() {
422        let validated = crate::session::ValidatedAllocations::new(
423            3,
424            Vec::new(),
425            Some("test-runtime".to_string()),
426        );
427
428        let staged = ledger()
429            .stage_validated_generation(&validated, Some(42))
430            .expect("empty validated generation");
431
432        assert_eq!(staged.current_generation, 4);
433        assert!(staged.allocation_history.records().is_empty());
434        assert_eq!(staged.allocation_history.generations().len(), 1);
435        assert_eq!(staged.allocation_history.generations()[0].generation(), 4);
436        assert_eq!(
437            staged.allocation_history.generations()[0].parent_generation(),
438            Some(3)
439        );
440        assert_eq!(
441            staged.allocation_history.generations()[0].runtime_fingerprint(),
442            Some("test-runtime")
443        );
444        assert_eq!(
445            staged.allocation_history.generations()[0].declaration_count(),
446            0
447        );
448        assert_eq!(
449            staged.allocation_history.generations()[0].committed_at(),
450            Some(42)
451        );
452    }
453
454    #[test]
455    fn stage_validated_generation_rejects_stale_validated_allocations() {
456        let validated = validated(2, vec![declaration("app.users.v1", 100, Some(1))]);
457
458        let err = ledger()
459            .stage_validated_generation(&validated, None)
460            .expect_err("stale validated allocations");
461
462        assert_eq!(
463            err,
464            AllocationStageError::StaleValidatedAllocations {
465                validated_generation: 2,
466                ledger_generation: 3
467            }
468        );
469    }
470
471    #[test]
472    fn stage_validated_generation_rejects_invalid_schema_metadata() {
473        let validated = crate::session::ValidatedAllocations::new(
474            3,
475            vec![declaration_with_invalid_schema("app.users.v1", 100)],
476            None,
477        );
478
479        let err = ledger()
480            .stage_validated_generation(&validated, None)
481            .expect_err("invalid schema metadata");
482
483        assert_eq!(
484            err,
485            AllocationStageError::InvalidSchemaMetadata {
486                stable_key: StableKey::parse("app.users.v1").expect("stable key"),
487                error: SchemaMetadataError::InvalidVersion,
488            }
489        );
490    }
491
492    #[test]
493    fn stage_validated_generation_rejects_generation_overflow() {
494        let ledger = AllocationLedger {
495            current_generation: u64::MAX,
496            ..ledger()
497        };
498        let validated = validated(u64::MAX, vec![declaration("app.users.v1", 100, Some(1))]);
499
500        let err = ledger
501            .stage_validated_generation(&validated, None)
502            .expect_err("overflow must fail");
503
504        assert_eq!(
505            err,
506            AllocationStageError::GenerationOverflow {
507                generation: u64::MAX
508            }
509        );
510    }
511
512    #[test]
513    fn stage_validated_generation_rejects_same_key_different_slot() {
514        let mut ledger = ledger();
515        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
516        let validated = validated(3, vec![declaration("app.users.v1", 101, None)]);
517
518        let err = ledger
519            .stage_validated_generation(&validated, None)
520            .expect_err("stable key cannot move slots");
521
522        assert!(matches!(
523            err,
524            AllocationStageError::StableKeySlotConflict { .. }
525        ));
526    }
527
528    #[test]
529    fn stage_validated_generation_rejects_same_slot_different_key() {
530        let mut ledger = ledger();
531        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
532        let validated = validated(3, vec![declaration("app.orders.v1", 100, None)]);
533
534        let err = ledger
535            .stage_validated_generation(&validated, None)
536            .expect_err("slot cannot be reused by another key");
537
538        assert!(matches!(
539            err,
540            AllocationStageError::SlotStableKeyConflict { .. }
541        ));
542    }
543
544    #[test]
545    fn stage_validated_generation_rejects_retired_redeclaration() {
546        let mut ledger = ledger();
547        let mut record = active_record("app.users.v1", 100);
548        record.state = AllocationState::Retired;
549        record.retired_generation = Some(3);
550        *ledger.allocation_history.records_mut() = vec![record];
551        let validated = validated(3, vec![declaration("app.users.v1", 100, None)]);
552
553        let err = ledger
554            .stage_validated_generation(&validated, None)
555            .expect_err("retired allocation cannot be redeclared");
556
557        assert!(matches!(
558            err,
559            AllocationStageError::RetiredAllocation { .. }
560        ));
561    }
562
563    #[test]
564    fn stage_validated_generation_preserves_omitted_records() {
565        let first = validated(
566            3,
567            vec![
568                declaration("app.users.v1", 100, Some(1)),
569                declaration("app.orders.v1", 101, Some(1)),
570            ],
571        );
572        let second = validated(4, vec![declaration("app.users.v1", 100, Some(1))]);
573
574        let staged = ledger()
575            .stage_validated_generation(&first, None)
576            .expect("first generation");
577        let staged = staged
578            .stage_validated_generation(&second, None)
579            .expect("second generation");
580
581        assert_eq!(staged.current_generation, 5);
582        assert_eq!(staged.allocation_history.records().len(), 2);
583        let omitted = staged
584            .allocation_history
585            .records()
586            .iter()
587            .find(|record| record.stable_key.as_str() == "app.orders.v1")
588            .expect("omitted record");
589        assert_eq!(omitted.state, AllocationState::Active);
590        assert_eq!(omitted.last_seen_generation, 4);
591    }
592
593    #[test]
594    fn stage_validated_generation_records_schema_metadata_history() {
595        let first = validated(3, vec![declaration("app.users.v1", 100, Some(1))]);
596        let second = validated(4, vec![declaration("app.users.v1", 100, Some(2))]);
597
598        let staged = ledger()
599            .stage_validated_generation(&first, None)
600            .expect("first generation");
601        let staged = staged
602            .stage_validated_generation(&second, None)
603            .expect("second generation");
604        let record = &staged.allocation_history.records()[0];
605
606        assert_eq!(record.schema_history.len(), 2);
607        assert_eq!(record.schema_history[0].generation, 4);
608        assert_eq!(record.schema_history[1].generation, 5);
609    }
610
611    #[test]
612    fn stage_reservation_generation_records_reserved_allocations() {
613        let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
614
615        let staged = ledger()
616            .stage_reservation_generation(&reservations, Some(42))
617            .expect("reserved generation");
618
619        assert_eq!(staged.current_generation, 4);
620        assert_eq!(staged.allocation_history.records().len(), 1);
621        assert_eq!(
622            staged.allocation_history.records()[0].state,
623            AllocationState::Reserved
624        );
625        assert_eq!(
626            staged.allocation_history.generations()[0].declaration_count,
627            1
628        );
629    }
630
631    #[test]
632    fn stage_reservation_generation_allows_empty_generation_boundary() {
633        let reservations = Vec::new();
634
635        let staged = ledger()
636            .stage_reservation_generation(&reservations, Some(42))
637            .expect("empty reservation generation");
638
639        assert_eq!(staged.current_generation, 4);
640        assert!(staged.allocation_history.records().is_empty());
641        assert_eq!(staged.allocation_history.generations().len(), 1);
642        assert_eq!(staged.allocation_history.generations()[0].generation(), 4);
643        assert_eq!(
644            staged.allocation_history.generations()[0].declaration_count(),
645            0
646        );
647        assert_eq!(
648            staged.allocation_history.generations()[0].committed_at(),
649            Some(42)
650        );
651    }
652
653    #[test]
654    fn stage_reservation_generation_refreshes_existing_reserved_allocation() {
655        let first = vec![declaration("app.future_store.v1", 100, Some(1))];
656        let staged = ledger()
657            .stage_reservation_generation(&first, Some(42))
658            .expect("first reservation generation");
659
660        let second = vec![declaration("app.future_store.v1", 100, Some(2))];
661        let staged = staged
662            .stage_reservation_generation(&second, Some(43))
663            .expect("reservation refresh");
664        let record = record(&staged, "app.future_store.v1");
665
666        assert_eq!(record.state(), AllocationState::Reserved);
667        assert_eq!(record.first_generation(), 4);
668        assert_eq!(record.last_seen_generation(), 5);
669        assert_eq!(record.schema_history().len(), 2);
670        assert_eq!(record.schema_history()[1].generation(), 5);
671        assert_eq!(
672            staged.allocation_history.generations()[1].declaration_count(),
673            1
674        );
675    }
676
677    #[test]
678    fn stage_reservation_generation_rejects_generation_overflow() {
679        let ledger = AllocationLedger {
680            current_generation: u64::MAX,
681            ..ledger()
682        };
683        let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
684
685        let err = ledger
686            .stage_reservation_generation(&reservations, None)
687            .expect_err("overflow must fail");
688
689        assert_eq!(
690            err,
691            AllocationReservationError::GenerationOverflow {
692                generation: u64::MAX
693            }
694        );
695    }
696
697    #[test]
698    fn stage_reservation_generation_rejects_invalid_schema_metadata() {
699        let reservations = vec![declaration_with_invalid_schema(
700            "ic_memory.generation_log.v1",
701            1,
702        )];
703
704        let err = ledger()
705            .stage_reservation_generation(&reservations, None)
706            .expect_err("invalid reservation schema metadata");
707
708        assert_eq!(
709            err,
710            AllocationReservationError::InvalidSchemaMetadata {
711                stable_key: StableKey::parse("ic_memory.generation_log.v1").expect("stable key"),
712                error: SchemaMetadataError::InvalidVersion,
713            }
714        );
715    }
716
717    #[test]
718    fn stage_reservation_generation_rejects_same_key_different_slot() {
719        let mut ledger = ledger();
720        *ledger.allocation_history.records_mut() = vec![AllocationRecord::reserved(
721            3,
722            declaration("app.future_store.v1", 100, None),
723        )];
724        let reservations = vec![declaration("app.future_store.v1", 101, None)];
725
726        let err = ledger
727            .stage_reservation_generation(&reservations, None)
728            .expect_err("reservation key cannot move slots");
729
730        assert!(matches!(
731            err,
732            AllocationReservationError::StableKeySlotConflict { .. }
733        ));
734    }
735
736    #[test]
737    fn stage_reservation_generation_rejects_same_slot_different_key() {
738        let mut ledger = ledger();
739        *ledger.allocation_history.records_mut() = vec![AllocationRecord::reserved(
740            3,
741            declaration("app.future_store.v1", 100, None),
742        )];
743        let reservations = vec![declaration("app.other_future_store.v1", 100, None)];
744
745        let err = ledger
746            .stage_reservation_generation(&reservations, None)
747            .expect_err("reservation slot cannot be reused by another key");
748
749        assert!(matches!(
750            err,
751            AllocationReservationError::SlotStableKeyConflict { .. }
752        ));
753    }
754
755    #[test]
756    fn stage_reservation_generation_rejects_active_allocation() {
757        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
758        let staged = ledger()
759            .stage_validated_generation(&active, None)
760            .expect("active generation");
761        let reservations = vec![declaration("app.users.v1", 100, None)];
762
763        let err = staged
764            .stage_reservation_generation(&reservations, None)
765            .expect_err("active cannot become reserved");
766
767        assert!(matches!(
768            err,
769            AllocationReservationError::ActiveAllocation { .. }
770        ));
771    }
772
773    #[test]
774    fn stage_reservation_generation_rejects_retired_allocation() {
775        let mut ledger = ledger();
776        let mut record = active_record("app.users.v1", 100);
777        record.state = AllocationState::Retired;
778        record.retired_generation = Some(3);
779        *ledger.allocation_history.records_mut() = vec![record];
780        let reservations = vec![declaration("app.users.v1", 100, None)];
781
782        let err = ledger
783            .stage_reservation_generation(&reservations, None)
784            .expect_err("retired cannot revive");
785
786        assert!(matches!(
787            err,
788            AllocationReservationError::RetiredAllocation { .. }
789        ));
790    }
791
792    #[test]
793    fn stage_validated_generation_activates_reserved_record() {
794        let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
795        let staged = ledger()
796            .stage_reservation_generation(&reservations, None)
797            .expect("reserved generation");
798        let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
799
800        let staged = staged
801            .stage_validated_generation(&active, None)
802            .expect("active generation");
803        let record = &staged.allocation_history.records()[0];
804
805        assert_eq!(record.state, AllocationState::Active);
806        assert_eq!(record.first_generation, 4);
807        assert_eq!(record.last_seen_generation, 5);
808        assert_eq!(record.schema_history.len(), 2);
809    }
810
811    #[test]
812    fn stage_retirement_generation_tombstones_named_allocation() {
813        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
814        let staged = ledger()
815            .stage_validated_generation(&active, None)
816            .expect("active generation");
817        let retirement = AllocationRetirement::new(
818            "app.users.v1",
819            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
820        )
821        .expect("retirement");
822
823        let staged = staged
824            .stage_retirement_generation(&retirement, Some(42))
825            .expect("retired generation");
826        let record = &staged.allocation_history.records()[0];
827
828        assert_eq!(staged.current_generation, 5);
829        assert_eq!(record.state, AllocationState::Retired);
830        assert_eq!(record.retired_generation, Some(5));
831        assert_eq!(
832            staged.allocation_history.generations()[1].declaration_count,
833            0
834        );
835    }
836
837    #[test]
838    fn stage_retirement_generation_tombstones_reserved_allocation() {
839        let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
840        let staged = ledger()
841            .stage_reservation_generation(&reservations, None)
842            .expect("reserved generation");
843        let retirement = AllocationRetirement::new(
844            "app.future_store.v1",
845            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
846        )
847        .expect("retirement");
848
849        let staged = staged
850            .stage_retirement_generation(&retirement, Some(42))
851            .expect("reserved retirement generation");
852        let record = &staged.allocation_history.records()[0];
853
854        assert_eq!(staged.current_generation, 5);
855        assert_eq!(record.state, AllocationState::Retired);
856        assert_eq!(record.first_generation, 4);
857        assert_eq!(record.retired_generation, Some(5));
858        assert_eq!(
859            staged.allocation_history.generations()[1].declaration_count(),
860            0
861        );
862    }
863
864    #[test]
865    fn stage_retirement_generation_rejects_generation_overflow() {
866        let mut ledger = ledger();
867        ledger.current_generation = u64::MAX;
868        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
869        let retirement = AllocationRetirement::new(
870            "app.users.v1",
871            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
872        )
873        .expect("retirement");
874
875        let err = ledger
876            .stage_retirement_generation(&retirement, None)
877            .expect_err("overflow must fail");
878
879        assert_eq!(
880            err,
881            AllocationRetirementError::GenerationOverflow {
882                generation: u64::MAX
883            }
884        );
885    }
886
887    #[test]
888    fn stage_retirement_generation_requires_matching_slot() {
889        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
890        let staged = ledger()
891            .stage_validated_generation(&active, None)
892            .expect("active generation");
893        let retirement = AllocationRetirement::new(
894            "app.users.v1",
895            AllocationSlotDescriptor::memory_manager(101).expect("usable slot"),
896        )
897        .expect("retirement");
898
899        let err = staged
900            .stage_retirement_generation(&retirement, None)
901            .expect_err("slot mismatch");
902
903        assert!(matches!(
904            err,
905            AllocationRetirementError::SlotMismatch { .. }
906        ));
907    }
908
909    #[test]
910    fn snapshot_can_feed_validated_generation() {
911        let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
912            .expect("snapshot");
913        let (declarations, runtime_fingerprint) = snapshot.into_parts();
914        let validated =
915            crate::session::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
916
917        let staged = ledger()
918            .stage_validated_generation(&validated, None)
919            .expect("validated generation");
920
921        assert_eq!(staged.allocation_history.records().len(), 1);
922    }
923
924    #[test]
925    fn stage_validated_generation_records_runtime_fingerprint() {
926        let validated = crate::session::ValidatedAllocations::new(
927            3,
928            vec![declaration("app.users.v1", 100, None)],
929            Some("wasm:abc123".to_string()),
930        );
931
932        let staged = ledger()
933            .stage_validated_generation(&validated, None)
934            .expect("validated generation");
935
936        assert_eq!(
937            staged.allocation_history.generations()[0].runtime_fingerprint,
938            Some("wasm:abc123".to_string())
939        );
940    }
941
942    #[test]
943    fn strict_committed_integrity_accepts_full_lifecycle() {
944        let mut ledger = committed_ledger(0);
945        ledger
946            .validate_committed_integrity()
947            .expect("genesis ledger with no history");
948
949        ledger = ledger
950            .stage_validated_generation(
951                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
952                Some(1),
953            )
954            .expect("first real commit after genesis");
955        ledger
956            .validate_committed_integrity()
957            .expect("first real commit");
958
959        ledger = ledger
960            .stage_validated_generation(
961                &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
962                Some(2),
963            )
964            .expect("repeated active declaration");
965        ledger
966            .validate_committed_integrity()
967            .expect("repeated active declaration");
968        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
969
970        ledger = ledger
971            .stage_validated_generation(
972                &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
973                Some(3),
974            )
975            .expect("schema drift");
976        ledger
977            .validate_committed_integrity()
978            .expect("schema metadata drift");
979        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
980
981        ledger = ledger
982            .stage_reservation_generation(
983                &[declaration("app.future_store.v1", 101, Some(1))],
984                Some(4),
985            )
986            .expect("reservation-only generation");
987        ledger
988            .validate_committed_integrity()
989            .expect("reservation-only generation");
990        assert_eq!(
991            record(&ledger, "app.future_store.v1").state,
992            AllocationState::Reserved
993        );
994
995        ledger = ledger
996            .stage_validated_generation(
997                &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
998                Some(5),
999            )
1000            .expect("reservation activation");
1001        ledger
1002            .validate_committed_integrity()
1003            .expect("reservation activation");
1004        assert_eq!(
1005            record(&ledger, "app.future_store.v1").state,
1006            AllocationState::Active
1007        );
1008
1009        let retirement = AllocationRetirement::new(
1010            "app.users.v1",
1011            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1012        )
1013        .expect("retirement");
1014        ledger = ledger
1015            .stage_retirement_generation(&retirement, Some(6))
1016            .expect("retirement generation");
1017        ledger
1018            .validate_committed_integrity()
1019            .expect("retirement generation");
1020        assert_eq!(ledger.current_generation, 6);
1021        assert_eq!(
1022            record(&ledger, "app.users.v1").state,
1023            AllocationState::Retired
1024        );
1025        assert_eq!(
1026            record(&ledger, "app.future_store.v1").last_seen_generation,
1027            5
1028        );
1029    }
1030
1031    #[test]
1032    fn new_committed_requires_strict_generation_history() {
1033        let structurally_valid = AllocationLedger::new(
1034            CURRENT_LEDGER_SCHEMA_VERSION,
1035            CURRENT_PHYSICAL_FORMAT_ID,
1036            3,
1037            AllocationHistory::default(),
1038        )
1039        .expect("structurally valid DTO");
1040
1041        assert_eq!(structurally_valid.current_generation, 3);
1042
1043        let err = AllocationLedger::new_committed(
1044            CURRENT_LEDGER_SCHEMA_VERSION,
1045            CURRENT_PHYSICAL_FORMAT_ID,
1046            3,
1047            AllocationHistory::default(),
1048        )
1049        .expect_err("committed ledger needs generation history");
1050
1051        assert_eq!(
1052            err,
1053            LedgerIntegrityError::MissingCurrentGenerationRecord {
1054                current_generation: 3
1055            }
1056        );
1057    }
1058
1059    #[test]
1060    fn validate_integrity_rejects_duplicate_stable_keys() {
1061        let mut ledger = ledger();
1062        *ledger.allocation_history.records_mut() = vec![
1063            active_record("app.users.v1", 100),
1064            active_record("app.users.v1", 101),
1065        ];
1066
1067        let err = ledger.validate_integrity().expect_err("duplicate key");
1068
1069        assert!(matches!(
1070            err,
1071            LedgerIntegrityError::DuplicateStableKey { .. }
1072        ));
1073    }
1074
1075    #[test]
1076    fn validate_integrity_rejects_duplicate_slots() {
1077        let mut ledger = ledger();
1078        *ledger.allocation_history.records_mut() = vec![
1079            active_record("app.users.v1", 100),
1080            active_record("app.orders.v1", 100),
1081        ];
1082
1083        let err = ledger.validate_integrity().expect_err("duplicate slot");
1084
1085        assert!(matches!(err, LedgerIntegrityError::DuplicateSlot { .. }));
1086    }
1087
1088    #[test]
1089    fn validate_committed_integrity_rejects_decoded_invalid_stable_key() {
1090        let mut ledger = committed_ledger(1);
1091        ledger
1092            .allocation_history
1093            .records
1094            .push(active_record("app.users.v1", 100));
1095        let mut bytes = serde_cbor::to_vec(&ledger).expect("encode ledger");
1096        let key_start = bytes
1097            .windows(b"app.users.v1".len())
1098            .position(|window| window == b"app.users.v1")
1099            .expect("encoded stable key");
1100        bytes[key_start] = b'A';
1101        let decoded: AllocationLedger = serde_cbor::from_slice(&bytes).expect("decode ledger");
1102
1103        let err = decoded
1104            .validate_committed_integrity()
1105            .expect_err("invalid decoded key must fail");
1106
1107        assert!(matches!(err, LedgerIntegrityError::InvalidStableKey(_)));
1108    }
1109
1110    #[test]
1111    fn validate_committed_integrity_rejects_decoded_invalid_memory_manager_slot() {
1112        let mut ledger = committed_ledger(1);
1113        let mut record = active_record("app.users.v1", 100);
1114        record.slot = AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1115        ledger.allocation_history.records.push(record);
1116
1117        let err = ledger
1118            .validate_committed_integrity()
1119            .expect_err("invalid decoded slot must fail");
1120
1121        assert!(matches!(
1122            err,
1123            LedgerIntegrityError::InvalidSlotDescriptor(
1124                crate::slot::AllocationSlotDescriptorError::MemoryManager(
1125                    MemoryManagerSlotError::InvalidMemoryManagerId { id }
1126                )
1127            ) if id == MEMORY_MANAGER_INVALID_ID
1128        ));
1129    }
1130
1131    #[test]
1132    fn validate_integrity_rejects_retired_record_without_retired_generation() {
1133        let mut ledger = ledger();
1134        let mut record = active_record("app.users.v1", 100);
1135        record.state = AllocationState::Retired;
1136        *ledger.allocation_history.records_mut() = vec![record];
1137
1138        let err = ledger
1139            .validate_integrity()
1140            .expect_err("missing retired generation");
1141
1142        assert!(matches!(
1143            err,
1144            LedgerIntegrityError::MissingRetiredGeneration { .. }
1145        ));
1146    }
1147
1148    #[test]
1149    fn validate_integrity_rejects_non_retired_record_with_retired_generation() {
1150        let mut ledger = ledger();
1151        let mut record = active_record("app.users.v1", 100);
1152        record.retired_generation = Some(2);
1153        *ledger.allocation_history.records_mut() = vec![record];
1154
1155        let err = ledger
1156            .validate_integrity()
1157            .expect_err("unexpected retired generation");
1158
1159        assert!(matches!(
1160            err,
1161            LedgerIntegrityError::UnexpectedRetiredGeneration { .. }
1162        ));
1163    }
1164
1165    #[test]
1166    fn validate_integrity_rejects_non_increasing_schema_history() {
1167        let mut ledger = ledger();
1168        let mut record = active_record("app.users.v1", 100);
1169        record.schema_history.push(SchemaMetadataRecord {
1170            generation: 1,
1171            schema: SchemaMetadata::default(),
1172        });
1173        *ledger.allocation_history.records_mut() = vec![record];
1174
1175        let err = ledger
1176            .validate_integrity()
1177            .expect_err("non-increasing schema history");
1178
1179        assert!(matches!(
1180            err,
1181            LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1182        ));
1183    }
1184
1185    #[test]
1186    fn validate_integrity_rejects_invalid_schema_metadata_history() {
1187        let mut ledger = committed_ledger(1);
1188        let mut record = active_record("app.users.v1", 100);
1189        record.schema_history[0].schema = invalid_schema_metadata();
1190        *ledger.allocation_history.records_mut() = vec![record];
1191
1192        let err = ledger
1193            .validate_committed_integrity()
1194            .expect_err("invalid committed schema metadata");
1195
1196        assert_eq!(
1197            err,
1198            LedgerIntegrityError::InvalidSchemaMetadata {
1199                stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1200                generation: 1,
1201                error: SchemaMetadataError::InvalidVersion,
1202            }
1203        );
1204    }
1205
1206    #[test]
1207    fn validate_committed_integrity_requires_current_generation_record() {
1208        let err = ledger()
1209            .validate_committed_integrity()
1210            .expect_err("missing current generation");
1211
1212        assert_eq!(
1213            err,
1214            LedgerIntegrityError::MissingCurrentGenerationRecord {
1215                current_generation: 3
1216            }
1217        );
1218    }
1219
1220    #[test]
1221    fn validate_committed_integrity_rejects_generation_history_gaps() {
1222        let mut ledger = committed_ledger(3);
1223        ledger.allocation_history.generations_mut().remove(1);
1224
1225        let err = ledger
1226            .validate_committed_integrity()
1227            .expect_err("generation history gap");
1228
1229        assert!(matches!(
1230            err,
1231            LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1232        ));
1233    }
1234
1235    #[test]
1236    fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1237        let mut store = LedgerCommitStore::default();
1238        let codec = TestCodec;
1239        let mut invalid = ledger();
1240        *invalid.allocation_history.records_mut() = vec![
1241            active_record("app.users.v1", 100),
1242            active_record("app.orders.v1", 100),
1243        ];
1244
1245        let err = store.commit(&invalid, &codec).expect_err("invalid ledger");
1246
1247        assert!(matches!(
1248            err,
1249            LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1250        ));
1251        assert!(store.physical().is_uninitialized());
1252    }
1253
1254    #[test]
1255    fn ledger_commit_store_recovers_latest_committed_ledger() {
1256        let mut store = LedgerCommitStore::default();
1257        let codec = TestCodec;
1258        let first = committed_ledger(1);
1259        let second = committed_ledger(2);
1260
1261        store.commit(&first, &codec).expect("first commit");
1262        store.commit(&second, &codec).expect("second commit");
1263        let recovered = store.recover(&codec).expect("recovered ledger");
1264
1265        assert_eq!(recovered.current_generation, 2);
1266    }
1267
1268    #[test]
1269    fn ledger_commit_store_recovers_compatible_genesis_and_first_real_commit() {
1270        let mut store = LedgerCommitStore::default();
1271        let codec = FullLedgerCodec::default();
1272        let genesis = committed_ledger(0);
1273
1274        let recovered = store
1275            .recover_or_initialize(&codec, &genesis)
1276            .expect("compatible genesis ledger");
1277        assert_eq!(recovered.current_generation, 0);
1278        assert!(recovered.allocation_history.generations().is_empty());
1279
1280        let first = recovered
1281            .stage_validated_generation(
1282                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1283                None,
1284            )
1285            .expect("first real generation");
1286        let recovered = store.commit(&first, &codec).expect("first commit");
1287
1288        assert_eq!(recovered.current_generation, 1);
1289        assert_eq!(recovered.allocation_history.generations()[0].generation, 1);
1290        assert_eq!(record(&recovered, "app.users.v1").first_generation, 1);
1291    }
1292
1293    #[test]
1294    fn ledger_commit_store_recovers_full_payload_after_corrupt_latest_slot() {
1295        let mut store = LedgerCommitStore::default();
1296        let codec = FullLedgerCodec::default();
1297        let genesis = committed_ledger(0);
1298        store.commit(&genesis, &codec).expect("genesis commit");
1299        let first = genesis
1300            .stage_validated_generation(
1301                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1302                None,
1303            )
1304            .expect("first generation");
1305        let first = store.commit(&first, &codec).expect("first commit");
1306        let second = first
1307            .stage_validated_generation(
1308                &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
1309                None,
1310            )
1311            .expect("second generation");
1312
1313        store
1314            .write_corrupt_inactive_ledger(&second, &codec)
1315            .expect("corrupt latest");
1316        let recovered = store.recover(&codec).expect("recover prior generation");
1317
1318        assert_eq!(recovered.current_generation, 1);
1319        assert_eq!(record(&recovered, "app.users.v1").schema_history.len(), 1);
1320    }
1321
1322    #[test]
1323    fn ledger_commit_store_recovers_identical_duplicate_slots() {
1324        let codec = FullLedgerCodec::default();
1325        let ledger = committed_ledger(0)
1326            .stage_validated_generation(
1327                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1328                None,
1329            )
1330            .expect("first generation");
1331        let payload = codec.encode(&ledger).expect("payload");
1332        let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
1333        let store = LedgerCommitStore {
1334            physical: DualCommitStore {
1335                slot0: Some(committed.clone()),
1336                slot1: Some(committed),
1337            },
1338        };
1339
1340        let recovered = store.recover(&codec).expect("recovered");
1341
1342        assert_eq!(recovered, ledger);
1343    }
1344
1345    #[test]
1346    fn ledger_commit_store_ignores_corrupt_inactive_ledger() {
1347        let mut store = LedgerCommitStore::default();
1348        let codec = TestCodec;
1349        let first = committed_ledger(1);
1350        let second = committed_ledger(2);
1351
1352        store.commit(&first, &codec).expect("first commit");
1353        store
1354            .write_corrupt_inactive_ledger(&second, &codec)
1355            .expect("corrupt write");
1356        let recovered = store.recover(&codec).expect("recovered ledger");
1357
1358        assert_eq!(recovered.current_generation, 1);
1359    }
1360
1361    #[test]
1362    fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
1363        let store = LedgerCommitStore {
1364            physical: DualCommitStore {
1365                slot0: Some(CommittedGenerationBytes::new(
1366                    7,
1367                    TestCodec.encode(&committed_ledger(6)).expect("payload"),
1368                )),
1369                slot1: None,
1370            },
1371        };
1372        let codec = TestCodec;
1373
1374        let err = store.recover(&codec).expect_err("mismatch");
1375
1376        assert_eq!(
1377            err,
1378            LedgerCommitError::PhysicalLogicalGenerationMismatch {
1379                physical_generation: 7,
1380                logical_generation: 6
1381            }
1382        );
1383    }
1384
1385    #[test]
1386    fn ledger_commit_store_rejects_non_next_logical_generation() {
1387        let mut store = LedgerCommitStore::default();
1388        let codec = TestCodec;
1389        store
1390            .commit(&committed_ledger(1), &codec)
1391            .expect("first commit");
1392
1393        let err = store
1394            .commit(&committed_ledger(3), &codec)
1395            .expect_err("skipped generation");
1396
1397        assert_eq!(
1398            err,
1399            LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
1400                expected: 2,
1401                actual: 3
1402            })
1403        );
1404    }
1405
1406    #[test]
1407    fn ledger_commit_store_initializes_empty_store_explicitly() {
1408        let mut store = LedgerCommitStore::default();
1409        let codec = TestCodec;
1410        let genesis = committed_ledger(3);
1411
1412        let recovered = store
1413            .recover_or_initialize(&codec, &genesis)
1414            .expect("initialized ledger");
1415
1416        assert_eq!(recovered.current_generation, 3);
1417        assert!(!store.physical().is_uninitialized());
1418    }
1419
1420    #[test]
1421    fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
1422        let mut store = LedgerCommitStore::default();
1423        let codec = TestCodec;
1424        store
1425            .write_corrupt_inactive_ledger(&ledger(), &codec)
1426            .expect("corrupt write");
1427
1428        let err = store
1429            .recover_or_initialize(&codec, &ledger())
1430            .expect_err("corrupt state");
1431
1432        assert!(matches!(
1433            err,
1434            LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)
1435        ));
1436    }
1437
1438    #[test]
1439    fn ledger_commit_store_rejects_incompatible_schema_before_write() {
1440        let mut store = LedgerCommitStore::default();
1441        let codec = TestCodec;
1442        let incompatible = AllocationLedger {
1443            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
1444            ..committed_ledger(0)
1445        };
1446
1447        let err = store
1448            .commit(&incompatible, &codec)
1449            .expect_err("incompatible schema");
1450
1451        assert!(matches!(
1452            err,
1453            LedgerCommitError::Compatibility(
1454                LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
1455            )
1456        ));
1457        assert!(store.physical().is_uninitialized());
1458    }
1459
1460    #[test]
1461    fn ledger_commit_store_rejects_incompatible_schema_on_recovery() {
1462        let mut store = LedgerCommitStore::default();
1463        let codec = TestCodec;
1464        let incompatible = AllocationLedger {
1465            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
1466            ..committed_ledger(3)
1467        };
1468        let payload = codec.encode(&incompatible).expect("payload");
1469        store
1470            .physical_mut()
1471            .commit_payload_at_generation(incompatible.current_generation, payload)
1472            .expect("physical commit");
1473
1474        let err = store.recover(&codec).expect_err("incompatible schema");
1475
1476        assert!(matches!(
1477            err,
1478            LedgerCommitError::Compatibility(
1479                LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
1480            )
1481        ));
1482    }
1483
1484    #[test]
1485    fn ledger_commit_store_rejects_incompatible_physical_format() {
1486        let mut store = LedgerCommitStore::default();
1487        let codec = TestCodec;
1488        let incompatible = AllocationLedger {
1489            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID + 1,
1490            ..committed_ledger(0)
1491        };
1492
1493        let err = store
1494            .recover_or_initialize(&codec, &incompatible)
1495            .expect_err("incompatible format");
1496
1497        assert!(matches!(
1498            err,
1499            LedgerCommitError::Compatibility(
1500                LedgerCompatibilityError::UnsupportedPhysicalFormat { .. }
1501            )
1502        ));
1503        assert!(store.physical().is_uninitialized());
1504    }
1505}