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