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