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