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