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