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