Skip to main content

ic_memory/ledger/
mod.rs

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