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