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