Skip to main content

ic_memory/ledger/
mod.rs

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///
36/// LedgerCommitStore
37///
38/// Generation-scoped allocation ledger commit store.
39///
40/// This type owns the logical commit lifecycle and the crate-owned CBOR ledger
41/// encoding. It deliberately does not own stable-memory IO; that remains the
42/// substrate or framework owner's responsibility.
43///
44/// This store commits allocation ledger generations. It does not open
45/// stable-memory handles and does not allocate application slots.
46///
47#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
48#[serde(deny_unknown_fields)]
49pub struct LedgerCommitStore {
50    /// Protected physical commit slots.
51    physical: DualCommitStore,
52}
53
54impl LedgerCommitStore {
55    /// Borrow the protected physical commit store for diagnostics.
56    #[must_use]
57    pub const fn physical(&self) -> &DualCommitStore {
58        &self.physical
59    }
60
61    /// Recover the authoritative allocation ledger using the native CBOR ledger codec.
62    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    /// Recover the authoritative ledger, or explicitly initialize an empty store.
102    ///
103    /// Initialization is allowed only when no physical commit slot has ever
104    /// been written. Corrupt or partially written stores fail closed even when
105    /// a genesis ledger is supplied.
106    ///
107    /// Supplying a non-empty `genesis` is a privileged import/migration action.
108    /// Normal runtime bootstraps should seed an empty current-format ledger.
109    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            // Physical selection returns this error only when both slots are absent.
116            Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)) => {
117                self.commit(genesis)
118            }
119            Err(err) => Err(err),
120        }
121    }
122
123    /// Commit one logical allocation ledger generation through the native CBOR ledger codec.
124    ///
125    /// # Panics
126    ///
127    /// Panics only if the private concrete ledger-encoding invariant is broken.
128    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        // The current writer encoded this integrity-checked ledger, and the
141        // physical commit checked its predecessor and established this generation.
142        // Existing persisted bytes still cross the full recovery boundary.
143        Ok(RecoveredLedger::from_trusted_ledger(ledger.clone()))
144    }
145
146    /// Simulate corruption of a logical ledger payload in the inactive slot.
147    #[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        // A proof minted against another valid history at the same generation
782        // cannot override this receiver's allocation facts.
783        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        // Duplicate keys take precedence over duplicate slots and the missing
1581        // current-generation record, even with only one integrity pass.
1582        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}