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