1mod claim;
2mod error;
3mod integrity;
4mod payload;
5mod record;
6mod stage;
7
8use crate::physical::{CommitRecoveryError, DualCommitStore};
9use serde::{Deserialize, Serialize};
10
11pub use claim::{
12 ClaimConflict, ClaimOutcome, claim_conflict_record, validate_declaration_claim,
13 validate_reservation_claim,
14};
15pub use error::{
16 AllocationReservationError, AllocationRetirementError, AllocationStageError, LedgerCommitError,
17 LedgerIntegrityError,
18};
19pub use payload::{
20 LEDGER_PAYLOAD_FORMAT_VERSION, LedgerPayloadEnvelope, LedgerPayloadEnvelopeError,
21};
22pub use record::{
23 AllocationHistory, AllocationLedger, AllocationRecord, AllocationRetirement, AllocationState,
24 GenerationRecord, RecoveredLedger, SchemaMetadataRecord,
25};
26pub use stage::validate_reservation_declaration;
27
28mod private {
29 pub trait Sealed {}
30}
31
32trait LedgerCodec: private::Sealed {
33 type Error;
35
36 fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error>;
38
39 fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error>;
41}
42
43#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
55struct CborLedgerCodec;
56
57impl private::Sealed for CborLedgerCodec {}
58
59impl LedgerCodec for CborLedgerCodec {
60 type Error = String;
61
62 fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
63 let mut bytes = Vec::new();
64 ciborium::into_writer(ledger, &mut bytes).map_err(|err| err.to_string())?;
65 Ok(bytes)
66 }
67
68 fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
69 crate::cbor::from_slice_exact(bytes).map_err(|err| err.to_string())
70 }
71}
72
73#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
86#[serde(deny_unknown_fields)]
87pub struct LedgerCommitStore {
88 physical: DualCommitStore,
90}
91
92impl LedgerCommitStore {
93 #[must_use]
95 pub const fn physical(&self) -> &DualCommitStore {
96 &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 let codec = CborLedgerCodec;
108 let ledger = codec
109 .decode(envelope.payload())
110 .map_err(LedgerCommitError::Codec)?;
111 if committed.generation() != ledger.current_generation {
112 return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
113 physical_generation: committed.generation(),
114 logical_generation: ledger.current_generation,
115 });
116 }
117 ledger
118 .validate_committed_integrity()
119 .map_err(LedgerCommitError::Integrity)?;
120 Ok(RecoveredLedger::from_trusted_parts(
121 ledger,
122 committed.generation(),
123 ))
124 }
125
126 pub fn recover_or_initialize(
135 &mut self,
136 genesis: &AllocationLedger,
137 ) -> Result<RecoveredLedger, LedgerCommitError> {
138 match self.recover() {
139 Ok(ledger) => Ok(ledger),
140 Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration))
141 if self.physical.is_uninitialized() =>
142 {
143 self.commit(genesis)
144 }
145 Err(err) => Err(err),
146 }
147 }
148
149 pub fn commit(
151 &mut self,
152 ledger: &AllocationLedger,
153 ) -> Result<RecoveredLedger, LedgerCommitError> {
154 ledger
155 .validate_committed_integrity()
156 .map_err(LedgerCommitError::Integrity)?;
157 let codec = CborLedgerCodec;
158 let payload =
159 LedgerPayloadEnvelope::current(codec.encode(ledger).map_err(LedgerCommitError::Codec)?)
160 .try_encode()
161 .map_err(LedgerCommitError::PayloadEnvelope)?;
162 self.physical
163 .commit_payload_at_generation(ledger.current_generation, payload)
164 .map_err(LedgerCommitError::Recovery)?;
165 self.recover()
166 }
167
168 #[cfg(test)]
170 pub fn write_corrupt_inactive_ledger(
171 &mut self,
172 ledger: &AllocationLedger,
173 ) -> Result<(), LedgerCommitError> {
174 let codec = CborLedgerCodec;
175 let payload =
176 LedgerPayloadEnvelope::current(codec.encode(ledger).map_err(LedgerCommitError::Codec)?)
177 .try_encode()
178 .map_err(LedgerCommitError::PayloadEnvelope)?;
179 self.physical
180 .write_corrupt_inactive_slot(ledger.current_generation, payload);
181 Ok(())
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::{
189 Validate,
190 declaration::{AllocationDeclaration, DeclarationSnapshot, DeclarationSnapshotError},
191 key::StableKey,
192 physical::CommittedGenerationBytes,
193 schema::{SchemaMetadata, SchemaMetadataError},
194 slot::{AllocationSlotDescriptor, MEMORY_MANAGER_INVALID_ID, MemoryManagerSlotError},
195 };
196 fn declaration(key: &str, id: u8, schema_version: Option<u32>) -> AllocationDeclaration {
197 AllocationDeclaration::new(
198 key,
199 AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
200 None,
201 SchemaMetadata { schema_version },
202 )
203 .expect("declaration")
204 }
205
206 fn invalid_schema_metadata() -> SchemaMetadata {
207 SchemaMetadata {
208 schema_version: Some(0),
209 }
210 }
211
212 fn declaration_with_invalid_schema(key: &str, id: u8) -> AllocationDeclaration {
213 let mut declaration = declaration(key, id, Some(1));
214 declaration.schema = invalid_schema_metadata();
215 declaration
216 }
217
218 fn ledger() -> AllocationLedger {
219 AllocationLedger {
220 current_generation: 3,
221 allocation_history: AllocationHistory::default(),
222 }
223 }
224
225 fn committed_ledger(current_generation: u64) -> AllocationLedger {
226 AllocationLedger {
227 current_generation,
228 allocation_history: AllocationHistory::from_parts(
229 Vec::new(),
230 (1..=current_generation)
231 .map(|generation| GenerationRecord {
232 generation,
233 parent_generation: if generation == 1 { 0 } else { generation - 1 },
234 runtime_fingerprint: None,
235 declaration_count: 0,
236 committed_at: None,
237 })
238 .collect(),
239 ),
240 }
241 }
242
243 fn active_record(key: &str, id: u8) -> AllocationRecord {
244 AllocationRecord::active(1, declaration(key, id, None)).expect("valid schema metadata")
245 }
246
247 fn validated(
248 generation: u64,
249 declarations: Vec<AllocationDeclaration>,
250 ) -> crate::capability::ValidatedAllocations {
251 crate::capability::ValidatedAllocations::new(generation, declarations, None)
252 }
253
254 fn record<'ledger>(ledger: &'ledger AllocationLedger, key: &str) -> &'ledger AllocationRecord {
255 ledger
256 .allocation_history
257 .records()
258 .iter()
259 .find(|record| record.stable_key.as_str() == key)
260 .expect("allocation record")
261 }
262
263 fn enveloped_payload(ledger: &AllocationLedger) -> Vec<u8> {
264 LedgerPayloadEnvelope::current(CborLedgerCodec.encode(ledger).expect("CBOR payload"))
265 .encode()
266 }
267
268 fn hex_fixture(contents: &str) -> Vec<u8> {
269 let hex = contents
270 .chars()
271 .filter(|char| !char.is_whitespace())
272 .collect::<String>();
273 assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
274 hex.as_bytes()
275 .as_chunks::<2>()
276 .0
277 .iter()
278 .map(|pair| {
279 let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
280 u8::from_str_radix(pair, 16).expect("fixture hex byte")
281 })
282 .collect()
283 }
284
285 fn ledger_from_payload_fixture(contents: &str) -> AllocationLedger {
286 let bytes = hex_fixture(contents);
287 let envelope = LedgerPayloadEnvelope::decode(&bytes).expect("fixture envelope");
288
289 let codec = CborLedgerCodec;
290 let ledger = codec.decode(envelope.payload()).expect("fixture ledger");
291 ledger
292 .validate_committed_integrity()
293 .expect("fixture ledger integrity");
294 assert_eq!(bytes, enveloped_payload(&ledger));
295 ledger
296 }
297
298 fn store_from_fixture(contents: &str) -> LedgerCommitStore {
299 let bytes = hex_fixture(contents);
300 let store: LedgerCommitStore = crate::test_cbor::from_slice(&bytes).expect("fixture store");
301 assert_eq!(
302 bytes,
303 crate::test_cbor::to_vec(&store).expect("re-encoded fixture store")
304 );
305 store
306 }
307
308 fn active_committed_ledger() -> AllocationLedger {
309 AllocationLedger {
310 current_generation: 1,
311 allocation_history: AllocationHistory::from_parts(
312 vec![active_record("app.users.v1", 100)],
313 vec![GenerationRecord {
314 generation: 1,
315 parent_generation: 0,
316 runtime_fingerprint: None,
317 declaration_count: 1,
318 committed_at: None,
319 }],
320 ),
321 }
322 }
323
324 fn active_ledger_value() -> crate::test_cbor::Value {
325 crate::test_cbor::to_value(active_committed_ledger()).expect("ledger value")
326 }
327
328 fn value_map_mut(
329 value: &mut crate::test_cbor::Value,
330 ) -> &mut Vec<(crate::test_cbor::Value, crate::test_cbor::Value)> {
331 let crate::test_cbor::Value::Map(map) = value else {
332 panic!("expected CBOR map");
333 };
334 map
335 }
336
337 fn map_field_mut<'map>(
338 map: &'map mut [(crate::test_cbor::Value, crate::test_cbor::Value)],
339 field: &str,
340 ) -> &'map mut crate::test_cbor::Value {
341 map.iter_mut()
342 .find(|(key, _)| key == &crate::test_cbor::Value::Text(field.to_string()))
343 .map(|(_, value)| value)
344 .expect("CBOR map field")
345 }
346
347 fn remove_map_field(
348 map: &mut Vec<(crate::test_cbor::Value, crate::test_cbor::Value)>,
349 field: &str,
350 ) {
351 let field = crate::test_cbor::Value::Text(field.to_string());
352 let index = map
353 .iter()
354 .position(|(key, _)| key == &field)
355 .expect("CBOR map field");
356 map.remove(index);
357 }
358
359 fn value_array_mut(value: &mut crate::test_cbor::Value) -> &mut Vec<crate::test_cbor::Value> {
360 let crate::test_cbor::Value::Array(values) = value else {
361 panic!("expected CBOR array");
362 };
363 values
364 }
365
366 fn decode_mutated_ledger(value: crate::test_cbor::Value) -> String {
367 let bytes = crate::test_cbor::to_vec(&value).expect("mutated ledger bytes");
368 CborLedgerCodec
369 .decode(&bytes)
370 .expect_err("mutated ledger must fail closed")
371 }
372
373 #[test]
374 fn allocation_history_accessors_expose_read_only_views() {
375 let history = AllocationHistory::from_parts(
376 vec![active_record("app.users.v1", 100)],
377 vec![GenerationRecord::new(1, 0, None, 1, Some(42)).expect("generation record")],
378 );
379
380 assert!(!history.is_empty());
381 assert_eq!(history.records().len(), 1);
382 assert_eq!(history.generations().len(), 1);
383 assert_eq!(history.generations()[0].committed_at(), Some(42));
384 }
385
386 #[test]
387 fn record_constructors_validate_metadata() {
388 let schema_err = SchemaMetadataRecord::new(1, invalid_schema_metadata())
389 .expect_err("invalid schema must fail");
390 assert_eq!(schema_err, SchemaMetadataError::InvalidVersion);
391
392 let generation_err = GenerationRecord::new(1, 0, Some(String::new()), 0, None)
393 .expect_err("empty fingerprint must fail");
394 assert_eq!(
395 generation_err,
396 DeclarationSnapshotError::EmptyRuntimeFingerprint
397 );
398 }
399
400 #[test]
401 fn cbor_ledger_codec_round_trips_allocation_ledger() {
402 let ledger = committed_ledger(2);
403 let codec = CborLedgerCodec;
404
405 let encoded = codec.encode(&ledger).expect("encode ledger");
406 let decoded = codec.decode(&encoded).expect("decode ledger");
407
408 assert_eq!(decoded, ledger);
409 }
410
411 #[test]
412 fn cbor_ledger_codec_rejects_trailing_bytes() {
413 let mut encoded = CborLedgerCodec
414 .encode(&committed_ledger(2))
415 .expect("encode ledger");
416 encoded.push(0);
417
418 let err = CborLedgerCodec
419 .decode(&encoded)
420 .expect_err("trailing bytes must fail closed");
421
422 assert!(err.contains("trailing bytes"));
423 }
424
425 #[test]
426 fn cbor_ledger_codec_rejects_unknown_top_level_fields() {
427 use crate::test_cbor::Value;
428
429 let mut map = Vec::new();
430 crate::test_cbor::map_insert(
431 &mut map,
432 Value::Text("current_generation".to_string()),
433 Value::Integer(0.into()),
434 );
435 crate::test_cbor::map_insert(
436 &mut map,
437 Value::Text("allocation_history".to_string()),
438 crate::test_cbor::to_value(AllocationHistory::default()).expect("history value"),
439 );
440 crate::test_cbor::map_insert(
441 &mut map,
442 Value::Text("future_field".to_string()),
443 Value::Bool(true),
444 );
445 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field ledger");
446
447 let err = CborLedgerCodec
448 .decode(&bytes)
449 .expect_err("unknown ledger field must fail closed");
450
451 assert!(err.contains("future_field"));
452 }
453
454 #[test]
455 fn cbor_ledger_codec_rejects_unknown_nested_history_fields() {
456 let mut value = active_ledger_value();
457 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
458 crate::test_cbor::map_insert(
459 value_map_mut(history),
460 crate::test_cbor::Value::Text("future_history_field".to_string()),
461 crate::test_cbor::Value::Bool(true),
462 );
463
464 let err = decode_mutated_ledger(value);
465
466 assert!(err.contains("future_history_field"));
467 }
468
469 #[test]
470 fn cbor_ledger_codec_rejects_unknown_nested_record_fields() {
471 let mut value = active_ledger_value();
472 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
473 let records = map_field_mut(value_map_mut(history), "records");
474 let record = value_array_mut(records)
475 .first_mut()
476 .expect("allocation record");
477 crate::test_cbor::map_insert(
478 value_map_mut(record),
479 crate::test_cbor::Value::Text("future_record_field".to_string()),
480 crate::test_cbor::Value::Bool(true),
481 );
482
483 let err = decode_mutated_ledger(value);
484
485 assert!(err.contains("future_record_field"));
486 }
487
488 #[test]
489 fn cbor_ledger_codec_rejects_unknown_nested_slot_descriptor_fields() {
490 let mut value = active_ledger_value();
491 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
492 let records = map_field_mut(value_map_mut(history), "records");
493 let record = value_array_mut(records)
494 .first_mut()
495 .expect("allocation record");
496 let slot = map_field_mut(value_map_mut(record), "slot");
497 crate::test_cbor::map_insert(
498 value_map_mut(slot),
499 crate::test_cbor::Value::Text("future_slot_field".to_string()),
500 crate::test_cbor::Value::Bool(true),
501 );
502
503 let err = decode_mutated_ledger(value);
504
505 assert!(err.contains("future_slot_field"));
506 }
507
508 #[test]
509 fn cbor_ledger_codec_rejects_unknown_nested_generation_fields() {
510 let mut value = active_ledger_value();
511 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
512 let generations = map_field_mut(value_map_mut(history), "generations");
513 let generation = value_array_mut(generations)
514 .first_mut()
515 .expect("generation record");
516 crate::test_cbor::map_insert(
517 value_map_mut(generation),
518 crate::test_cbor::Value::Text("future_generation_field".to_string()),
519 crate::test_cbor::Value::Bool(true),
520 );
521
522 let err = decode_mutated_ledger(value);
523
524 assert!(err.contains("future_generation_field"));
525 }
526
527 #[test]
528 fn cbor_ledger_codec_requires_generation_optional_fields() {
529 for field in ["runtime_fingerprint", "committed_at"] {
530 let mut value = active_ledger_value();
531 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
532 let generations = map_field_mut(value_map_mut(history), "generations");
533 let generation = value_array_mut(generations)
534 .first_mut()
535 .expect("generation record");
536 remove_map_field(value_map_mut(generation), field);
537
538 let err = decode_mutated_ledger(value);
539
540 assert!(err.contains(field));
541 }
542 }
543
544 #[test]
545 fn cbor_ledger_codec_requires_schema_version_field() {
546 let mut value = active_ledger_value();
547 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
548 let records = map_field_mut(value_map_mut(history), "records");
549 let record = value_array_mut(records)
550 .first_mut()
551 .expect("allocation record");
552 let schema_history = map_field_mut(value_map_mut(record), "schema_history");
553 let schema_record = value_array_mut(schema_history)
554 .first_mut()
555 .expect("schema record");
556 let schema = map_field_mut(value_map_mut(schema_record), "schema");
557 remove_map_field(value_map_mut(schema), "schema_version");
558
559 let err = decode_mutated_ledger(value);
560
561 assert!(err.contains("schema_version"));
562 }
563
564 #[test]
565 fn ledger_commit_store_rejects_unknown_top_level_fields() {
566 use crate::test_cbor::Value;
567
568 let mut map = Vec::new();
569 crate::test_cbor::map_insert(
570 &mut map,
571 Value::Text("physical".to_string()),
572 crate::test_cbor::to_value(DualCommitStore::default()).expect("physical value"),
573 );
574 crate::test_cbor::map_insert(
575 &mut map,
576 Value::Text("future_field".to_string()),
577 Value::Bool(true),
578 );
579 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field store");
580
581 let err = crate::test_cbor::from_slice::<LedgerCommitStore>(&bytes)
582 .expect_err("unknown store field must fail closed");
583
584 assert!(err.to_string().contains("future_field"));
585 }
586
587 #[test]
588 fn current_empty_genesis_payload_fixture_recovers() {
589 let ledger = ledger_from_payload_fixture(include_str!(
590 "../../fixtures/current/empty_genesis_payload_envelope.hex"
591 ));
592
593 assert_eq!(ledger.current_generation, 0);
594 assert!(ledger.allocation_history.records().is_empty());
595 assert!(ledger.allocation_history.generations().is_empty());
596 }
597
598 #[test]
599 fn current_single_active_payload_fixture_recovers() {
600 let ledger = ledger_from_payload_fixture(include_str!(
601 "../../fixtures/current/single_active_allocation_payload_envelope.hex"
602 ));
603 let record = record(&ledger, "app.users.v1");
604
605 assert_eq!(ledger.current_generation, 1);
606 assert_eq!(record.state(), AllocationState::Active);
607 assert_eq!(record.slot().memory_manager_id().expect("memory id"), 100);
608 assert_eq!(record.first_generation(), 1);
609 assert_eq!(record.last_seen_generation(), 1);
610 }
611
612 #[test]
613 fn current_reserved_payload_fixture_recovers() {
614 let ledger = ledger_from_payload_fixture(include_str!(
615 "../../fixtures/current/reserved_allocation_payload_envelope.hex"
616 ));
617 let record = record(&ledger, "app.future_store.v1");
618
619 assert_eq!(ledger.current_generation, 1);
620 assert_eq!(record.state(), AllocationState::Reserved);
621 assert_eq!(record.slot().memory_manager_id().expect("memory id"), 101);
622 }
623
624 #[test]
625 fn current_retired_payload_fixture_recovers() {
626 let ledger = ledger_from_payload_fixture(include_str!(
627 "../../fixtures/current/retired_allocation_payload_envelope.hex"
628 ));
629 let record = record(&ledger, "app.users.v1");
630
631 assert_eq!(ledger.current_generation, 2);
632 assert_eq!(record.state(), AllocationState::Retired { generation: 2 });
633 }
634
635 #[test]
636 fn current_memory_manager_descriptor_fixture_validates() {
637 let bytes = hex_fixture(include_str!(
638 "../../fixtures/current/memory_manager_descriptor.cbor.hex"
639 ));
640 let descriptor: AllocationSlotDescriptor =
641 crate::test_cbor::from_slice(&bytes).expect("descriptor fixture");
642
643 descriptor.validate().expect("valid descriptor");
644 assert_eq!(descriptor.memory_manager_id().expect("memory id"), 100);
645 assert_eq!(
646 bytes,
647 crate::test_cbor::to_vec(&descriptor).expect("re-encoded descriptor")
648 );
649 }
650
651 #[test]
652 fn current_ledger_commit_store_single_active_fixture_recovers() {
653 let store = store_from_fixture(include_str!(
654 "../../fixtures/current/ledger_commit_store_single_active.cbor.hex"
655 ));
656
657 let recovered = store.recover().expect("fixture store recovers");
658 assert_eq!(recovered.current_generation(), 1);
659 assert_eq!(
660 record(recovered.ledger(), "app.users.v1")
661 .slot()
662 .memory_manager_id(),
663 Ok(100)
664 );
665 }
666
667 #[test]
668 fn current_dual_slot_valid_newer_fixture_recovers_newer() {
669 let store = store_from_fixture(include_str!(
670 "../../fixtures/current/dual_slot_store_valid_newer.cbor.hex"
671 ));
672
673 assert_eq!(store.physical().diagnostic().recovery, Ok(2));
674 let recovered = store.recover().expect("fixture store recovers");
675 assert_eq!(recovered.current_generation(), 2);
676 }
677
678 #[test]
679 fn current_dual_slot_corrupt_newer_fixture_fails_closed() {
680 let store = store_from_fixture(include_str!(
681 "../../fixtures/current/dual_slot_store_corrupt_newer.cbor.hex"
682 ));
683
684 let diagnostic = store.physical().diagnostic();
685 assert_eq!(
686 diagnostic.recovery,
687 Err(CommitRecoveryError::InvalidCommitSlots {
688 slot0_invalid: false,
689 slot1_invalid: true,
690 })
691 );
692 assert_eq!(
693 diagnostic.slot1,
694 crate::CommitSlotDiagnostic::Invalid { generation: 2 }
695 );
696 let err = store
697 .recover()
698 .expect_err("corrupt fixture must fail closed");
699 assert!(matches!(
700 err,
701 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
702 slot0_invalid: false,
703 slot1_invalid: true,
704 })
705 ));
706 }
707
708 #[test]
709 fn stage_validated_generation_records_new_allocations() {
710 let declarations = vec![declaration("app.users.v1", 100, Some(1))];
711 let validated = validated(3, declarations);
712
713 let staged = ledger()
714 .stage_validated_generation(&validated, Some(42))
715 .expect("staged generation");
716
717 assert_eq!(staged.current_generation, 4);
718 assert_eq!(staged.allocation_history.records().len(), 1);
719 assert_eq!(staged.allocation_history.records()[0].first_generation, 4);
720 assert_eq!(staged.allocation_history.generations()[0].generation, 4);
721 assert_eq!(
722 staged.allocation_history.generations()[0].committed_at,
723 Some(42)
724 );
725 }
726
727 #[test]
728 fn stage_validated_generation_allows_empty_generation_boundary() {
729 let validated = crate::capability::ValidatedAllocations::new(
730 3,
731 Vec::new(),
732 Some("test-runtime".to_string()),
733 );
734
735 let staged = ledger()
736 .stage_validated_generation(&validated, Some(42))
737 .expect("empty validated generation");
738
739 assert_eq!(staged.current_generation, 4);
740 assert!(staged.allocation_history.records().is_empty());
741 assert_eq!(staged.allocation_history.generations().len(), 1);
742 assert_eq!(staged.allocation_history.generations()[0].generation(), 4);
743 assert_eq!(
744 staged.allocation_history.generations()[0].parent_generation(),
745 3
746 );
747 assert_eq!(
748 staged.allocation_history.generations()[0].runtime_fingerprint(),
749 Some("test-runtime")
750 );
751 assert_eq!(
752 staged.allocation_history.generations()[0].declaration_count(),
753 0
754 );
755 assert_eq!(
756 staged.allocation_history.generations()[0].committed_at(),
757 Some(42)
758 );
759 }
760
761 #[test]
762 fn stage_validated_generation_rejects_stale_validated_allocations() {
763 let validated = validated(2, vec![declaration("app.users.v1", 100, Some(1))]);
764
765 let err = ledger()
766 .stage_validated_generation(&validated, None)
767 .expect_err("stale validated allocations");
768
769 assert_eq!(
770 err,
771 AllocationStageError::StaleValidatedAllocations {
772 validated_generation: 2,
773 ledger_generation: 3
774 }
775 );
776 }
777
778 #[test]
779 fn stage_validated_generation_rejects_invalid_schema_metadata() {
780 let validated = crate::capability::ValidatedAllocations::new(
781 3,
782 vec![declaration_with_invalid_schema("app.users.v1", 100)],
783 None,
784 );
785
786 let err = ledger()
787 .stage_validated_generation(&validated, None)
788 .expect_err("invalid schema metadata");
789
790 assert_eq!(
791 err,
792 AllocationStageError::InvalidSchemaMetadata {
793 stable_key: StableKey::parse("app.users.v1").expect("stable key"),
794 error: SchemaMetadataError::InvalidVersion,
795 }
796 );
797 }
798
799 #[test]
800 fn stage_validated_generation_rejects_generation_overflow() {
801 let ledger = AllocationLedger {
802 current_generation: u64::MAX,
803 ..ledger()
804 };
805 let validated = validated(u64::MAX, vec![declaration("app.users.v1", 100, Some(1))]);
806
807 let err = ledger
808 .stage_validated_generation(&validated, None)
809 .expect_err("overflow must fail");
810
811 assert_eq!(
812 err,
813 AllocationStageError::GenerationOverflow {
814 generation: u64::MAX
815 }
816 );
817 }
818
819 #[test]
820 fn stage_validated_generation_rejects_same_key_different_slot() {
821 let mut ledger = ledger();
822 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
823 let validated = validated(3, vec![declaration("app.users.v1", 101, None)]);
824
825 let err = ledger
826 .stage_validated_generation(&validated, None)
827 .expect_err("stable key cannot move slots");
828
829 assert!(matches!(
830 err,
831 AllocationStageError::StableKeySlotConflict { .. }
832 ));
833 }
834
835 #[test]
836 fn stage_validated_generation_rejects_same_slot_different_key() {
837 let mut ledger = ledger();
838 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
839 let validated = validated(3, vec![declaration("app.orders.v1", 100, None)]);
840
841 let err = ledger
842 .stage_validated_generation(&validated, None)
843 .expect_err("slot cannot be reused by another key");
844
845 assert!(matches!(
846 err,
847 AllocationStageError::SlotStableKeyConflict { .. }
848 ));
849 }
850
851 #[test]
852 fn stage_validated_generation_rejects_retired_redeclaration() {
853 let mut ledger = ledger();
854 let mut record = active_record("app.users.v1", 100);
855 record.state = AllocationState::Retired { generation: 3 };
856 *ledger.allocation_history.records_mut() = vec![record];
857 let validated = validated(3, vec![declaration("app.users.v1", 100, None)]);
858
859 let err = ledger
860 .stage_validated_generation(&validated, None)
861 .expect_err("retired allocation cannot be redeclared");
862
863 assert!(matches!(
864 err,
865 AllocationStageError::RetiredAllocation { .. }
866 ));
867 }
868
869 #[test]
870 fn stage_validated_generation_preserves_omitted_records() {
871 let first = validated(
872 3,
873 vec![
874 declaration("app.users.v1", 100, Some(1)),
875 declaration("app.orders.v1", 101, Some(1)),
876 ],
877 );
878 let second = validated(4, vec![declaration("app.users.v1", 100, Some(1))]);
879
880 let staged = ledger()
881 .stage_validated_generation(&first, None)
882 .expect("first generation");
883 let staged = staged
884 .stage_validated_generation(&second, None)
885 .expect("second generation");
886
887 assert_eq!(staged.current_generation, 5);
888 assert_eq!(staged.allocation_history.records().len(), 2);
889 let omitted = staged
890 .allocation_history
891 .records()
892 .iter()
893 .find(|record| record.stable_key.as_str() == "app.orders.v1")
894 .expect("omitted record");
895 assert_eq!(omitted.state, AllocationState::Active);
896 assert_eq!(omitted.last_seen_generation, 4);
897 }
898
899 #[test]
900 fn stage_validated_generation_records_schema_metadata_history() {
901 let first = validated(3, vec![declaration("app.users.v1", 100, Some(1))]);
902 let second = validated(4, vec![declaration("app.users.v1", 100, Some(2))]);
903
904 let staged = ledger()
905 .stage_validated_generation(&first, None)
906 .expect("first generation");
907 let staged = staged
908 .stage_validated_generation(&second, None)
909 .expect("second generation");
910 let record = &staged.allocation_history.records()[0];
911
912 assert_eq!(record.schema_history.len(), 2);
913 assert_eq!(record.schema_history[0].generation, 4);
914 assert_eq!(record.schema_history[1].generation, 5);
915 }
916
917 #[test]
918 fn stage_reservation_generation_records_reserved_allocations() {
919 let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
920
921 let staged = ledger()
922 .stage_reservation_generation(&reservations, Some(42))
923 .expect("reserved generation");
924
925 assert_eq!(staged.current_generation, 4);
926 assert_eq!(staged.allocation_history.records().len(), 1);
927 assert_eq!(
928 staged.allocation_history.records()[0].state,
929 AllocationState::Reserved
930 );
931 assert_eq!(
932 staged.allocation_history.generations()[0].declaration_count,
933 1
934 );
935 }
936
937 #[test]
938 fn stage_reservation_generation_allows_empty_generation_boundary() {
939 let reservations = Vec::new();
940
941 let staged = ledger()
942 .stage_reservation_generation(&reservations, Some(42))
943 .expect("empty reservation generation");
944
945 assert_eq!(staged.current_generation, 4);
946 assert!(staged.allocation_history.records().is_empty());
947 assert_eq!(staged.allocation_history.generations().len(), 1);
948 assert_eq!(staged.allocation_history.generations()[0].generation(), 4);
949 assert_eq!(
950 staged.allocation_history.generations()[0].declaration_count(),
951 0
952 );
953 assert_eq!(
954 staged.allocation_history.generations()[0].committed_at(),
955 Some(42)
956 );
957 }
958
959 #[test]
960 fn stage_reservation_generation_refreshes_existing_reserved_allocation() {
961 let first = vec![declaration("app.future_store.v1", 100, Some(1))];
962 let staged = ledger()
963 .stage_reservation_generation(&first, Some(42))
964 .expect("first reservation generation");
965
966 let second = vec![declaration("app.future_store.v1", 100, Some(2))];
967 let staged = staged
968 .stage_reservation_generation(&second, Some(43))
969 .expect("reservation refresh");
970 let record = record(&staged, "app.future_store.v1");
971
972 assert_eq!(record.state(), AllocationState::Reserved);
973 assert_eq!(record.first_generation(), 4);
974 assert_eq!(record.last_seen_generation(), 5);
975 assert_eq!(record.schema_history().len(), 2);
976 assert_eq!(record.schema_history()[1].generation(), 5);
977 assert_eq!(
978 staged.allocation_history.generations()[1].declaration_count(),
979 1
980 );
981 }
982
983 #[test]
984 fn stage_reservation_generation_rejects_generation_overflow() {
985 let ledger = AllocationLedger {
986 current_generation: u64::MAX,
987 ..ledger()
988 };
989 let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
990
991 let err = ledger
992 .stage_reservation_generation(&reservations, None)
993 .expect_err("overflow must fail");
994
995 assert_eq!(
996 err,
997 AllocationReservationError::GenerationOverflow {
998 generation: u64::MAX
999 }
1000 );
1001 }
1002
1003 #[test]
1004 fn stage_reservation_generation_rejects_invalid_schema_metadata() {
1005 let reservations = vec![declaration_with_invalid_schema(
1006 "ic_memory.generation_log.v1",
1007 1,
1008 )];
1009
1010 let err = ledger()
1011 .stage_reservation_generation(&reservations, None)
1012 .expect_err("invalid reservation schema metadata");
1013
1014 assert_eq!(
1015 err,
1016 AllocationReservationError::InvalidSchemaMetadata {
1017 stable_key: StableKey::parse("ic_memory.generation_log.v1").expect("stable key"),
1018 error: SchemaMetadataError::InvalidVersion,
1019 }
1020 );
1021 }
1022
1023 #[test]
1024 fn stage_reservation_generation_rejects_invalid_decoded_slot() {
1025 let mut reservation = declaration("ic_memory.generation_log.v1", 1, None);
1026 reservation.slot =
1027 AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1028
1029 let err = ledger()
1030 .stage_reservation_generation(&[reservation], None)
1031 .expect_err("invalid decoded reservation slot");
1032
1033 assert!(matches!(
1034 err,
1035 AllocationReservationError::InvalidDeclaration(
1036 DeclarationSnapshotError::MemoryManagerSlot(
1037 MemoryManagerSlotError::InvalidMemoryManagerId { id }
1038 )
1039 ) if id == MEMORY_MANAGER_INVALID_ID
1040 ));
1041 }
1042
1043 #[test]
1044 fn stage_reservation_generation_rejects_same_key_different_slot() {
1045 let mut ledger = ledger();
1046 *ledger.allocation_history.records_mut() = vec![
1047 AllocationRecord::reserved(3, declaration("app.future_store.v1", 100, None))
1048 .expect("valid schema metadata"),
1049 ];
1050 let reservations = vec![declaration("app.future_store.v1", 101, None)];
1051
1052 let err = ledger
1053 .stage_reservation_generation(&reservations, None)
1054 .expect_err("reservation key cannot move slots");
1055
1056 assert!(matches!(
1057 err,
1058 AllocationReservationError::StableKeySlotConflict { .. }
1059 ));
1060 }
1061
1062 #[test]
1063 fn stage_reservation_generation_rejects_same_slot_different_key() {
1064 let mut ledger = ledger();
1065 *ledger.allocation_history.records_mut() = vec![
1066 AllocationRecord::reserved(3, declaration("app.future_store.v1", 100, None))
1067 .expect("valid schema metadata"),
1068 ];
1069 let reservations = vec![declaration("app.other_future_store.v1", 100, None)];
1070
1071 let err = ledger
1072 .stage_reservation_generation(&reservations, None)
1073 .expect_err("reservation slot cannot be reused by another key");
1074
1075 assert!(matches!(
1076 err,
1077 AllocationReservationError::SlotStableKeyConflict { .. }
1078 ));
1079 }
1080
1081 #[test]
1082 fn stage_reservation_generation_rejects_active_allocation() {
1083 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1084 let staged = ledger()
1085 .stage_validated_generation(&active, None)
1086 .expect("active generation");
1087 let reservations = vec![declaration("app.users.v1", 100, None)];
1088
1089 let err = staged
1090 .stage_reservation_generation(&reservations, None)
1091 .expect_err("active cannot become reserved");
1092
1093 assert!(matches!(
1094 err,
1095 AllocationReservationError::ActiveAllocation { .. }
1096 ));
1097 }
1098
1099 #[test]
1100 fn stage_reservation_generation_rejects_retired_allocation() {
1101 let mut ledger = ledger();
1102 let mut record = active_record("app.users.v1", 100);
1103 record.state = AllocationState::Retired { generation: 3 };
1104 *ledger.allocation_history.records_mut() = vec![record];
1105 let reservations = vec![declaration("app.users.v1", 100, None)];
1106
1107 let err = ledger
1108 .stage_reservation_generation(&reservations, None)
1109 .expect_err("retired cannot revive");
1110
1111 assert!(matches!(
1112 err,
1113 AllocationReservationError::RetiredAllocation { .. }
1114 ));
1115 }
1116
1117 #[test]
1118 fn stage_validated_generation_activates_reserved_record() {
1119 let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1120 let staged = ledger()
1121 .stage_reservation_generation(&reservations, None)
1122 .expect("reserved generation");
1123 let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
1124
1125 let staged = staged
1126 .stage_validated_generation(&active, None)
1127 .expect("active generation");
1128 let record = &staged.allocation_history.records()[0];
1129
1130 assert_eq!(record.state, AllocationState::Active);
1131 assert_eq!(record.first_generation, 4);
1132 assert_eq!(record.last_seen_generation, 5);
1133 assert_eq!(record.schema_history.len(), 2);
1134 }
1135
1136 #[test]
1137 fn stage_retirement_generation_tombstones_named_allocation() {
1138 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1139 let staged = ledger()
1140 .stage_validated_generation(&active, None)
1141 .expect("active generation");
1142 let retirement = AllocationRetirement::new(
1143 "app.users.v1",
1144 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1145 )
1146 .expect("retirement");
1147
1148 let staged = staged
1149 .stage_retirement_generation(&retirement, Some(42))
1150 .expect("retired generation");
1151 let record = &staged.allocation_history.records()[0];
1152
1153 assert_eq!(staged.current_generation, 5);
1154 assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1155 assert_eq!(
1156 staged.allocation_history.generations()[1].declaration_count,
1157 0
1158 );
1159 }
1160
1161 #[test]
1162 fn stage_retirement_generation_tombstones_reserved_allocation() {
1163 let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1164 let staged = ledger()
1165 .stage_reservation_generation(&reservations, None)
1166 .expect("reserved generation");
1167 let retirement = AllocationRetirement::new(
1168 "app.future_store.v1",
1169 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1170 )
1171 .expect("retirement");
1172
1173 let staged = staged
1174 .stage_retirement_generation(&retirement, Some(42))
1175 .expect("reserved retirement generation");
1176 let record = &staged.allocation_history.records()[0];
1177
1178 assert_eq!(staged.current_generation, 5);
1179 assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1180 assert_eq!(record.first_generation, 4);
1181 assert_eq!(
1182 staged.allocation_history.generations()[1].declaration_count(),
1183 0
1184 );
1185 }
1186
1187 #[test]
1188 fn stage_retirement_generation_rejects_generation_overflow() {
1189 let mut ledger = ledger();
1190 ledger.current_generation = u64::MAX;
1191 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1192 let retirement = AllocationRetirement::new(
1193 "app.users.v1",
1194 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1195 )
1196 .expect("retirement");
1197
1198 let err = ledger
1199 .stage_retirement_generation(&retirement, None)
1200 .expect_err("overflow must fail");
1201
1202 assert_eq!(
1203 err,
1204 AllocationRetirementError::GenerationOverflow {
1205 generation: u64::MAX
1206 }
1207 );
1208 }
1209
1210 #[test]
1211 fn stage_retirement_generation_requires_matching_slot() {
1212 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1213 let staged = ledger()
1214 .stage_validated_generation(&active, None)
1215 .expect("active generation");
1216 let retirement = AllocationRetirement::new(
1217 "app.users.v1",
1218 AllocationSlotDescriptor::memory_manager(101).expect("usable slot"),
1219 )
1220 .expect("retirement");
1221
1222 let err = staged
1223 .stage_retirement_generation(&retirement, None)
1224 .expect_err("slot mismatch");
1225
1226 assert!(matches!(
1227 err,
1228 AllocationRetirementError::SlotMismatch { .. }
1229 ));
1230 }
1231
1232 #[test]
1233 fn allocation_retirement_constructor_rejects_invalid_slot() {
1234 let err = AllocationRetirement::new(
1235 "app.users.v1",
1236 AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID),
1237 )
1238 .expect_err("invalid retirement slot must fail at construction");
1239
1240 assert!(matches!(
1241 err,
1242 AllocationRetirementError::MemoryManagerSlot(
1243 MemoryManagerSlotError::InvalidMemoryManagerId { id }
1244 ) if id == MEMORY_MANAGER_INVALID_ID
1245 ));
1246 }
1247
1248 #[test]
1249 fn stage_retirement_generation_rejects_invalid_decoded_slot() {
1250 let mut ledger = ledger();
1251 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1252 let mut retirement = AllocationRetirement::new(
1253 "app.users.v1",
1254 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1255 )
1256 .expect("retirement");
1257 retirement.slot =
1258 AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1259
1260 let err = ledger
1261 .stage_retirement_generation(&retirement, None)
1262 .expect_err("decoded invalid retirement must fail at the boundary");
1263
1264 assert!(matches!(
1265 err,
1266 AllocationRetirementError::MemoryManagerSlot(
1267 MemoryManagerSlotError::InvalidMemoryManagerId { id }
1268 ) if id == MEMORY_MANAGER_INVALID_ID
1269 ));
1270 }
1271
1272 #[test]
1273 fn stage_retirement_generation_rejects_invalid_decoded_stable_key() {
1274 let mut ledger = ledger();
1275 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1276 let retirement = AllocationRetirement::new(
1277 "app.users.v1",
1278 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1279 )
1280 .expect("retirement");
1281 let mut value = crate::test_cbor::to_value(retirement).expect("retirement value");
1282 *map_field_mut(value_map_mut(&mut value), "stable_key") =
1283 crate::test_cbor::Value::Text("App.users.v1".to_string());
1284 let bytes = crate::test_cbor::to_vec(&value).expect("retirement bytes");
1285 let retirement: AllocationRetirement =
1286 crate::test_cbor::from_slice(&bytes).expect("decoded DTO");
1287
1288 let err = ledger
1289 .stage_retirement_generation(&retirement, None)
1290 .expect_err("decoded invalid stable key must fail at the boundary");
1291
1292 assert!(matches!(err, AllocationRetirementError::Key(_)));
1293 }
1294
1295 #[test]
1296 fn snapshot_can_feed_validated_generation() {
1297 let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1298 .expect("snapshot");
1299 let (declarations, runtime_fingerprint) = snapshot.into_parts();
1300 let validated =
1301 crate::capability::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1302
1303 let staged = ledger()
1304 .stage_validated_generation(&validated, None)
1305 .expect("validated generation");
1306
1307 assert_eq!(staged.allocation_history.records().len(), 1);
1308 }
1309
1310 #[test]
1311 fn stage_validated_generation_records_runtime_fingerprint() {
1312 let validated = crate::capability::ValidatedAllocations::new(
1313 3,
1314 vec![declaration("app.users.v1", 100, None)],
1315 Some("wasm:abc123".to_string()),
1316 );
1317
1318 let staged = ledger()
1319 .stage_validated_generation(&validated, None)
1320 .expect("validated generation");
1321
1322 assert_eq!(
1323 staged.allocation_history.generations()[0].runtime_fingerprint,
1324 Some("wasm:abc123".to_string())
1325 );
1326 }
1327
1328 #[test]
1329 fn strict_committed_integrity_accepts_full_lifecycle() {
1330 let mut ledger = committed_ledger(0);
1331 ledger
1332 .validate_committed_integrity()
1333 .expect("genesis ledger with no history");
1334
1335 ledger = ledger
1336 .stage_validated_generation(
1337 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1338 Some(1),
1339 )
1340 .expect("first real commit after genesis");
1341 ledger
1342 .validate_committed_integrity()
1343 .expect("first real commit");
1344
1345 ledger = ledger
1346 .stage_validated_generation(
1347 &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1348 Some(2),
1349 )
1350 .expect("repeated active declaration");
1351 ledger
1352 .validate_committed_integrity()
1353 .expect("repeated active declaration");
1354 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1355
1356 ledger = ledger
1357 .stage_validated_generation(
1358 &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1359 Some(3),
1360 )
1361 .expect("schema drift");
1362 ledger
1363 .validate_committed_integrity()
1364 .expect("schema metadata drift");
1365 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1366
1367 ledger = ledger
1368 .stage_reservation_generation(
1369 &[declaration("app.future_store.v1", 101, Some(1))],
1370 Some(4),
1371 )
1372 .expect("reservation-only generation");
1373 ledger
1374 .validate_committed_integrity()
1375 .expect("reservation-only generation");
1376 assert_eq!(
1377 record(&ledger, "app.future_store.v1").state,
1378 AllocationState::Reserved
1379 );
1380
1381 ledger = ledger
1382 .stage_validated_generation(
1383 &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1384 Some(5),
1385 )
1386 .expect("reservation activation");
1387 ledger
1388 .validate_committed_integrity()
1389 .expect("reservation activation");
1390 assert_eq!(
1391 record(&ledger, "app.future_store.v1").state,
1392 AllocationState::Active
1393 );
1394
1395 let retirement = AllocationRetirement::new(
1396 "app.users.v1",
1397 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1398 )
1399 .expect("retirement");
1400 ledger = ledger
1401 .stage_retirement_generation(&retirement, Some(6))
1402 .expect("retirement generation");
1403 ledger
1404 .validate_committed_integrity()
1405 .expect("retirement generation");
1406 assert_eq!(ledger.current_generation, 6);
1407 assert_eq!(
1408 record(&ledger, "app.users.v1").state,
1409 AllocationState::Retired { generation: 6 }
1410 );
1411 assert_eq!(
1412 record(&ledger, "app.future_store.v1").last_seen_generation,
1413 5
1414 );
1415 }
1416
1417 #[derive(Clone, Copy, Debug)]
1418 enum Transition {
1419 DeclareUsers,
1420 DeclareOrders,
1421 ReserveAudit,
1422 ActivateAudit,
1423 RetireUsers,
1424 RetireAudit,
1425 EmptyValidated,
1426 EmptyReservation,
1427 }
1428
1429 const TRANSITIONS: [Transition; 8] = [
1430 Transition::DeclareUsers,
1431 Transition::DeclareOrders,
1432 Transition::ReserveAudit,
1433 Transition::ActivateAudit,
1434 Transition::RetireUsers,
1435 Transition::RetireAudit,
1436 Transition::EmptyValidated,
1437 Transition::EmptyReservation,
1438 ];
1439
1440 fn apply_transition(
1441 ledger: &AllocationLedger,
1442 transition: Transition,
1443 committed_at: u64,
1444 ) -> Result<AllocationLedger, String> {
1445 match transition {
1446 Transition::DeclareUsers => ledger
1447 .stage_validated_generation(
1448 &validated(
1449 ledger.current_generation,
1450 vec![declaration("app.users.v1", 100, Some(1))],
1451 ),
1452 Some(committed_at),
1453 )
1454 .map_err(|err| err.to_string()),
1455 Transition::DeclareOrders => ledger
1456 .stage_validated_generation(
1457 &validated(
1458 ledger.current_generation,
1459 vec![declaration("app.orders.v1", 101, Some(1))],
1460 ),
1461 Some(committed_at),
1462 )
1463 .map_err(|err| err.to_string()),
1464 Transition::ReserveAudit => ledger
1465 .stage_reservation_generation(
1466 &[declaration("app.audit.v1", 102, Some(1))],
1467 Some(committed_at),
1468 )
1469 .map_err(|err| err.to_string()),
1470 Transition::ActivateAudit => ledger
1471 .stage_validated_generation(
1472 &validated(
1473 ledger.current_generation,
1474 vec![declaration("app.audit.v1", 102, Some(1))],
1475 ),
1476 Some(committed_at),
1477 )
1478 .map_err(|err| err.to_string()),
1479 Transition::RetireUsers => ledger
1480 .stage_retirement_generation(
1481 &AllocationRetirement::new(
1482 "app.users.v1",
1483 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1484 )
1485 .expect("retirement"),
1486 Some(committed_at),
1487 )
1488 .map_err(|err| err.to_string()),
1489 Transition::RetireAudit => ledger
1490 .stage_retirement_generation(
1491 &AllocationRetirement::new(
1492 "app.audit.v1",
1493 AllocationSlotDescriptor::memory_manager(102).expect("usable slot"),
1494 )
1495 .expect("retirement"),
1496 Some(committed_at),
1497 )
1498 .map_err(|err| err.to_string()),
1499 Transition::EmptyValidated => ledger
1500 .stage_validated_generation(
1501 &validated(ledger.current_generation, Vec::new()),
1502 Some(committed_at),
1503 )
1504 .map_err(|err| err.to_string()),
1505 Transition::EmptyReservation => ledger
1506 .stage_reservation_generation(&[], Some(committed_at))
1507 .map_err(|err| err.to_string()),
1508 }
1509 }
1510
1511 fn check_transition_sequences(
1512 ledger: AllocationLedger,
1513 depth: usize,
1514 sequence: &mut Vec<Transition>,
1515 ) {
1516 ledger
1517 .validate_committed_integrity()
1518 .unwrap_or_else(|err| panic!("sequence {sequence:?} violated integrity: {err}"));
1519 if depth == 0 {
1520 return;
1521 }
1522
1523 for transition in TRANSITIONS {
1524 sequence.push(transition);
1525 let next = apply_transition(&ledger, transition, sequence.len() as u64)
1526 .unwrap_or_else(|_| ledger.clone());
1527 check_transition_sequences(next, depth - 1, sequence);
1528 sequence.pop();
1529 }
1530 }
1531
1532 #[test]
1533 fn transition_matrix_preserves_committed_invariants() {
1534 check_transition_sequences(committed_ledger(0), 4, &mut Vec::new());
1535 }
1536
1537 #[test]
1538 fn new_committed_requires_strict_generation_history() {
1539 let structurally_valid =
1540 AllocationLedger::new(3, AllocationHistory::default()).expect("structurally valid DTO");
1541
1542 assert_eq!(structurally_valid.current_generation, 3);
1543
1544 let err = AllocationLedger::new_committed(3, AllocationHistory::default())
1545 .expect_err("committed ledger needs generation history");
1546
1547 assert_eq!(
1548 err,
1549 LedgerIntegrityError::MissingCurrentGenerationRecord {
1550 current_generation: 3
1551 }
1552 );
1553 }
1554
1555 #[test]
1556 fn validate_integrity_rejects_duplicate_stable_keys() {
1557 let mut ledger = ledger();
1558 *ledger.allocation_history.records_mut() = vec![
1559 active_record("app.users.v1", 100),
1560 active_record("app.users.v1", 101),
1561 ];
1562
1563 let err = ledger.validate_integrity().expect_err("duplicate key");
1564
1565 assert!(matches!(
1566 err,
1567 LedgerIntegrityError::DuplicateStableKey { .. }
1568 ));
1569 }
1570
1571 #[test]
1572 fn validate_integrity_rejects_duplicate_slots() {
1573 let mut ledger = ledger();
1574 *ledger.allocation_history.records_mut() = vec![
1575 active_record("app.users.v1", 100),
1576 active_record("app.orders.v1", 100),
1577 ];
1578
1579 let err = ledger.validate_integrity().expect_err("duplicate slot");
1580
1581 assert!(matches!(err, LedgerIntegrityError::DuplicateSlot { .. }));
1582 }
1583
1584 #[test]
1585 fn validate_committed_integrity_rejects_decoded_invalid_stable_key() {
1586 let mut ledger = committed_ledger(1);
1587 ledger
1588 .allocation_history
1589 .records
1590 .push(active_record("app.users.v1", 100));
1591 let mut bytes = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
1592 let key_start = bytes
1593 .windows(b"app.users.v1".len())
1594 .position(|window| window == b"app.users.v1")
1595 .expect("encoded stable key");
1596 bytes[key_start] = b'A';
1597 let decoded: AllocationLedger =
1598 crate::test_cbor::from_slice(&bytes).expect("decode ledger");
1599
1600 let err = decoded
1601 .validate_committed_integrity()
1602 .expect_err("invalid decoded key must fail");
1603
1604 assert!(matches!(err, LedgerIntegrityError::InvalidStableKey(_)));
1605 }
1606
1607 #[test]
1608 fn validate_committed_integrity_rejects_decoded_invalid_memory_manager_slot() {
1609 let mut ledger = committed_ledger(1);
1610 let mut record = active_record("app.users.v1", 100);
1611 record.slot = AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1612 ledger.allocation_history.records.push(record);
1613
1614 let err = ledger
1615 .validate_committed_integrity()
1616 .expect_err("invalid decoded slot must fail");
1617
1618 assert!(matches!(
1619 err,
1620 LedgerIntegrityError::InvalidSlotDescriptor(
1621 MemoryManagerSlotError::InvalidMemoryManagerId { id }
1622 ) if id == MEMORY_MANAGER_INVALID_ID
1623 ));
1624 }
1625
1626 #[test]
1627 fn validate_integrity_rejects_retirement_before_last_observation() {
1628 let mut ledger = committed_ledger(5);
1629 let mut record = active_record("app.users.v1", 100);
1630 record.last_seen_generation = 4;
1631 record.state = AllocationState::Retired { generation: 3 };
1632 *ledger.allocation_history.records_mut() = vec![record];
1633
1634 let err = ledger
1635 .validate_committed_integrity()
1636 .expect_err("retirement must follow the last observation");
1637
1638 assert!(matches!(
1639 err,
1640 LedgerIntegrityError::RetirementNotAfterLastSeen { .. }
1641 ));
1642 }
1643
1644 #[test]
1645 fn validate_integrity_rejects_retirement_before_allocation() {
1646 let mut ledger = committed_ledger(2);
1647 let mut record = active_record("app.users.v1", 100);
1648 record.state = AllocationState::Retired { generation: 0 };
1649 *ledger.allocation_history.records_mut() = vec![record];
1650
1651 let err = ledger
1652 .validate_committed_integrity()
1653 .expect_err("retirement cannot predate allocation");
1654
1655 assert!(matches!(
1656 err,
1657 LedgerIntegrityError::RetiredBeforeFirstGeneration { .. }
1658 ));
1659 }
1660
1661 #[test]
1662 fn validate_integrity_requires_schema_history_at_first_generation() {
1663 let mut ledger = committed_ledger(2);
1664 let mut record = active_record("app.users.v1", 100);
1665 record.last_seen_generation = 2;
1666 record.schema_history[0].generation = 2;
1667 *ledger.allocation_history.records_mut() = vec![record];
1668
1669 let err = ledger
1670 .validate_committed_integrity()
1671 .expect_err("schema history must begin with the allocation");
1672
1673 assert!(matches!(
1674 err,
1675 LedgerIntegrityError::SchemaHistoryStartMismatch { .. }
1676 ));
1677 }
1678
1679 #[test]
1680 fn validate_integrity_rejects_schema_history_after_last_observation() {
1681 let mut ledger = committed_ledger(3);
1682 let mut record = active_record("app.users.v1", 100);
1683 record.last_seen_generation = 2;
1684 record.schema_history.push(
1685 SchemaMetadataRecord::new(3, SchemaMetadata::new(Some(2)).expect("schema"))
1686 .expect("schema record"),
1687 );
1688 *ledger.allocation_history.records_mut() = vec![record];
1689
1690 let err = ledger
1691 .validate_committed_integrity()
1692 .expect_err("schema metadata cannot postdate the last observation");
1693
1694 assert!(matches!(
1695 err,
1696 LedgerIntegrityError::SchemaHistoryAfterLastSeen { .. }
1697 ));
1698 }
1699
1700 #[test]
1701 fn validate_integrity_rejects_non_increasing_schema_history() {
1702 let mut ledger = ledger();
1703 let mut record = active_record("app.users.v1", 100);
1704 record.schema_history.push(SchemaMetadataRecord {
1705 generation: 1,
1706 schema: SchemaMetadata::default(),
1707 });
1708 *ledger.allocation_history.records_mut() = vec![record];
1709
1710 let err = ledger
1711 .validate_integrity()
1712 .expect_err("non-increasing schema history");
1713
1714 assert!(matches!(
1715 err,
1716 LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1717 ));
1718 }
1719
1720 #[test]
1721 fn validate_integrity_rejects_invalid_schema_metadata_history() {
1722 let mut ledger = committed_ledger(1);
1723 let mut record = active_record("app.users.v1", 100);
1724 record.schema_history[0].schema = invalid_schema_metadata();
1725 *ledger.allocation_history.records_mut() = vec![record];
1726
1727 let err = ledger
1728 .validate_committed_integrity()
1729 .expect_err("invalid committed schema metadata");
1730
1731 assert_eq!(
1732 err,
1733 LedgerIntegrityError::InvalidSchemaMetadata {
1734 stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1735 generation: 1,
1736 error: SchemaMetadataError::InvalidVersion,
1737 }
1738 );
1739 }
1740
1741 #[test]
1742 fn validate_committed_integrity_requires_current_generation_record() {
1743 let err = ledger()
1744 .validate_committed_integrity()
1745 .expect_err("missing current generation");
1746
1747 assert_eq!(
1748 err,
1749 LedgerIntegrityError::MissingCurrentGenerationRecord {
1750 current_generation: 3
1751 }
1752 );
1753 }
1754
1755 #[test]
1756 fn validate_committed_integrity_rejects_generation_history_gaps() {
1757 let mut ledger = committed_ledger(3);
1758 ledger.allocation_history.generations_mut().remove(1);
1759
1760 let err = ledger
1761 .validate_committed_integrity()
1762 .expect_err("generation history gap");
1763
1764 assert!(matches!(
1765 err,
1766 LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1767 ));
1768 }
1769
1770 #[test]
1771 fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1772 let mut store = LedgerCommitStore::default();
1773 let mut invalid = ledger();
1774 *invalid.allocation_history.records_mut() = vec![
1775 active_record("app.users.v1", 100),
1776 active_record("app.orders.v1", 100),
1777 ];
1778
1779 let err = store.commit(&invalid).expect_err("invalid ledger");
1780
1781 assert!(matches!(
1782 err,
1783 LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1784 ));
1785 assert!(store.physical().is_uninitialized());
1786 }
1787
1788 #[test]
1789 fn ledger_commit_store_recovers_latest_committed_ledger() {
1790 let mut store = LedgerCommitStore::default();
1791 let first = committed_ledger(1);
1792 let second = committed_ledger(2);
1793
1794 store.commit(&first).expect("first commit");
1795 store.commit(&second).expect("second commit");
1796 let recovered = store.recover().expect("recovered ledger");
1797
1798 assert_eq!(recovered.current_generation(), 2);
1799 }
1800
1801 #[test]
1802 fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1803 let mut store = LedgerCommitStore::default();
1804 let ledger = committed_ledger(1);
1805
1806 store.commit(&ledger).expect("commit");
1807
1808 let committed = store.physical().authoritative().expect("authoritative");
1809 let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1810 assert_eq!(
1811 envelope.payload(),
1812 CborLedgerCodec.encode(&ledger).expect("payload")
1813 );
1814 }
1815
1816 #[test]
1817 fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1818 let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1819 let store = LedgerCommitStore {
1820 physical: DualCommitStore {
1821 slot0: Some(committed),
1822 slot1: None,
1823 },
1824 };
1825
1826 let err = store.recover().expect_err("bad envelope");
1827
1828 assert!(matches!(
1829 err,
1830 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1831 ));
1832 }
1833
1834 #[test]
1835 fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1836 let mut payload = enveloped_payload(&committed_ledger(1));
1837 payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1838 let store = LedgerCommitStore {
1839 physical: DualCommitStore {
1840 slot0: Some(CommittedGenerationBytes::new(1, payload)),
1841 slot1: None,
1842 },
1843 };
1844
1845 let err = store
1846 .recover()
1847 .expect_err("unknown ledger format version must fail");
1848
1849 assert_eq!(
1850 err,
1851 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1852 marker: *b"ICMF",
1853 version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1854 })
1855 );
1856 }
1857
1858 #[test]
1859 fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
1860 let mut store = LedgerCommitStore::default();
1861 let genesis = committed_ledger(0);
1862
1863 let recovered = store
1864 .recover_or_initialize(&genesis)
1865 .expect("current-format genesis ledger");
1866 assert_eq!(recovered.current_generation(), 0);
1867 assert!(
1868 recovered
1869 .ledger()
1870 .allocation_history()
1871 .generations()
1872 .is_empty()
1873 );
1874
1875 let first = recovered
1876 .ledger()
1877 .stage_validated_generation(
1878 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1879 None,
1880 )
1881 .expect("first real generation");
1882 let recovered = store.commit(&first).expect("first commit");
1883
1884 assert_eq!(recovered.current_generation(), 1);
1885 assert_eq!(
1886 recovered.ledger().allocation_history().generations()[0].generation,
1887 1
1888 );
1889 assert_eq!(
1890 record(recovered.ledger(), "app.users.v1").first_generation,
1891 1
1892 );
1893 }
1894
1895 #[test]
1896 fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
1897 let mut store = LedgerCommitStore::default();
1898 let genesis = committed_ledger(0);
1899 store.commit(&genesis).expect("genesis commit");
1900 let first = genesis
1901 .stage_validated_generation(
1902 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1903 None,
1904 )
1905 .expect("first generation");
1906 let first = store.commit(&first).expect("first commit");
1907 let second = first
1908 .ledger()
1909 .stage_validated_generation(
1910 &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
1911 None,
1912 )
1913 .expect("second generation");
1914
1915 store.commit(&second).expect("second commit");
1916 store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
1917
1918 let err = store
1919 .recover()
1920 .expect_err("corrupt latest must not roll back");
1921
1922 assert_eq!(
1923 err,
1924 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
1925 slot0_invalid: true,
1926 slot1_invalid: false,
1927 })
1928 );
1929 }
1930
1931 #[test]
1932 fn ledger_commit_store_recovers_identical_duplicate_slots() {
1933 let ledger = committed_ledger(0)
1934 .stage_validated_generation(
1935 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1936 None,
1937 )
1938 .expect("first generation");
1939 let payload = enveloped_payload(&ledger);
1940 let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
1941 let store = LedgerCommitStore {
1942 physical: DualCommitStore {
1943 slot0: Some(committed.clone()),
1944 slot1: Some(committed),
1945 },
1946 };
1947
1948 let recovered = store.recover().expect("recovered");
1949
1950 assert_eq!(recovered.ledger(), &ledger);
1951 }
1952
1953 #[test]
1954 fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
1955 let mut store = LedgerCommitStore::default();
1956 let first = committed_ledger(1);
1957 let second = committed_ledger(2);
1958
1959 store.commit(&first).expect("first commit");
1960 store
1961 .write_corrupt_inactive_ledger(&second)
1962 .expect("corrupt write");
1963 let err = store.recover().expect_err("corrupt slot");
1964
1965 assert_eq!(
1966 err,
1967 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
1968 slot0_invalid: false,
1969 slot1_invalid: true,
1970 })
1971 );
1972 }
1973
1974 #[test]
1975 fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
1976 let store = LedgerCommitStore {
1977 physical: DualCommitStore {
1978 slot0: Some(CommittedGenerationBytes::new(
1979 7,
1980 enveloped_payload(&committed_ledger(6)),
1981 )),
1982 slot1: None,
1983 },
1984 };
1985 let err = store.recover().expect_err("mismatch");
1986
1987 assert_eq!(
1988 err,
1989 LedgerCommitError::PhysicalLogicalGenerationMismatch {
1990 physical_generation: 7,
1991 logical_generation: 6
1992 }
1993 );
1994 }
1995
1996 #[test]
1997 fn ledger_commit_store_rejects_non_next_logical_generation() {
1998 let mut store = LedgerCommitStore::default();
1999 store.commit(&committed_ledger(1)).expect("first commit");
2000
2001 let err = store
2002 .commit(&committed_ledger(3))
2003 .expect_err("skipped generation");
2004
2005 assert_eq!(
2006 err,
2007 LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2008 expected: 2,
2009 actual: 3
2010 })
2011 );
2012 }
2013
2014 #[test]
2015 fn ledger_commit_store_initializes_empty_store_explicitly() {
2016 let mut store = LedgerCommitStore::default();
2017 let genesis = committed_ledger(3);
2018
2019 let recovered = store
2020 .recover_or_initialize(&genesis)
2021 .expect("initialized ledger");
2022
2023 assert_eq!(recovered.current_generation(), 3);
2024 assert!(!store.physical().is_uninitialized());
2025 }
2026
2027 #[test]
2028 fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2029 let mut store = LedgerCommitStore::default();
2030 store
2031 .write_corrupt_inactive_ledger(&ledger())
2032 .expect("corrupt write");
2033
2034 let err = store
2035 .recover_or_initialize(&ledger())
2036 .expect_err("corrupt state");
2037
2038 assert!(matches!(
2039 err,
2040 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2041 slot0_invalid: true,
2042 slot1_invalid: false,
2043 })
2044 ));
2045 }
2046}