Skip to main content

ic_memory/ledger/
mod.rs

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