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