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