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