Skip to main content

ic_memory/ledger/
mod.rs

1mod claim;
2mod error;
3mod integrity;
4mod payload;
5mod record;
6mod stage;
7
8use crate::physical::{
9    CommitRecoveryError, CommitStoreDiagnostic, CommittedGenerationBytes, DualCommitStore,
10};
11use serde::{Deserialize, Serialize};
12
13pub use claim::{
14    ClaimConflict, ClaimOutcome, ReservationClaimConflict, validate_declaration_claim,
15    validate_reservation_claim,
16};
17pub use error::{
18    AllocationReservationError, AllocationRetirementError, AllocationStageError, LedgerCommitError,
19    LedgerIntegrityError,
20};
21pub use payload::{
22    LEDGER_PAYLOAD_FORMAT_VERSION, LedgerPayloadEnvelope, LedgerPayloadEnvelopeError,
23};
24pub use record::{
25    AllocationHistory, AllocationLedger, AllocationRecord, AllocationRetirement, AllocationState,
26    GenerationRecord, RecoveredLedger, SchemaMetadataRecord,
27};
28pub use stage::checked_reservation_count;
29pub use stage::validate_reservation_declaration;
30pub use stage::{
31    stage_reservation_generation, stage_retirement_generation, stage_validated_generation,
32};
33
34fn decode_ledger(bytes: &[u8]) -> Result<AllocationLedger, String> {
35    crate::cbor::from_slice_exact(bytes).map_err(|err| err.to_string())
36}
37
38///
39/// LedgerCommitStore
40///
41/// Generation-scoped allocation ledger commit store.
42///
43/// This type owns the logical commit lifecycle and the crate-owned CBOR ledger
44/// encoding. It deliberately does not own stable-memory IO; that remains the
45/// substrate or framework owner's responsibility.
46///
47/// This store commits allocation ledger generations. It does not open
48/// stable-memory handles and does not allocate application slots.
49///
50#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
51#[serde(deny_unknown_fields)]
52pub struct LedgerCommitStore {
53    /// Protected physical commit slots.
54    physical: DualCommitStore,
55}
56
57impl LedgerCommitStore {
58    /// Borrow the protected physical commit store for diagnostics.
59    #[must_use]
60    pub const fn physical(&self) -> &DualCommitStore {
61        &self.physical
62    }
63
64    /// Recover the authoritative allocation ledger using the native CBOR ledger codec.
65    pub fn recover(&self) -> Result<RecoveredLedger, LedgerCommitError> {
66        let committed = self
67            .physical
68            .authoritative()
69            .map_err(LedgerCommitError::Recovery)?;
70        Self::recover_committed(committed)
71    }
72
73    pub(crate) fn recover_with_diagnostic(
74        &self,
75    ) -> (
76        Result<RecoveredLedger, LedgerCommitError>,
77        CommitStoreDiagnostic,
78    ) {
79        let (committed, diagnostic) = self.physical.authoritative_with_diagnostic();
80        let recovered = committed
81            .map_err(LedgerCommitError::Recovery)
82            .and_then(Self::recover_committed);
83        (recovered, diagnostic)
84    }
85
86    fn recover_committed(
87        committed: &CommittedGenerationBytes,
88    ) -> Result<RecoveredLedger, LedgerCommitError> {
89        let payload = LedgerPayloadEnvelope::decode_payload(committed.payload())
90            .map_err(LedgerCommitError::PayloadEnvelope)?;
91        let ledger = decode_ledger(payload).map_err(LedgerCommitError::Codec)?;
92        if committed.generation() != ledger.current_generation {
93            return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
94                physical_generation: committed.generation(),
95                logical_generation: ledger.current_generation,
96            });
97        }
98        ledger
99            .validate_committed_integrity()
100            .map_err(LedgerCommitError::Integrity)?;
101        Ok(RecoveredLedger::from_trusted_ledger(ledger))
102    }
103
104    /// Recover the authoritative ledger, or explicitly initialize an empty store.
105    ///
106    /// Initialization is allowed only when no physical commit slot has ever
107    /// been written. Corrupt or partially written stores fail closed even when
108    /// a genesis ledger is supplied.
109    ///
110    /// Supplying a non-empty `genesis` is a privileged import/migration action.
111    /// Normal runtime bootstraps should seed an empty current-format ledger.
112    pub fn recover_or_initialize(
113        &mut self,
114        genesis: &AllocationLedger,
115    ) -> Result<RecoveredLedger, LedgerCommitError> {
116        match self.recover() {
117            Ok(ledger) => Ok(ledger),
118            // Physical selection returns this error only when both slots are absent.
119            Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)) => {
120                self.commit(genesis)
121            }
122            Err(err) => Err(err),
123        }
124    }
125
126    /// Commit one logical allocation ledger generation through the native CBOR ledger codec.
127    ///
128    /// # Panics
129    ///
130    /// Panics only if the private concrete ledger-encoding invariant is broken.
131    pub fn commit(
132        &mut self,
133        ledger: &AllocationLedger,
134    ) -> Result<RecoveredLedger, LedgerCommitError> {
135        self.commit_generation(ledger)?;
136        // The current writer encoded this integrity-checked ledger, and the
137        // physical commit checked its predecessor and established this generation.
138        // Existing persisted bytes still cross the full recovery boundary.
139        Ok(RecoveredLedger::from_trusted_ledger(ledger.clone()))
140    }
141
142    // Bootstrap already owns its staged ledger. Share the checked mutation
143    // without cloning that ledger into a proof only to unwrap it immediately.
144    pub(crate) fn commit_generation(
145        &mut self,
146        ledger: &AllocationLedger,
147    ) -> Result<(), LedgerCommitError> {
148        ledger
149            .validate_committed_integrity()
150            .map_err(LedgerCommitError::Integrity)?;
151        let payload =
152            LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
153        self.physical
154            .commit_payload_at_generation(ledger.current_generation, payload)
155            .map(|_| ())
156            .map_err(LedgerCommitError::Recovery)
157    }
158
159    /// Simulate corruption of a logical ledger payload in the inactive slot.
160    #[cfg(test)]
161    pub fn write_corrupt_inactive_ledger(
162        &mut self,
163        ledger: &AllocationLedger,
164    ) -> Result<(), LedgerCommitError> {
165        let payload =
166            LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
167        self.physical
168            .write_corrupt_inactive_slot(ledger.current_generation, payload);
169        Ok(())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::test_cbor::hex_fixture;
177    use crate::{
178        declaration::{AllocationDeclaration, DeclarationSnapshot, DeclarationSnapshotError},
179        key::StableKey,
180        physical::CommittedGenerationBytes,
181        schema::{SchemaMetadata, SchemaMetadataError},
182        slot::{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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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[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 = 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 = 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 = 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        for generation in [3, 4] {
1761            let mut ledger = committed_ledger(3);
1762            let mut record = active_record("app.users.v1", 100);
1763            record.last_seen_generation = 2;
1764            record.schema_history.push(
1765                SchemaMetadataRecord::new(
1766                    generation,
1767                    SchemaMetadata::new(Some(2)).expect("schema"),
1768                )
1769                .expect("schema record"),
1770            );
1771            ledger.allocation_history.records = vec![record];
1772
1773            let err = ledger
1774                .validate_committed_integrity()
1775                .expect_err("schema metadata cannot postdate the last observation");
1776
1777            assert!(matches!(
1778                (generation, err),
1779                (
1780                    3,
1781                    LedgerIntegrityError::SchemaHistoryAfterLastSeen {
1782                        generation: 3,
1783                        last_seen_generation: 2,
1784                        ..
1785                    }
1786                ) | (
1787                    4,
1788                    LedgerIntegrityError::SchemaHistoryOutOfBounds { generation: 4, .. }
1789                )
1790            ));
1791        }
1792    }
1793
1794    #[test]
1795    fn validate_integrity_rejects_non_increasing_schema_history() {
1796        for generation in [0, 1] {
1797            let mut ledger = ledger();
1798            let mut record = active_record("app.users.v1", 100);
1799            record.schema_history.push(SchemaMetadataRecord {
1800                generation,
1801                schema: SchemaMetadata::default(),
1802            });
1803            ledger.allocation_history.records = vec![record];
1804
1805            let err = ledger
1806                .validate_integrity()
1807                .expect_err("non-increasing schema history");
1808
1809            assert!(matches!(
1810                err,
1811                LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1812            ));
1813        }
1814    }
1815
1816    #[test]
1817    fn validate_integrity_rejects_invalid_schema_metadata_history() {
1818        let mut ledger = committed_ledger(1);
1819        let mut record = active_record("app.users.v1", 100);
1820        record.schema_history[0].schema = invalid_schema_metadata();
1821        ledger.allocation_history.records = vec![record];
1822
1823        let err = ledger
1824            .validate_committed_integrity()
1825            .expect_err("invalid committed schema metadata");
1826
1827        assert_eq!(
1828            err,
1829            LedgerIntegrityError::InvalidSchemaMetadata {
1830                stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1831                generation: 1,
1832                error: SchemaMetadataError::InvalidVersion,
1833            }
1834        );
1835    }
1836
1837    #[test]
1838    fn validate_committed_integrity_requires_current_generation_record() {
1839        let err = ledger()
1840            .validate_committed_integrity()
1841            .expect_err("missing current generation");
1842
1843        assert_eq!(
1844            err,
1845            LedgerIntegrityError::MissingCurrentGenerationRecord {
1846                current_generation: 3
1847            }
1848        );
1849    }
1850
1851    #[test]
1852    fn committed_integrity_rejects_allocation_references_to_genesis() {
1853        for generation in [0, 1, 2] {
1854            let mut ledger = committed_ledger(generation);
1855            let mut record = AllocationRecord::active(0, &declaration("app.genesis.v1", 100, None));
1856            if generation == 2 {
1857                record.last_seen_generation = 1;
1858                record
1859                    .schema_history
1860                    .push(SchemaMetadataRecord::new(1, SchemaMetadata::default()).unwrap());
1861                record.state = AllocationState::Retired { generation: 2 };
1862            }
1863            ledger.allocation_history.records.push(record);
1864            ledger.validate_integrity().expect("structurally valid DTO");
1865            assert_eq!(
1866                ledger.validate_committed_integrity(),
1867                Err(LedgerIntegrityError::UnknownRecordGeneration {
1868                    stable_key: StableKey::parse("app.genesis.v1").unwrap(),
1869                    generation: 0,
1870                })
1871            );
1872        }
1873    }
1874
1875    #[test]
1876    fn validate_committed_integrity_rejects_generation_history_gaps() {
1877        let mut ledger = committed_ledger(3);
1878        ledger.allocation_history.generations.remove(1);
1879        ledger
1880            .allocation_history
1881            .records
1882            .push(AllocationRecord::active(
1883                0,
1884                &declaration("app.genesis.v1", 100, None),
1885            ));
1886
1887        let err = ledger
1888            .validate_committed_integrity()
1889            .expect_err("generation history gap takes precedence over genesis reference");
1890
1891        assert!(matches!(
1892            err,
1893            LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1894        ));
1895    }
1896
1897    #[test]
1898    fn committed_integrity_rejects_a_skipped_parent_in_contiguous_history() {
1899        let mut ledger = committed_ledger(3);
1900        ledger.allocation_history.generations[2].parent_generation = 1;
1901        ledger
1902            .validate_integrity()
1903            .expect("structurally valid history");
1904
1905        assert_eq!(
1906            ledger.validate_committed_integrity(),
1907            Err(LedgerIntegrityError::BrokenGenerationChain {
1908                generation: 3,
1909                expected_parent: 2,
1910                actual_parent: 1,
1911            })
1912        );
1913    }
1914
1915    #[test]
1916    fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1917        let mut store = LedgerCommitStore::default();
1918        let mut invalid = ledger();
1919        invalid.allocation_history.records = vec![
1920            active_record("app.users.v1", 100),
1921            active_record("app.orders.v1", 100),
1922        ];
1923
1924        let err = store.commit(&invalid).expect_err("invalid ledger");
1925
1926        assert!(matches!(
1927            err,
1928            LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1929        ));
1930        assert!(store.physical().is_uninitialized());
1931    }
1932
1933    #[test]
1934    fn ledger_commit_store_recovers_latest_committed_ledger() {
1935        let mut store = LedgerCommitStore::default();
1936        let first = committed_ledger(1);
1937        let second = committed_ledger(2);
1938
1939        store.commit(&first).expect("first commit");
1940        store.commit(&second).expect("second commit");
1941        let recovered = store.recover().expect("recovered ledger");
1942
1943        assert_eq!(recovered.current_generation(), 2);
1944        assert_eq!(recovered.physical_generation(), 2);
1945    }
1946
1947    #[test]
1948    fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1949        let mut store = LedgerCommitStore::default();
1950        let ledger = committed_ledger(1);
1951
1952        store.commit(&ledger).expect("commit");
1953
1954        let committed = store.physical().authoritative().expect("authoritative");
1955        assert_eq!(
1956            committed.payload(),
1957            LedgerPayloadEnvelope::current(crate::test_cbor::to_vec(&ledger).unwrap())
1958                .try_encode()
1959                .unwrap(),
1960            "direct commit encoding preserves the complete public envelope bytes"
1961        );
1962        let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1963        assert_eq!(
1964            envelope.payload(),
1965            crate::test_cbor::to_vec(&ledger).expect("encode ledger")
1966        );
1967    }
1968
1969    #[test]
1970    fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1971        let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1972        let store = LedgerCommitStore {
1973            physical: DualCommitStore {
1974                slot0: Some(committed),
1975                slot1: None,
1976            },
1977        };
1978
1979        let err = store.recover().expect_err("bad envelope");
1980
1981        assert!(matches!(
1982            err,
1983            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1984        ));
1985    }
1986
1987    #[test]
1988    fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1989        let mut payload = enveloped_payload(&committed_ledger(1));
1990        payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1991        let store = LedgerCommitStore {
1992            physical: DualCommitStore {
1993                slot0: Some(CommittedGenerationBytes::new(1, payload)),
1994                slot1: None,
1995            },
1996        };
1997
1998        let err = store
1999            .recover()
2000            .expect_err("unknown ledger format version must fail");
2001
2002        assert_eq!(
2003            err,
2004            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
2005                marker: *b"ICMF",
2006                version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
2007            })
2008        );
2009    }
2010
2011    #[test]
2012    fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
2013        let genesis = enveloped_payload(&committed_ledger(0));
2014        let committed = CommittedGenerationBytes::new(0, genesis.clone());
2015        let mut corrupt = committed.clone();
2016        corrupt.checksum ^= 1;
2017        let mut unsupported = genesis.clone();
2018        unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
2019        let cases = [
2020            ("empty", None, None),
2021            ("one slot", Some(committed.clone()), None),
2022            (
2023                "identical slots",
2024                Some(committed.clone()),
2025                Some(committed.clone()),
2026            ),
2027            (
2028                "corrupt inactive",
2029                Some(committed.clone()),
2030                Some(corrupt.clone()),
2031            ),
2032            ("corrupt active", Some(corrupt), Some(committed.clone())),
2033            (
2034                "ambiguous",
2035                Some(committed),
2036                Some(CommittedGenerationBytes::new(0, vec![1])),
2037            ),
2038            (
2039                "unsupported format",
2040                Some(CommittedGenerationBytes::new(0, unsupported)),
2041                None,
2042            ),
2043            (
2044                "invalid CBOR",
2045                Some(CommittedGenerationBytes::new(
2046                    0,
2047                    LedgerPayloadEnvelope::current(vec![255])
2048                        .try_encode()
2049                        .unwrap(),
2050                )),
2051                None,
2052            ),
2053            (
2054                "generation mismatch",
2055                Some(CommittedGenerationBytes::new(1, genesis)),
2056                None,
2057            ),
2058            (
2059                "invalid committed integrity",
2060                Some(CommittedGenerationBytes::new(
2061                    3,
2062                    enveloped_payload(&ledger()),
2063                )),
2064                None,
2065            ),
2066        ];
2067
2068        for (label, slot0, slot1) in cases {
2069            let empty = slot0.is_none() && slot1.is_none();
2070            let store = LedgerCommitStore {
2071                physical: DualCommitStore { slot0, slot1 },
2072            };
2073            let before = store.clone();
2074            let (recovered, diagnostic) = store.recover_with_diagnostic();
2075            assert_eq!(recovered, store.recover(), "{label}");
2076            assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
2077            assert_eq!(store, before, "{label}");
2078
2079            let mut initializing = store.clone();
2080            let initialized = initializing.recover_or_initialize(&committed_ledger(0));
2081            if empty {
2082                let initialized = initialized.expect("empty store accepts genesis");
2083                assert_eq!(initialized.current_generation(), 0);
2084                assert_eq!(initialized.physical_generation(), 0);
2085                assert_eq!(initializing.recover().unwrap(), initialized);
2086            } else {
2087                assert_eq!(initialized, recovered, "{label}");
2088                assert_eq!(initializing, before, "{label}");
2089            }
2090
2091            if let Err(LedgerCommitError::Recovery(error)) = recovered {
2092                assert_eq!(diagnostic.recovery, Err(error), "{label}");
2093            } else {
2094                assert!(diagnostic.recovery.is_ok(), "{label}");
2095            }
2096        }
2097    }
2098
2099    #[test]
2100    fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
2101        let mut store = LedgerCommitStore::default();
2102        let genesis = committed_ledger(0);
2103
2104        let recovered = store
2105            .recover_or_initialize(&genesis)
2106            .expect("current-format genesis ledger");
2107        assert_eq!(recovered.current_generation(), 0);
2108        assert_eq!(recovered.physical_generation(), 0);
2109        assert_eq!(recovered.ledger().allocation_history().generations(), []);
2110        assert_eq!(recovered, store.recover().unwrap());
2111
2112        let first = recovered
2113            .ledger()
2114            .stage_validated_generation(
2115                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2116                None,
2117            )
2118            .expect("first real generation");
2119        let recovered = store.commit(&first).expect("first commit");
2120
2121        assert_eq!(recovered, store.recover().unwrap());
2122        assert_eq!(recovered.current_generation(), 1);
2123        assert_eq!(recovered.physical_generation(), 1);
2124        assert_eq!(
2125            recovered.ledger().allocation_history().generations()[0].generation,
2126            1
2127        );
2128        assert_eq!(
2129            record(recovered.ledger(), "app.users.v1").first_generation,
2130            1
2131        );
2132    }
2133
2134    #[test]
2135    fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
2136        let mut store = LedgerCommitStore::default();
2137        let genesis = committed_ledger(0);
2138        store.commit(&genesis).expect("genesis commit");
2139        let first = genesis
2140            .stage_validated_generation(
2141                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2142                None,
2143            )
2144            .expect("first generation");
2145        let first = store.commit(&first).expect("first commit");
2146        let second = first
2147            .ledger()
2148            .stage_validated_generation(
2149                &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
2150                None,
2151            )
2152            .expect("second generation");
2153
2154        store.commit(&second).expect("second commit");
2155        store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
2156
2157        let err = store
2158            .recover()
2159            .expect_err("corrupt latest must not roll back");
2160
2161        assert_eq!(
2162            err,
2163            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2164                slot0_invalid: true,
2165                slot1_invalid: false,
2166            })
2167        );
2168    }
2169
2170    #[test]
2171    fn ledger_commit_store_recovers_identical_duplicate_slots() {
2172        let ledger = committed_ledger(0)
2173            .stage_validated_generation(
2174                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2175                None,
2176            )
2177            .expect("first generation");
2178        let payload = enveloped_payload(&ledger);
2179        let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
2180        let store = LedgerCommitStore {
2181            physical: DualCommitStore {
2182                slot0: Some(committed.clone()),
2183                slot1: Some(committed),
2184            },
2185        };
2186
2187        let recovered = store.recover().expect("recovered");
2188
2189        assert_eq!(recovered.ledger(), &ledger);
2190    }
2191
2192    #[test]
2193    fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
2194        let mut store = LedgerCommitStore::default();
2195        let first = committed_ledger(1);
2196        let second = committed_ledger(2);
2197
2198        store.commit(&first).expect("first commit");
2199        store
2200            .write_corrupt_inactive_ledger(&second)
2201            .expect("corrupt write");
2202        let err = store.recover().expect_err("corrupt slot");
2203
2204        assert_eq!(
2205            err,
2206            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2207                slot0_invalid: false,
2208                slot1_invalid: true,
2209            })
2210        );
2211    }
2212
2213    #[test]
2214    fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
2215        let store = LedgerCommitStore {
2216            physical: DualCommitStore {
2217                slot0: Some(CommittedGenerationBytes::new(
2218                    7,
2219                    enveloped_payload(&committed_ledger(6)),
2220                )),
2221                slot1: None,
2222            },
2223        };
2224        let err = store.recover().expect_err("mismatch");
2225
2226        assert_eq!(
2227            err,
2228            LedgerCommitError::PhysicalLogicalGenerationMismatch {
2229                physical_generation: 7,
2230                logical_generation: 6
2231            }
2232        );
2233    }
2234
2235    #[test]
2236    fn ledger_commit_store_rejects_non_next_logical_generation() {
2237        let mut store = LedgerCommitStore::default();
2238        store.commit(&committed_ledger(1)).expect("first commit");
2239
2240        let err = store
2241            .commit(&committed_ledger(3))
2242            .expect_err("skipped generation");
2243
2244        assert_eq!(
2245            err,
2246            LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2247                expected: 2,
2248                actual: 3
2249            })
2250        );
2251    }
2252
2253    #[test]
2254    fn ledger_commit_store_initializes_empty_store_explicitly() {
2255        let mut store = LedgerCommitStore::default();
2256        let genesis = committed_ledger(3);
2257
2258        let recovered = store
2259            .recover_or_initialize(&genesis)
2260            .expect("initialized ledger");
2261
2262        assert_eq!(recovered.current_generation(), 3);
2263        assert_eq!(recovered.physical_generation(), 3);
2264        assert!(!store.physical().is_uninitialized());
2265    }
2266
2267    #[test]
2268    fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2269        let mut store = LedgerCommitStore::default();
2270        store
2271            .write_corrupt_inactive_ledger(&ledger())
2272            .expect("corrupt write");
2273
2274        let err = store
2275            .recover_or_initialize(&ledger())
2276            .expect_err("corrupt state");
2277
2278        assert!(matches!(
2279            err,
2280            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2281                slot0_invalid: true,
2282                slot1_invalid: false,
2283            })
2284        ));
2285    }
2286    #[test]
2287    fn history_boundary_round_trips_and_rejects_next_generation_without_mutation() {
2288        let mut source = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 - 1);
2289        source
2290            .allocation_history
2291            .records
2292            .push(active_record("app.users.v1", 100));
2293        let retirement = AllocationRetirement::new(
2294            "app.users.v1",
2295            AllocationSlotDescriptor::memory_manager(100).unwrap(),
2296        )
2297        .unwrap();
2298        let retired = source
2299            .stage_retirement_generation(&retirement, None)
2300            .unwrap();
2301        retired.validate_committed_integrity().unwrap();
2302        assert_eq!(
2303            retired.allocation_history.generations().len(),
2304            crate::constants::MAX_LEDGER_GENERATIONS
2305        );
2306        assert_eq!(
2307            retired.allocation_history.records()[0].schema_history,
2308            source.allocation_history.records()[0].schema_history
2309        );
2310        assert_eq!(
2311            source.allocation_history.records()[0].state,
2312            AllocationState::Active
2313        );
2314        assert!(matches!(
2315            retired.stage_retirement_generation(&retirement, None),
2316            Err(AllocationRetirementError::Integrity(
2317                LedgerIntegrityError::LimitExceeded {
2318                    resource: "generation history",
2319                    ..
2320                }
2321            ))
2322        ));
2323
2324        let ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2325        let mut store = LedgerCommitStore::default();
2326        let recovered = store.commit(&ledger).expect("boundary is admissible");
2327        let record = crate::StableCellLedgerRecord::new(store.clone());
2328        let bytes = crate::test_cbor::to_vec(&record).unwrap();
2329        assert_eq!(record.encoded_size(), bytes.len());
2330        let recovered_record = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2331        assert_eq!(
2332            recovered_record.store().recover().unwrap().ledger(),
2333            &ledger
2334        );
2335        let before = store.clone();
2336        let err = crate::AllocationBootstrap::new(&mut store)
2337            .validate_and_commit(
2338                DeclarationSnapshot::new(Vec::new()).unwrap(),
2339                &crate::GenericRangePolicy,
2340                None,
2341            )
2342            .unwrap_err();
2343        assert!(matches!(
2344            err,
2345            crate::BootstrapError::Staging(AllocationStageError::Integrity(
2346                LedgerIntegrityError::LimitExceeded { .. }
2347            ))
2348        ));
2349        assert_eq!(store, before);
2350        assert_eq!(
2351            recovered.current_generation(),
2352            crate::constants::MAX_LEDGER_GENERATIONS as u64
2353        );
2354        let oversize = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 + 1);
2355        assert!(matches!(
2356            store.commit(&oversize),
2357            Err(LedgerCommitError::Integrity(
2358                LedgerIntegrityError::LimitExceeded { .. }
2359            ))
2360        ));
2361        assert_eq!(store, before);
2362    }
2363
2364    #[test]
2365    fn record_count_boundary_and_malicious_history_reject_on_recovery() {
2366        let mut ledger = committed_ledger(1);
2367        ledger.allocation_history.records = (0..255_u8)
2368            .map(|id| active_record(&format!("app.slot{id}.v1"), id))
2369            .collect();
2370        let mut store = LedgerCommitStore::default();
2371        store.commit(&ledger).unwrap();
2372        assert_eq!(
2373            store
2374                .recover()
2375                .unwrap()
2376                .ledger()
2377                .allocation_history()
2378                .records()
2379                .len(),
2380            255
2381        );
2382        ledger
2383            .allocation_history
2384            .records
2385            .push(active_record("app.extra.v1", 254));
2386        assert!(matches!(
2387            store.commit(&ledger),
2388            Err(LedgerCommitError::Integrity(
2389                LedgerIntegrityError::LimitExceeded { .. }
2390            ))
2391        ));
2392        let payload = enveloped_payload(&ledger);
2393        let mut invalid = LedgerCommitStore {
2394            physical: DualCommitStore {
2395                slot0: Some(CommittedGenerationBytes::new(1, payload)),
2396                slot1: None,
2397            },
2398        };
2399        assert!(invalid.recover().is_err());
2400        assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
2401        for bytes in [
2402            vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2403            vec![0x81; 34],
2404            vec![0xa1],
2405        ] {
2406            let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
2407            let invalid = LedgerCommitStore {
2408                physical: DualCommitStore {
2409                    slot0: Some(CommittedGenerationBytes::new(1, payload)),
2410                    slot1: None,
2411                },
2412            };
2413            assert!(matches!(
2414                invalid.recover(),
2415                Err(LedgerCommitError::Codec(_))
2416            ));
2417        }
2418    }
2419    #[test]
2420    fn encoded_byte_limit_rejects_before_commit_mutation() {
2421        let mut store = LedgerCommitStore::default();
2422        store.commit(&committed_ledger(0)).unwrap();
2423        let before = store.clone();
2424        let mut ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2425        for generation in &mut ledger.allocation_history.generations {
2426            generation.runtime_fingerprint =
2427                Some("x".repeat(crate::constants::DIAGNOSTIC_STRING_MAX_BYTES));
2428        }
2429        assert!(matches!(
2430            store.commit(&ledger),
2431            Err(LedgerCommitError::Integrity(
2432                LedgerIntegrityError::LimitExceeded {
2433                    resource: "ledger bytes",
2434                    ..
2435                }
2436            ))
2437        ));
2438        assert_eq!(store, before);
2439        let mut envelope = LedgerPayloadEnvelope::current(Vec::new())
2440            .try_encode()
2441            .unwrap();
2442        envelope[16..24]
2443            .copy_from_slice(&((crate::constants::MAX_LEDGER_BYTES + 1) as u64).to_le_bytes());
2444        assert!(matches!(
2445            LedgerPayloadEnvelope::decode(&envelope),
2446            Err(LedgerPayloadEnvelopeError::PayloadTooLarge { .. })
2447        ));
2448    }
2449}