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 let err = AllocationRetirement::new(
1268 "App.users.v1",
1269 AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID),
1270 )
1271 .expect_err("invalid key precedes invalid slot");
1272 assert!(matches!(err, AllocationRetirementError::Key(_)));
1273 }
1274
1275 #[test]
1276 fn stage_retirement_generation_rejects_invalid_decoded_slot() {
1277 let mut ledger = ledger();
1278 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1279 let mut retirement = AllocationRetirement::new(
1280 "app.users.v1",
1281 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1282 )
1283 .expect("retirement");
1284 retirement.slot =
1285 AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1286
1287 let err = ledger
1288 .stage_retirement_generation(&retirement, None)
1289 .expect_err("decoded invalid retirement must fail at the boundary");
1290
1291 assert!(matches!(
1292 err,
1293 AllocationRetirementError::MemoryManagerSlot(
1294 MemoryManagerSlotError::InvalidMemoryManagerId { id }
1295 ) if id == MEMORY_MANAGER_INVALID_ID
1296 ));
1297 }
1298
1299 #[test]
1300 fn stage_retirement_generation_rejects_invalid_decoded_stable_key() {
1301 let mut ledger = ledger();
1302 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1303 let retirement = AllocationRetirement::new(
1304 "app.users.v1",
1305 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1306 )
1307 .expect("retirement");
1308 let mut value = crate::test_cbor::to_value(retirement).expect("retirement value");
1309 *map_field_mut(value_map_mut(&mut value), "stable_key") =
1310 crate::test_cbor::Value::Text("App.users.v1".to_string());
1311 let bytes = crate::test_cbor::to_vec(&value).expect("retirement bytes");
1312 let retirement: AllocationRetirement =
1313 crate::test_cbor::from_slice(&bytes).expect("decoded DTO");
1314
1315 let err = ledger
1316 .stage_retirement_generation(&retirement, None)
1317 .expect_err("decoded invalid stable key must fail at the boundary");
1318
1319 assert!(matches!(err, AllocationRetirementError::Key(_)));
1320 }
1321
1322 #[test]
1323 fn snapshot_can_feed_validated_generation() {
1324 let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1325 .expect("snapshot");
1326 let (declarations, runtime_fingerprint) = snapshot.into_parts();
1327 let validated =
1328 crate::capability::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1329
1330 let staged = ledger()
1331 .stage_validated_generation(&validated, None)
1332 .expect("validated generation");
1333
1334 assert_eq!(staged.allocation_history.records().len(), 1);
1335 }
1336
1337 #[test]
1338 fn stage_validated_generation_records_runtime_fingerprint() {
1339 let validated = crate::capability::ValidatedAllocations::new(
1340 3,
1341 vec![declaration("app.users.v1", 100, None)],
1342 Some("wasm:abc123".to_string()),
1343 );
1344
1345 let staged = ledger()
1346 .stage_validated_generation(&validated, None)
1347 .expect("validated generation");
1348
1349 assert_eq!(
1350 staged.allocation_history.generations()[0].runtime_fingerprint,
1351 Some("wasm:abc123".to_string())
1352 );
1353 }
1354
1355 #[test]
1356 fn strict_committed_integrity_accepts_full_lifecycle() {
1357 let mut ledger = committed_ledger(0);
1358 ledger
1359 .validate_committed_integrity()
1360 .expect("genesis ledger with no history");
1361
1362 ledger = ledger
1363 .stage_validated_generation(
1364 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1365 Some(1),
1366 )
1367 .expect("first real commit after genesis");
1368 ledger
1369 .validate_committed_integrity()
1370 .expect("first real commit");
1371
1372 ledger = ledger
1373 .stage_validated_generation(
1374 &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1375 Some(2),
1376 )
1377 .expect("repeated active declaration");
1378 ledger
1379 .validate_committed_integrity()
1380 .expect("repeated active declaration");
1381 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1382
1383 ledger = ledger
1384 .stage_validated_generation(
1385 &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1386 Some(3),
1387 )
1388 .expect("schema drift");
1389 ledger
1390 .validate_committed_integrity()
1391 .expect("schema metadata drift");
1392 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1393
1394 ledger = ledger
1395 .stage_reservation_generation(
1396 &[declaration("app.future_store.v1", 101, Some(1))],
1397 Some(4),
1398 )
1399 .expect("reservation-only generation");
1400 ledger
1401 .validate_committed_integrity()
1402 .expect("reservation-only generation");
1403 assert_eq!(
1404 record(&ledger, "app.future_store.v1").state,
1405 AllocationState::Reserved
1406 );
1407
1408 ledger = ledger
1409 .stage_validated_generation(
1410 &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1411 Some(5),
1412 )
1413 .expect("reservation activation");
1414 ledger
1415 .validate_committed_integrity()
1416 .expect("reservation activation");
1417 assert_eq!(
1418 record(&ledger, "app.future_store.v1").state,
1419 AllocationState::Active
1420 );
1421
1422 let retirement = AllocationRetirement::new(
1423 "app.users.v1",
1424 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1425 )
1426 .expect("retirement");
1427 ledger = ledger
1428 .stage_retirement_generation(&retirement, Some(6))
1429 .expect("retirement generation");
1430 ledger
1431 .validate_committed_integrity()
1432 .expect("retirement generation");
1433 assert_eq!(ledger.current_generation, 6);
1434 assert_eq!(
1435 record(&ledger, "app.users.v1").state,
1436 AllocationState::Retired { generation: 6 }
1437 );
1438 assert_eq!(
1439 record(&ledger, "app.future_store.v1").last_seen_generation,
1440 5
1441 );
1442 }
1443
1444 #[derive(Clone, Copy, Debug)]
1445 enum Transition {
1446 DeclareUsers,
1447 DeclareOrders,
1448 ReserveAudit,
1449 ActivateAudit,
1450 RetireUsers,
1451 RetireAudit,
1452 EmptyValidated,
1453 EmptyReservation,
1454 }
1455
1456 const TRANSITIONS: [Transition; 8] = [
1457 Transition::DeclareUsers,
1458 Transition::DeclareOrders,
1459 Transition::ReserveAudit,
1460 Transition::ActivateAudit,
1461 Transition::RetireUsers,
1462 Transition::RetireAudit,
1463 Transition::EmptyValidated,
1464 Transition::EmptyReservation,
1465 ];
1466
1467 fn apply_transition(
1468 ledger: &AllocationLedger,
1469 transition: Transition,
1470 committed_at: u64,
1471 ) -> Result<AllocationLedger, String> {
1472 match transition {
1473 Transition::DeclareUsers => ledger
1474 .stage_validated_generation(
1475 &validated(
1476 ledger.current_generation,
1477 vec![declaration("app.users.v1", 100, Some(1))],
1478 ),
1479 Some(committed_at),
1480 )
1481 .map_err(|err| err.to_string()),
1482 Transition::DeclareOrders => ledger
1483 .stage_validated_generation(
1484 &validated(
1485 ledger.current_generation,
1486 vec![declaration("app.orders.v1", 101, Some(1))],
1487 ),
1488 Some(committed_at),
1489 )
1490 .map_err(|err| err.to_string()),
1491 Transition::ReserveAudit => ledger
1492 .stage_reservation_generation(
1493 &[declaration("app.audit.v1", 102, Some(1))],
1494 Some(committed_at),
1495 )
1496 .map_err(|err| err.to_string()),
1497 Transition::ActivateAudit => ledger
1498 .stage_validated_generation(
1499 &validated(
1500 ledger.current_generation,
1501 vec![declaration("app.audit.v1", 102, Some(1))],
1502 ),
1503 Some(committed_at),
1504 )
1505 .map_err(|err| err.to_string()),
1506 Transition::RetireUsers => ledger
1507 .stage_retirement_generation(
1508 &AllocationRetirement::new(
1509 "app.users.v1",
1510 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1511 )
1512 .expect("retirement"),
1513 Some(committed_at),
1514 )
1515 .map_err(|err| err.to_string()),
1516 Transition::RetireAudit => ledger
1517 .stage_retirement_generation(
1518 &AllocationRetirement::new(
1519 "app.audit.v1",
1520 AllocationSlotDescriptor::memory_manager(102).expect("usable slot"),
1521 )
1522 .expect("retirement"),
1523 Some(committed_at),
1524 )
1525 .map_err(|err| err.to_string()),
1526 Transition::EmptyValidated => ledger
1527 .stage_validated_generation(
1528 &validated(ledger.current_generation, Vec::new()),
1529 Some(committed_at),
1530 )
1531 .map_err(|err| err.to_string()),
1532 Transition::EmptyReservation => ledger
1533 .stage_reservation_generation(&[], Some(committed_at))
1534 .map_err(|err| err.to_string()),
1535 }
1536 }
1537
1538 fn check_transition_sequences(
1539 ledger: AllocationLedger,
1540 depth: usize,
1541 sequence: &mut Vec<Transition>,
1542 ) {
1543 ledger
1544 .validate_committed_integrity()
1545 .unwrap_or_else(|err| panic!("sequence {sequence:?} violated integrity: {err}"));
1546 if depth == 0 {
1547 return;
1548 }
1549
1550 for transition in TRANSITIONS {
1551 sequence.push(transition);
1552 let next = apply_transition(&ledger, transition, sequence.len() as u64)
1553 .unwrap_or_else(|_| ledger.clone());
1554 check_transition_sequences(next, depth - 1, sequence);
1555 sequence.pop();
1556 }
1557 }
1558
1559 #[test]
1560 fn transition_matrix_preserves_committed_invariants() {
1561 check_transition_sequences(committed_ledger(0), 4, &mut Vec::new());
1562 }
1563
1564 #[test]
1565 fn new_committed_requires_strict_generation_history() {
1566 let structurally_valid =
1567 AllocationLedger::new(3, AllocationHistory::default()).expect("structurally valid DTO");
1568
1569 assert_eq!(structurally_valid.current_generation, 3);
1570
1571 let err = AllocationLedger::new_committed(3, AllocationHistory::default())
1572 .expect_err("committed ledger needs generation history");
1573
1574 assert_eq!(
1575 err,
1576 LedgerIntegrityError::MissingCurrentGenerationRecord {
1577 current_generation: 3
1578 }
1579 );
1580 }
1581
1582 #[test]
1583 fn new_committed_preserves_structural_rejection_before_history_checks() {
1584 let history = AllocationHistory::from_parts(
1585 vec![
1586 active_record("app.users.v1", 100),
1587 active_record("app.users.v1", 100),
1588 ],
1589 Vec::new(),
1590 );
1591
1592 assert_eq!(
1595 AllocationLedger::new_committed(3, history).expect_err("duplicate records"),
1596 LedgerIntegrityError::DuplicateStableKey {
1597 stable_key: StableKey::parse("app.users.v1").unwrap(),
1598 }
1599 );
1600
1601 let valid = active_committed_ledger();
1602 assert_eq!(
1603 AllocationLedger::new_committed(
1604 valid.current_generation(),
1605 valid.allocation_history().clone(),
1606 )
1607 .expect("valid committed history"),
1608 valid
1609 );
1610 }
1611
1612 #[test]
1613 fn validate_integrity_rejects_duplicate_stable_keys() {
1614 let mut ledger = ledger();
1615 for second_id in [101, 100] {
1616 *ledger.allocation_history.records_mut() = vec![
1617 active_record("app.users.v1", 100),
1618 active_record("app.users.v1", second_id),
1619 ];
1620
1621 let err = ledger
1622 .validate_integrity()
1623 .expect_err("duplicate key precedes duplicate slot");
1624
1625 assert_eq!(
1626 err,
1627 LedgerIntegrityError::DuplicateStableKey {
1628 stable_key: StableKey::parse("app.users.v1").unwrap(),
1629 }
1630 );
1631 }
1632 }
1633
1634 #[test]
1635 fn validate_integrity_rejects_duplicate_slots() {
1636 let mut ledger = ledger();
1637 *ledger.allocation_history.records_mut() = vec![
1638 active_record("app.users.v1", 100),
1639 active_record("app.orders.v1", 100),
1640 ];
1641
1642 let err = ledger.validate_integrity().expect_err("duplicate slot");
1643
1644 assert_eq!(
1645 err,
1646 LedgerIntegrityError::DuplicateSlot {
1647 slot: AllocationSlotDescriptor::memory_manager(100).unwrap(),
1648 }
1649 );
1650
1651 ledger.allocation_history.records_mut()[1].stable_key =
1652 serde_json::from_str("\"App.orders.v1\"").expect("decoded unvalidated key");
1653 assert_eq!(
1654 ledger.validate_integrity().unwrap_err(),
1655 LedgerIntegrityError::DuplicateSlot {
1656 slot: AllocationSlotDescriptor::memory_manager(100).unwrap(),
1657 },
1658 "duplicate slot precedes the second record's key validation"
1659 );
1660 }
1661
1662 #[test]
1663 fn validate_committed_integrity_rejects_decoded_invalid_stable_key() {
1664 let mut ledger = committed_ledger(1);
1665 ledger
1666 .allocation_history
1667 .records
1668 .push(active_record("app.users.v1", 100));
1669 let mut bytes = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
1670 let key_start = bytes
1671 .windows(b"app.users.v1".len())
1672 .position(|window| window == b"app.users.v1")
1673 .expect("encoded stable key");
1674 bytes[key_start] = b'A';
1675 let decoded: AllocationLedger =
1676 crate::test_cbor::from_slice(&bytes).expect("decode ledger");
1677
1678 let err = decoded
1679 .validate_committed_integrity()
1680 .expect_err("invalid decoded key must fail");
1681
1682 assert!(matches!(err, LedgerIntegrityError::InvalidStableKey(_)));
1683 }
1684
1685 #[test]
1686 fn validate_committed_integrity_rejects_decoded_invalid_memory_manager_slot() {
1687 let mut ledger = committed_ledger(1);
1688 let mut record = active_record("app.users.v1", 100);
1689 record.slot = AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1690 ledger.allocation_history.records.push(record);
1691
1692 let err = ledger
1693 .validate_committed_integrity()
1694 .expect_err("invalid decoded slot must fail");
1695
1696 assert!(matches!(
1697 err,
1698 LedgerIntegrityError::InvalidSlotDescriptor(
1699 MemoryManagerSlotError::InvalidMemoryManagerId { id }
1700 ) if id == MEMORY_MANAGER_INVALID_ID
1701 ));
1702 }
1703
1704 #[test]
1705 fn validate_integrity_rejects_retirement_before_last_observation() {
1706 let mut ledger = committed_ledger(5);
1707 let mut record = active_record("app.users.v1", 100);
1708 record.last_seen_generation = 4;
1709 record.state = AllocationState::Retired { generation: 3 };
1710 *ledger.allocation_history.records_mut() = vec![record];
1711
1712 let err = ledger
1713 .validate_committed_integrity()
1714 .expect_err("retirement must follow the last observation");
1715
1716 assert!(matches!(
1717 err,
1718 LedgerIntegrityError::RetirementNotAfterLastSeen { .. }
1719 ));
1720 }
1721
1722 #[test]
1723 fn validate_integrity_rejects_retirement_before_allocation() {
1724 let mut ledger = committed_ledger(2);
1725 let mut record = active_record("app.users.v1", 100);
1726 record.state = AllocationState::Retired { generation: 0 };
1727 *ledger.allocation_history.records_mut() = vec![record];
1728
1729 let err = ledger
1730 .validate_committed_integrity()
1731 .expect_err("retirement cannot predate allocation");
1732
1733 assert!(matches!(
1734 err,
1735 LedgerIntegrityError::RetiredBeforeFirstGeneration { .. }
1736 ));
1737 }
1738
1739 #[test]
1740 fn validate_integrity_requires_schema_history_at_first_generation() {
1741 let mut ledger = committed_ledger(2);
1742 let mut record = active_record("app.users.v1", 100);
1743 record.last_seen_generation = 2;
1744 record.schema_history[0].generation = 2;
1745 *ledger.allocation_history.records_mut() = vec![record];
1746
1747 let err = ledger
1748 .validate_committed_integrity()
1749 .expect_err("schema history must begin with the allocation");
1750
1751 assert!(matches!(
1752 err,
1753 LedgerIntegrityError::SchemaHistoryStartMismatch { .. }
1754 ));
1755 }
1756
1757 #[test]
1758 fn validate_integrity_rejects_schema_history_after_last_observation() {
1759 let mut ledger = committed_ledger(3);
1760 let mut record = active_record("app.users.v1", 100);
1761 record.last_seen_generation = 2;
1762 record.schema_history.push(
1763 SchemaMetadataRecord::new(3, SchemaMetadata::new(Some(2)).expect("schema"))
1764 .expect("schema record"),
1765 );
1766 *ledger.allocation_history.records_mut() = vec![record];
1767
1768 let err = ledger
1769 .validate_committed_integrity()
1770 .expect_err("schema metadata cannot postdate the last observation");
1771
1772 assert!(matches!(
1773 err,
1774 LedgerIntegrityError::SchemaHistoryAfterLastSeen { .. }
1775 ));
1776 }
1777
1778 #[test]
1779 fn validate_integrity_rejects_non_increasing_schema_history() {
1780 let mut ledger = ledger();
1781 let mut record = active_record("app.users.v1", 100);
1782 record.schema_history.push(SchemaMetadataRecord {
1783 generation: 1,
1784 schema: SchemaMetadata::default(),
1785 });
1786 *ledger.allocation_history.records_mut() = vec![record];
1787
1788 let err = ledger
1789 .validate_integrity()
1790 .expect_err("non-increasing schema history");
1791
1792 assert!(matches!(
1793 err,
1794 LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1795 ));
1796 }
1797
1798 #[test]
1799 fn validate_integrity_rejects_invalid_schema_metadata_history() {
1800 let mut ledger = committed_ledger(1);
1801 let mut record = active_record("app.users.v1", 100);
1802 record.schema_history[0].schema = invalid_schema_metadata();
1803 *ledger.allocation_history.records_mut() = vec![record];
1804
1805 let err = ledger
1806 .validate_committed_integrity()
1807 .expect_err("invalid committed schema metadata");
1808
1809 assert_eq!(
1810 err,
1811 LedgerIntegrityError::InvalidSchemaMetadata {
1812 stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1813 generation: 1,
1814 error: SchemaMetadataError::InvalidVersion,
1815 }
1816 );
1817 }
1818
1819 #[test]
1820 fn validate_committed_integrity_requires_current_generation_record() {
1821 let err = ledger()
1822 .validate_committed_integrity()
1823 .expect_err("missing current generation");
1824
1825 assert_eq!(
1826 err,
1827 LedgerIntegrityError::MissingCurrentGenerationRecord {
1828 current_generation: 3
1829 }
1830 );
1831 }
1832
1833 #[test]
1834 fn committed_integrity_rejects_allocation_references_to_genesis() {
1835 for generation in [0, 1, 2] {
1836 let mut ledger = committed_ledger(generation);
1837 let mut record = AllocationRecord::active(0, declaration("app.genesis.v1", 100, None));
1838 if generation == 2 {
1839 record.last_seen_generation = 1;
1840 record
1841 .schema_history
1842 .push(SchemaMetadataRecord::new(1, SchemaMetadata::default()).unwrap());
1843 record.state = AllocationState::Retired { generation: 2 };
1844 }
1845 ledger.allocation_history.push_record(record);
1846 ledger.validate_integrity().expect("structurally valid DTO");
1847 assert_eq!(
1848 ledger.validate_committed_integrity(),
1849 Err(LedgerIntegrityError::UnknownRecordGeneration {
1850 stable_key: StableKey::parse("app.genesis.v1").unwrap(),
1851 generation: 0,
1852 })
1853 );
1854 }
1855 }
1856
1857 #[test]
1858 fn validate_committed_integrity_rejects_generation_history_gaps() {
1859 let mut ledger = committed_ledger(3);
1860 ledger.allocation_history.generations_mut().remove(1);
1861 ledger
1862 .allocation_history
1863 .push_record(AllocationRecord::active(
1864 0,
1865 declaration("app.genesis.v1", 100, None),
1866 ));
1867
1868 let err = ledger
1869 .validate_committed_integrity()
1870 .expect_err("generation history gap takes precedence over genesis reference");
1871
1872 assert!(matches!(
1873 err,
1874 LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1875 ));
1876 }
1877
1878 #[test]
1879 fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1880 let mut store = LedgerCommitStore::default();
1881 let mut invalid = ledger();
1882 *invalid.allocation_history.records_mut() = vec![
1883 active_record("app.users.v1", 100),
1884 active_record("app.orders.v1", 100),
1885 ];
1886
1887 let err = store.commit(&invalid).expect_err("invalid ledger");
1888
1889 assert!(matches!(
1890 err,
1891 LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1892 ));
1893 assert!(store.physical().is_uninitialized());
1894 }
1895
1896 #[test]
1897 fn ledger_commit_store_recovers_latest_committed_ledger() {
1898 let mut store = LedgerCommitStore::default();
1899 let first = committed_ledger(1);
1900 let second = committed_ledger(2);
1901
1902 store.commit(&first).expect("first commit");
1903 store.commit(&second).expect("second commit");
1904 let recovered = store.recover().expect("recovered ledger");
1905
1906 assert_eq!(recovered.current_generation(), 2);
1907 assert_eq!(recovered.physical_generation(), 2);
1908 }
1909
1910 #[test]
1911 fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1912 let mut store = LedgerCommitStore::default();
1913 let ledger = committed_ledger(1);
1914
1915 store.commit(&ledger).expect("commit");
1916
1917 let committed = store.physical().authoritative().expect("authoritative");
1918 let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1919 assert_eq!(envelope.payload(), CborLedgerCodec::encode(&ledger));
1920 }
1921
1922 #[test]
1923 fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1924 let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1925 let store = LedgerCommitStore {
1926 physical: DualCommitStore {
1927 slot0: Some(committed),
1928 slot1: None,
1929 },
1930 };
1931
1932 let err = store.recover().expect_err("bad envelope");
1933
1934 assert!(matches!(
1935 err,
1936 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1937 ));
1938 }
1939
1940 #[test]
1941 fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1942 let mut payload = enveloped_payload(&committed_ledger(1));
1943 payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1944 let store = LedgerCommitStore {
1945 physical: DualCommitStore {
1946 slot0: Some(CommittedGenerationBytes::new(1, payload)),
1947 slot1: None,
1948 },
1949 };
1950
1951 let err = store
1952 .recover()
1953 .expect_err("unknown ledger format version must fail");
1954
1955 assert_eq!(
1956 err,
1957 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1958 marker: *b"ICMF",
1959 version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1960 })
1961 );
1962 }
1963
1964 #[test]
1965 fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
1966 let genesis = enveloped_payload(&committed_ledger(0));
1967 let committed = CommittedGenerationBytes::new(0, genesis.clone());
1968 let mut corrupt = committed.clone();
1969 corrupt.checksum ^= 1;
1970 let mut unsupported = genesis.clone();
1971 unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1972 let cases = [
1973 ("empty", None, None),
1974 ("one slot", Some(committed.clone()), None),
1975 (
1976 "identical slots",
1977 Some(committed.clone()),
1978 Some(committed.clone()),
1979 ),
1980 (
1981 "corrupt inactive",
1982 Some(committed.clone()),
1983 Some(corrupt.clone()),
1984 ),
1985 ("corrupt active", Some(corrupt), Some(committed.clone())),
1986 (
1987 "ambiguous",
1988 Some(committed),
1989 Some(CommittedGenerationBytes::new(0, vec![1])),
1990 ),
1991 (
1992 "unsupported format",
1993 Some(CommittedGenerationBytes::new(0, unsupported)),
1994 None,
1995 ),
1996 (
1997 "invalid CBOR",
1998 Some(CommittedGenerationBytes::new(
1999 0,
2000 LedgerPayloadEnvelope::current(vec![255])
2001 .try_encode()
2002 .unwrap(),
2003 )),
2004 None,
2005 ),
2006 (
2007 "generation mismatch",
2008 Some(CommittedGenerationBytes::new(1, genesis)),
2009 None,
2010 ),
2011 (
2012 "invalid committed integrity",
2013 Some(CommittedGenerationBytes::new(
2014 3,
2015 enveloped_payload(&ledger()),
2016 )),
2017 None,
2018 ),
2019 ];
2020
2021 for (label, slot0, slot1) in cases {
2022 let empty = slot0.is_none() && slot1.is_none();
2023 let store = LedgerCommitStore {
2024 physical: DualCommitStore { slot0, slot1 },
2025 };
2026 let before = store.clone();
2027 let (recovered, diagnostic) = store.recover_with_diagnostic();
2028 assert_eq!(recovered, store.recover(), "{label}");
2029 assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
2030 assert_eq!(store, before, "{label}");
2031
2032 let mut initializing = store.clone();
2033 let initialized = initializing.recover_or_initialize(&committed_ledger(0));
2034 if empty {
2035 let initialized = initialized.expect("empty store accepts genesis");
2036 assert_eq!(initialized.current_generation(), 0);
2037 assert_eq!(initialized.physical_generation(), 0);
2038 assert_eq!(initializing.recover().unwrap(), initialized);
2039 } else {
2040 assert_eq!(initialized, recovered, "{label}");
2041 assert_eq!(initializing, before, "{label}");
2042 }
2043
2044 if let Err(LedgerCommitError::Recovery(error)) = recovered {
2045 assert_eq!(diagnostic.recovery, Err(error), "{label}");
2046 } else {
2047 assert!(diagnostic.recovery.is_ok(), "{label}");
2048 }
2049 }
2050 }
2051
2052 #[test]
2053 fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
2054 let mut store = LedgerCommitStore::default();
2055 let genesis = committed_ledger(0);
2056
2057 let recovered = store
2058 .recover_or_initialize(&genesis)
2059 .expect("current-format genesis ledger");
2060 assert_eq!(recovered.current_generation(), 0);
2061 assert_eq!(recovered.physical_generation(), 0);
2062 assert_eq!(recovered.ledger().allocation_history().generations(), []);
2063 assert_eq!(recovered, store.recover().unwrap());
2064
2065 let first = recovered
2066 .ledger()
2067 .stage_validated_generation(
2068 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2069 None,
2070 )
2071 .expect("first real generation");
2072 let recovered = store.commit(&first).expect("first commit");
2073
2074 assert_eq!(recovered, store.recover().unwrap());
2075 assert_eq!(recovered.current_generation(), 1);
2076 assert_eq!(recovered.physical_generation(), 1);
2077 assert_eq!(
2078 recovered.ledger().allocation_history().generations()[0].generation,
2079 1
2080 );
2081 assert_eq!(
2082 record(recovered.ledger(), "app.users.v1").first_generation,
2083 1
2084 );
2085 }
2086
2087 #[test]
2088 fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
2089 let mut store = LedgerCommitStore::default();
2090 let genesis = committed_ledger(0);
2091 store.commit(&genesis).expect("genesis commit");
2092 let first = genesis
2093 .stage_validated_generation(
2094 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2095 None,
2096 )
2097 .expect("first generation");
2098 let first = store.commit(&first).expect("first commit");
2099 let second = first
2100 .ledger()
2101 .stage_validated_generation(
2102 &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
2103 None,
2104 )
2105 .expect("second generation");
2106
2107 store.commit(&second).expect("second commit");
2108 store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
2109
2110 let err = store
2111 .recover()
2112 .expect_err("corrupt latest must not roll back");
2113
2114 assert_eq!(
2115 err,
2116 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2117 slot0_invalid: true,
2118 slot1_invalid: false,
2119 })
2120 );
2121 }
2122
2123 #[test]
2124 fn ledger_commit_store_recovers_identical_duplicate_slots() {
2125 let ledger = committed_ledger(0)
2126 .stage_validated_generation(
2127 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2128 None,
2129 )
2130 .expect("first generation");
2131 let payload = enveloped_payload(&ledger);
2132 let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
2133 let store = LedgerCommitStore {
2134 physical: DualCommitStore {
2135 slot0: Some(committed.clone()),
2136 slot1: Some(committed),
2137 },
2138 };
2139
2140 let recovered = store.recover().expect("recovered");
2141
2142 assert_eq!(recovered.ledger(), &ledger);
2143 }
2144
2145 #[test]
2146 fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
2147 let mut store = LedgerCommitStore::default();
2148 let first = committed_ledger(1);
2149 let second = committed_ledger(2);
2150
2151 store.commit(&first).expect("first commit");
2152 store
2153 .write_corrupt_inactive_ledger(&second)
2154 .expect("corrupt write");
2155 let err = store.recover().expect_err("corrupt slot");
2156
2157 assert_eq!(
2158 err,
2159 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2160 slot0_invalid: false,
2161 slot1_invalid: true,
2162 })
2163 );
2164 }
2165
2166 #[test]
2167 fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
2168 let store = LedgerCommitStore {
2169 physical: DualCommitStore {
2170 slot0: Some(CommittedGenerationBytes::new(
2171 7,
2172 enveloped_payload(&committed_ledger(6)),
2173 )),
2174 slot1: None,
2175 },
2176 };
2177 let err = store.recover().expect_err("mismatch");
2178
2179 assert_eq!(
2180 err,
2181 LedgerCommitError::PhysicalLogicalGenerationMismatch {
2182 physical_generation: 7,
2183 logical_generation: 6
2184 }
2185 );
2186 }
2187
2188 #[test]
2189 fn ledger_commit_store_rejects_non_next_logical_generation() {
2190 let mut store = LedgerCommitStore::default();
2191 store.commit(&committed_ledger(1)).expect("first commit");
2192
2193 let err = store
2194 .commit(&committed_ledger(3))
2195 .expect_err("skipped generation");
2196
2197 assert_eq!(
2198 err,
2199 LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2200 expected: 2,
2201 actual: 3
2202 })
2203 );
2204 }
2205
2206 #[test]
2207 fn ledger_commit_store_initializes_empty_store_explicitly() {
2208 let mut store = LedgerCommitStore::default();
2209 let genesis = committed_ledger(3);
2210
2211 let recovered = store
2212 .recover_or_initialize(&genesis)
2213 .expect("initialized ledger");
2214
2215 assert_eq!(recovered.current_generation(), 3);
2216 assert_eq!(recovered.physical_generation(), 3);
2217 assert!(!store.physical().is_uninitialized());
2218 }
2219
2220 #[test]
2221 fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2222 let mut store = LedgerCommitStore::default();
2223 store
2224 .write_corrupt_inactive_ledger(&ledger())
2225 .expect("corrupt write");
2226
2227 let err = store
2228 .recover_or_initialize(&ledger())
2229 .expect_err("corrupt state");
2230
2231 assert!(matches!(
2232 err,
2233 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2234 slot0_invalid: true,
2235 slot1_invalid: false,
2236 })
2237 ));
2238 }
2239 #[test]
2240 fn history_boundary_round_trips_and_rejects_next_generation_without_mutation() {
2241 let ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2242 let mut store = LedgerCommitStore::default();
2243 let recovered = store.commit(&ledger).expect("boundary is admissible");
2244 let record = crate::StableCellLedgerRecord::new(store.clone());
2245 let bytes = crate::test_cbor::to_vec(&record).unwrap();
2246 assert_eq!(record.encoded_size(), bytes.len());
2247 let recovered_record = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2248 assert_eq!(
2249 recovered_record.store().recover().unwrap().ledger(),
2250 &ledger
2251 );
2252 let before = store.clone();
2253 let err = crate::AllocationBootstrap::new(&mut store)
2254 .validate_and_commit(
2255 DeclarationSnapshot::new(Vec::new()).unwrap(),
2256 &crate::GenericRangePolicy,
2257 None,
2258 )
2259 .unwrap_err();
2260 assert!(matches!(
2261 err,
2262 crate::BootstrapError::Staging(AllocationStageError::Integrity(
2263 LedgerIntegrityError::LimitExceeded { .. }
2264 ))
2265 ));
2266 assert_eq!(store, before);
2267 assert_eq!(
2268 recovered.current_generation(),
2269 crate::constants::MAX_LEDGER_GENERATIONS as u64
2270 );
2271 let oversize = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 + 1);
2272 assert!(matches!(
2273 store.commit(&oversize),
2274 Err(LedgerCommitError::Integrity(
2275 LedgerIntegrityError::LimitExceeded { .. }
2276 ))
2277 ));
2278 assert_eq!(store, before);
2279 }
2280
2281 #[test]
2282 fn record_count_boundary_and_malicious_history_reject_on_recovery() {
2283 let mut ledger = committed_ledger(1);
2284 ledger.allocation_history.records = (0..255_u8)
2285 .map(|id| active_record(&format!("app.slot{id}.v1"), id))
2286 .collect();
2287 let mut store = LedgerCommitStore::default();
2288 store.commit(&ledger).unwrap();
2289 assert_eq!(
2290 store
2291 .recover()
2292 .unwrap()
2293 .ledger()
2294 .allocation_history()
2295 .records()
2296 .len(),
2297 255
2298 );
2299 ledger
2300 .allocation_history
2301 .records
2302 .push(active_record("app.extra.v1", 254));
2303 assert!(matches!(
2304 store.commit(&ledger),
2305 Err(LedgerCommitError::Integrity(
2306 LedgerIntegrityError::LimitExceeded { .. }
2307 ))
2308 ));
2309 let payload = enveloped_payload(&ledger);
2310 let mut invalid = LedgerCommitStore {
2311 physical: DualCommitStore {
2312 slot0: Some(CommittedGenerationBytes::new(1, payload)),
2313 slot1: None,
2314 },
2315 };
2316 assert!(invalid.recover().is_err());
2317 assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
2318 for bytes in [
2319 vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2320 vec![0x81; 34],
2321 vec![0xa1],
2322 ] {
2323 let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
2324 let invalid = LedgerCommitStore {
2325 physical: DualCommitStore {
2326 slot0: Some(CommittedGenerationBytes::new(1, payload)),
2327 slot1: None,
2328 },
2329 };
2330 assert!(matches!(
2331 invalid.recover(),
2332 Err(LedgerCommitError::Codec(_))
2333 ));
2334 }
2335 }
2336 #[test]
2337 fn encoded_byte_limit_rejects_before_commit_mutation() {
2338 let mut store = LedgerCommitStore::default();
2339 store.commit(&committed_ledger(0)).unwrap();
2340 let before = store.clone();
2341 let mut ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2342 for generation in ledger.allocation_history.generations_mut() {
2343 generation.runtime_fingerprint =
2344 Some("x".repeat(crate::constants::DIAGNOSTIC_STRING_MAX_BYTES));
2345 }
2346 assert!(matches!(
2347 store.commit(&ledger),
2348 Err(LedgerCommitError::Integrity(
2349 LedgerIntegrityError::LimitExceeded {
2350 resource: "ledger bytes",
2351 ..
2352 }
2353 ))
2354 ));
2355 assert_eq!(store, before);
2356 let mut envelope = LedgerPayloadEnvelope::current(Vec::new())
2357 .try_encode()
2358 .unwrap();
2359 envelope[16..24]
2360 .copy_from_slice(&((crate::constants::MAX_LEDGER_BYTES + 1) as u64).to_le_bytes());
2361 assert!(matches!(
2362 LedgerPayloadEnvelope::decode(&envelope),
2363 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { .. })
2364 ));
2365 }
2366}