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