Skip to main content

ic_memory/ledger/
mod.rs

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