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