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