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 stage_retirement_generation_rejects_invalid_decoded_stable_key() {
1237        let mut ledger = ledger();
1238        ledger.allocation_history.records = vec![active_record("app.users.v1", 100)];
1239        let retirement = AllocationRetirement::new(
1240            "app.users.v1",
1241            MemoryManagerSlot::new(100).expect("usable slot"),
1242        )
1243        .expect("retirement");
1244        let mut value = crate::test_cbor::to_value(retirement).expect("retirement value");
1245        *map_field_mut(value_map_mut(&mut value), "stable_key") =
1246            crate::test_cbor::Value::Text("App.users.v1".to_string());
1247        let bytes = crate::test_cbor::to_vec(&value).expect("retirement bytes");
1248        let retirement: AllocationRetirement =
1249            crate::test_cbor::from_slice(&bytes).expect("decoded DTO");
1250
1251        let err = ledger
1252            .stage_retirement_generation(&retirement, None)
1253            .expect_err("decoded invalid stable key must fail at the boundary");
1254
1255        assert!(matches!(err, AllocationRetirementError::Key(_)));
1256    }
1257
1258    #[test]
1259    fn snapshot_can_feed_validated_generation() {
1260        let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1261            .expect("snapshot");
1262        let (declarations, runtime_fingerprint) = snapshot.into_parts();
1263        let validated =
1264            crate::capability::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1265
1266        let staged = ledger()
1267            .stage_validated_generation(&validated, None)
1268            .expect("validated generation");
1269
1270        assert_eq!(staged.allocation_history.records().len(), 1);
1271    }
1272
1273    #[test]
1274    fn stage_validated_generation_records_runtime_fingerprint() {
1275        let validated = crate::capability::ValidatedAllocations::new(
1276            3,
1277            vec![declaration("app.users.v1", 100, None)],
1278            Some("wasm:abc123".to_string()),
1279        );
1280
1281        let staged = ledger()
1282            .stage_validated_generation(&validated, None)
1283            .expect("validated generation");
1284
1285        assert_eq!(
1286            staged.allocation_history.generations()[0].runtime_fingerprint,
1287            Some("wasm:abc123".to_string())
1288        );
1289    }
1290
1291    #[test]
1292    fn strict_committed_integrity_accepts_full_lifecycle() {
1293        let mut ledger = committed_ledger(0);
1294        ledger
1295            .validate_committed_integrity()
1296            .expect("genesis ledger with no history");
1297
1298        ledger = ledger
1299            .stage_validated_generation(
1300                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1301                Some(1),
1302            )
1303            .expect("first real commit after genesis");
1304        ledger
1305            .validate_committed_integrity()
1306            .expect("first real commit");
1307
1308        ledger = ledger
1309            .stage_validated_generation(
1310                &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1311                Some(2),
1312            )
1313            .expect("repeated active declaration");
1314        ledger
1315            .validate_committed_integrity()
1316            .expect("repeated active declaration");
1317        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1318
1319        ledger = ledger
1320            .stage_validated_generation(
1321                &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1322                Some(3),
1323            )
1324            .expect("schema drift");
1325        ledger
1326            .validate_committed_integrity()
1327            .expect("schema metadata drift");
1328        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1329
1330        ledger = ledger
1331            .stage_reservation_generation(
1332                &[declaration("app.future_store.v1", 101, Some(1))],
1333                Some(4),
1334            )
1335            .expect("reservation-only generation");
1336        ledger
1337            .validate_committed_integrity()
1338            .expect("reservation-only generation");
1339        assert_eq!(
1340            record(&ledger, "app.future_store.v1").state,
1341            AllocationState::Reserved
1342        );
1343
1344        ledger = ledger
1345            .stage_validated_generation(
1346                &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1347                Some(5),
1348            )
1349            .expect("reservation activation");
1350        ledger
1351            .validate_committed_integrity()
1352            .expect("reservation activation");
1353        assert_eq!(
1354            record(&ledger, "app.future_store.v1").state,
1355            AllocationState::Active
1356        );
1357
1358        let retirement = AllocationRetirement::new(
1359            "app.users.v1",
1360            MemoryManagerSlot::new(100).expect("usable slot"),
1361        )
1362        .expect("retirement");
1363        ledger = ledger
1364            .stage_retirement_generation(&retirement, Some(6))
1365            .expect("retirement generation");
1366        ledger
1367            .validate_committed_integrity()
1368            .expect("retirement generation");
1369        assert_eq!(ledger.current_generation, 6);
1370        assert_eq!(
1371            record(&ledger, "app.users.v1").state,
1372            AllocationState::Retired { generation: 6 }
1373        );
1374        assert_eq!(
1375            record(&ledger, "app.future_store.v1").last_seen_generation,
1376            5
1377        );
1378    }
1379
1380    #[derive(Clone, Copy, Debug)]
1381    enum Transition {
1382        DeclareUsers,
1383        DeclareOrders,
1384        ReserveAudit,
1385        ActivateAudit,
1386        RetireUsers,
1387        RetireAudit,
1388        EmptyValidated,
1389        EmptyReservation,
1390    }
1391
1392    const TRANSITIONS: [Transition; 8] = [
1393        Transition::DeclareUsers,
1394        Transition::DeclareOrders,
1395        Transition::ReserveAudit,
1396        Transition::ActivateAudit,
1397        Transition::RetireUsers,
1398        Transition::RetireAudit,
1399        Transition::EmptyValidated,
1400        Transition::EmptyReservation,
1401    ];
1402
1403    fn apply_transition(
1404        ledger: &AllocationLedger,
1405        transition: Transition,
1406        committed_at: u64,
1407    ) -> Result<AllocationLedger, String> {
1408        match transition {
1409            Transition::DeclareUsers => ledger
1410                .stage_validated_generation(
1411                    &validated(
1412                        ledger.current_generation,
1413                        vec![declaration("app.users.v1", 100, Some(1))],
1414                    ),
1415                    Some(committed_at),
1416                )
1417                .map_err(|err| err.to_string()),
1418            Transition::DeclareOrders => ledger
1419                .stage_validated_generation(
1420                    &validated(
1421                        ledger.current_generation,
1422                        vec![declaration("app.orders.v1", 101, Some(1))],
1423                    ),
1424                    Some(committed_at),
1425                )
1426                .map_err(|err| err.to_string()),
1427            Transition::ReserveAudit => ledger
1428                .stage_reservation_generation(
1429                    &[declaration("app.audit.v1", 102, Some(1))],
1430                    Some(committed_at),
1431                )
1432                .map_err(|err| err.to_string()),
1433            Transition::ActivateAudit => ledger
1434                .stage_validated_generation(
1435                    &validated(
1436                        ledger.current_generation,
1437                        vec![declaration("app.audit.v1", 102, Some(1))],
1438                    ),
1439                    Some(committed_at),
1440                )
1441                .map_err(|err| err.to_string()),
1442            Transition::RetireUsers => ledger
1443                .stage_retirement_generation(
1444                    &AllocationRetirement::new(
1445                        "app.users.v1",
1446                        MemoryManagerSlot::new(100).expect("usable slot"),
1447                    )
1448                    .expect("retirement"),
1449                    Some(committed_at),
1450                )
1451                .map_err(|err| err.to_string()),
1452            Transition::RetireAudit => ledger
1453                .stage_retirement_generation(
1454                    &AllocationRetirement::new(
1455                        "app.audit.v1",
1456                        MemoryManagerSlot::new(102).expect("usable slot"),
1457                    )
1458                    .expect("retirement"),
1459                    Some(committed_at),
1460                )
1461                .map_err(|err| err.to_string()),
1462            Transition::EmptyValidated => ledger
1463                .stage_validated_generation(
1464                    &validated(ledger.current_generation, Vec::new()),
1465                    Some(committed_at),
1466                )
1467                .map_err(|err| err.to_string()),
1468            Transition::EmptyReservation => ledger
1469                .stage_reservation_generation(&[], Some(committed_at))
1470                .map_err(|err| err.to_string()),
1471        }
1472    }
1473
1474    fn check_transition_sequences(
1475        ledger: AllocationLedger,
1476        depth: usize,
1477        sequence: &mut Vec<Transition>,
1478    ) {
1479        ledger
1480            .validate_committed_integrity()
1481            .unwrap_or_else(|err| panic!("sequence {sequence:?} violated integrity: {err}"));
1482        if depth == 0 {
1483            return;
1484        }
1485
1486        for transition in TRANSITIONS {
1487            sequence.push(transition);
1488            let next = apply_transition(&ledger, transition, sequence.len() as u64)
1489                .unwrap_or_else(|_| ledger.clone());
1490            check_transition_sequences(next, depth - 1, sequence);
1491            sequence.pop();
1492        }
1493    }
1494
1495    #[test]
1496    fn transition_matrix_preserves_committed_invariants() {
1497        check_transition_sequences(committed_ledger(0), 4, &mut Vec::new());
1498    }
1499
1500    #[test]
1501    fn new_committed_requires_strict_generation_history() {
1502        let structurally_valid =
1503            AllocationLedger::new(3, AllocationHistory::default()).expect("structurally valid DTO");
1504
1505        assert_eq!(structurally_valid.current_generation, 3);
1506
1507        let err = AllocationLedger::new_committed(3, AllocationHistory::default())
1508            .expect_err("committed ledger needs generation history");
1509
1510        assert_eq!(
1511            err,
1512            LedgerIntegrityError::MissingCurrentGenerationRecord {
1513                current_generation: 3
1514            }
1515        );
1516    }
1517
1518    #[test]
1519    fn new_committed_preserves_structural_rejection_before_history_checks() {
1520        let history = AllocationHistory::from_parts(
1521            vec![
1522                active_record("app.users.v1", 100),
1523                active_record("app.users.v1", 100),
1524            ],
1525            Vec::new(),
1526        );
1527
1528        // Duplicate keys take precedence over duplicate slots and the missing
1529        // current-generation record, even with only one integrity pass.
1530        assert_eq!(
1531            AllocationLedger::new_committed(3, history).expect_err("duplicate records"),
1532            LedgerIntegrityError::DuplicateStableKey {
1533                stable_key: StableKey::parse("app.users.v1").unwrap(),
1534            }
1535        );
1536
1537        let valid = active_committed_ledger();
1538        assert_eq!(
1539            AllocationLedger::new_committed(
1540                valid.current_generation(),
1541                valid.allocation_history().clone(),
1542            )
1543            .expect("valid committed history"),
1544            valid
1545        );
1546    }
1547
1548    #[test]
1549    fn validate_integrity_rejects_duplicate_stable_keys() {
1550        let mut ledger = ledger();
1551        for second_id in [101, 100] {
1552            ledger.allocation_history.records = vec![
1553                active_record("app.users.v1", 100),
1554                active_record("app.users.v1", second_id),
1555            ];
1556
1557            let err = ledger
1558                .validate_integrity()
1559                .expect_err("duplicate key precedes duplicate slot");
1560
1561            assert_eq!(
1562                err,
1563                LedgerIntegrityError::DuplicateStableKey {
1564                    stable_key: StableKey::parse("app.users.v1").unwrap(),
1565                }
1566            );
1567        }
1568    }
1569
1570    #[test]
1571    fn validate_integrity_rejects_duplicate_slots() {
1572        let mut ledger = ledger();
1573        ledger.allocation_history.records = vec![
1574            active_record("app.users.v1", 100),
1575            active_record("app.orders.v1", 100),
1576        ];
1577
1578        let err = ledger.validate_integrity().expect_err("duplicate slot");
1579
1580        assert_eq!(
1581            err,
1582            LedgerIntegrityError::DuplicateSlot {
1583                slot: MemoryManagerSlot::new(100).unwrap(),
1584            }
1585        );
1586
1587        ledger.allocation_history.records[1].stable_key =
1588            serde_json::from_str("\"App.orders.v1\"").expect("decoded unvalidated key");
1589        assert_eq!(
1590            ledger.validate_integrity().unwrap_err(),
1591            LedgerIntegrityError::DuplicateSlot {
1592                slot: MemoryManagerSlot::new(100).unwrap(),
1593            },
1594            "duplicate slot precedes the second record's key validation"
1595        );
1596    }
1597
1598    #[test]
1599    fn validate_committed_integrity_rejects_decoded_invalid_stable_key() {
1600        let mut ledger = committed_ledger(1);
1601        ledger
1602            .allocation_history
1603            .records
1604            .push(active_record("app.users.v1", 100));
1605        let mut bytes = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
1606        let key_start = bytes
1607            .windows(b"app.users.v1".len())
1608            .position(|window| window == b"app.users.v1")
1609            .expect("encoded stable key");
1610        bytes[key_start] = b'A';
1611        let decoded: AllocationLedger =
1612            crate::test_cbor::from_slice(&bytes).expect("decode ledger");
1613
1614        let err = decoded
1615            .validate_committed_integrity()
1616            .expect_err("invalid decoded key must fail");
1617
1618        assert!(matches!(err, LedgerIntegrityError::InvalidStableKey(_)));
1619    }
1620
1621    #[test]
1622    fn ledger_decode_rejects_unusable_memory_manager_slot() {
1623        let mut ledger = committed_ledger(1);
1624        ledger
1625            .allocation_history
1626            .records
1627            .push(active_record("app.users.v1", 100));
1628        let mut value = serde_json::to_value(ledger).unwrap();
1629        value["allocation_history"]["records"][0]["slot"]["slot"]["MemoryManagerId"] =
1630            serde_json::json!(255);
1631        let bytes = crate::test_cbor::to_vec(&value).unwrap();
1632        assert!(decode_ledger(&bytes).is_err());
1633    }
1634
1635    #[test]
1636    fn validate_integrity_rejects_retirement_before_last_observation() {
1637        let mut ledger = committed_ledger(5);
1638        let mut record = active_record("app.users.v1", 100);
1639        record.last_seen_generation = 4;
1640        record.state = AllocationState::Retired { generation: 3 };
1641        ledger.allocation_history.records = vec![record];
1642
1643        let err = ledger
1644            .validate_committed_integrity()
1645            .expect_err("retirement must follow the last observation");
1646
1647        assert!(matches!(
1648            err,
1649            LedgerIntegrityError::RetirementNotAfterLastSeen { .. }
1650        ));
1651    }
1652
1653    #[test]
1654    fn validate_integrity_rejects_retirement_before_allocation() {
1655        let mut ledger = committed_ledger(2);
1656        let mut record = active_record("app.users.v1", 100);
1657        record.state = AllocationState::Retired { generation: 0 };
1658        ledger.allocation_history.records = vec![record];
1659
1660        let err = ledger
1661            .validate_committed_integrity()
1662            .expect_err("retirement cannot predate allocation");
1663
1664        assert!(matches!(
1665            err,
1666            LedgerIntegrityError::RetiredBeforeFirstGeneration { .. }
1667        ));
1668    }
1669
1670    #[test]
1671    fn validate_integrity_requires_schema_history_at_first_generation() {
1672        let mut ledger = committed_ledger(2);
1673        let mut record = active_record("app.users.v1", 100);
1674        record.last_seen_generation = 2;
1675        record.schema_history[0].generation = 2;
1676        ledger.allocation_history.records = vec![record];
1677
1678        let err = ledger
1679            .validate_committed_integrity()
1680            .expect_err("schema history must begin with the allocation");
1681
1682        assert!(matches!(
1683            err,
1684            LedgerIntegrityError::SchemaHistoryStartMismatch { .. }
1685        ));
1686    }
1687
1688    #[test]
1689    fn validate_integrity_rejects_schema_history_after_last_observation() {
1690        for generation in [3, 4] {
1691            let mut ledger = committed_ledger(3);
1692            let mut record = active_record("app.users.v1", 100);
1693            record.last_seen_generation = 2;
1694            record.schema_history.push(
1695                SchemaMetadataRecord::new(
1696                    generation,
1697                    SchemaMetadata::new(Some(2)).expect("schema"),
1698                )
1699                .expect("schema record"),
1700            );
1701            ledger.allocation_history.records = vec![record];
1702
1703            let err = ledger
1704                .validate_committed_integrity()
1705                .expect_err("schema metadata cannot postdate the last observation");
1706
1707            assert!(matches!(
1708                (generation, err),
1709                (
1710                    3,
1711                    LedgerIntegrityError::SchemaHistoryAfterLastSeen {
1712                        generation: 3,
1713                        last_seen_generation: 2,
1714                        ..
1715                    }
1716                ) | (
1717                    4,
1718                    LedgerIntegrityError::SchemaHistoryOutOfBounds { generation: 4, .. }
1719                )
1720            ));
1721        }
1722    }
1723
1724    #[test]
1725    fn validate_integrity_rejects_non_increasing_schema_history() {
1726        for generation in [0, 1] {
1727            let mut ledger = ledger();
1728            let mut record = active_record("app.users.v1", 100);
1729            record.schema_history.push(SchemaMetadataRecord {
1730                generation,
1731                schema: SchemaMetadata::default(),
1732            });
1733            ledger.allocation_history.records = vec![record];
1734
1735            let err = ledger
1736                .validate_integrity()
1737                .expect_err("non-increasing schema history");
1738
1739            assert!(matches!(
1740                err,
1741                LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1742            ));
1743        }
1744    }
1745
1746    #[test]
1747    fn validate_integrity_rejects_invalid_schema_metadata_history() {
1748        let mut ledger = committed_ledger(1);
1749        let mut record = active_record("app.users.v1", 100);
1750        record.schema_history[0].schema = invalid_schema_metadata();
1751        ledger.allocation_history.records = vec![record];
1752
1753        let err = ledger
1754            .validate_committed_integrity()
1755            .expect_err("invalid committed schema metadata");
1756
1757        assert_eq!(
1758            err,
1759            LedgerIntegrityError::InvalidSchemaMetadata {
1760                stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1761                generation: 1,
1762                error: SchemaMetadataError::InvalidVersion,
1763            }
1764        );
1765    }
1766
1767    #[test]
1768    fn validate_committed_integrity_requires_current_generation_record() {
1769        let err = ledger()
1770            .validate_committed_integrity()
1771            .expect_err("missing current generation");
1772
1773        assert_eq!(
1774            err,
1775            LedgerIntegrityError::MissingCurrentGenerationRecord {
1776                current_generation: 3
1777            }
1778        );
1779    }
1780
1781    #[test]
1782    fn committed_integrity_rejects_allocation_references_to_genesis() {
1783        for generation in [0, 1, 2] {
1784            let mut ledger = committed_ledger(generation);
1785            let mut record = AllocationRecord::active(0, &declaration("app.genesis.v1", 100, None));
1786            if generation == 2 {
1787                record.last_seen_generation = 1;
1788                record
1789                    .schema_history
1790                    .push(SchemaMetadataRecord::new(1, SchemaMetadata::default()).unwrap());
1791                record.state = AllocationState::Retired { generation: 2 };
1792            }
1793            ledger.allocation_history.records.push(record);
1794            ledger.validate_integrity().expect("structurally valid DTO");
1795            assert_eq!(
1796                ledger.validate_committed_integrity(),
1797                Err(LedgerIntegrityError::UnknownRecordGeneration {
1798                    stable_key: StableKey::parse("app.genesis.v1").unwrap(),
1799                    generation: 0,
1800                })
1801            );
1802        }
1803    }
1804
1805    #[test]
1806    fn validate_committed_integrity_rejects_generation_history_gaps() {
1807        let mut ledger = committed_ledger(3);
1808        ledger.allocation_history.generations.remove(1);
1809        ledger
1810            .allocation_history
1811            .records
1812            .push(AllocationRecord::active(
1813                0,
1814                &declaration("app.genesis.v1", 100, None),
1815            ));
1816
1817        let err = ledger
1818            .validate_committed_integrity()
1819            .expect_err("generation history gap takes precedence over genesis reference");
1820
1821        assert!(matches!(
1822            err,
1823            LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1824        ));
1825    }
1826
1827    #[test]
1828    fn committed_integrity_rejects_a_skipped_parent_in_contiguous_history() {
1829        let mut ledger = committed_ledger(3);
1830        ledger.allocation_history.generations[2].parent_generation = 1;
1831        ledger
1832            .validate_integrity()
1833            .expect("structurally valid history");
1834
1835        assert_eq!(
1836            ledger.validate_committed_integrity(),
1837            Err(LedgerIntegrityError::BrokenGenerationChain {
1838                generation: 3,
1839                expected_parent: 2,
1840                actual_parent: 1,
1841            })
1842        );
1843    }
1844
1845    #[test]
1846    fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1847        let mut store = LedgerCommitStore::default();
1848        let mut invalid = ledger();
1849        invalid.allocation_history.records = vec![
1850            active_record("app.users.v1", 100),
1851            active_record("app.orders.v1", 100),
1852        ];
1853
1854        let err = store.commit(&invalid).expect_err("invalid ledger");
1855
1856        assert!(matches!(
1857            err,
1858            LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1859        ));
1860        assert!(store.physical().is_uninitialized());
1861    }
1862
1863    #[test]
1864    fn ledger_commit_store_recovers_latest_committed_ledger() {
1865        let mut store = LedgerCommitStore::default();
1866        let first = committed_ledger(1);
1867        let second = committed_ledger(2);
1868
1869        store.commit(&first).expect("first commit");
1870        store.commit(&second).expect("second commit");
1871        let recovered = store.recover().expect("recovered ledger");
1872
1873        assert_eq!(recovered.current_generation(), 2);
1874        assert_eq!(recovered.physical_generation(), 2);
1875    }
1876
1877    #[test]
1878    fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1879        let mut store = LedgerCommitStore::default();
1880        let ledger = committed_ledger(1);
1881
1882        store.commit(&ledger).expect("commit");
1883
1884        let committed = store.physical().authoritative().expect("authoritative");
1885        assert_eq!(
1886            committed.payload(),
1887            LedgerPayloadEnvelope::current(crate::test_cbor::to_vec(&ledger).unwrap())
1888                .try_encode()
1889                .unwrap(),
1890            "direct commit encoding preserves the complete public envelope bytes"
1891        );
1892        let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1893        assert_eq!(
1894            envelope.payload(),
1895            crate::test_cbor::to_vec(&ledger).expect("encode ledger")
1896        );
1897    }
1898
1899    #[test]
1900    fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1901        let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1902        let store = LedgerCommitStore {
1903            physical: DualCommitStore {
1904                slot0: Some(committed),
1905                slot1: None,
1906            },
1907        };
1908
1909        let err = store.recover().expect_err("bad envelope");
1910
1911        assert!(matches!(
1912            err,
1913            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1914        ));
1915    }
1916
1917    #[test]
1918    fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1919        let mut payload = enveloped_payload(&committed_ledger(1));
1920        payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1921        let store = LedgerCommitStore {
1922            physical: DualCommitStore {
1923                slot0: Some(CommittedGenerationBytes::new(1, payload)),
1924                slot1: None,
1925            },
1926        };
1927
1928        let err = store
1929            .recover()
1930            .expect_err("unknown ledger format version must fail");
1931
1932        assert_eq!(
1933            err,
1934            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1935                marker: *b"ICMF",
1936                version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1937            })
1938        );
1939    }
1940
1941    #[test]
1942    fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
1943        let genesis = enveloped_payload(&committed_ledger(0));
1944        let committed = CommittedGenerationBytes::new(0, genesis.clone());
1945        let mut corrupt = committed.clone();
1946        corrupt.checksum ^= 1;
1947        let mut unsupported = genesis.clone();
1948        unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1949        let cases = [
1950            ("empty", None, None),
1951            ("one slot", Some(committed.clone()), None),
1952            (
1953                "identical slots",
1954                Some(committed.clone()),
1955                Some(committed.clone()),
1956            ),
1957            (
1958                "corrupt inactive",
1959                Some(committed.clone()),
1960                Some(corrupt.clone()),
1961            ),
1962            ("corrupt active", Some(corrupt), Some(committed.clone())),
1963            (
1964                "ambiguous",
1965                Some(committed),
1966                Some(CommittedGenerationBytes::new(0, vec![1])),
1967            ),
1968            (
1969                "unsupported format",
1970                Some(CommittedGenerationBytes::new(0, unsupported)),
1971                None,
1972            ),
1973            (
1974                "invalid CBOR",
1975                Some(CommittedGenerationBytes::new(
1976                    0,
1977                    LedgerPayloadEnvelope::current(vec![255])
1978                        .try_encode()
1979                        .unwrap(),
1980                )),
1981                None,
1982            ),
1983            (
1984                "generation mismatch",
1985                Some(CommittedGenerationBytes::new(1, genesis)),
1986                None,
1987            ),
1988            (
1989                "invalid committed integrity",
1990                Some(CommittedGenerationBytes::new(
1991                    3,
1992                    enveloped_payload(&ledger()),
1993                )),
1994                None,
1995            ),
1996        ];
1997
1998        for (label, slot0, slot1) in cases {
1999            let empty = slot0.is_none() && slot1.is_none();
2000            let store = LedgerCommitStore {
2001                physical: DualCommitStore { slot0, slot1 },
2002            };
2003            let before = store.clone();
2004            let (recovered, diagnostic) = store.recover_with_diagnostic();
2005            assert_eq!(recovered, store.recover(), "{label}");
2006            assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
2007            assert_eq!(store, before, "{label}");
2008
2009            let mut initializing = store.clone();
2010            let initialized = initializing.recover_or_initialize(&committed_ledger(0));
2011            if empty {
2012                let initialized = initialized.expect("empty store accepts genesis");
2013                assert_eq!(initialized.current_generation(), 0);
2014                assert_eq!(initialized.physical_generation(), 0);
2015                assert_eq!(initializing.recover().unwrap(), initialized);
2016            } else {
2017                assert_eq!(initialized, recovered, "{label}");
2018                assert_eq!(initializing, before, "{label}");
2019            }
2020
2021            if let Err(LedgerCommitError::Recovery(error)) = recovered {
2022                assert_eq!(diagnostic.recovery, Err(error), "{label}");
2023            } else {
2024                assert!(diagnostic.recovery.is_ok(), "{label}");
2025            }
2026        }
2027    }
2028
2029    #[test]
2030    fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
2031        let mut store = LedgerCommitStore::default();
2032        let genesis = committed_ledger(0);
2033
2034        let recovered = store
2035            .recover_or_initialize(&genesis)
2036            .expect("current-format genesis ledger");
2037        assert_eq!(recovered.current_generation(), 0);
2038        assert_eq!(recovered.physical_generation(), 0);
2039        assert_eq!(recovered.ledger().allocation_history().generations(), []);
2040        assert_eq!(recovered, store.recover().unwrap());
2041
2042        let first = recovered
2043            .ledger()
2044            .stage_validated_generation(
2045                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2046                None,
2047            )
2048            .expect("first real generation");
2049        let recovered = store.commit(&first).expect("first commit");
2050
2051        assert_eq!(recovered, store.recover().unwrap());
2052        assert_eq!(recovered.current_generation(), 1);
2053        assert_eq!(recovered.physical_generation(), 1);
2054        assert_eq!(
2055            recovered.ledger().allocation_history().generations()[0].generation,
2056            1
2057        );
2058        assert_eq!(
2059            record(recovered.ledger(), "app.users.v1").first_generation,
2060            1
2061        );
2062    }
2063
2064    #[test]
2065    fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
2066        let mut store = LedgerCommitStore::default();
2067        let genesis = committed_ledger(0);
2068        store.commit(&genesis).expect("genesis commit");
2069        let first = genesis
2070            .stage_validated_generation(
2071                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2072                None,
2073            )
2074            .expect("first generation");
2075        let first = store.commit(&first).expect("first commit");
2076        let second = first
2077            .ledger()
2078            .stage_validated_generation(
2079                &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
2080                None,
2081            )
2082            .expect("second generation");
2083
2084        store.commit(&second).expect("second commit");
2085        store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
2086
2087        let err = store
2088            .recover()
2089            .expect_err("corrupt latest must not roll back");
2090
2091        assert_eq!(
2092            err,
2093            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2094                slot0_invalid: true,
2095                slot1_invalid: false,
2096            })
2097        );
2098    }
2099
2100    #[test]
2101    fn ledger_commit_store_recovers_identical_duplicate_slots() {
2102        let ledger = committed_ledger(0)
2103            .stage_validated_generation(
2104                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2105                None,
2106            )
2107            .expect("first generation");
2108        let payload = enveloped_payload(&ledger);
2109        let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
2110        let store = LedgerCommitStore {
2111            physical: DualCommitStore {
2112                slot0: Some(committed.clone()),
2113                slot1: Some(committed),
2114            },
2115        };
2116
2117        let recovered = store.recover().expect("recovered");
2118
2119        assert_eq!(recovered.ledger(), &ledger);
2120    }
2121
2122    #[test]
2123    fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
2124        let mut store = LedgerCommitStore::default();
2125        let first = committed_ledger(1);
2126        let second = committed_ledger(2);
2127
2128        store.commit(&first).expect("first commit");
2129        store
2130            .write_corrupt_inactive_ledger(&second)
2131            .expect("corrupt write");
2132        let err = store.recover().expect_err("corrupt slot");
2133
2134        assert_eq!(
2135            err,
2136            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2137                slot0_invalid: false,
2138                slot1_invalid: true,
2139            })
2140        );
2141    }
2142
2143    #[test]
2144    fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
2145        let store = LedgerCommitStore {
2146            physical: DualCommitStore {
2147                slot0: Some(CommittedGenerationBytes::new(
2148                    7,
2149                    enveloped_payload(&committed_ledger(6)),
2150                )),
2151                slot1: None,
2152            },
2153        };
2154        let err = store.recover().expect_err("mismatch");
2155
2156        assert_eq!(
2157            err,
2158            LedgerCommitError::PhysicalLogicalGenerationMismatch {
2159                physical_generation: 7,
2160                logical_generation: 6
2161            }
2162        );
2163    }
2164
2165    #[test]
2166    fn ledger_commit_store_rejects_non_next_logical_generation() {
2167        let mut store = LedgerCommitStore::default();
2168        store.commit(&committed_ledger(1)).expect("first commit");
2169
2170        let err = store
2171            .commit(&committed_ledger(3))
2172            .expect_err("skipped generation");
2173
2174        assert_eq!(
2175            err,
2176            LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2177                expected: 2,
2178                actual: 3
2179            })
2180        );
2181    }
2182
2183    #[test]
2184    fn ledger_commit_store_initializes_empty_store_explicitly() {
2185        let mut store = LedgerCommitStore::default();
2186        let genesis = committed_ledger(3);
2187
2188        let recovered = store
2189            .recover_or_initialize(&genesis)
2190            .expect("initialized ledger");
2191
2192        assert_eq!(recovered.current_generation(), 3);
2193        assert_eq!(recovered.physical_generation(), 3);
2194        assert!(!store.physical().is_uninitialized());
2195    }
2196
2197    #[test]
2198    fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2199        let mut store = LedgerCommitStore::default();
2200        store
2201            .write_corrupt_inactive_ledger(&ledger())
2202            .expect("corrupt write");
2203
2204        let err = store
2205            .recover_or_initialize(&ledger())
2206            .expect_err("corrupt state");
2207
2208        assert!(matches!(
2209            err,
2210            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2211                slot0_invalid: true,
2212                slot1_invalid: false,
2213            })
2214        ));
2215    }
2216    #[test]
2217    fn history_boundary_round_trips_and_rejects_next_generation_without_mutation() {
2218        let mut source = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 - 1);
2219        source
2220            .allocation_history
2221            .records
2222            .push(active_record("app.users.v1", 100));
2223        let retirement =
2224            AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
2225                .unwrap();
2226        let retired = source
2227            .stage_retirement_generation(&retirement, None)
2228            .unwrap();
2229        retired.validate_committed_integrity().unwrap();
2230        assert_eq!(
2231            retired.allocation_history.generations().len(),
2232            crate::constants::MAX_LEDGER_GENERATIONS
2233        );
2234        assert_eq!(
2235            retired.allocation_history.records()[0].schema_history,
2236            source.allocation_history.records()[0].schema_history
2237        );
2238        assert_eq!(
2239            source.allocation_history.records()[0].state,
2240            AllocationState::Active
2241        );
2242        assert!(matches!(
2243            retired.stage_retirement_generation(&retirement, None),
2244            Err(AllocationRetirementError::Integrity(
2245                LedgerIntegrityError::LimitExceeded {
2246                    resource: "generation history",
2247                    ..
2248                }
2249            ))
2250        ));
2251
2252        let ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2253        let mut store = LedgerCommitStore::default();
2254        let recovered = store.commit(&ledger).expect("boundary is admissible");
2255        let record = crate::StableCellLedgerRecord::new(store.clone());
2256        let bytes = crate::test_cbor::to_vec(&record).unwrap();
2257        assert_eq!(record.encoded_size(), bytes.len());
2258        let recovered_record = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2259        assert_eq!(
2260            recovered_record.store().recover().unwrap().ledger(),
2261            &ledger
2262        );
2263        let before = store.clone();
2264        let err = crate::AllocationBootstrap::new(&mut store)
2265            .validate_and_commit(
2266                DeclarationSnapshot::new(Vec::new()).unwrap(),
2267                &crate::GenericRangePolicy,
2268                None,
2269            )
2270            .unwrap_err();
2271        assert!(matches!(
2272            err,
2273            crate::BootstrapError::Staging(AllocationStageError::Integrity(
2274                LedgerIntegrityError::LimitExceeded { .. }
2275            ))
2276        ));
2277        assert_eq!(store, before);
2278        assert_eq!(
2279            recovered.current_generation(),
2280            crate::constants::MAX_LEDGER_GENERATIONS as u64
2281        );
2282        let oversize = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 + 1);
2283        assert!(matches!(
2284            store.commit(&oversize),
2285            Err(LedgerCommitError::Integrity(
2286                LedgerIntegrityError::LimitExceeded { .. }
2287            ))
2288        ));
2289        assert_eq!(store, before);
2290    }
2291
2292    #[test]
2293    fn record_count_boundary_and_malicious_history_reject_on_recovery() {
2294        let mut ledger = committed_ledger(1);
2295        ledger.allocation_history.records = (0..255_u8)
2296            .map(|id| active_record(&format!("app.slot{id}.v1"), id))
2297            .collect();
2298        let mut store = LedgerCommitStore::default();
2299        store.commit(&ledger).unwrap();
2300        assert_eq!(
2301            store
2302                .recover()
2303                .unwrap()
2304                .ledger()
2305                .allocation_history()
2306                .records()
2307                .len(),
2308            255
2309        );
2310        ledger
2311            .allocation_history
2312            .records
2313            .push(active_record("app.extra.v1", 254));
2314        assert!(matches!(
2315            store.commit(&ledger),
2316            Err(LedgerCommitError::Integrity(
2317                LedgerIntegrityError::LimitExceeded { .. }
2318            ))
2319        ));
2320        let payload = enveloped_payload(&ledger);
2321        let mut invalid = LedgerCommitStore {
2322            physical: DualCommitStore {
2323                slot0: Some(CommittedGenerationBytes::new(1, payload)),
2324                slot1: None,
2325            },
2326        };
2327        assert!(invalid.recover().is_err());
2328        assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
2329        for bytes in [
2330            vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2331            vec![0x81; 34],
2332            vec![0xa1],
2333        ] {
2334            let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
2335            let invalid = LedgerCommitStore {
2336                physical: DualCommitStore {
2337                    slot0: Some(CommittedGenerationBytes::new(1, payload)),
2338                    slot1: None,
2339                },
2340            };
2341            assert!(matches!(
2342                invalid.recover(),
2343                Err(LedgerCommitError::Codec(_))
2344            ));
2345        }
2346    }
2347    #[test]
2348    fn encoded_byte_limit_rejects_before_commit_mutation() {
2349        let mut store = LedgerCommitStore::default();
2350        store.commit(&committed_ledger(0)).unwrap();
2351        let before = store.clone();
2352        let mut ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2353        for generation in &mut ledger.allocation_history.generations {
2354            generation.runtime_fingerprint =
2355                Some("x".repeat(crate::constants::DIAGNOSTIC_STRING_MAX_BYTES));
2356        }
2357        assert!(matches!(
2358            store.commit(&ledger),
2359            Err(LedgerCommitError::Integrity(
2360                LedgerIntegrityError::LimitExceeded {
2361                    resource: "ledger bytes",
2362                    ..
2363                }
2364            ))
2365        ));
2366        assert_eq!(store, before);
2367        let mut envelope = LedgerPayloadEnvelope::current(Vec::new())
2368            .try_encode()
2369            .unwrap();
2370        envelope[16..24]
2371            .copy_from_slice(&((crate::constants::MAX_LEDGER_BYTES + 1) as u64).to_le_bytes());
2372        assert!(matches!(
2373            LedgerPayloadEnvelope::decode(&envelope),
2374            Err(LedgerPayloadEnvelopeError::PayloadTooLarge { .. })
2375        ));
2376    }
2377}