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