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