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