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