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