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