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