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, validate_declaration_claim,
15 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 AllocationLedger, AllocationRecord, AllocationRetirement, AllocationState, RecoveredLedger,
26};
27pub use stage::checked_reservation_count;
28pub use stage::{
29 stage_reservation_generation, stage_retirement_generation, stage_validated_generation,
30};
31
32fn decode_ledger(bytes: &[u8]) -> Result<AllocationLedger, String> {
33 crate::cbor::from_slice_exact(bytes).map_err(|err| err.to_string())
34}
35
36#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
49#[serde(deny_unknown_fields)]
50pub struct LedgerCommitStore {
51 physical: DualCommitStore,
53}
54
55impl LedgerCommitStore {
56 #[must_use]
58 pub const fn physical(&self) -> &DualCommitStore {
59 &self.physical
60 }
61
62 pub fn recover(&self) -> Result<RecoveredLedger, LedgerCommitError> {
64 let committed = self
65 .physical
66 .authoritative()
67 .map_err(LedgerCommitError::Recovery)?;
68 Self::recover_committed(committed)
69 }
70
71 pub(crate) fn recover_with_diagnostic(
72 &self,
73 ) -> (
74 Result<RecoveredLedger, LedgerCommitError>,
75 CommitStoreDiagnostic,
76 ) {
77 let (committed, diagnostic) = self.physical.authoritative_with_diagnostic();
78 let recovered = committed
79 .map_err(LedgerCommitError::Recovery)
80 .and_then(Self::recover_committed);
81 (recovered, diagnostic)
82 }
83
84 fn recover_committed(
85 committed: &CommittedGenerationBytes,
86 ) -> Result<RecoveredLedger, LedgerCommitError> {
87 let payload = LedgerPayloadEnvelope::decode_payload(committed.payload())
88 .map_err(LedgerCommitError::PayloadEnvelope)?;
89 let ledger = decode_ledger(payload).map_err(LedgerCommitError::Codec)?;
90 if committed.generation() != ledger.current_generation {
91 return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
92 physical_generation: committed.generation(),
93 logical_generation: ledger.current_generation,
94 });
95 }
96 ledger
97 .validate_integrity()
98 .map_err(LedgerCommitError::Integrity)?;
99 Ok(RecoveredLedger::from_trusted_ledger(ledger))
100 }
101
102 pub fn recover_or_initialize(
111 &mut self,
112 genesis: &AllocationLedger,
113 ) -> Result<RecoveredLedger, LedgerCommitError> {
114 match self.recover() {
115 Ok(ledger) => Ok(ledger),
116 Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)) => {
118 self.commit(genesis)
119 }
120 Err(err) => Err(err),
121 }
122 }
123
124 pub fn commit(
130 &mut self,
131 ledger: &AllocationLedger,
132 ) -> Result<RecoveredLedger, LedgerCommitError> {
133 self.commit_generation(ledger)?;
134 Ok(RecoveredLedger::from_trusted_ledger(ledger.clone()))
138 }
139
140 pub(crate) fn commit_generation(
143 &mut self,
144 ledger: &AllocationLedger,
145 ) -> Result<(), LedgerCommitError> {
146 ledger
147 .validate_integrity()
148 .map_err(LedgerCommitError::Integrity)?;
149 let payload =
150 LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
151 self.physical
152 .commit_payload_at_generation(ledger.current_generation, payload)
153 .map(|_| ())
154 .map_err(LedgerCommitError::Recovery)
155 }
156
157 #[cfg(test)]
159 pub fn write_corrupt_inactive_ledger(
160 &mut self,
161 ledger: &AllocationLedger,
162 ) -> Result<(), LedgerCommitError> {
163 let payload =
164 LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
165 self.physical
166 .write_corrupt_inactive_slot(ledger.current_generation, payload);
167 Ok(())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::test_cbor::hex_fixture;
175
176 #[test]
177 fn repeated_commits_and_schema_churn_keep_metadata_bounded() {
178 let mut store = LedgerCommitStore::default();
179 store
180 .commit(&AllocationLedger::new(65_530, Vec::new()).unwrap())
181 .unwrap();
182 let mut largest_payload = 0;
183 for version in 1..=1024 {
184 let snapshot =
185 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, Some(version))])
186 .unwrap();
187 let pending = crate::AllocationBootstrap::new(&mut store)
188 .validate_and_commit(snapshot, &crate::GenericRangePolicy)
189 .unwrap();
190 let committed = pending.confirm_persisted();
191 assert_eq!(committed.generation(), 65_530 + u64::from(version));
192 let recovered = store.recover().unwrap();
193 assert_eq!(recovered.ledger().records().len(), 1);
194 assert_eq!(
195 recovered.ledger().records()[0].schema().schema_version(),
196 Some(version)
197 );
198 largest_payload =
199 largest_payload.max(store.physical().authoritative().unwrap().payload().len());
200 }
201 assert!(largest_payload < 200);
203 assert!(crate::StableCellLedgerRecord::new(store).to_bytes().len() < 600);
204 }
205
206 #[test]
207 fn largest_current_record_set_fits_writer_and_reader_bounds() {
208 let records = (0..255_u8)
209 .map(|id| {
210 let key = format!("app.store{id:03}.{}.v1", "x".repeat(112));
211 assert_eq!(key.len(), 128);
212 let declaration = declaration(&key, id, Some(u32::MAX));
213 let mut record = AllocationRecord::active(&declaration);
214 record.state = AllocationState::Reserved;
216 record
217 })
218 .collect();
219 let ledger = AllocationLedger::new(u64::MAX - 1, records).unwrap();
220 let payload = enveloped_payload(&ledger);
221 assert!(payload.len() <= crate::constants::MAX_COMMITTED_PAYLOAD_BYTES);
222 let mut store = LedgerCommitStore::default();
223 store.commit(&ledger).unwrap();
224 let next = AllocationLedger::new(u64::MAX, ledger.records().to_vec()).unwrap();
225 store.commit(&next).unwrap();
226 let record = crate::StableCellLedgerRecord::new(store);
227 let bytes = record.to_bytes();
228 assert!(bytes.len() <= crate::constants::MAX_LEDGER_RECORD_BYTES);
229 let decoded = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
230 assert_eq!(decoded.store().recover().unwrap().ledger(), &next);
231 }
232
233 #[test]
234 fn validated_counter_overflow_rejects_without_commit_mutation() {
235 let mut store = LedgerCommitStore::default();
236 store
237 .commit(&AllocationLedger::new(u64::MAX, Vec::new()).unwrap())
238 .unwrap();
239 let before = store.clone();
240 let result = crate::AllocationBootstrap::new(&mut store).validate_and_commit(
241 DeclarationSnapshot::new(Vec::new()).unwrap(),
242 &crate::GenericRangePolicy,
243 );
244 assert!(matches!(
245 result,
246 Err(crate::BootstrapError::Staging(
247 AllocationStageError::GenerationOverflow {
248 generation: u64::MAX
249 }
250 ))
251 ));
252 assert_eq!(store, before);
253 }
254
255 #[test]
256 fn unsupported_discriminator_precedes_logical_decode_and_preserves_storage() {
257 let mut payload = LedgerPayloadEnvelope::current(vec![0xff]).encode();
258 payload[8..12].copy_from_slice(b"BAD!");
259 let mut physical = DualCommitStore::default();
260 physical.commit_payload_at_generation(1, payload).unwrap();
261 let mut store = LedgerCommitStore { physical };
262 let before = store.clone();
263 let expected =
264 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
265 marker: *b"BAD!",
266 version: None,
267 });
268 assert_eq!(store.recover(), Err(expected.clone()));
269 assert_eq!(
270 store.recover_or_initialize(&AllocationLedger::empty_genesis()),
271 Err(expected)
272 );
273 assert_eq!(store, before);
274 }
275 use crate::{
276 declaration::{AllocationDeclaration, DeclarationSnapshot},
277 key::StableKey,
278 physical::CommittedGenerationBytes,
279 schema::SchemaMetadata,
280 slot::MemoryManagerSlot,
281 };
282 use ic_stable_structures::Storable;
283 fn declaration(key: &str, id: u8, schema_version: Option<u32>) -> AllocationDeclaration {
284 AllocationDeclaration::new(
285 key,
286 MemoryManagerSlot::new(id).expect("usable slot"),
287 None,
288 SchemaMetadata::new(schema_version).expect("schema metadata"),
289 )
290 .expect("declaration")
291 }
292
293 fn ledger() -> AllocationLedger {
294 AllocationLedger {
295 current_generation: 3,
296 records: Vec::new(),
297 }
298 }
299
300 fn committed_ledger(current_generation: u64) -> AllocationLedger {
301 AllocationLedger {
302 current_generation,
303 records: Vec::new(),
304 }
305 }
306
307 fn active_record(key: &str, id: u8) -> AllocationRecord {
308 AllocationRecord::active(&declaration(key, id, None))
309 }
310
311 fn validated(
312 generation: u64,
313 declarations: Vec<AllocationDeclaration>,
314 ) -> crate::capability::ValidatedAllocations {
315 crate::capability::ValidatedAllocations::new(generation, declarations)
316 }
317
318 fn record<'ledger>(ledger: &'ledger AllocationLedger, key: &str) -> &'ledger AllocationRecord {
319 ledger
320 .records()
321 .iter()
322 .find(|record| record.stable_key.as_str() == key)
323 .expect("allocation record")
324 }
325
326 fn enveloped_payload(ledger: &AllocationLedger) -> Vec<u8> {
327 LedgerPayloadEnvelope::encode_ledger(ledger).expect("fixture envelope")
328 }
329
330 fn ledger_from_payload_fixture(contents: &str) -> AllocationLedger {
331 let bytes = hex_fixture(contents);
332 let envelope = LedgerPayloadEnvelope::decode(&bytes).expect("fixture envelope");
333
334 let ledger = decode_ledger(envelope.payload()).expect("fixture ledger");
335 ledger
336 .validate_integrity()
337 .expect("fixture ledger integrity");
338 assert_eq!(bytes, enveloped_payload(&ledger));
339 ledger
340 }
341
342 fn store_from_fixture(contents: &str) -> LedgerCommitStore {
343 let bytes = hex_fixture(contents);
344 let store: LedgerCommitStore = crate::test_cbor::from_slice(&bytes).expect("fixture store");
345 assert_eq!(
346 bytes,
347 crate::test_cbor::to_vec(&store).expect("re-encoded fixture store")
348 );
349 store
350 }
351
352 fn active_committed_ledger() -> AllocationLedger {
353 AllocationLedger {
354 current_generation: 1,
355 records: vec![active_record("app.users.v1", 100)],
356 }
357 }
358
359 fn active_ledger_value() -> crate::test_cbor::Value {
360 crate::test_cbor::to_value(active_committed_ledger()).expect("ledger value")
361 }
362
363 #[test]
364 fn recovery_rejects_unknown_fields_inside_retirement_state() {
365 use crate::{AllocationRetirement, StableCellLedgerRecord};
366
367 let active = active_committed_ledger();
368 let retirement =
369 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
370 .unwrap();
371 let retired = active.stage_retirement_generation(&retirement).unwrap();
372 let mut store = LedgerCommitStore::default();
373 let committed = store.commit(&retired).unwrap();
374 assert_eq!(committed, store.recover().unwrap());
375
376 let mut value = crate::test_cbor::to_value(&retired).unwrap();
377 let records = value_array_mut(map_field_mut(value_map_mut(&mut value), "records"));
378 let state = map_field_mut(value_map_mut(&mut records[0]), "state");
379 *state = serde_json::from_value::<crate::test_cbor::Value>(
380 serde_json::json!({"Retired": {"unexpected": true}}),
381 )
382 .unwrap();
383 let bytes = crate::test_cbor::to_vec(&value).unwrap();
384 let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
385 let mut physical = DualCommitStore::default();
386 physical.commit_payload_at_generation(2, payload).unwrap();
387 let record = StableCellLedgerRecord::new(LedgerCommitStore { physical });
388 let encoded = crate::test_cbor::to_vec(&record).unwrap();
389 let decoded = crate::decode_stable_cell_ledger_record(&encoded).unwrap();
390 let before = decoded.clone();
391 let error = decoded.store().recover().unwrap_err();
392 assert!(matches!(error, LedgerCommitError::Codec(_)));
393
394 assert_eq!(decoded, before);
395 }
396
397 fn value_map_mut(
398 value: &mut crate::test_cbor::Value,
399 ) -> &mut Vec<(crate::test_cbor::Value, crate::test_cbor::Value)> {
400 let crate::test_cbor::Value::Map(map) = value else {
401 panic!("expected CBOR map");
402 };
403 map
404 }
405
406 fn map_field_mut<'map>(
407 map: &'map mut [(crate::test_cbor::Value, crate::test_cbor::Value)],
408 field: &str,
409 ) -> &'map mut crate::test_cbor::Value {
410 map.iter_mut()
411 .find(|(key, _)| key == &crate::test_cbor::Value::Text(field.to_string()))
412 .map(|(_, value)| value)
413 .expect("CBOR map field")
414 }
415
416 fn remove_map_field(
417 map: &mut Vec<(crate::test_cbor::Value, crate::test_cbor::Value)>,
418 field: &str,
419 ) {
420 let field = crate::test_cbor::Value::Text(field.to_string());
421 let index = map
422 .iter()
423 .position(|(key, _)| key == &field)
424 .expect("CBOR map field");
425 map.remove(index);
426 }
427
428 fn value_array_mut(value: &mut crate::test_cbor::Value) -> &mut Vec<crate::test_cbor::Value> {
429 let crate::test_cbor::Value::Array(values) = value else {
430 panic!("expected CBOR array");
431 };
432 values
433 }
434
435 fn decode_mutated_ledger(value: crate::test_cbor::Value) -> String {
436 let bytes = crate::test_cbor::to_vec(&value).expect("mutated ledger bytes");
437 decode_ledger(&bytes).expect_err("mutated ledger must fail closed")
438 }
439
440 #[test]
441 fn allocation_record_accessors_expose_read_only_views() {
442 let ledger = AllocationLedger::new(1, vec![active_record("app.users.v1", 100)]).unwrap();
443 assert_eq!(ledger.records().len(), 1);
444 let record = &ledger.records()[0];
445 assert_eq!(record.stable_key().as_str(), "app.users.v1");
446 assert_eq!(record.slot().id(), 100);
447 assert_eq!(record.state(), AllocationState::Active);
448 assert_eq!(record.schema(), &SchemaMetadata::default());
449 }
450
451 #[test]
452 fn ledger_cbor_round_trips_allocation_ledger() {
453 let ledger = committed_ledger(2);
454
455 let encoded = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
456 let decoded = decode_ledger(&encoded).expect("decode ledger");
457
458 assert_eq!(decoded, ledger);
459 }
460
461 #[test]
462 fn ledger_cbor_rejects_trailing_bytes() {
463 let mut encoded = crate::test_cbor::to_vec(&committed_ledger(2)).expect("encode ledger");
464 encoded.push(0);
465
466 let err = decode_ledger(&encoded).expect_err("trailing bytes must fail closed");
467
468 assert!(err.contains("trailing bytes"));
469 }
470
471 #[test]
472 fn cbor_ledger_codec_rejects_unknown_top_level_fields() {
473 use crate::test_cbor::Value;
474
475 let map = vec![
476 (
477 Value::Text("current_generation".to_string()),
478 Value::Integer(0.into()),
479 ),
480 (Value::Text("records".to_string()), Value::Array(vec![])),
481 (Value::Text("future_field".to_string()), Value::Bool(true)),
482 ];
483 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field ledger");
484
485 let err = decode_ledger(&bytes).expect_err("unknown ledger field must fail closed");
486
487 assert!(err.contains("future_field"));
488 }
489
490 #[test]
491 fn cbor_ledger_codec_rejects_unknown_nested_record_fields() {
492 let mut value = active_ledger_value();
493 let records = map_field_mut(value_map_mut(&mut value), "records");
494 let record = value_array_mut(records)
495 .first_mut()
496 .expect("allocation record");
497 value_map_mut(record).push((
498 crate::test_cbor::Value::Text("future_record_field".to_string()),
499 crate::test_cbor::Value::Bool(true),
500 ));
501
502 let err = decode_mutated_ledger(value);
503
504 assert!(err.contains("future_record_field"));
505 }
506
507 #[test]
508 fn cbor_ledger_codec_rejects_unknown_nested_slot_fields() {
509 let mut value = active_ledger_value();
510 let records = map_field_mut(value_map_mut(&mut value), "records");
511 let record = value_array_mut(records)
512 .first_mut()
513 .expect("allocation record");
514 let slot = map_field_mut(value_map_mut(record), "slot");
515 value_map_mut(slot).push((
516 crate::test_cbor::Value::Text("future_slot_field".to_string()),
517 crate::test_cbor::Value::Bool(true),
518 ));
519
520 let err = decode_mutated_ledger(value);
521
522 assert!(err.contains("future_slot_field"));
523 }
524
525 #[test]
526 fn cbor_ledger_codec_requires_schema_version_field() {
527 let mut value = active_ledger_value();
528 let records = map_field_mut(value_map_mut(&mut value), "records");
529 let record = value_array_mut(records)
530 .first_mut()
531 .expect("allocation record");
532 let schema = map_field_mut(value_map_mut(record), "schema");
533 remove_map_field(value_map_mut(schema), "schema_version");
534
535 let err = decode_mutated_ledger(value);
536
537 assert!(err.contains("schema_version"));
538 }
539
540 #[test]
541 fn ledger_commit_store_rejects_unknown_top_level_fields() {
542 use crate::test_cbor::Value;
543
544 let map = vec![
545 (
546 Value::Text("physical".to_string()),
547 crate::test_cbor::to_value(DualCommitStore::default()).expect("physical value"),
548 ),
549 (Value::Text("future_field".to_string()), Value::Bool(true)),
550 ];
551 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field store");
552
553 let err = crate::test_cbor::from_slice::<LedgerCommitStore>(&bytes)
554 .expect_err("unknown store field must fail closed");
555
556 assert!(err.to_string().contains("future_field"));
557 }
558
559 #[test]
560 fn current_empty_genesis_payload_fixture_recovers() {
561 let ledger = ledger_from_payload_fixture(include_str!(
562 "../../fixtures/current/empty_genesis_payload_envelope.hex"
563 ));
564
565 assert_eq!(ledger.current_generation, 0);
566 assert_eq!(ledger.records(), []);
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().id(), 100);
579 }
580
581 #[test]
582 fn current_reserved_payload_fixture_recovers() {
583 let ledger = ledger_from_payload_fixture(include_str!(
584 "../../fixtures/current/reserved_allocation_payload_envelope.hex"
585 ));
586 let record = record(&ledger, "app.future_store.v1");
587
588 assert_eq!(ledger.current_generation, 1);
589 assert_eq!(record.state(), AllocationState::Reserved);
590 assert_eq!(record.slot().id(), 101);
591 }
592
593 #[test]
594 fn current_retired_payload_fixture_recovers() {
595 let ledger = ledger_from_payload_fixture(include_str!(
596 "../../fixtures/current/retired_allocation_payload_envelope.hex"
597 ));
598 let record = record(&ledger, "app.users.v1");
599
600 assert_eq!(ledger.current_generation, 2);
601 assert_eq!(record.state(), AllocationState::Retired);
602 }
603
604 #[test]
605 fn current_memory_manager_slot_fixture_decodes() {
606 let bytes = hex_fixture(include_str!(
607 "../../fixtures/current/memory_manager_slot.cbor.hex"
608 ));
609 let slot: MemoryManagerSlot = crate::test_cbor::from_slice(&bytes).expect("slot fixture");
610
611 assert_eq!(slot.id(), 100);
612 assert_eq!(
613 bytes,
614 crate::test_cbor::to_vec(&slot).expect("re-encoded slot")
615 );
616 }
617
618 #[test]
619 fn current_ledger_commit_store_single_active_fixture_recovers() {
620 let store = store_from_fixture(include_str!(
621 "../../fixtures/current/ledger_commit_store_single_active.cbor.hex"
622 ));
623
624 let recovered = store.recover().expect("fixture store recovers");
625 assert_eq!(recovered.current_generation(), 1);
626 assert_eq!(record(recovered.ledger(), "app.users.v1").slot().id(), 100);
627 }
628
629 #[test]
630 fn current_dual_slot_valid_newer_fixture_recovers_newer() {
631 let store = store_from_fixture(include_str!(
632 "../../fixtures/current/dual_slot_store_valid_newer.cbor.hex"
633 ));
634
635 assert_eq!(store.physical().diagnostic().recovery, Ok(2));
636 let recovered = store.recover().expect("fixture store recovers");
637 assert_eq!(recovered.current_generation(), 2);
638 }
639
640 #[test]
641 fn current_dual_slot_corrupt_newer_fixture_fails_closed() {
642 let store = store_from_fixture(include_str!(
643 "../../fixtures/current/dual_slot_store_corrupt_newer.cbor.hex"
644 ));
645
646 let diagnostic = store.physical().diagnostic();
647 assert_eq!(
648 diagnostic.recovery,
649 Err(CommitRecoveryError::InvalidCommitSlots {
650 slot0_invalid: false,
651 slot1_invalid: true,
652 })
653 );
654 assert_eq!(
655 diagnostic.slot1,
656 crate::CommitSlotDiagnostic::Invalid { generation: 2 }
657 );
658 let err = store
659 .recover()
660 .expect_err("corrupt fixture must fail closed");
661 assert!(matches!(
662 err,
663 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
664 slot0_invalid: false,
665 slot1_invalid: true,
666 })
667 ));
668 }
669
670 #[test]
671 fn stage_validated_generation_records_new_allocations() {
672 let declarations = vec![declaration("app.users.v1", 100, Some(1))];
673 let validated = validated(3, declarations);
674
675 let staged = ledger()
676 .stage_validated_generation(&validated)
677 .expect("staged generation");
678
679 assert_eq!(staged.current_generation, 4);
680 assert_eq!(staged.records().len(), 1);
681 }
682
683 #[test]
684 fn stage_validated_generation_allows_empty_generation_boundary() {
685 let validated = crate::capability::ValidatedAllocations::new(3, Vec::new());
686
687 let staged = ledger()
688 .stage_validated_generation(&validated)
689 .expect("empty validated generation");
690
691 assert_eq!(staged.current_generation, 4);
692 assert_eq!(staged.records(), []);
693 }
694
695 #[test]
696 fn stage_validated_generation_rejects_stale_validated_allocations() {
697 let validated = validated(2, vec![declaration("app.users.v1", 100, Some(1))]);
698
699 let err = ledger()
700 .stage_validated_generation(&validated)
701 .expect_err("stale validated allocations");
702
703 assert_eq!(
704 err,
705 AllocationStageError::StaleValidatedAllocations {
706 validated_generation: 2,
707 ledger_generation: 3
708 }
709 );
710 }
711
712 #[test]
713 fn stage_validated_generation_rejects_overflowing_receiver_as_stale() {
714 let mut store = LedgerCommitStore::default();
715 let recovered = store.commit(&committed_ledger(0)).unwrap();
716 let validated = crate::validate_allocations(
717 &recovered,
718 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, Some(1))]).unwrap(),
719 &crate::GenericRangePolicy,
720 )
721 .unwrap();
722 let ledger = AllocationLedger::new(u64::MAX, Vec::new()).unwrap();
723 let before = ledger.clone();
724
725 let err = ledger
726 .stage_validated_generation(&validated)
727 .expect_err("receiver cannot match a bounded recovered generation");
728
729 assert_eq!(
730 err,
731 AllocationStageError::StaleValidatedAllocations {
732 validated_generation: 0,
733 ledger_generation: u64::MAX,
734 }
735 );
736 assert_eq!(ledger, before);
737 }
738
739 #[test]
740 fn stage_validated_generation_rejects_same_key_different_slot() {
741 let mut ledger = committed_ledger(3);
742 ledger.records = vec![active_record("app.users.v1", 100)];
743 ledger.validate_integrity().unwrap();
744 let mut other_store = LedgerCommitStore::default();
747 let other_recovered = other_store.commit(&committed_ledger(3)).unwrap();
748 let validated = crate::validate_allocations(
749 &other_recovered,
750 DeclarationSnapshot::new(vec![declaration("app.users.v1", 101, None)]).unwrap(),
751 &crate::GenericRangePolicy,
752 )
753 .unwrap();
754 let before = ledger.clone();
755
756 let err = ledger
757 .stage_validated_generation(&validated)
758 .expect_err("stable key cannot move slots");
759
760 assert!(matches!(
761 err,
762 AllocationStageError::StableKeySlotConflict { .. }
763 ));
764 assert_eq!(ledger, before);
765 }
766
767 #[test]
768 fn stage_validated_generation_rejects_same_slot_different_key() {
769 let mut ledger = ledger();
770 ledger.records = vec![active_record("app.users.v1", 100)];
771 let validated = validated(3, vec![declaration("app.orders.v1", 100, None)]);
772
773 let err = ledger
774 .stage_validated_generation(&validated)
775 .expect_err("slot cannot be reused by another key");
776
777 assert!(matches!(
778 err,
779 AllocationStageError::SlotStableKeyConflict { .. }
780 ));
781 }
782
783 #[test]
784 fn stage_validated_generation_rejects_retired_redeclaration() {
785 let mut ledger = ledger();
786 let mut record = active_record("app.users.v1", 100);
787 record.state = AllocationState::Retired;
788 ledger.records = vec![record];
789 let validated = validated(3, vec![declaration("app.users.v1", 100, None)]);
790
791 let err = ledger
792 .stage_validated_generation(&validated)
793 .expect_err("retired allocation cannot be redeclared");
794
795 assert!(matches!(
796 err,
797 AllocationStageError::RetiredAllocation { .. }
798 ));
799 }
800
801 #[test]
802 fn stage_validated_generation_preserves_omitted_records() {
803 let first = validated(
804 3,
805 vec![
806 declaration("app.users.v1", 100, Some(1)),
807 declaration("app.orders.v1", 101, Some(1)),
808 ],
809 );
810 let second = validated(4, vec![declaration("app.users.v1", 100, Some(1))]);
811
812 let staged = ledger()
813 .stage_validated_generation(&first)
814 .expect("first generation");
815 let staged = staged
816 .stage_validated_generation(&second)
817 .expect("second generation");
818
819 assert_eq!(staged.current_generation, 5);
820 assert_eq!(staged.records().len(), 2);
821 let omitted = staged
822 .records()
823 .iter()
824 .find(|record| record.stable_key.as_str() == "app.orders.v1")
825 .expect("omitted record");
826 assert_eq!(omitted.state, AllocationState::Active);
827 }
828
829 #[test]
830 fn stage_validated_generation_replaces_current_schema_metadata() {
831 let first = validated(3, vec![declaration("app.users.v1", 100, Some(1))]);
832 let second = validated(4, vec![declaration("app.users.v1", 100, Some(2))]);
833 let initial = ledger().stage_validated_generation(&first).unwrap();
834 let updated = initial.stage_validated_generation(&second).unwrap();
835 assert_eq!(initial.records()[0].schema().schema_version(), Some(1));
836 assert_eq!(updated.records()[0].schema().schema_version(), Some(2));
837 assert_eq!(updated.records().len(), 1);
838 assert_eq!(updated.records()[0].slot(), initial.records()[0].slot());
839 }
840
841 #[test]
842 fn stage_reservation_generation_records_reserved_allocations() {
843 let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
844
845 let staged = ledger()
846 .stage_reservation_generation(&reservations)
847 .expect("reserved generation");
848
849 assert_eq!(staged.current_generation, 4);
850 assert_eq!(staged.records().len(), 1);
851 assert_eq!(staged.records()[0].state, AllocationState::Reserved);
852 }
853
854 #[test]
855 fn stage_reservation_generation_allows_empty_generation_boundary() {
856 let reservations = Vec::new();
857
858 let staged = ledger()
859 .stage_reservation_generation(&reservations)
860 .expect("empty reservation generation");
861
862 assert_eq!(staged.current_generation, 4);
863 assert_eq!(staged.records(), []);
864 }
865
866 #[test]
867 fn stage_reservation_generation_refreshes_existing_reserved_allocation() {
868 let first = vec![declaration("app.future_store.v1", 100, Some(1))];
869 let staged = ledger()
870 .stage_reservation_generation(&first)
871 .expect("first reservation generation");
872
873 let second = vec![declaration("app.future_store.v1", 100, Some(2))];
874 let staged = staged
875 .stage_reservation_generation(&second)
876 .expect("reservation refresh");
877 let record = record(&staged, "app.future_store.v1");
878
879 assert_eq!(record.state(), AllocationState::Reserved);
880 assert_eq!(record.schema().schema_version(), Some(2));
881 assert_eq!(record.slot().id(), 100);
882 assert_eq!(staged.records().len(), 1);
883 }
884
885 #[test]
886 fn stage_reservation_generation_rejects_generation_overflow() {
887 let ledger = AllocationLedger {
888 current_generation: u64::MAX,
889 ..ledger()
890 };
891 let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
892
893 let err = ledger
894 .stage_reservation_generation(&reservations)
895 .expect_err("overflow must fail");
896
897 assert_eq!(
898 err,
899 AllocationReservationError::GenerationOverflow {
900 generation: u64::MAX
901 }
902 );
903 }
904
905 #[test]
906 fn stage_reservation_generation_rejects_same_key_different_slot() {
907 let mut ledger = ledger();
908 ledger.records = vec![AllocationRecord::reserved(&declaration(
909 "app.future_store.v1",
910 100,
911 None,
912 ))];
913 let reservations = vec![declaration("app.future_store.v1", 101, None)];
914
915 let err = ledger
916 .stage_reservation_generation(&reservations)
917 .expect_err("reservation key cannot move slots");
918
919 assert!(matches!(
920 err,
921 AllocationReservationError::StableKeySlotConflict { .. }
922 ));
923 }
924
925 #[test]
926 fn stage_reservation_generation_rejects_same_slot_different_key() {
927 let mut ledger = ledger();
928 ledger.records = vec![AllocationRecord::reserved(&declaration(
929 "app.future_store.v1",
930 100,
931 None,
932 ))];
933 let reservations = vec![declaration("app.other_future_store.v1", 100, None)];
934
935 let err = ledger
936 .stage_reservation_generation(&reservations)
937 .expect_err("reservation slot cannot be reused by another key");
938
939 assert!(matches!(
940 err,
941 AllocationReservationError::SlotStableKeyConflict { .. }
942 ));
943 }
944
945 #[test]
946 fn stage_reservation_generation_rejects_active_allocation() {
947 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
948 let staged = ledger()
949 .stage_validated_generation(&active)
950 .expect("active generation");
951 let reservations = vec![declaration("app.users.v1", 100, None)];
952
953 let err = staged
954 .stage_reservation_generation(&reservations)
955 .expect_err("active cannot become reserved");
956
957 assert!(matches!(
958 err,
959 AllocationReservationError::ActiveAllocation { .. }
960 ));
961 }
962
963 #[test]
964 fn stage_reservation_generation_rejects_retired_allocation() {
965 let mut ledger = ledger();
966 let mut record = active_record("app.users.v1", 100);
967 record.state = AllocationState::Retired;
968 ledger.records = vec![record];
969 let reservations = vec![declaration("app.users.v1", 100, None)];
970
971 let err = ledger
972 .stage_reservation_generation(&reservations)
973 .expect_err("retired cannot revive");
974
975 assert!(matches!(
976 err,
977 AllocationReservationError::RetiredAllocation { .. }
978 ));
979 }
980
981 #[test]
982 fn stage_validated_generation_activates_reserved_record() {
983 let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
984 let staged = ledger()
985 .stage_reservation_generation(&reservations)
986 .expect("reserved generation");
987 let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
988
989 let staged = staged
990 .stage_validated_generation(&active)
991 .expect("active generation");
992 let record = &staged.records()[0];
993
994 assert_eq!(record.state, AllocationState::Active);
995 }
996
997 #[test]
998 fn stage_retirement_generation_tombstones_named_allocation() {
999 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1000 let staged = ledger()
1001 .stage_validated_generation(&active)
1002 .expect("active generation");
1003 let retirement = AllocationRetirement::new(
1004 "app.users.v1",
1005 MemoryManagerSlot::new(100).expect("usable slot"),
1006 )
1007 .expect("retirement");
1008
1009 let staged = staged
1010 .stage_retirement_generation(&retirement)
1011 .expect("retired generation");
1012 let record = &staged.records()[0];
1013
1014 assert_eq!(staged.current_generation, 5);
1015 assert_eq!(record.state, AllocationState::Retired);
1016 }
1017
1018 #[test]
1019 fn stage_retirement_generation_tombstones_reserved_allocation() {
1020 let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1021 let staged = ledger()
1022 .stage_reservation_generation(&reservations)
1023 .expect("reserved generation");
1024 let retirement = AllocationRetirement::new(
1025 "app.future_store.v1",
1026 MemoryManagerSlot::new(100).expect("usable slot"),
1027 )
1028 .expect("retirement");
1029
1030 let staged = staged
1031 .stage_retirement_generation(&retirement)
1032 .expect("reserved retirement generation");
1033 let record = &staged.records()[0];
1034
1035 assert_eq!(staged.current_generation, 5);
1036 assert_eq!(record.state, AllocationState::Retired);
1037 }
1038
1039 #[test]
1040 fn stage_retirement_generation_rejects_generation_overflow() {
1041 let mut ledger = ledger();
1042 ledger.current_generation = u64::MAX;
1043 ledger.records = vec![active_record("app.users.v1", 100)];
1044 let retirement = AllocationRetirement::new(
1045 "app.users.v1",
1046 MemoryManagerSlot::new(100).expect("usable slot"),
1047 )
1048 .expect("retirement");
1049
1050 let err = ledger
1051 .stage_retirement_generation(&retirement)
1052 .expect_err("overflow must fail");
1053
1054 assert_eq!(
1055 err,
1056 AllocationRetirementError::GenerationOverflow {
1057 generation: u64::MAX
1058 }
1059 );
1060 }
1061
1062 #[test]
1063 fn stage_retirement_generation_requires_matching_slot() {
1064 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1065 let staged = ledger()
1066 .stage_validated_generation(&active)
1067 .expect("active generation");
1068 let retirement = AllocationRetirement::new(
1069 "app.users.v1",
1070 MemoryManagerSlot::new(101).expect("usable slot"),
1071 )
1072 .expect("retirement");
1073
1074 let err = staged
1075 .stage_retirement_generation(&retirement)
1076 .expect_err("slot mismatch");
1077
1078 assert!(matches!(
1079 err,
1080 AllocationRetirementError::SlotMismatch { .. }
1081 ));
1082 }
1083
1084 #[test]
1085 fn retirement_rejections_preserve_source_and_slot_error_precedence() {
1086 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1087 let source = ledger().stage_validated_generation(&active).unwrap();
1088 let retirement =
1089 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
1090 .unwrap();
1091 let source = source.stage_retirement_generation(&retirement).unwrap();
1092 let before = source.clone();
1093
1094 assert!(matches!(
1095 source.stage_retirement_generation(&retirement),
1096 Err(AllocationRetirementError::AlreadyRetired { .. })
1097 ));
1098 let wrong_slot =
1099 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(101).unwrap())
1100 .unwrap();
1101 assert!(matches!(
1102 source.stage_retirement_generation(&wrong_slot),
1103 Err(AllocationRetirementError::SlotMismatch { .. })
1104 ));
1105 let unknown =
1106 AllocationRetirement::new("app.unknown.v1", MemoryManagerSlot::new(100).unwrap())
1107 .unwrap();
1108 assert!(matches!(
1109 source.stage_retirement_generation(&unknown),
1110 Err(AllocationRetirementError::UnknownStableKey(_))
1111 ));
1112 assert_eq!(source, before);
1113 }
1114
1115 #[test]
1116 fn allocation_retirement_constructor_rejects_invalid_key() {
1117 let err = AllocationRetirement::new("App.users.v1", MemoryManagerSlot::new(100).unwrap())
1118 .expect_err("invalid key must fail at construction");
1119 assert!(matches!(err, AllocationRetirementError::Key(_)));
1120 }
1121
1122 #[test]
1123 fn retirement_decode_rejects_unusable_memory_manager_slot() {
1124 let retirement =
1125 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
1126 .unwrap();
1127 let mut value = serde_json::to_value(retirement).unwrap();
1128 value["slot"]["slot"]["MemoryManagerId"] = serde_json::json!(255);
1129 assert!(serde_json::from_value::<AllocationRetirement>(value).is_err());
1130 }
1131
1132 #[test]
1133 fn retirement_decode_rejects_invalid_stable_key() {
1134 let retirement = AllocationRetirement::new(
1135 "app.users.v1",
1136 MemoryManagerSlot::new(100).expect("usable slot"),
1137 )
1138 .expect("retirement");
1139 let mut value = crate::test_cbor::to_value(retirement).expect("retirement value");
1140 *map_field_mut(value_map_mut(&mut value), "stable_key") =
1141 crate::test_cbor::Value::Text("App.users.v1".to_string());
1142 let bytes = crate::test_cbor::to_vec(&value).expect("retirement bytes");
1143 assert!(crate::test_cbor::from_slice::<AllocationRetirement>(&bytes).is_err());
1144 }
1145
1146 #[test]
1147 fn snapshot_can_feed_validated_generation() {
1148 let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1149 .expect("snapshot");
1150 let declarations = snapshot.into_declarations();
1151 let validated = crate::capability::ValidatedAllocations::new(3, declarations);
1152
1153 let staged = ledger()
1154 .stage_validated_generation(&validated)
1155 .expect("validated generation");
1156
1157 assert_eq!(staged.records().len(), 1);
1158 }
1159
1160 #[test]
1161 fn strict_committed_integrity_accepts_full_lifecycle() {
1162 let mut ledger = committed_ledger(0);
1163 ledger
1164 .validate_integrity()
1165 .expect("genesis ledger with no history");
1166
1167 ledger = ledger
1168 .stage_validated_generation(&validated(
1169 0,
1170 vec![declaration("app.users.v1", 100, Some(1))],
1171 ))
1172 .expect("first real commit after genesis");
1173 ledger.validate_integrity().expect("first real commit");
1174
1175 ledger = ledger
1176 .stage_validated_generation(&validated(
1177 1,
1178 vec![declaration("app.users.v1", 100, Some(1))],
1179 ))
1180 .expect("repeated active declaration");
1181 ledger
1182 .validate_integrity()
1183 .expect("repeated active declaration");
1184
1185 ledger = ledger
1186 .stage_validated_generation(&validated(
1187 2,
1188 vec![declaration("app.users.v1", 100, Some(2))],
1189 ))
1190 .expect("schema drift");
1191 ledger.validate_integrity().expect("schema metadata drift");
1192
1193 ledger = ledger
1194 .stage_reservation_generation(&[declaration("app.future_store.v1", 101, Some(1))])
1195 .expect("reservation-only generation");
1196 ledger
1197 .validate_integrity()
1198 .expect("reservation-only generation");
1199 assert_eq!(
1200 record(&ledger, "app.future_store.v1").state,
1201 AllocationState::Reserved
1202 );
1203
1204 ledger = ledger
1205 .stage_validated_generation(&validated(
1206 4,
1207 vec![declaration("app.future_store.v1", 101, Some(2))],
1208 ))
1209 .expect("reservation activation");
1210 ledger.validate_integrity().expect("reservation activation");
1211 assert_eq!(
1212 record(&ledger, "app.future_store.v1").state,
1213 AllocationState::Active
1214 );
1215
1216 let retirement = AllocationRetirement::new(
1217 "app.users.v1",
1218 MemoryManagerSlot::new(100).expect("usable slot"),
1219 )
1220 .expect("retirement");
1221 ledger = ledger
1222 .stage_retirement_generation(&retirement)
1223 .expect("retirement generation");
1224 ledger.validate_integrity().expect("retirement generation");
1225 assert_eq!(ledger.current_generation, 6);
1226 assert_eq!(
1227 record(&ledger, "app.users.v1").state,
1228 AllocationState::Retired
1229 );
1230 }
1231
1232 #[derive(Clone, Copy, Debug)]
1233 enum Transition {
1234 DeclareUsers,
1235 DeclareOrders,
1236 ReserveAudit,
1237 ActivateAudit,
1238 RetireUsers,
1239 RetireAudit,
1240 EmptyValidated,
1241 EmptyReservation,
1242 }
1243
1244 const TRANSITIONS: [Transition; 8] = [
1245 Transition::DeclareUsers,
1246 Transition::DeclareOrders,
1247 Transition::ReserveAudit,
1248 Transition::ActivateAudit,
1249 Transition::RetireUsers,
1250 Transition::RetireAudit,
1251 Transition::EmptyValidated,
1252 Transition::EmptyReservation,
1253 ];
1254
1255 fn apply_transition(
1256 ledger: &AllocationLedger,
1257 transition: Transition,
1258 ) -> Result<AllocationLedger, String> {
1259 match transition {
1260 Transition::DeclareUsers => ledger
1261 .stage_validated_generation(&validated(
1262 ledger.current_generation,
1263 vec![declaration("app.users.v1", 100, Some(1))],
1264 ))
1265 .map_err(|err| err.to_string()),
1266 Transition::DeclareOrders => ledger
1267 .stage_validated_generation(&validated(
1268 ledger.current_generation,
1269 vec![declaration("app.orders.v1", 101, Some(1))],
1270 ))
1271 .map_err(|err| err.to_string()),
1272 Transition::ReserveAudit => ledger
1273 .stage_reservation_generation(&[declaration("app.audit.v1", 102, Some(1))])
1274 .map_err(|err| err.to_string()),
1275 Transition::ActivateAudit => ledger
1276 .stage_validated_generation(&validated(
1277 ledger.current_generation,
1278 vec![declaration("app.audit.v1", 102, Some(1))],
1279 ))
1280 .map_err(|err| err.to_string()),
1281 Transition::RetireUsers => ledger
1282 .stage_retirement_generation(
1283 &AllocationRetirement::new(
1284 "app.users.v1",
1285 MemoryManagerSlot::new(100).expect("usable slot"),
1286 )
1287 .expect("retirement"),
1288 )
1289 .map_err(|err| err.to_string()),
1290 Transition::RetireAudit => ledger
1291 .stage_retirement_generation(
1292 &AllocationRetirement::new(
1293 "app.audit.v1",
1294 MemoryManagerSlot::new(102).expect("usable slot"),
1295 )
1296 .expect("retirement"),
1297 )
1298 .map_err(|err| err.to_string()),
1299 Transition::EmptyValidated => ledger
1300 .stage_validated_generation(&validated(ledger.current_generation, Vec::new()))
1301 .map_err(|err| err.to_string()),
1302 Transition::EmptyReservation => ledger
1303 .stage_reservation_generation(&[])
1304 .map_err(|err| err.to_string()),
1305 }
1306 }
1307
1308 fn check_transition_sequences(
1309 ledger: AllocationLedger,
1310 depth: usize,
1311 sequence: &mut Vec<Transition>,
1312 ) {
1313 ledger
1314 .validate_integrity()
1315 .unwrap_or_else(|err| panic!("sequence {sequence:?} violated integrity: {err}"));
1316 if depth == 0 {
1317 return;
1318 }
1319
1320 for transition in TRANSITIONS {
1321 sequence.push(transition);
1322 let next = apply_transition(&ledger, transition).unwrap_or_else(|_| ledger.clone());
1323 check_transition_sequences(next, depth - 1, sequence);
1324 sequence.pop();
1325 }
1326 }
1327
1328 #[test]
1329 fn transition_matrix_preserves_committed_invariants() {
1330 check_transition_sequences(committed_ledger(0), 4, &mut Vec::new());
1331 }
1332
1333 #[test]
1334 fn validate_integrity_rejects_duplicate_stable_keys() {
1335 let mut ledger = ledger();
1336 for second_id in [101, 100] {
1337 ledger.records = vec![
1338 active_record("app.users.v1", 100),
1339 active_record("app.users.v1", second_id),
1340 ];
1341
1342 let err = ledger
1343 .validate_integrity()
1344 .expect_err("duplicate key precedes duplicate slot");
1345
1346 assert_eq!(
1347 err,
1348 LedgerIntegrityError::DuplicateStableKey {
1349 stable_key: StableKey::parse("app.users.v1").unwrap(),
1350 }
1351 );
1352 }
1353 }
1354
1355 #[test]
1356 fn validate_integrity_rejects_duplicate_slots() {
1357 let mut ledger = ledger();
1358 ledger.records = vec![
1359 active_record("app.users.v1", 100),
1360 active_record("app.orders.v1", 100),
1361 ];
1362
1363 let err = ledger.validate_integrity().expect_err("duplicate slot");
1364
1365 assert_eq!(
1366 err,
1367 LedgerIntegrityError::DuplicateSlot {
1368 slot: MemoryManagerSlot::new(100).unwrap(),
1369 }
1370 );
1371 }
1372
1373 #[test]
1374 fn recovery_rejects_invalid_stable_key_during_ledger_decode() {
1375 let mut ledger = committed_ledger(1);
1376 ledger.records.push(active_record("app.users.v1", 100));
1377 let mut bytes = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
1378 let key_start = bytes
1379 .windows(b"app.users.v1".len())
1380 .position(|window| window == b"app.users.v1")
1381 .expect("encoded stable key");
1382 bytes[key_start] = b'A';
1383 assert!(crate::test_cbor::from_slice::<AllocationLedger>(&bytes).is_err());
1384 let payload = LedgerPayloadEnvelope::current(bytes).encode();
1385 let mut physical = DualCommitStore::default();
1386 physical.commit_payload_at_generation(1, payload).unwrap();
1387 let store = LedgerCommitStore { physical };
1388 let before = store.clone();
1389 assert!(matches!(store.recover(), Err(LedgerCommitError::Codec(_))));
1390 assert_eq!(store, before, "malformed-key recovery must preserve slots");
1391 }
1392
1393 #[test]
1394 fn ledger_decode_rejects_unusable_memory_manager_slot() {
1395 let mut ledger = committed_ledger(1);
1396 ledger.records.push(active_record("app.users.v1", 100));
1397 let mut value = serde_json::to_value(ledger).unwrap();
1398 value["records"][0]["slot"]["slot"]["MemoryManagerId"] = serde_json::json!(255);
1399 let bytes = crate::test_cbor::to_vec(&value).unwrap();
1400 assert!(decode_ledger(&bytes).is_err());
1401 }
1402
1403 #[test]
1404 fn recovery_rejects_zero_schema_version_during_ledger_decode() {
1405 let mut ledger = committed_ledger(1);
1406 ledger.records.push(AllocationRecord::active(&declaration(
1407 "app.users.v1",
1408 100,
1409 Some(1),
1410 )));
1411 let mut value = serde_json::to_value(&ledger).unwrap();
1412 value["records"][0]["schema"]["schema_version"] = 0.into();
1413 let bytes = crate::test_cbor::to_vec(&value).unwrap();
1414 assert!(crate::test_cbor::from_slice::<AllocationLedger>(&bytes).is_err());
1415 let payload = LedgerPayloadEnvelope::current(bytes).encode();
1416 let mut physical = DualCommitStore::default();
1417 physical.commit_payload_at_generation(1, payload).unwrap();
1418 let store = LedgerCommitStore { physical };
1419 let before = store.clone();
1420 assert!(matches!(store.recover(), Err(LedgerCommitError::Codec(_))));
1421 assert_eq!(
1422 store, before,
1423 "malformed-schema recovery must preserve slots"
1424 );
1425 }
1426
1427 #[test]
1428 fn integrity_rejects_nonempty_genesis_before_commit() {
1429 let invalid = AllocationLedger {
1430 current_generation: 0,
1431 records: vec![active_record("app.users.v1", 100)],
1432 };
1433 assert_eq!(
1434 invalid.validate_integrity(),
1435 Err(LedgerIntegrityError::NonemptyGenesis)
1436 );
1437 let mut store = LedgerCommitStore::default();
1438 let before = store.clone();
1439 assert_eq!(
1440 store.commit(&invalid),
1441 Err(LedgerCommitError::Integrity(
1442 LedgerIntegrityError::NonemptyGenesis
1443 ))
1444 );
1445 assert_eq!(store, before);
1446 }
1447
1448 #[test]
1449 fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1450 let mut store = LedgerCommitStore::default();
1451 let mut invalid = ledger();
1452 invalid.records = vec![
1453 active_record("app.users.v1", 100),
1454 active_record("app.orders.v1", 100),
1455 ];
1456
1457 let err = store.commit(&invalid).expect_err("invalid ledger");
1458
1459 assert!(matches!(
1460 err,
1461 LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1462 ));
1463 assert!(store.physical().is_uninitialized());
1464 }
1465
1466 #[test]
1467 fn ledger_commit_store_recovers_latest_committed_ledger() {
1468 let mut store = LedgerCommitStore::default();
1469 let first = committed_ledger(1);
1470 let second = committed_ledger(2);
1471
1472 store.commit(&first).expect("first commit");
1473 store.commit(&second).expect("second commit");
1474 let recovered = store.recover().expect("recovered ledger");
1475
1476 assert_eq!(recovered.current_generation(), 2);
1477 assert_eq!(recovered.physical_generation(), 2);
1478 }
1479
1480 #[test]
1481 fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1482 let mut store = LedgerCommitStore::default();
1483 let ledger = committed_ledger(1);
1484
1485 store.commit(&ledger).expect("commit");
1486
1487 let committed = store.physical().authoritative().expect("authoritative");
1488 assert_eq!(
1489 committed.payload(),
1490 LedgerPayloadEnvelope::current(crate::test_cbor::to_vec(&ledger).unwrap())
1491 .try_encode()
1492 .unwrap(),
1493 "direct commit encoding preserves the complete public envelope bytes"
1494 );
1495 let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1496 assert_eq!(
1497 envelope.payload(),
1498 crate::test_cbor::to_vec(&ledger).expect("encode ledger")
1499 );
1500 }
1501
1502 #[test]
1503 fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1504 let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1505 let store = LedgerCommitStore {
1506 physical: DualCommitStore {
1507 slot0: Some(committed),
1508 slot1: None,
1509 },
1510 };
1511
1512 let err = store.recover().expect_err("bad envelope");
1513
1514 assert!(matches!(
1515 err,
1516 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1517 ));
1518 }
1519
1520 #[test]
1521 fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1522 let mut payload = enveloped_payload(&committed_ledger(1));
1523 payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1524 let store = LedgerCommitStore {
1525 physical: DualCommitStore {
1526 slot0: Some(CommittedGenerationBytes::new(1, payload)),
1527 slot1: None,
1528 },
1529 };
1530
1531 let err = store
1532 .recover()
1533 .expect_err("unknown ledger format version must fail");
1534
1535 assert_eq!(
1536 err,
1537 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1538 marker: *b"ICMS",
1539 version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1540 })
1541 );
1542 }
1543
1544 #[test]
1545 fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
1546 let genesis = enveloped_payload(&committed_ledger(0));
1547 let committed = CommittedGenerationBytes::new(0, genesis.clone());
1548 let mut corrupt = committed.clone();
1549 corrupt.checksum ^= 1;
1550 let mut unsupported = genesis.clone();
1551 unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1552 let cases = [
1553 ("empty", None, None),
1554 ("one slot", Some(committed.clone()), None),
1555 (
1556 "identical slots",
1557 Some(committed.clone()),
1558 Some(committed.clone()),
1559 ),
1560 (
1561 "corrupt inactive",
1562 Some(committed.clone()),
1563 Some(corrupt.clone()),
1564 ),
1565 ("corrupt active", Some(corrupt), Some(committed.clone())),
1566 (
1567 "ambiguous",
1568 Some(committed),
1569 Some(CommittedGenerationBytes::new(0, vec![1])),
1570 ),
1571 (
1572 "unsupported format",
1573 Some(CommittedGenerationBytes::new(0, unsupported)),
1574 None,
1575 ),
1576 (
1577 "invalid CBOR",
1578 Some(CommittedGenerationBytes::new(
1579 0,
1580 LedgerPayloadEnvelope::current(vec![255])
1581 .try_encode()
1582 .unwrap(),
1583 )),
1584 None,
1585 ),
1586 (
1587 "generation mismatch",
1588 Some(CommittedGenerationBytes::new(1, genesis)),
1589 None,
1590 ),
1591 (
1592 "invalid committed integrity",
1593 Some(CommittedGenerationBytes::new(
1594 3,
1595 enveloped_payload(&ledger()),
1596 )),
1597 None,
1598 ),
1599 ];
1600
1601 for (label, slot0, slot1) in cases {
1602 let empty = slot0.is_none() && slot1.is_none();
1603 let store = LedgerCommitStore {
1604 physical: DualCommitStore { slot0, slot1 },
1605 };
1606 let before = store.clone();
1607 let (recovered, diagnostic) = store.recover_with_diagnostic();
1608 assert_eq!(recovered, store.recover(), "{label}");
1609 assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
1610 assert_eq!(store, before, "{label}");
1611
1612 let mut initializing = store.clone();
1613 let initialized = initializing.recover_or_initialize(&committed_ledger(0));
1614 if empty {
1615 let initialized = initialized.expect("empty store accepts genesis");
1616 assert_eq!(initialized.current_generation(), 0);
1617 assert_eq!(initialized.physical_generation(), 0);
1618 assert_eq!(initializing.recover().unwrap(), initialized);
1619 } else {
1620 assert_eq!(initialized, recovered, "{label}");
1621 assert_eq!(initializing, before, "{label}");
1622 }
1623
1624 if let Err(LedgerCommitError::Recovery(error)) = recovered {
1625 assert_eq!(diagnostic.recovery, Err(error), "{label}");
1626 } else {
1627 assert!(diagnostic.recovery.is_ok(), "{label}");
1628 }
1629 }
1630 }
1631
1632 #[test]
1633 fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
1634 let mut store = LedgerCommitStore::default();
1635 let genesis = committed_ledger(0);
1636
1637 let recovered = store
1638 .recover_or_initialize(&genesis)
1639 .expect("current-format genesis ledger");
1640 assert_eq!(recovered.current_generation(), 0);
1641 assert_eq!(recovered.physical_generation(), 0);
1642
1643 assert_eq!(recovered, store.recover().unwrap());
1644
1645 let first = recovered
1646 .ledger()
1647 .stage_validated_generation(&validated(
1648 0,
1649 vec![declaration("app.users.v1", 100, Some(1))],
1650 ))
1651 .expect("first real generation");
1652 let recovered = store.commit(&first).expect("first commit");
1653
1654 assert_eq!(recovered, store.recover().unwrap());
1655 assert_eq!(recovered.current_generation(), 1);
1656 assert_eq!(recovered.physical_generation(), 1);
1657 }
1658
1659 #[test]
1660 fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
1661 let mut store = LedgerCommitStore::default();
1662 let genesis = committed_ledger(0);
1663 store.commit(&genesis).expect("genesis commit");
1664 let first = genesis
1665 .stage_validated_generation(&validated(
1666 0,
1667 vec![declaration("app.users.v1", 100, Some(1))],
1668 ))
1669 .expect("first generation");
1670 let first = store.commit(&first).expect("first commit");
1671 let second = first
1672 .ledger()
1673 .stage_validated_generation(&validated(
1674 1,
1675 vec![declaration("app.users.v1", 100, Some(2))],
1676 ))
1677 .expect("second generation");
1678
1679 store.commit(&second).expect("second commit");
1680 store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
1681
1682 let err = store
1683 .recover()
1684 .expect_err("corrupt latest must not roll back");
1685
1686 assert_eq!(
1687 err,
1688 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
1689 slot0_invalid: true,
1690 slot1_invalid: false,
1691 })
1692 );
1693 }
1694
1695 #[test]
1696 fn ledger_commit_store_recovers_identical_duplicate_slots() {
1697 let ledger = committed_ledger(0)
1698 .stage_validated_generation(&validated(
1699 0,
1700 vec![declaration("app.users.v1", 100, Some(1))],
1701 ))
1702 .expect("first generation");
1703 let payload = enveloped_payload(&ledger);
1704 let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
1705 let store = LedgerCommitStore {
1706 physical: DualCommitStore {
1707 slot0: Some(committed.clone()),
1708 slot1: Some(committed),
1709 },
1710 };
1711
1712 let recovered = store.recover().expect("recovered");
1713
1714 assert_eq!(recovered.ledger(), &ledger);
1715 }
1716
1717 #[test]
1718 fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
1719 let mut store = LedgerCommitStore::default();
1720 let first = committed_ledger(1);
1721 let second = committed_ledger(2);
1722
1723 store.commit(&first).expect("first commit");
1724 store
1725 .write_corrupt_inactive_ledger(&second)
1726 .expect("corrupt write");
1727 let err = store.recover().expect_err("corrupt slot");
1728
1729 assert_eq!(
1730 err,
1731 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
1732 slot0_invalid: false,
1733 slot1_invalid: true,
1734 })
1735 );
1736 }
1737
1738 #[test]
1739 fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
1740 let store = LedgerCommitStore {
1741 physical: DualCommitStore {
1742 slot0: Some(CommittedGenerationBytes::new(
1743 7,
1744 enveloped_payload(&committed_ledger(6)),
1745 )),
1746 slot1: None,
1747 },
1748 };
1749 let err = store.recover().expect_err("mismatch");
1750
1751 assert_eq!(
1752 err,
1753 LedgerCommitError::PhysicalLogicalGenerationMismatch {
1754 physical_generation: 7,
1755 logical_generation: 6
1756 }
1757 );
1758 }
1759
1760 #[test]
1761 fn ledger_commit_store_rejects_non_next_logical_generation() {
1762 let mut store = LedgerCommitStore::default();
1763 store.commit(&committed_ledger(1)).expect("first commit");
1764
1765 let err = store
1766 .commit(&committed_ledger(3))
1767 .expect_err("skipped generation");
1768
1769 assert_eq!(
1770 err,
1771 LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
1772 expected: 2,
1773 actual: 3
1774 })
1775 );
1776 }
1777
1778 #[test]
1779 fn ledger_commit_store_initializes_empty_store_explicitly() {
1780 let mut store = LedgerCommitStore::default();
1781 let genesis = committed_ledger(3);
1782
1783 let recovered = store
1784 .recover_or_initialize(&genesis)
1785 .expect("initialized ledger");
1786
1787 assert_eq!(recovered.current_generation(), 3);
1788 assert_eq!(recovered.physical_generation(), 3);
1789 assert!(!store.physical().is_uninitialized());
1790 }
1791
1792 #[test]
1793 fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
1794 let mut store = LedgerCommitStore::default();
1795 store
1796 .write_corrupt_inactive_ledger(&ledger())
1797 .expect("corrupt write");
1798
1799 let err = store
1800 .recover_or_initialize(&ledger())
1801 .expect_err("corrupt state");
1802
1803 assert!(matches!(
1804 err,
1805 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
1806 slot0_invalid: true,
1807 slot1_invalid: false,
1808 })
1809 ));
1810 }
1811
1812 #[test]
1813 fn record_count_boundary_and_malicious_count_reject_on_recovery() {
1814 let mut ledger = committed_ledger(1);
1815 ledger.records = (0..255_u8)
1816 .map(|id| active_record(&format!("app.slot{id}.v1"), id))
1817 .collect();
1818 let mut store = LedgerCommitStore::default();
1819 store.commit(&ledger).unwrap();
1820 assert_eq!(store.recover().unwrap().ledger().records().len(), 255);
1821 ledger.records.push(active_record("app.extra.v1", 254));
1822 assert!(matches!(
1823 store.commit(&ledger),
1824 Err(LedgerCommitError::Integrity(
1825 LedgerIntegrityError::LimitExceeded { .. }
1826 ))
1827 ));
1828 let payload = enveloped_payload(&ledger);
1829 let mut invalid = LedgerCommitStore {
1830 physical: DualCommitStore {
1831 slot0: Some(CommittedGenerationBytes::new(1, payload)),
1832 slot1: None,
1833 },
1834 };
1835 assert!(invalid.recover().is_err());
1836 assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
1837 for bytes in [
1838 vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
1839 vec![0x81; 34],
1840 vec![0xa1],
1841 ] {
1842 let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
1843 let invalid = LedgerCommitStore {
1844 physical: DualCommitStore {
1845 slot0: Some(CommittedGenerationBytes::new(1, payload)),
1846 slot1: None,
1847 },
1848 };
1849 assert!(matches!(
1850 invalid.recover(),
1851 Err(LedgerCommitError::Codec(_))
1852 ));
1853 }
1854 }
1855
1856 #[test]
1857 fn oversized_logical_text_remains_opaque_to_record_decode_and_fails_closed() {
1858 for len in [257, 8192] {
1859 let mut malformed = serde_json::to_value(active_committed_ledger()).unwrap();
1860 malformed["records"][0]["stable_key"] = "x".repeat(len).into();
1861 let payload =
1862 LedgerPayloadEnvelope::current(crate::test_cbor::to_vec(&malformed).unwrap())
1863 .try_encode()
1864 .unwrap();
1865 let mut store = LedgerCommitStore::default();
1866 store
1867 .physical
1868 .commit_payload_at_generation(1, payload)
1869 .unwrap();
1870 let record = crate::StableCellLedgerRecord::new(store);
1871 let bytes = record.to_bytes();
1872 let mut decoded = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
1873 assert_eq!(decoded, record);
1874 let before = decoded.clone();
1875 assert!(matches!(
1876 decoded.store().recover(),
1877 Err(LedgerCommitError::Codec(_))
1878 ));
1879 assert!(matches!(
1880 decoded
1881 .store_mut()
1882 .recover_or_initialize(&AllocationLedger::empty_genesis()),
1883 Err(LedgerCommitError::Codec(_))
1884 ));
1885 assert_eq!(decoded, before);
1886 }
1887 }
1888}