Skip to main content

ic_memory/ledger/
mod.rs

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