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;
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_mut() = 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_mut() = 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_mut() = 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_mut() = 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_mut() = 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_mut() = 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_mut() = 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_mut() = 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_mut() = 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_mut() = 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_mut() = 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_mut()[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_mut() = 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_mut() = 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_mut() = 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 let mut ledger = committed_ledger(3);
1761 let mut record = active_record("app.users.v1", 100);
1762 record.last_seen_generation = 2;
1763 record.schema_history.push(
1764 SchemaMetadataRecord::new(3, SchemaMetadata::new(Some(2)).expect("schema"))
1765 .expect("schema record"),
1766 );
1767 *ledger.allocation_history.records_mut() = vec![record];
1768
1769 let err = ledger
1770 .validate_committed_integrity()
1771 .expect_err("schema metadata cannot postdate the last observation");
1772
1773 assert!(matches!(
1774 err,
1775 LedgerIntegrityError::SchemaHistoryAfterLastSeen { .. }
1776 ));
1777 }
1778
1779 #[test]
1780 fn validate_integrity_rejects_non_increasing_schema_history() {
1781 let mut ledger = ledger();
1782 let mut record = active_record("app.users.v1", 100);
1783 record.schema_history.push(SchemaMetadataRecord {
1784 generation: 1,
1785 schema: SchemaMetadata::default(),
1786 });
1787 *ledger.allocation_history.records_mut() = vec![record];
1788
1789 let err = ledger
1790 .validate_integrity()
1791 .expect_err("non-increasing schema history");
1792
1793 assert!(matches!(
1794 err,
1795 LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1796 ));
1797 }
1798
1799 #[test]
1800 fn validate_integrity_rejects_invalid_schema_metadata_history() {
1801 let mut ledger = committed_ledger(1);
1802 let mut record = active_record("app.users.v1", 100);
1803 record.schema_history[0].schema = invalid_schema_metadata();
1804 *ledger.allocation_history.records_mut() = vec![record];
1805
1806 let err = ledger
1807 .validate_committed_integrity()
1808 .expect_err("invalid committed schema metadata");
1809
1810 assert_eq!(
1811 err,
1812 LedgerIntegrityError::InvalidSchemaMetadata {
1813 stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1814 generation: 1,
1815 error: SchemaMetadataError::InvalidVersion,
1816 }
1817 );
1818 }
1819
1820 #[test]
1821 fn validate_committed_integrity_requires_current_generation_record() {
1822 let err = ledger()
1823 .validate_committed_integrity()
1824 .expect_err("missing current generation");
1825
1826 assert_eq!(
1827 err,
1828 LedgerIntegrityError::MissingCurrentGenerationRecord {
1829 current_generation: 3
1830 }
1831 );
1832 }
1833
1834 #[test]
1835 fn committed_integrity_rejects_allocation_references_to_genesis() {
1836 for generation in [0, 1, 2] {
1837 let mut ledger = committed_ledger(generation);
1838 let mut record = AllocationRecord::active(0, &declaration("app.genesis.v1", 100, None));
1839 if generation == 2 {
1840 record.last_seen_generation = 1;
1841 record
1842 .schema_history
1843 .push(SchemaMetadataRecord::new(1, SchemaMetadata::default()).unwrap());
1844 record.state = AllocationState::Retired { generation: 2 };
1845 }
1846 ledger.allocation_history.push_record(record);
1847 ledger.validate_integrity().expect("structurally valid DTO");
1848 assert_eq!(
1849 ledger.validate_committed_integrity(),
1850 Err(LedgerIntegrityError::UnknownRecordGeneration {
1851 stable_key: StableKey::parse("app.genesis.v1").unwrap(),
1852 generation: 0,
1853 })
1854 );
1855 }
1856 }
1857
1858 #[test]
1859 fn validate_committed_integrity_rejects_generation_history_gaps() {
1860 let mut ledger = committed_ledger(3);
1861 ledger.allocation_history.generations_mut().remove(1);
1862 ledger
1863 .allocation_history
1864 .push_record(AllocationRecord::active(
1865 0,
1866 &declaration("app.genesis.v1", 100, None),
1867 ));
1868
1869 let err = ledger
1870 .validate_committed_integrity()
1871 .expect_err("generation history gap takes precedence over genesis reference");
1872
1873 assert!(matches!(
1874 err,
1875 LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1876 ));
1877 }
1878
1879 #[test]
1880 fn committed_integrity_rejects_a_skipped_parent_in_contiguous_history() {
1881 let mut ledger = committed_ledger(3);
1882 ledger.allocation_history.generations_mut()[2].parent_generation = 1;
1883 ledger
1884 .validate_integrity()
1885 .expect("structurally valid history");
1886
1887 assert_eq!(
1888 ledger.validate_committed_integrity(),
1889 Err(LedgerIntegrityError::BrokenGenerationChain {
1890 generation: 3,
1891 expected_parent: 2,
1892 actual_parent: 1,
1893 })
1894 );
1895 }
1896
1897 #[test]
1898 fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1899 let mut store = LedgerCommitStore::default();
1900 let mut invalid = ledger();
1901 *invalid.allocation_history.records_mut() = vec![
1902 active_record("app.users.v1", 100),
1903 active_record("app.orders.v1", 100),
1904 ];
1905
1906 let err = store.commit(&invalid).expect_err("invalid ledger");
1907
1908 assert!(matches!(
1909 err,
1910 LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1911 ));
1912 assert!(store.physical().is_uninitialized());
1913 }
1914
1915 #[test]
1916 fn ledger_commit_store_recovers_latest_committed_ledger() {
1917 let mut store = LedgerCommitStore::default();
1918 let first = committed_ledger(1);
1919 let second = committed_ledger(2);
1920
1921 store.commit(&first).expect("first commit");
1922 store.commit(&second).expect("second commit");
1923 let recovered = store.recover().expect("recovered ledger");
1924
1925 assert_eq!(recovered.current_generation(), 2);
1926 assert_eq!(recovered.physical_generation(), 2);
1927 }
1928
1929 #[test]
1930 fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1931 let mut store = LedgerCommitStore::default();
1932 let ledger = committed_ledger(1);
1933
1934 store.commit(&ledger).expect("commit");
1935
1936 let committed = store.physical().authoritative().expect("authoritative");
1937 assert_eq!(
1938 committed.payload(),
1939 LedgerPayloadEnvelope::current(crate::test_cbor::to_vec(&ledger).unwrap())
1940 .try_encode()
1941 .unwrap(),
1942 "direct commit encoding preserves the complete public envelope bytes"
1943 );
1944 let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1945 assert_eq!(
1946 envelope.payload(),
1947 crate::test_cbor::to_vec(&ledger).expect("encode ledger")
1948 );
1949 }
1950
1951 #[test]
1952 fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1953 let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1954 let store = LedgerCommitStore {
1955 physical: DualCommitStore {
1956 slot0: Some(committed),
1957 slot1: None,
1958 },
1959 };
1960
1961 let err = store.recover().expect_err("bad envelope");
1962
1963 assert!(matches!(
1964 err,
1965 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1966 ));
1967 }
1968
1969 #[test]
1970 fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1971 let mut payload = enveloped_payload(&committed_ledger(1));
1972 payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1973 let store = LedgerCommitStore {
1974 physical: DualCommitStore {
1975 slot0: Some(CommittedGenerationBytes::new(1, payload)),
1976 slot1: None,
1977 },
1978 };
1979
1980 let err = store
1981 .recover()
1982 .expect_err("unknown ledger format version must fail");
1983
1984 assert_eq!(
1985 err,
1986 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1987 marker: *b"ICMF",
1988 version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1989 })
1990 );
1991 }
1992
1993 #[test]
1994 fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
1995 let genesis = enveloped_payload(&committed_ledger(0));
1996 let committed = CommittedGenerationBytes::new(0, genesis.clone());
1997 let mut corrupt = committed.clone();
1998 corrupt.checksum ^= 1;
1999 let mut unsupported = genesis.clone();
2000 unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
2001 let cases = [
2002 ("empty", None, None),
2003 ("one slot", Some(committed.clone()), None),
2004 (
2005 "identical slots",
2006 Some(committed.clone()),
2007 Some(committed.clone()),
2008 ),
2009 (
2010 "corrupt inactive",
2011 Some(committed.clone()),
2012 Some(corrupt.clone()),
2013 ),
2014 ("corrupt active", Some(corrupt), Some(committed.clone())),
2015 (
2016 "ambiguous",
2017 Some(committed),
2018 Some(CommittedGenerationBytes::new(0, vec![1])),
2019 ),
2020 (
2021 "unsupported format",
2022 Some(CommittedGenerationBytes::new(0, unsupported)),
2023 None,
2024 ),
2025 (
2026 "invalid CBOR",
2027 Some(CommittedGenerationBytes::new(
2028 0,
2029 LedgerPayloadEnvelope::current(vec![255])
2030 .try_encode()
2031 .unwrap(),
2032 )),
2033 None,
2034 ),
2035 (
2036 "generation mismatch",
2037 Some(CommittedGenerationBytes::new(1, genesis)),
2038 None,
2039 ),
2040 (
2041 "invalid committed integrity",
2042 Some(CommittedGenerationBytes::new(
2043 3,
2044 enveloped_payload(&ledger()),
2045 )),
2046 None,
2047 ),
2048 ];
2049
2050 for (label, slot0, slot1) in cases {
2051 let empty = slot0.is_none() && slot1.is_none();
2052 let store = LedgerCommitStore {
2053 physical: DualCommitStore { slot0, slot1 },
2054 };
2055 let before = store.clone();
2056 let (recovered, diagnostic) = store.recover_with_diagnostic();
2057 assert_eq!(recovered, store.recover(), "{label}");
2058 assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
2059 assert_eq!(store, before, "{label}");
2060
2061 let mut initializing = store.clone();
2062 let initialized = initializing.recover_or_initialize(&committed_ledger(0));
2063 if empty {
2064 let initialized = initialized.expect("empty store accepts genesis");
2065 assert_eq!(initialized.current_generation(), 0);
2066 assert_eq!(initialized.physical_generation(), 0);
2067 assert_eq!(initializing.recover().unwrap(), initialized);
2068 } else {
2069 assert_eq!(initialized, recovered, "{label}");
2070 assert_eq!(initializing, before, "{label}");
2071 }
2072
2073 if let Err(LedgerCommitError::Recovery(error)) = recovered {
2074 assert_eq!(diagnostic.recovery, Err(error), "{label}");
2075 } else {
2076 assert!(diagnostic.recovery.is_ok(), "{label}");
2077 }
2078 }
2079 }
2080
2081 #[test]
2082 fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
2083 let mut store = LedgerCommitStore::default();
2084 let genesis = committed_ledger(0);
2085
2086 let recovered = store
2087 .recover_or_initialize(&genesis)
2088 .expect("current-format genesis ledger");
2089 assert_eq!(recovered.current_generation(), 0);
2090 assert_eq!(recovered.physical_generation(), 0);
2091 assert_eq!(recovered.ledger().allocation_history().generations(), []);
2092 assert_eq!(recovered, store.recover().unwrap());
2093
2094 let first = recovered
2095 .ledger()
2096 .stage_validated_generation(
2097 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2098 None,
2099 )
2100 .expect("first real generation");
2101 let recovered = store.commit(&first).expect("first commit");
2102
2103 assert_eq!(recovered, store.recover().unwrap());
2104 assert_eq!(recovered.current_generation(), 1);
2105 assert_eq!(recovered.physical_generation(), 1);
2106 assert_eq!(
2107 recovered.ledger().allocation_history().generations()[0].generation,
2108 1
2109 );
2110 assert_eq!(
2111 record(recovered.ledger(), "app.users.v1").first_generation,
2112 1
2113 );
2114 }
2115
2116 #[test]
2117 fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
2118 let mut store = LedgerCommitStore::default();
2119 let genesis = committed_ledger(0);
2120 store.commit(&genesis).expect("genesis commit");
2121 let first = genesis
2122 .stage_validated_generation(
2123 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2124 None,
2125 )
2126 .expect("first generation");
2127 let first = store.commit(&first).expect("first commit");
2128 let second = first
2129 .ledger()
2130 .stage_validated_generation(
2131 &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
2132 None,
2133 )
2134 .expect("second generation");
2135
2136 store.commit(&second).expect("second commit");
2137 store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
2138
2139 let err = store
2140 .recover()
2141 .expect_err("corrupt latest must not roll back");
2142
2143 assert_eq!(
2144 err,
2145 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2146 slot0_invalid: true,
2147 slot1_invalid: false,
2148 })
2149 );
2150 }
2151
2152 #[test]
2153 fn ledger_commit_store_recovers_identical_duplicate_slots() {
2154 let ledger = committed_ledger(0)
2155 .stage_validated_generation(
2156 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2157 None,
2158 )
2159 .expect("first generation");
2160 let payload = enveloped_payload(&ledger);
2161 let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
2162 let store = LedgerCommitStore {
2163 physical: DualCommitStore {
2164 slot0: Some(committed.clone()),
2165 slot1: Some(committed),
2166 },
2167 };
2168
2169 let recovered = store.recover().expect("recovered");
2170
2171 assert_eq!(recovered.ledger(), &ledger);
2172 }
2173
2174 #[test]
2175 fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
2176 let mut store = LedgerCommitStore::default();
2177 let first = committed_ledger(1);
2178 let second = committed_ledger(2);
2179
2180 store.commit(&first).expect("first commit");
2181 store
2182 .write_corrupt_inactive_ledger(&second)
2183 .expect("corrupt write");
2184 let err = store.recover().expect_err("corrupt slot");
2185
2186 assert_eq!(
2187 err,
2188 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2189 slot0_invalid: false,
2190 slot1_invalid: true,
2191 })
2192 );
2193 }
2194
2195 #[test]
2196 fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
2197 let store = LedgerCommitStore {
2198 physical: DualCommitStore {
2199 slot0: Some(CommittedGenerationBytes::new(
2200 7,
2201 enveloped_payload(&committed_ledger(6)),
2202 )),
2203 slot1: None,
2204 },
2205 };
2206 let err = store.recover().expect_err("mismatch");
2207
2208 assert_eq!(
2209 err,
2210 LedgerCommitError::PhysicalLogicalGenerationMismatch {
2211 physical_generation: 7,
2212 logical_generation: 6
2213 }
2214 );
2215 }
2216
2217 #[test]
2218 fn ledger_commit_store_rejects_non_next_logical_generation() {
2219 let mut store = LedgerCommitStore::default();
2220 store.commit(&committed_ledger(1)).expect("first commit");
2221
2222 let err = store
2223 .commit(&committed_ledger(3))
2224 .expect_err("skipped generation");
2225
2226 assert_eq!(
2227 err,
2228 LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2229 expected: 2,
2230 actual: 3
2231 })
2232 );
2233 }
2234
2235 #[test]
2236 fn ledger_commit_store_initializes_empty_store_explicitly() {
2237 let mut store = LedgerCommitStore::default();
2238 let genesis = committed_ledger(3);
2239
2240 let recovered = store
2241 .recover_or_initialize(&genesis)
2242 .expect("initialized ledger");
2243
2244 assert_eq!(recovered.current_generation(), 3);
2245 assert_eq!(recovered.physical_generation(), 3);
2246 assert!(!store.physical().is_uninitialized());
2247 }
2248
2249 #[test]
2250 fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2251 let mut store = LedgerCommitStore::default();
2252 store
2253 .write_corrupt_inactive_ledger(&ledger())
2254 .expect("corrupt write");
2255
2256 let err = store
2257 .recover_or_initialize(&ledger())
2258 .expect_err("corrupt state");
2259
2260 assert!(matches!(
2261 err,
2262 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2263 slot0_invalid: true,
2264 slot1_invalid: false,
2265 })
2266 ));
2267 }
2268 #[test]
2269 fn history_boundary_round_trips_and_rejects_next_generation_without_mutation() {
2270 let mut source = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 - 1);
2271 source
2272 .allocation_history
2273 .push_record(active_record("app.users.v1", 100));
2274 let retirement = AllocationRetirement::new(
2275 "app.users.v1",
2276 AllocationSlotDescriptor::memory_manager(100).unwrap(),
2277 )
2278 .unwrap();
2279 let retired = source
2280 .stage_retirement_generation(&retirement, None)
2281 .unwrap();
2282 retired.validate_committed_integrity().unwrap();
2283 assert_eq!(
2284 retired.allocation_history.generations().len(),
2285 crate::constants::MAX_LEDGER_GENERATIONS
2286 );
2287 assert_eq!(
2288 retired.allocation_history.records()[0].schema_history,
2289 source.allocation_history.records()[0].schema_history
2290 );
2291 assert_eq!(
2292 source.allocation_history.records()[0].state,
2293 AllocationState::Active
2294 );
2295 assert!(matches!(
2296 retired.stage_retirement_generation(&retirement, None),
2297 Err(AllocationRetirementError::Integrity(
2298 LedgerIntegrityError::LimitExceeded {
2299 resource: "generation history",
2300 ..
2301 }
2302 ))
2303 ));
2304
2305 let ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2306 let mut store = LedgerCommitStore::default();
2307 let recovered = store.commit(&ledger).expect("boundary is admissible");
2308 let record = crate::StableCellLedgerRecord::new(store.clone());
2309 let bytes = crate::test_cbor::to_vec(&record).unwrap();
2310 assert_eq!(record.encoded_size(), bytes.len());
2311 let recovered_record = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2312 assert_eq!(
2313 recovered_record.store().recover().unwrap().ledger(),
2314 &ledger
2315 );
2316 let before = store.clone();
2317 let err = crate::AllocationBootstrap::new(&mut store)
2318 .validate_and_commit(
2319 DeclarationSnapshot::new(Vec::new()).unwrap(),
2320 &crate::GenericRangePolicy,
2321 None,
2322 )
2323 .unwrap_err();
2324 assert!(matches!(
2325 err,
2326 crate::BootstrapError::Staging(AllocationStageError::Integrity(
2327 LedgerIntegrityError::LimitExceeded { .. }
2328 ))
2329 ));
2330 assert_eq!(store, before);
2331 assert_eq!(
2332 recovered.current_generation(),
2333 crate::constants::MAX_LEDGER_GENERATIONS as u64
2334 );
2335 let oversize = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 + 1);
2336 assert!(matches!(
2337 store.commit(&oversize),
2338 Err(LedgerCommitError::Integrity(
2339 LedgerIntegrityError::LimitExceeded { .. }
2340 ))
2341 ));
2342 assert_eq!(store, before);
2343 }
2344
2345 #[test]
2346 fn record_count_boundary_and_malicious_history_reject_on_recovery() {
2347 let mut ledger = committed_ledger(1);
2348 ledger.allocation_history.records = (0..255_u8)
2349 .map(|id| active_record(&format!("app.slot{id}.v1"), id))
2350 .collect();
2351 let mut store = LedgerCommitStore::default();
2352 store.commit(&ledger).unwrap();
2353 assert_eq!(
2354 store
2355 .recover()
2356 .unwrap()
2357 .ledger()
2358 .allocation_history()
2359 .records()
2360 .len(),
2361 255
2362 );
2363 ledger
2364 .allocation_history
2365 .records
2366 .push(active_record("app.extra.v1", 254));
2367 assert!(matches!(
2368 store.commit(&ledger),
2369 Err(LedgerCommitError::Integrity(
2370 LedgerIntegrityError::LimitExceeded { .. }
2371 ))
2372 ));
2373 let payload = enveloped_payload(&ledger);
2374 let mut invalid = LedgerCommitStore {
2375 physical: DualCommitStore {
2376 slot0: Some(CommittedGenerationBytes::new(1, payload)),
2377 slot1: None,
2378 },
2379 };
2380 assert!(invalid.recover().is_err());
2381 assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
2382 for bytes in [
2383 vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2384 vec![0x81; 34],
2385 vec![0xa1],
2386 ] {
2387 let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
2388 let invalid = LedgerCommitStore {
2389 physical: DualCommitStore {
2390 slot0: Some(CommittedGenerationBytes::new(1, payload)),
2391 slot1: None,
2392 },
2393 };
2394 assert!(matches!(
2395 invalid.recover(),
2396 Err(LedgerCommitError::Codec(_))
2397 ));
2398 }
2399 }
2400 #[test]
2401 fn encoded_byte_limit_rejects_before_commit_mutation() {
2402 let mut store = LedgerCommitStore::default();
2403 store.commit(&committed_ledger(0)).unwrap();
2404 let before = store.clone();
2405 let mut ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2406 for generation in ledger.allocation_history.generations_mut() {
2407 generation.runtime_fingerprint =
2408 Some("x".repeat(crate::constants::DIAGNOSTIC_STRING_MAX_BYTES));
2409 }
2410 assert!(matches!(
2411 store.commit(&ledger),
2412 Err(LedgerCommitError::Integrity(
2413 LedgerIntegrityError::LimitExceeded {
2414 resource: "ledger bytes",
2415 ..
2416 }
2417 ))
2418 ));
2419 assert_eq!(store, before);
2420 let mut envelope = LedgerPayloadEnvelope::current(Vec::new())
2421 .try_encode()
2422 .unwrap();
2423 envelope[16..24]
2424 .copy_from_slice(&((crate::constants::MAX_LEDGER_BYTES + 1) as u64).to_le_bytes());
2425 assert!(matches!(
2426 LedgerPayloadEnvelope::decode(&envelope),
2427 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { .. })
2428 ));
2429 }
2430}