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