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::{
30 stage_reservation_generation, stage_retirement_generation, stage_validated_generation,
31};
32
33fn decode_ledger(bytes: &[u8]) -> Result<AllocationLedger, String> {
34 crate::cbor::from_slice_exact(bytes).map_err(|err| err.to_string())
35}
36
37#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
50#[serde(deny_unknown_fields)]
51pub struct LedgerCommitStore {
52 physical: DualCommitStore,
54}
55
56impl LedgerCommitStore {
57 #[must_use]
59 pub const fn physical(&self) -> &DualCommitStore {
60 &self.physical
61 }
62
63 pub fn recover(&self) -> Result<RecoveredLedger, LedgerCommitError> {
65 let committed = self
66 .physical
67 .authoritative()
68 .map_err(LedgerCommitError::Recovery)?;
69 Self::recover_committed(committed)
70 }
71
72 pub(crate) fn recover_with_diagnostic(
73 &self,
74 ) -> (
75 Result<RecoveredLedger, LedgerCommitError>,
76 CommitStoreDiagnostic,
77 ) {
78 let (committed, diagnostic) = self.physical.authoritative_with_diagnostic();
79 let recovered = committed
80 .map_err(LedgerCommitError::Recovery)
81 .and_then(Self::recover_committed);
82 (recovered, diagnostic)
83 }
84
85 fn recover_committed(
86 committed: &CommittedGenerationBytes,
87 ) -> Result<RecoveredLedger, LedgerCommitError> {
88 let payload = LedgerPayloadEnvelope::decode_payload(committed.payload())
89 .map_err(LedgerCommitError::PayloadEnvelope)?;
90 let ledger = decode_ledger(payload).map_err(LedgerCommitError::Codec)?;
91 if committed.generation() != ledger.current_generation {
92 return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
93 physical_generation: committed.generation(),
94 logical_generation: ledger.current_generation,
95 });
96 }
97 ledger
98 .validate_committed_integrity()
99 .map_err(LedgerCommitError::Integrity)?;
100 Ok(RecoveredLedger::from_trusted_ledger(ledger))
101 }
102
103 pub fn recover_or_initialize(
112 &mut self,
113 genesis: &AllocationLedger,
114 ) -> Result<RecoveredLedger, LedgerCommitError> {
115 match self.recover() {
116 Ok(ledger) => Ok(ledger),
117 Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)) => {
119 self.commit(genesis)
120 }
121 Err(err) => Err(err),
122 }
123 }
124
125 pub fn commit(
131 &mut self,
132 ledger: &AllocationLedger,
133 ) -> Result<RecoveredLedger, LedgerCommitError> {
134 self.commit_generation(ledger)?;
135 Ok(RecoveredLedger::from_trusted_ledger(ledger.clone()))
139 }
140
141 pub(crate) fn commit_generation(
144 &mut self,
145 ledger: &AllocationLedger,
146 ) -> Result<(), LedgerCommitError> {
147 ledger
148 .validate_committed_integrity()
149 .map_err(LedgerCommitError::Integrity)?;
150 let payload =
151 LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
152 self.physical
153 .commit_payload_at_generation(ledger.current_generation, payload)
154 .map(|_| ())
155 .map_err(LedgerCommitError::Recovery)
156 }
157
158 #[cfg(test)]
160 pub fn write_corrupt_inactive_ledger(
161 &mut self,
162 ledger: &AllocationLedger,
163 ) -> Result<(), LedgerCommitError> {
164 let payload =
165 LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
166 self.physical
167 .write_corrupt_inactive_slot(ledger.current_generation, payload);
168 Ok(())
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::test_cbor::hex_fixture;
176 use crate::{
177 declaration::{AllocationDeclaration, DeclarationSnapshot, DeclarationSnapshotError},
178 key::StableKey,
179 physical::CommittedGenerationBytes,
180 schema::SchemaMetadata,
181 slot::MemoryManagerSlot,
182 };
183 use ic_stable_structures::Storable;
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_same_key_different_slot() {
962 let mut ledger = ledger();
963 ledger.allocation_history.records = vec![AllocationRecord::reserved(
964 3,
965 &declaration("app.future_store.v1", 100, None),
966 )];
967 let reservations = vec![declaration("app.future_store.v1", 101, None)];
968
969 let err = ledger
970 .stage_reservation_generation(&reservations, None)
971 .expect_err("reservation key cannot move slots");
972
973 assert!(matches!(
974 err,
975 AllocationReservationError::StableKeySlotConflict { .. }
976 ));
977 }
978
979 #[test]
980 fn stage_reservation_generation_rejects_same_slot_different_key() {
981 let mut ledger = ledger();
982 ledger.allocation_history.records = vec![AllocationRecord::reserved(
983 3,
984 &declaration("app.future_store.v1", 100, None),
985 )];
986 let reservations = vec![declaration("app.other_future_store.v1", 100, None)];
987
988 let err = ledger
989 .stage_reservation_generation(&reservations, None)
990 .expect_err("reservation slot cannot be reused by another key");
991
992 assert!(matches!(
993 err,
994 AllocationReservationError::SlotStableKeyConflict { .. }
995 ));
996 }
997
998 #[test]
999 fn stage_reservation_generation_rejects_active_allocation() {
1000 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1001 let staged = ledger()
1002 .stage_validated_generation(&active, None)
1003 .expect("active generation");
1004 let reservations = vec![declaration("app.users.v1", 100, None)];
1005
1006 let err = staged
1007 .stage_reservation_generation(&reservations, None)
1008 .expect_err("active cannot become reserved");
1009
1010 assert!(matches!(
1011 err,
1012 AllocationReservationError::ActiveAllocation { .. }
1013 ));
1014 }
1015
1016 #[test]
1017 fn stage_reservation_generation_rejects_retired_allocation() {
1018 let mut ledger = ledger();
1019 let mut record = active_record("app.users.v1", 100);
1020 record.state = AllocationState::Retired { generation: 3 };
1021 ledger.allocation_history.records = vec![record];
1022 let reservations = vec![declaration("app.users.v1", 100, None)];
1023
1024 let err = ledger
1025 .stage_reservation_generation(&reservations, None)
1026 .expect_err("retired cannot revive");
1027
1028 assert!(matches!(
1029 err,
1030 AllocationReservationError::RetiredAllocation { .. }
1031 ));
1032 }
1033
1034 #[test]
1035 fn stage_validated_generation_activates_reserved_record() {
1036 let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1037 let staged = ledger()
1038 .stage_reservation_generation(&reservations, None)
1039 .expect("reserved generation");
1040 let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
1041
1042 let staged = staged
1043 .stage_validated_generation(&active, None)
1044 .expect("active generation");
1045 let record = &staged.allocation_history.records()[0];
1046
1047 assert_eq!(record.state, AllocationState::Active);
1048 assert_eq!(record.first_generation, 4);
1049 assert_eq!(record.last_seen_generation, 5);
1050 assert_eq!(record.schema_history.len(), 2);
1051 }
1052
1053 #[test]
1054 fn stage_retirement_generation_tombstones_named_allocation() {
1055 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1056 let staged = ledger()
1057 .stage_validated_generation(&active, None)
1058 .expect("active generation");
1059 let retirement = AllocationRetirement::new(
1060 "app.users.v1",
1061 MemoryManagerSlot::new(100).expect("usable slot"),
1062 )
1063 .expect("retirement");
1064
1065 let staged = staged
1066 .stage_retirement_generation(&retirement, Some(42))
1067 .expect("retired generation");
1068 let record = &staged.allocation_history.records()[0];
1069
1070 assert_eq!(staged.current_generation, 5);
1071 assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1072 assert_eq!(
1073 staged.allocation_history.generations()[1].declaration_count,
1074 0
1075 );
1076 }
1077
1078 #[test]
1079 fn stage_retirement_generation_tombstones_reserved_allocation() {
1080 let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1081 let staged = ledger()
1082 .stage_reservation_generation(&reservations, None)
1083 .expect("reserved generation");
1084 let retirement = AllocationRetirement::new(
1085 "app.future_store.v1",
1086 MemoryManagerSlot::new(100).expect("usable slot"),
1087 )
1088 .expect("retirement");
1089
1090 let staged = staged
1091 .stage_retirement_generation(&retirement, Some(42))
1092 .expect("reserved retirement generation");
1093 let record = &staged.allocation_history.records()[0];
1094
1095 assert_eq!(staged.current_generation, 5);
1096 assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1097 assert_eq!(record.first_generation, 4);
1098 assert_eq!(
1099 staged.allocation_history.generations()[1].declaration_count(),
1100 0
1101 );
1102 }
1103
1104 #[test]
1105 fn stage_retirement_generation_rejects_generation_overflow() {
1106 let mut ledger = ledger();
1107 ledger.current_generation = u64::MAX;
1108 ledger.allocation_history.records = vec![active_record("app.users.v1", 100)];
1109 let retirement = AllocationRetirement::new(
1110 "app.users.v1",
1111 MemoryManagerSlot::new(100).expect("usable slot"),
1112 )
1113 .expect("retirement");
1114
1115 let err = ledger
1116 .stage_retirement_generation(&retirement, None)
1117 .expect_err("overflow must fail");
1118
1119 assert_eq!(
1120 err,
1121 AllocationRetirementError::GenerationOverflow {
1122 generation: u64::MAX
1123 }
1124 );
1125 }
1126
1127 #[test]
1128 fn stage_retirement_generation_requires_matching_slot() {
1129 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1130 let staged = ledger()
1131 .stage_validated_generation(&active, None)
1132 .expect("active generation");
1133 let retirement = AllocationRetirement::new(
1134 "app.users.v1",
1135 MemoryManagerSlot::new(101).expect("usable slot"),
1136 )
1137 .expect("retirement");
1138
1139 let err = staged
1140 .stage_retirement_generation(&retirement, None)
1141 .expect_err("slot mismatch");
1142
1143 assert!(matches!(
1144 err,
1145 AllocationRetirementError::SlotMismatch { .. }
1146 ));
1147 }
1148
1149 #[test]
1150 fn retirement_rejections_preserve_source_and_slot_error_precedence() {
1151 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1152 let source = ledger().stage_validated_generation(&active, None).unwrap();
1153 let retirement =
1154 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
1155 .unwrap();
1156 let source = source
1157 .stage_retirement_generation(&retirement, None)
1158 .unwrap();
1159 let before = source.clone();
1160
1161 assert!(matches!(
1162 source.stage_retirement_generation(&retirement, None),
1163 Err(AllocationRetirementError::AlreadyRetired { .. })
1164 ));
1165 let wrong_slot =
1166 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(101).unwrap())
1167 .unwrap();
1168 assert!(matches!(
1169 source.stage_retirement_generation(&wrong_slot, None),
1170 Err(AllocationRetirementError::SlotMismatch { .. })
1171 ));
1172 let unknown =
1173 AllocationRetirement::new("app.unknown.v1", MemoryManagerSlot::new(100).unwrap())
1174 .unwrap();
1175 assert!(matches!(
1176 source.stage_retirement_generation(&unknown, None),
1177 Err(AllocationRetirementError::UnknownStableKey(_))
1178 ));
1179 assert_eq!(source, before);
1180 }
1181
1182 #[test]
1183 fn allocation_retirement_constructor_rejects_invalid_key() {
1184 let err = AllocationRetirement::new("App.users.v1", MemoryManagerSlot::new(100).unwrap())
1185 .expect_err("invalid key must fail at construction");
1186 assert!(matches!(err, AllocationRetirementError::Key(_)));
1187 }
1188
1189 #[test]
1190 fn retirement_decode_rejects_unusable_memory_manager_slot() {
1191 let retirement =
1192 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
1193 .unwrap();
1194 let mut value = serde_json::to_value(retirement).unwrap();
1195 value["slot"]["slot"]["MemoryManagerId"] = serde_json::json!(255);
1196 assert!(serde_json::from_value::<AllocationRetirement>(value).is_err());
1197 }
1198
1199 #[test]
1200 fn retirement_decode_rejects_invalid_stable_key() {
1201 let retirement = AllocationRetirement::new(
1202 "app.users.v1",
1203 MemoryManagerSlot::new(100).expect("usable slot"),
1204 )
1205 .expect("retirement");
1206 let mut value = crate::test_cbor::to_value(retirement).expect("retirement value");
1207 *map_field_mut(value_map_mut(&mut value), "stable_key") =
1208 crate::test_cbor::Value::Text("App.users.v1".to_string());
1209 let bytes = crate::test_cbor::to_vec(&value).expect("retirement bytes");
1210 assert!(crate::test_cbor::from_slice::<AllocationRetirement>(&bytes).is_err());
1211 }
1212
1213 #[test]
1214 fn snapshot_can_feed_validated_generation() {
1215 let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1216 .expect("snapshot");
1217 let (declarations, runtime_fingerprint) = snapshot.into_parts();
1218 let validated =
1219 crate::capability::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1220
1221 let staged = ledger()
1222 .stage_validated_generation(&validated, None)
1223 .expect("validated generation");
1224
1225 assert_eq!(staged.allocation_history.records().len(), 1);
1226 }
1227
1228 #[test]
1229 fn stage_validated_generation_records_runtime_fingerprint() {
1230 let validated = crate::capability::ValidatedAllocations::new(
1231 3,
1232 vec![declaration("app.users.v1", 100, None)],
1233 Some("wasm:abc123".to_string()),
1234 );
1235
1236 let staged = ledger()
1237 .stage_validated_generation(&validated, None)
1238 .expect("validated generation");
1239
1240 assert_eq!(
1241 staged.allocation_history.generations()[0].runtime_fingerprint,
1242 Some("wasm:abc123".to_string())
1243 );
1244 }
1245
1246 #[test]
1247 fn strict_committed_integrity_accepts_full_lifecycle() {
1248 let mut ledger = committed_ledger(0);
1249 ledger
1250 .validate_committed_integrity()
1251 .expect("genesis ledger with no history");
1252
1253 ledger = ledger
1254 .stage_validated_generation(
1255 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1256 Some(1),
1257 )
1258 .expect("first real commit after genesis");
1259 ledger
1260 .validate_committed_integrity()
1261 .expect("first real commit");
1262
1263 ledger = ledger
1264 .stage_validated_generation(
1265 &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1266 Some(2),
1267 )
1268 .expect("repeated active declaration");
1269 ledger
1270 .validate_committed_integrity()
1271 .expect("repeated active declaration");
1272 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1273
1274 ledger = ledger
1275 .stage_validated_generation(
1276 &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1277 Some(3),
1278 )
1279 .expect("schema drift");
1280 ledger
1281 .validate_committed_integrity()
1282 .expect("schema metadata drift");
1283 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1284
1285 ledger = ledger
1286 .stage_reservation_generation(
1287 &[declaration("app.future_store.v1", 101, Some(1))],
1288 Some(4),
1289 )
1290 .expect("reservation-only generation");
1291 ledger
1292 .validate_committed_integrity()
1293 .expect("reservation-only generation");
1294 assert_eq!(
1295 record(&ledger, "app.future_store.v1").state,
1296 AllocationState::Reserved
1297 );
1298
1299 ledger = ledger
1300 .stage_validated_generation(
1301 &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1302 Some(5),
1303 )
1304 .expect("reservation activation");
1305 ledger
1306 .validate_committed_integrity()
1307 .expect("reservation activation");
1308 assert_eq!(
1309 record(&ledger, "app.future_store.v1").state,
1310 AllocationState::Active
1311 );
1312
1313 let retirement = AllocationRetirement::new(
1314 "app.users.v1",
1315 MemoryManagerSlot::new(100).expect("usable slot"),
1316 )
1317 .expect("retirement");
1318 ledger = ledger
1319 .stage_retirement_generation(&retirement, Some(6))
1320 .expect("retirement generation");
1321 ledger
1322 .validate_committed_integrity()
1323 .expect("retirement generation");
1324 assert_eq!(ledger.current_generation, 6);
1325 assert_eq!(
1326 record(&ledger, "app.users.v1").state,
1327 AllocationState::Retired { generation: 6 }
1328 );
1329 assert_eq!(
1330 record(&ledger, "app.future_store.v1").last_seen_generation,
1331 5
1332 );
1333 }
1334
1335 #[derive(Clone, Copy, Debug)]
1336 enum Transition {
1337 DeclareUsers,
1338 DeclareOrders,
1339 ReserveAudit,
1340 ActivateAudit,
1341 RetireUsers,
1342 RetireAudit,
1343 EmptyValidated,
1344 EmptyReservation,
1345 }
1346
1347 const TRANSITIONS: [Transition; 8] = [
1348 Transition::DeclareUsers,
1349 Transition::DeclareOrders,
1350 Transition::ReserveAudit,
1351 Transition::ActivateAudit,
1352 Transition::RetireUsers,
1353 Transition::RetireAudit,
1354 Transition::EmptyValidated,
1355 Transition::EmptyReservation,
1356 ];
1357
1358 fn apply_transition(
1359 ledger: &AllocationLedger,
1360 transition: Transition,
1361 committed_at: u64,
1362 ) -> Result<AllocationLedger, String> {
1363 match transition {
1364 Transition::DeclareUsers => ledger
1365 .stage_validated_generation(
1366 &validated(
1367 ledger.current_generation,
1368 vec![declaration("app.users.v1", 100, Some(1))],
1369 ),
1370 Some(committed_at),
1371 )
1372 .map_err(|err| err.to_string()),
1373 Transition::DeclareOrders => ledger
1374 .stage_validated_generation(
1375 &validated(
1376 ledger.current_generation,
1377 vec![declaration("app.orders.v1", 101, Some(1))],
1378 ),
1379 Some(committed_at),
1380 )
1381 .map_err(|err| err.to_string()),
1382 Transition::ReserveAudit => ledger
1383 .stage_reservation_generation(
1384 &[declaration("app.audit.v1", 102, Some(1))],
1385 Some(committed_at),
1386 )
1387 .map_err(|err| err.to_string()),
1388 Transition::ActivateAudit => ledger
1389 .stage_validated_generation(
1390 &validated(
1391 ledger.current_generation,
1392 vec![declaration("app.audit.v1", 102, Some(1))],
1393 ),
1394 Some(committed_at),
1395 )
1396 .map_err(|err| err.to_string()),
1397 Transition::RetireUsers => ledger
1398 .stage_retirement_generation(
1399 &AllocationRetirement::new(
1400 "app.users.v1",
1401 MemoryManagerSlot::new(100).expect("usable slot"),
1402 )
1403 .expect("retirement"),
1404 Some(committed_at),
1405 )
1406 .map_err(|err| err.to_string()),
1407 Transition::RetireAudit => ledger
1408 .stage_retirement_generation(
1409 &AllocationRetirement::new(
1410 "app.audit.v1",
1411 MemoryManagerSlot::new(102).expect("usable slot"),
1412 )
1413 .expect("retirement"),
1414 Some(committed_at),
1415 )
1416 .map_err(|err| err.to_string()),
1417 Transition::EmptyValidated => ledger
1418 .stage_validated_generation(
1419 &validated(ledger.current_generation, Vec::new()),
1420 Some(committed_at),
1421 )
1422 .map_err(|err| err.to_string()),
1423 Transition::EmptyReservation => ledger
1424 .stage_reservation_generation(&[], Some(committed_at))
1425 .map_err(|err| err.to_string()),
1426 }
1427 }
1428
1429 fn check_transition_sequences(
1430 ledger: AllocationLedger,
1431 depth: usize,
1432 sequence: &mut Vec<Transition>,
1433 ) {
1434 ledger
1435 .validate_committed_integrity()
1436 .unwrap_or_else(|err| panic!("sequence {sequence:?} violated integrity: {err}"));
1437 if depth == 0 {
1438 return;
1439 }
1440
1441 for transition in TRANSITIONS {
1442 sequence.push(transition);
1443 let next = apply_transition(&ledger, transition, sequence.len() as u64)
1444 .unwrap_or_else(|_| ledger.clone());
1445 check_transition_sequences(next, depth - 1, sequence);
1446 sequence.pop();
1447 }
1448 }
1449
1450 #[test]
1451 fn transition_matrix_preserves_committed_invariants() {
1452 check_transition_sequences(committed_ledger(0), 4, &mut Vec::new());
1453 }
1454
1455 #[test]
1456 fn new_committed_requires_strict_generation_history() {
1457 let structurally_valid =
1458 AllocationLedger::new(3, AllocationHistory::default()).expect("structurally valid DTO");
1459
1460 assert_eq!(structurally_valid.current_generation, 3);
1461
1462 let err = AllocationLedger::new_committed(3, AllocationHistory::default())
1463 .expect_err("committed ledger needs generation history");
1464
1465 assert_eq!(
1466 err,
1467 LedgerIntegrityError::MissingCurrentGenerationRecord {
1468 current_generation: 3
1469 }
1470 );
1471 }
1472
1473 #[test]
1474 fn new_committed_preserves_structural_rejection_before_history_checks() {
1475 let history = AllocationHistory::from_parts(
1476 vec![
1477 active_record("app.users.v1", 100),
1478 active_record("app.users.v1", 100),
1479 ],
1480 Vec::new(),
1481 );
1482
1483 assert_eq!(
1486 AllocationLedger::new_committed(3, history).expect_err("duplicate records"),
1487 LedgerIntegrityError::DuplicateStableKey {
1488 stable_key: StableKey::parse("app.users.v1").unwrap(),
1489 }
1490 );
1491
1492 let valid = active_committed_ledger();
1493 assert_eq!(
1494 AllocationLedger::new_committed(
1495 valid.current_generation(),
1496 valid.allocation_history().clone(),
1497 )
1498 .expect("valid committed history"),
1499 valid
1500 );
1501 }
1502
1503 #[test]
1504 fn validate_integrity_rejects_duplicate_stable_keys() {
1505 let mut ledger = ledger();
1506 for second_id in [101, 100] {
1507 ledger.allocation_history.records = vec![
1508 active_record("app.users.v1", 100),
1509 active_record("app.users.v1", second_id),
1510 ];
1511
1512 let err = ledger
1513 .validate_integrity()
1514 .expect_err("duplicate key precedes duplicate slot");
1515
1516 assert_eq!(
1517 err,
1518 LedgerIntegrityError::DuplicateStableKey {
1519 stable_key: StableKey::parse("app.users.v1").unwrap(),
1520 }
1521 );
1522 }
1523 }
1524
1525 #[test]
1526 fn validate_integrity_rejects_duplicate_slots() {
1527 let mut ledger = ledger();
1528 ledger.allocation_history.records = vec![
1529 active_record("app.users.v1", 100),
1530 active_record("app.orders.v1", 100),
1531 ];
1532
1533 let err = ledger.validate_integrity().expect_err("duplicate slot");
1534
1535 assert_eq!(
1536 err,
1537 LedgerIntegrityError::DuplicateSlot {
1538 slot: MemoryManagerSlot::new(100).unwrap(),
1539 }
1540 );
1541 }
1542
1543 #[test]
1544 fn recovery_rejects_invalid_stable_key_during_ledger_decode() {
1545 let mut ledger = committed_ledger(1);
1546 ledger
1547 .allocation_history
1548 .records
1549 .push(active_record("app.users.v1", 100));
1550 let mut bytes = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
1551 let key_start = bytes
1552 .windows(b"app.users.v1".len())
1553 .position(|window| window == b"app.users.v1")
1554 .expect("encoded stable key");
1555 bytes[key_start] = b'A';
1556 assert!(crate::test_cbor::from_slice::<AllocationLedger>(&bytes).is_err());
1557 let payload = LedgerPayloadEnvelope::current(bytes).encode();
1558 let mut physical = DualCommitStore::default();
1559 physical.commit_payload_at_generation(1, payload).unwrap();
1560 let store = LedgerCommitStore { physical };
1561 let before = store.clone();
1562 assert!(matches!(store.recover(), Err(LedgerCommitError::Codec(_))));
1563 assert_eq!(store, before, "malformed-key recovery must preserve slots");
1564 }
1565
1566 #[test]
1567 fn ledger_decode_rejects_unusable_memory_manager_slot() {
1568 let mut ledger = committed_ledger(1);
1569 ledger
1570 .allocation_history
1571 .records
1572 .push(active_record("app.users.v1", 100));
1573 let mut value = serde_json::to_value(ledger).unwrap();
1574 value["allocation_history"]["records"][0]["slot"]["slot"]["MemoryManagerId"] =
1575 serde_json::json!(255);
1576 let bytes = crate::test_cbor::to_vec(&value).unwrap();
1577 assert!(decode_ledger(&bytes).is_err());
1578 }
1579
1580 #[test]
1581 fn validate_integrity_rejects_retirement_before_last_observation() {
1582 let mut ledger = committed_ledger(5);
1583 let mut record = active_record("app.users.v1", 100);
1584 record.last_seen_generation = 4;
1585 record.state = AllocationState::Retired { generation: 3 };
1586 ledger.allocation_history.records = vec![record];
1587
1588 let err = ledger
1589 .validate_committed_integrity()
1590 .expect_err("retirement must follow the last observation");
1591
1592 assert!(matches!(
1593 err,
1594 LedgerIntegrityError::RetirementNotAfterLastSeen { .. }
1595 ));
1596 }
1597
1598 #[test]
1599 fn validate_integrity_rejects_retirement_before_allocation() {
1600 let mut ledger = committed_ledger(2);
1601 let mut record = active_record("app.users.v1", 100);
1602 record.state = AllocationState::Retired { generation: 0 };
1603 ledger.allocation_history.records = vec![record];
1604
1605 let err = ledger
1606 .validate_committed_integrity()
1607 .expect_err("retirement cannot predate allocation");
1608
1609 assert!(matches!(
1610 err,
1611 LedgerIntegrityError::RetiredBeforeFirstGeneration { .. }
1612 ));
1613 }
1614
1615 #[test]
1616 fn validate_integrity_requires_schema_history_at_first_generation() {
1617 let mut ledger = committed_ledger(2);
1618 let mut record = active_record("app.users.v1", 100);
1619 record.last_seen_generation = 2;
1620 record.schema_history[0].generation = 2;
1621 ledger.allocation_history.records = vec![record];
1622
1623 let err = ledger
1624 .validate_committed_integrity()
1625 .expect_err("schema history must begin with the allocation");
1626
1627 assert!(matches!(
1628 err,
1629 LedgerIntegrityError::SchemaHistoryStartMismatch { .. }
1630 ));
1631 }
1632
1633 #[test]
1634 fn validate_integrity_rejects_schema_history_after_last_observation() {
1635 for generation in [3, 4] {
1636 let mut ledger = committed_ledger(3);
1637 let mut record = active_record("app.users.v1", 100);
1638 record.last_seen_generation = 2;
1639 record.schema_history.push(SchemaMetadataRecord::new(
1640 generation,
1641 SchemaMetadata::new(Some(2)).expect("schema"),
1642 ));
1643 ledger.allocation_history.records = vec![record];
1644
1645 let err = ledger
1646 .validate_committed_integrity()
1647 .expect_err("schema metadata cannot postdate the last observation");
1648
1649 assert!(matches!(
1650 (generation, err),
1651 (
1652 3,
1653 LedgerIntegrityError::SchemaHistoryAfterLastSeen {
1654 generation: 3,
1655 last_seen_generation: 2,
1656 ..
1657 }
1658 ) | (
1659 4,
1660 LedgerIntegrityError::SchemaHistoryOutOfBounds { generation: 4, .. }
1661 )
1662 ));
1663 }
1664 }
1665
1666 #[test]
1667 fn validate_integrity_rejects_non_increasing_schema_history() {
1668 for generation in [0, 1] {
1669 let mut ledger = ledger();
1670 let mut record = active_record("app.users.v1", 100);
1671 record.schema_history.push(SchemaMetadataRecord {
1672 generation,
1673 schema: SchemaMetadata::default(),
1674 });
1675 ledger.allocation_history.records = vec![record];
1676
1677 let err = ledger
1678 .validate_integrity()
1679 .expect_err("non-increasing schema history");
1680
1681 assert!(matches!(
1682 err,
1683 LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1684 ));
1685 }
1686 }
1687
1688 #[test]
1689 fn recovery_rejects_zero_schema_version_during_ledger_decode() {
1690 let mut ledger = committed_ledger(1);
1691 ledger
1692 .allocation_history
1693 .records
1694 .push(AllocationRecord::active(
1695 1,
1696 &declaration("app.users.v1", 100, Some(1)),
1697 ));
1698 let mut value = serde_json::to_value(&ledger).unwrap();
1699 value["allocation_history"]["records"][0]["schema_history"][0]["schema"]["schema_version"] =
1700 0.into();
1701 let bytes = crate::test_cbor::to_vec(&value).unwrap();
1702 assert!(crate::test_cbor::from_slice::<AllocationLedger>(&bytes).is_err());
1703 let payload = LedgerPayloadEnvelope::current(bytes).encode();
1704 let mut physical = DualCommitStore::default();
1705 physical.commit_payload_at_generation(1, payload).unwrap();
1706 let store = LedgerCommitStore { physical };
1707 let before = store.clone();
1708 assert!(matches!(store.recover(), Err(LedgerCommitError::Codec(_))));
1709 assert_eq!(
1710 store, before,
1711 "malformed-schema recovery must preserve slots"
1712 );
1713 }
1714
1715 #[test]
1716 fn malformed_generation_fingerprints_reject_as_codec_errors_and_preserve_slots() {
1717 for fingerprint in [
1718 String::new(),
1719 "x".repeat(257),
1720 "é".to_string(),
1721 "build\n".to_string(),
1722 ] {
1723 let mut value = serde_json::to_value(committed_ledger(1)).unwrap();
1724 value["allocation_history"]["generations"][0]["runtime_fingerprint"] =
1725 fingerprint.into();
1726 let bytes = crate::test_cbor::to_vec(&value).unwrap();
1727 let payload = LedgerPayloadEnvelope::current(bytes).encode();
1728 let invalid = CommittedGenerationBytes::new(1, payload);
1729 let genesis = committed_ledger(0);
1730 let valid = CommittedGenerationBytes::new(0, enveloped_payload(&genesis));
1731 for (slot0, slot1) in [
1732 (valid.clone(), invalid.clone()),
1733 (invalid.clone(), valid.clone()),
1734 ] {
1735 let mut store = LedgerCommitStore {
1736 physical: DualCommitStore {
1737 slot0: Some(slot0),
1738 slot1: Some(slot1),
1739 },
1740 };
1741 let before = store.clone();
1742 let error = store.recover().unwrap_err();
1743 assert!(matches!(error, LedgerCommitError::Codec(_)));
1744 let (recovered, diagnostic) = store.recover_with_diagnostic();
1745 assert_eq!(recovered, Err(error.clone()));
1746 assert!(diagnostic.recovery.is_ok(), "physical slots remain valid");
1747 assert_eq!(store.recover_or_initialize(&genesis), Err(error));
1748 assert_eq!(
1749 store, before,
1750 "all rejected paths preserve both protected slots"
1751 );
1752 }
1753 }
1754 }
1755
1756 #[test]
1757 fn validate_committed_integrity_requires_current_generation_record() {
1758 let err = ledger()
1759 .validate_committed_integrity()
1760 .expect_err("missing current generation");
1761
1762 assert_eq!(
1763 err,
1764 LedgerIntegrityError::MissingCurrentGenerationRecord {
1765 current_generation: 3
1766 }
1767 );
1768 }
1769
1770 #[test]
1771 fn committed_integrity_rejects_allocation_references_to_genesis() {
1772 for generation in [0, 1, 2] {
1773 let mut ledger = committed_ledger(generation);
1774 let mut record = AllocationRecord::active(0, &declaration("app.genesis.v1", 100, None));
1775 if generation == 2 {
1776 record.last_seen_generation = 1;
1777 record
1778 .schema_history
1779 .push(SchemaMetadataRecord::new(1, SchemaMetadata::default()));
1780 record.state = AllocationState::Retired { generation: 2 };
1781 }
1782 ledger.allocation_history.records.push(record);
1783 ledger.validate_integrity().expect("structurally valid DTO");
1784 assert_eq!(
1785 ledger.validate_committed_integrity(),
1786 Err(LedgerIntegrityError::UnknownRecordGeneration {
1787 stable_key: StableKey::parse("app.genesis.v1").unwrap(),
1788 generation: 0,
1789 })
1790 );
1791 }
1792 }
1793
1794 #[test]
1795 fn validate_committed_integrity_rejects_generation_history_gaps() {
1796 let mut ledger = committed_ledger(3);
1797 ledger.allocation_history.generations.remove(1);
1798 ledger
1799 .allocation_history
1800 .records
1801 .push(AllocationRecord::active(
1802 0,
1803 &declaration("app.genesis.v1", 100, None),
1804 ));
1805
1806 let err = ledger
1807 .validate_committed_integrity()
1808 .expect_err("generation history gap takes precedence over genesis reference");
1809
1810 assert!(matches!(
1811 err,
1812 LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1813 ));
1814 }
1815
1816 #[test]
1817 fn committed_integrity_rejects_a_skipped_parent_in_contiguous_history() {
1818 let mut ledger = committed_ledger(3);
1819 ledger.allocation_history.generations[2].parent_generation = 1;
1820 ledger
1821 .validate_integrity()
1822 .expect("structurally valid history");
1823
1824 assert_eq!(
1825 ledger.validate_committed_integrity(),
1826 Err(LedgerIntegrityError::BrokenGenerationChain {
1827 generation: 3,
1828 expected_parent: 2,
1829 actual_parent: 1,
1830 })
1831 );
1832 }
1833
1834 #[test]
1835 fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1836 let mut store = LedgerCommitStore::default();
1837 let mut invalid = ledger();
1838 invalid.allocation_history.records = vec![
1839 active_record("app.users.v1", 100),
1840 active_record("app.orders.v1", 100),
1841 ];
1842
1843 let err = store.commit(&invalid).expect_err("invalid ledger");
1844
1845 assert!(matches!(
1846 err,
1847 LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1848 ));
1849 assert!(store.physical().is_uninitialized());
1850 }
1851
1852 #[test]
1853 fn ledger_commit_store_recovers_latest_committed_ledger() {
1854 let mut store = LedgerCommitStore::default();
1855 let first = committed_ledger(1);
1856 let second = committed_ledger(2);
1857
1858 store.commit(&first).expect("first commit");
1859 store.commit(&second).expect("second commit");
1860 let recovered = store.recover().expect("recovered ledger");
1861
1862 assert_eq!(recovered.current_generation(), 2);
1863 assert_eq!(recovered.physical_generation(), 2);
1864 }
1865
1866 #[test]
1867 fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1868 let mut store = LedgerCommitStore::default();
1869 let ledger = committed_ledger(1);
1870
1871 store.commit(&ledger).expect("commit");
1872
1873 let committed = store.physical().authoritative().expect("authoritative");
1874 assert_eq!(
1875 committed.payload(),
1876 LedgerPayloadEnvelope::current(crate::test_cbor::to_vec(&ledger).unwrap())
1877 .try_encode()
1878 .unwrap(),
1879 "direct commit encoding preserves the complete public envelope bytes"
1880 );
1881 let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1882 assert_eq!(
1883 envelope.payload(),
1884 crate::test_cbor::to_vec(&ledger).expect("encode ledger")
1885 );
1886 }
1887
1888 #[test]
1889 fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1890 let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1891 let store = LedgerCommitStore {
1892 physical: DualCommitStore {
1893 slot0: Some(committed),
1894 slot1: None,
1895 },
1896 };
1897
1898 let err = store.recover().expect_err("bad envelope");
1899
1900 assert!(matches!(
1901 err,
1902 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1903 ));
1904 }
1905
1906 #[test]
1907 fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1908 let mut payload = enveloped_payload(&committed_ledger(1));
1909 payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1910 let store = LedgerCommitStore {
1911 physical: DualCommitStore {
1912 slot0: Some(CommittedGenerationBytes::new(1, payload)),
1913 slot1: None,
1914 },
1915 };
1916
1917 let err = store
1918 .recover()
1919 .expect_err("unknown ledger format version must fail");
1920
1921 assert_eq!(
1922 err,
1923 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1924 marker: *b"ICMF",
1925 version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1926 })
1927 );
1928 }
1929
1930 #[test]
1931 fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
1932 let genesis = enveloped_payload(&committed_ledger(0));
1933 let committed = CommittedGenerationBytes::new(0, genesis.clone());
1934 let mut corrupt = committed.clone();
1935 corrupt.checksum ^= 1;
1936 let mut unsupported = genesis.clone();
1937 unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1938 let cases = [
1939 ("empty", None, None),
1940 ("one slot", Some(committed.clone()), None),
1941 (
1942 "identical slots",
1943 Some(committed.clone()),
1944 Some(committed.clone()),
1945 ),
1946 (
1947 "corrupt inactive",
1948 Some(committed.clone()),
1949 Some(corrupt.clone()),
1950 ),
1951 ("corrupt active", Some(corrupt), Some(committed.clone())),
1952 (
1953 "ambiguous",
1954 Some(committed),
1955 Some(CommittedGenerationBytes::new(0, vec![1])),
1956 ),
1957 (
1958 "unsupported format",
1959 Some(CommittedGenerationBytes::new(0, unsupported)),
1960 None,
1961 ),
1962 (
1963 "invalid CBOR",
1964 Some(CommittedGenerationBytes::new(
1965 0,
1966 LedgerPayloadEnvelope::current(vec![255])
1967 .try_encode()
1968 .unwrap(),
1969 )),
1970 None,
1971 ),
1972 (
1973 "generation mismatch",
1974 Some(CommittedGenerationBytes::new(1, genesis)),
1975 None,
1976 ),
1977 (
1978 "invalid committed integrity",
1979 Some(CommittedGenerationBytes::new(
1980 3,
1981 enveloped_payload(&ledger()),
1982 )),
1983 None,
1984 ),
1985 ];
1986
1987 for (label, slot0, slot1) in cases {
1988 let empty = slot0.is_none() && slot1.is_none();
1989 let store = LedgerCommitStore {
1990 physical: DualCommitStore { slot0, slot1 },
1991 };
1992 let before = store.clone();
1993 let (recovered, diagnostic) = store.recover_with_diagnostic();
1994 assert_eq!(recovered, store.recover(), "{label}");
1995 assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
1996 assert_eq!(store, before, "{label}");
1997
1998 let mut initializing = store.clone();
1999 let initialized = initializing.recover_or_initialize(&committed_ledger(0));
2000 if empty {
2001 let initialized = initialized.expect("empty store accepts genesis");
2002 assert_eq!(initialized.current_generation(), 0);
2003 assert_eq!(initialized.physical_generation(), 0);
2004 assert_eq!(initializing.recover().unwrap(), initialized);
2005 } else {
2006 assert_eq!(initialized, recovered, "{label}");
2007 assert_eq!(initializing, before, "{label}");
2008 }
2009
2010 if let Err(LedgerCommitError::Recovery(error)) = recovered {
2011 assert_eq!(diagnostic.recovery, Err(error), "{label}");
2012 } else {
2013 assert!(diagnostic.recovery.is_ok(), "{label}");
2014 }
2015 }
2016 }
2017
2018 #[test]
2019 fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
2020 let mut store = LedgerCommitStore::default();
2021 let genesis = committed_ledger(0);
2022
2023 let recovered = store
2024 .recover_or_initialize(&genesis)
2025 .expect("current-format genesis ledger");
2026 assert_eq!(recovered.current_generation(), 0);
2027 assert_eq!(recovered.physical_generation(), 0);
2028 assert_eq!(recovered.ledger().allocation_history().generations(), []);
2029 assert_eq!(recovered, store.recover().unwrap());
2030
2031 let first = recovered
2032 .ledger()
2033 .stage_validated_generation(
2034 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2035 None,
2036 )
2037 .expect("first real generation");
2038 let recovered = store.commit(&first).expect("first commit");
2039
2040 assert_eq!(recovered, store.recover().unwrap());
2041 assert_eq!(recovered.current_generation(), 1);
2042 assert_eq!(recovered.physical_generation(), 1);
2043 assert_eq!(
2044 recovered.ledger().allocation_history().generations()[0].generation,
2045 1
2046 );
2047 assert_eq!(
2048 record(recovered.ledger(), "app.users.v1").first_generation,
2049 1
2050 );
2051 }
2052
2053 #[test]
2054 fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
2055 let mut store = LedgerCommitStore::default();
2056 let genesis = committed_ledger(0);
2057 store.commit(&genesis).expect("genesis commit");
2058 let first = genesis
2059 .stage_validated_generation(
2060 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2061 None,
2062 )
2063 .expect("first generation");
2064 let first = store.commit(&first).expect("first commit");
2065 let second = first
2066 .ledger()
2067 .stage_validated_generation(
2068 &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
2069 None,
2070 )
2071 .expect("second generation");
2072
2073 store.commit(&second).expect("second commit");
2074 store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
2075
2076 let err = store
2077 .recover()
2078 .expect_err("corrupt latest must not roll back");
2079
2080 assert_eq!(
2081 err,
2082 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2083 slot0_invalid: true,
2084 slot1_invalid: false,
2085 })
2086 );
2087 }
2088
2089 #[test]
2090 fn ledger_commit_store_recovers_identical_duplicate_slots() {
2091 let ledger = committed_ledger(0)
2092 .stage_validated_generation(
2093 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2094 None,
2095 )
2096 .expect("first generation");
2097 let payload = enveloped_payload(&ledger);
2098 let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
2099 let store = LedgerCommitStore {
2100 physical: DualCommitStore {
2101 slot0: Some(committed.clone()),
2102 slot1: Some(committed),
2103 },
2104 };
2105
2106 let recovered = store.recover().expect("recovered");
2107
2108 assert_eq!(recovered.ledger(), &ledger);
2109 }
2110
2111 #[test]
2112 fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
2113 let mut store = LedgerCommitStore::default();
2114 let first = committed_ledger(1);
2115 let second = committed_ledger(2);
2116
2117 store.commit(&first).expect("first commit");
2118 store
2119 .write_corrupt_inactive_ledger(&second)
2120 .expect("corrupt write");
2121 let err = store.recover().expect_err("corrupt slot");
2122
2123 assert_eq!(
2124 err,
2125 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2126 slot0_invalid: false,
2127 slot1_invalid: true,
2128 })
2129 );
2130 }
2131
2132 #[test]
2133 fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
2134 let store = LedgerCommitStore {
2135 physical: DualCommitStore {
2136 slot0: Some(CommittedGenerationBytes::new(
2137 7,
2138 enveloped_payload(&committed_ledger(6)),
2139 )),
2140 slot1: None,
2141 },
2142 };
2143 let err = store.recover().expect_err("mismatch");
2144
2145 assert_eq!(
2146 err,
2147 LedgerCommitError::PhysicalLogicalGenerationMismatch {
2148 physical_generation: 7,
2149 logical_generation: 6
2150 }
2151 );
2152 }
2153
2154 #[test]
2155 fn ledger_commit_store_rejects_non_next_logical_generation() {
2156 let mut store = LedgerCommitStore::default();
2157 store.commit(&committed_ledger(1)).expect("first commit");
2158
2159 let err = store
2160 .commit(&committed_ledger(3))
2161 .expect_err("skipped generation");
2162
2163 assert_eq!(
2164 err,
2165 LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2166 expected: 2,
2167 actual: 3
2168 })
2169 );
2170 }
2171
2172 #[test]
2173 fn ledger_commit_store_initializes_empty_store_explicitly() {
2174 let mut store = LedgerCommitStore::default();
2175 let genesis = committed_ledger(3);
2176
2177 let recovered = store
2178 .recover_or_initialize(&genesis)
2179 .expect("initialized ledger");
2180
2181 assert_eq!(recovered.current_generation(), 3);
2182 assert_eq!(recovered.physical_generation(), 3);
2183 assert!(!store.physical().is_uninitialized());
2184 }
2185
2186 #[test]
2187 fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2188 let mut store = LedgerCommitStore::default();
2189 store
2190 .write_corrupt_inactive_ledger(&ledger())
2191 .expect("corrupt write");
2192
2193 let err = store
2194 .recover_or_initialize(&ledger())
2195 .expect_err("corrupt state");
2196
2197 assert!(matches!(
2198 err,
2199 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2200 slot0_invalid: true,
2201 slot1_invalid: false,
2202 })
2203 ));
2204 }
2205 #[test]
2206 fn history_boundary_round_trips_and_rejects_next_generation_without_mutation() {
2207 let mut source = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 - 1);
2208 source
2209 .allocation_history
2210 .records
2211 .push(active_record("app.users.v1", 100));
2212 let retirement =
2213 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
2214 .unwrap();
2215 let retired = source
2216 .stage_retirement_generation(&retirement, None)
2217 .unwrap();
2218 retired.validate_committed_integrity().unwrap();
2219 assert_eq!(
2220 retired.allocation_history.generations().len(),
2221 crate::constants::MAX_LEDGER_GENERATIONS
2222 );
2223 assert_eq!(
2224 retired.allocation_history.records()[0].schema_history,
2225 source.allocation_history.records()[0].schema_history
2226 );
2227 assert_eq!(
2228 source.allocation_history.records()[0].state,
2229 AllocationState::Active
2230 );
2231 assert!(matches!(
2232 retired.stage_retirement_generation(&retirement, None),
2233 Err(AllocationRetirementError::Integrity(
2234 LedgerIntegrityError::LimitExceeded {
2235 resource: "generation history",
2236 ..
2237 }
2238 ))
2239 ));
2240
2241 let ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2242 let mut store = LedgerCommitStore::default();
2243 let recovered = store.commit(&ledger).expect("boundary is admissible");
2244 let record = crate::StableCellLedgerRecord::new(store.clone());
2245 let bytes = crate::test_cbor::to_vec(&record).unwrap();
2246 assert_eq!(record.encoded_size(), bytes.len());
2247 let recovered_record = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2248 assert_eq!(
2249 recovered_record.store().recover().unwrap().ledger(),
2250 &ledger
2251 );
2252 let before = store.clone();
2253 let err = crate::AllocationBootstrap::new(&mut store)
2254 .validate_and_commit(
2255 DeclarationSnapshot::new(Vec::new()).unwrap(),
2256 &crate::GenericRangePolicy,
2257 None,
2258 )
2259 .unwrap_err();
2260 assert!(matches!(
2261 err,
2262 crate::BootstrapError::Staging(AllocationStageError::Integrity(
2263 LedgerIntegrityError::LimitExceeded { .. }
2264 ))
2265 ));
2266 assert_eq!(store, before);
2267 assert_eq!(
2268 recovered.current_generation(),
2269 crate::constants::MAX_LEDGER_GENERATIONS as u64
2270 );
2271 let oversize = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 + 1);
2272 assert!(matches!(
2273 store.commit(&oversize),
2274 Err(LedgerCommitError::Integrity(
2275 LedgerIntegrityError::LimitExceeded { .. }
2276 ))
2277 ));
2278 assert_eq!(store, before);
2279 }
2280
2281 #[test]
2282 fn record_count_boundary_and_malicious_history_reject_on_recovery() {
2283 let mut ledger = committed_ledger(1);
2284 ledger.allocation_history.records = (0..255_u8)
2285 .map(|id| active_record(&format!("app.slot{id}.v1"), id))
2286 .collect();
2287 let mut store = LedgerCommitStore::default();
2288 store.commit(&ledger).unwrap();
2289 assert_eq!(
2290 store
2291 .recover()
2292 .unwrap()
2293 .ledger()
2294 .allocation_history()
2295 .records()
2296 .len(),
2297 255
2298 );
2299 ledger
2300 .allocation_history
2301 .records
2302 .push(active_record("app.extra.v1", 254));
2303 assert!(matches!(
2304 store.commit(&ledger),
2305 Err(LedgerCommitError::Integrity(
2306 LedgerIntegrityError::LimitExceeded { .. }
2307 ))
2308 ));
2309 let payload = enveloped_payload(&ledger);
2310 let mut invalid = LedgerCommitStore {
2311 physical: DualCommitStore {
2312 slot0: Some(CommittedGenerationBytes::new(1, payload)),
2313 slot1: None,
2314 },
2315 };
2316 assert!(invalid.recover().is_err());
2317 assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
2318 for bytes in [
2319 vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2320 vec![0x81; 34],
2321 vec![0xa1],
2322 ] {
2323 let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
2324 let invalid = LedgerCommitStore {
2325 physical: DualCommitStore {
2326 slot0: Some(CommittedGenerationBytes::new(1, payload)),
2327 slot1: None,
2328 },
2329 };
2330 assert!(matches!(
2331 invalid.recover(),
2332 Err(LedgerCommitError::Codec(_))
2333 ));
2334 }
2335 }
2336
2337 #[test]
2338 fn ledger_collection_limits_preserve_refusal_order_and_committed_slots() {
2339 let limit = crate::constants::MAX_LEDGER_GENERATIONS;
2340 let mut oversized = committed_ledger(1);
2341 let mut record = active_record("app.users.v1", 100);
2342 record.schema_history =
2343 vec![SchemaMetadataRecord::new(1, crate::SchemaMetadata::default()); limit + 1];
2344 oversized.allocation_history.records =
2345 vec![active_record("app.users.v1", 100); crate::constants::MAX_ALLOCATIONS + 1];
2346 oversized.allocation_history.records[0] = record;
2347 let generation = oversized.allocation_history.generations[0].clone();
2348 oversized
2349 .allocation_history
2350 .generations
2351 .resize(limit + 1, generation);
2352
2353 let mut store = LedgerCommitStore::default();
2354 store.commit(&committed_ledger(0)).unwrap();
2355 store.commit(&committed_ledger(1)).unwrap();
2356 let before = store.clone();
2357 for (resource, maximum) in [
2360 ("allocation records", crate::constants::MAX_ALLOCATIONS),
2361 ("generation history", limit),
2362 ("schema history", limit),
2363 ] {
2364 let expected = LedgerIntegrityError::LimitExceeded {
2365 resource,
2366 limit: maximum,
2367 };
2368 assert_eq!(oversized.validate_integrity(), Err(expected.clone()));
2369 assert_eq!(
2370 oversized.validate_committed_integrity(),
2371 Err(expected.clone())
2372 );
2373 assert_eq!(
2374 store.commit(&oversized),
2375 Err(LedgerCommitError::Integrity(expected.clone()))
2376 );
2377 assert_eq!(
2378 oversized.stage_validated_generation(&validated(1, Vec::new()), None),
2379 Err(AllocationStageError::Integrity(expected))
2380 );
2381 assert_eq!(store, before, "{resource} refusal preserves both slots");
2382 match resource {
2383 "allocation records" => oversized.allocation_history.records.truncate(1),
2384 "generation history" => oversized.allocation_history.generations.truncate(1),
2385 _ => (),
2386 }
2387 }
2388 assert_eq!(store.recover(), before.recover());
2389 }
2390
2391 #[test]
2392 fn schema_collection_limit_accepts_exact_history_and_rejects_the_next_record() {
2393 let limit = crate::constants::MAX_LEDGER_GENERATIONS;
2394 let mut ledger = committed_ledger(limit as u64);
2395 let mut record = active_record("app.users.v1", 100);
2396 record.last_seen_generation = limit as u64;
2397 record.schema_history = (1..=limit as u64)
2398 .map(|generation| {
2399 SchemaMetadataRecord::new(generation, crate::SchemaMetadata::default())
2400 })
2401 .collect();
2402 ledger.allocation_history.records.push(record);
2403 assert_eq!(ledger.validate_committed_integrity(), Ok(()));
2404
2405 ledger
2407 .allocation_history
2408 .records
2409 .push(active_record("app.orders.v1", 101));
2410 assert_eq!(
2411 ledger.validate_committed_integrity(),
2412 Err(LedgerIntegrityError::LimitExceeded {
2413 resource: "schema history",
2414 limit,
2415 })
2416 );
2417 }
2418
2419 #[test]
2420 fn encoded_byte_limit_rejects_before_commit_mutation() {
2421 let mut store = LedgerCommitStore::default();
2422 store.commit(&committed_ledger(0)).unwrap();
2423 let before = store.clone();
2424 let mut ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2425 for generation in &mut ledger.allocation_history.generations {
2426 generation.runtime_fingerprint =
2427 Some("x".repeat(crate::constants::DIAGNOSTIC_STRING_MAX_BYTES));
2428 }
2429 assert!(matches!(
2430 store.commit(&ledger),
2431 Err(LedgerCommitError::Integrity(
2432 LedgerIntegrityError::LimitExceeded {
2433 resource: "ledger bytes",
2434 ..
2435 }
2436 ))
2437 ));
2438 assert_eq!(store, before);
2439 let mut envelope = LedgerPayloadEnvelope::current(Vec::new())
2440 .try_encode()
2441 .unwrap();
2442 envelope[16..24]
2443 .copy_from_slice(&((crate::constants::MAX_LEDGER_BYTES + 1) as u64).to_le_bytes());
2444 assert!(matches!(
2445 LedgerPayloadEnvelope::decode(&envelope),
2446 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { .. })
2447 ));
2448 }
2449
2450 #[test]
2451 fn oversized_logical_text_remains_opaque_to_record_decode_and_fails_closed() {
2452 for len in [257, 8192] {
2453 let mut malformed = committed_ledger(1);
2454 malformed.allocation_history.generations[0].runtime_fingerprint = Some("x".repeat(len));
2455 let mut store = LedgerCommitStore::default();
2456 store
2457 .physical
2458 .commit_payload_at_generation(1, enveloped_payload(&malformed))
2459 .unwrap();
2460 let record = crate::StableCellLedgerRecord::new(store);
2461 let bytes = record.to_bytes();
2462 let mut decoded = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2463 assert_eq!(decoded, record);
2464 let before = decoded.clone();
2465 assert!(matches!(
2466 decoded.store().recover(),
2467 Err(LedgerCommitError::Codec(_))
2468 ));
2469 assert!(matches!(
2470 decoded
2471 .store_mut()
2472 .recover_or_initialize(&AllocationLedger::empty_genesis()),
2473 Err(LedgerCommitError::Codec(_))
2474 ));
2475 assert_eq!(decoded, before);
2476 }
2477 }
2478}