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)).expect("valid schema metadata")
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_invalid_schema_metadata() {
800        let validated = crate::capability::ValidatedAllocations::new(
801            3,
802            vec![declaration_with_invalid_schema("app.users.v1", 100)],
803            None,
804        );
805
806        let err = ledger()
807            .stage_validated_generation(&validated, None)
808            .expect_err("invalid schema metadata");
809
810        assert_eq!(
811            err,
812            AllocationStageError::InvalidSchemaMetadata {
813                stable_key: StableKey::parse("app.users.v1").expect("stable key"),
814                error: SchemaMetadataError::InvalidVersion,
815            }
816        );
817    }
818
819    #[test]
820    fn stage_validated_generation_rejects_generation_overflow() {
821        let ledger = AllocationLedger {
822            current_generation: u64::MAX,
823            ..ledger()
824        };
825        let validated = validated(u64::MAX, vec![declaration("app.users.v1", 100, Some(1))]);
826
827        let err = ledger
828            .stage_validated_generation(&validated, None)
829            .expect_err("overflow must fail");
830
831        assert_eq!(
832            err,
833            AllocationStageError::GenerationOverflow {
834                generation: u64::MAX
835            }
836        );
837    }
838
839    #[test]
840    fn stage_validated_generation_rejects_same_key_different_slot() {
841        let mut ledger = ledger();
842        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
843        let validated = validated(3, vec![declaration("app.users.v1", 101, None)]);
844
845        let err = ledger
846            .stage_validated_generation(&validated, None)
847            .expect_err("stable key cannot move slots");
848
849        assert!(matches!(
850            err,
851            AllocationStageError::StableKeySlotConflict { .. }
852        ));
853    }
854
855    #[test]
856    fn stage_validated_generation_rejects_same_slot_different_key() {
857        let mut ledger = ledger();
858        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
859        let validated = validated(3, vec![declaration("app.orders.v1", 100, None)]);
860
861        let err = ledger
862            .stage_validated_generation(&validated, None)
863            .expect_err("slot cannot be reused by another key");
864
865        assert!(matches!(
866            err,
867            AllocationStageError::SlotStableKeyConflict { .. }
868        ));
869    }
870
871    #[test]
872    fn stage_validated_generation_rejects_retired_redeclaration() {
873        let mut ledger = ledger();
874        let mut record = active_record("app.users.v1", 100);
875        record.state = AllocationState::Retired { generation: 3 };
876        *ledger.allocation_history.records_mut() = vec![record];
877        let validated = validated(3, vec![declaration("app.users.v1", 100, None)]);
878
879        let err = ledger
880            .stage_validated_generation(&validated, None)
881            .expect_err("retired allocation cannot be redeclared");
882
883        assert!(matches!(
884            err,
885            AllocationStageError::RetiredAllocation { .. }
886        ));
887    }
888
889    #[test]
890    fn stage_validated_generation_preserves_omitted_records() {
891        let first = validated(
892            3,
893            vec![
894                declaration("app.users.v1", 100, Some(1)),
895                declaration("app.orders.v1", 101, Some(1)),
896            ],
897        );
898        let second = validated(4, vec![declaration("app.users.v1", 100, Some(1))]);
899
900        let staged = ledger()
901            .stage_validated_generation(&first, None)
902            .expect("first generation");
903        let staged = staged
904            .stage_validated_generation(&second, None)
905            .expect("second generation");
906
907        assert_eq!(staged.current_generation, 5);
908        assert_eq!(staged.allocation_history.records().len(), 2);
909        let omitted = staged
910            .allocation_history
911            .records()
912            .iter()
913            .find(|record| record.stable_key.as_str() == "app.orders.v1")
914            .expect("omitted record");
915        assert_eq!(omitted.state, AllocationState::Active);
916        assert_eq!(omitted.last_seen_generation, 4);
917    }
918
919    #[test]
920    fn stage_validated_generation_records_schema_metadata_history() {
921        let first = validated(3, vec![declaration("app.users.v1", 100, Some(1))]);
922        let second = validated(4, vec![declaration("app.users.v1", 100, Some(2))]);
923
924        let staged = ledger()
925            .stage_validated_generation(&first, None)
926            .expect("first generation");
927        let staged = staged
928            .stage_validated_generation(&second, None)
929            .expect("second generation");
930        let record = &staged.allocation_history.records()[0];
931
932        assert_eq!(record.schema_history.len(), 2);
933        assert_eq!(record.schema_history[0].generation, 4);
934        assert_eq!(record.schema_history[1].generation, 5);
935    }
936
937    #[test]
938    fn stage_reservation_generation_records_reserved_allocations() {
939        let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
940
941        let staged = ledger()
942            .stage_reservation_generation(&reservations, Some(42))
943            .expect("reserved generation");
944
945        assert_eq!(staged.current_generation, 4);
946        assert_eq!(staged.allocation_history.records().len(), 1);
947        assert_eq!(
948            staged.allocation_history.records()[0].state,
949            AllocationState::Reserved
950        );
951        assert_eq!(
952            staged.allocation_history.generations()[0].declaration_count,
953            1
954        );
955    }
956
957    #[test]
958    fn stage_reservation_generation_allows_empty_generation_boundary() {
959        let reservations = Vec::new();
960
961        let staged = ledger()
962            .stage_reservation_generation(&reservations, Some(42))
963            .expect("empty reservation generation");
964
965        assert_eq!(staged.current_generation, 4);
966        assert_eq!(staged.allocation_history.records(), []);
967        assert_eq!(staged.allocation_history.generations().len(), 1);
968        assert_eq!(staged.allocation_history.generations()[0].generation(), 4);
969        assert_eq!(
970            staged.allocation_history.generations()[0].declaration_count(),
971            0
972        );
973        assert_eq!(
974            staged.allocation_history.generations()[0].committed_at(),
975            Some(42)
976        );
977    }
978
979    #[test]
980    fn stage_reservation_generation_refreshes_existing_reserved_allocation() {
981        let first = vec![declaration("app.future_store.v1", 100, Some(1))];
982        let staged = ledger()
983            .stage_reservation_generation(&first, Some(42))
984            .expect("first reservation generation");
985
986        let second = vec![declaration("app.future_store.v1", 100, Some(2))];
987        let staged = staged
988            .stage_reservation_generation(&second, Some(43))
989            .expect("reservation refresh");
990        let record = record(&staged, "app.future_store.v1");
991
992        assert_eq!(record.state(), AllocationState::Reserved);
993        assert_eq!(record.first_generation(), 4);
994        assert_eq!(record.last_seen_generation(), 5);
995        assert_eq!(record.schema_history().len(), 2);
996        assert_eq!(record.schema_history()[1].generation(), 5);
997        assert_eq!(
998            staged.allocation_history.generations()[1].declaration_count(),
999            1
1000        );
1001    }
1002
1003    #[test]
1004    fn stage_reservation_generation_rejects_generation_overflow() {
1005        let ledger = AllocationLedger {
1006            current_generation: u64::MAX,
1007            ..ledger()
1008        };
1009        let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
1010
1011        let err = ledger
1012            .stage_reservation_generation(&reservations, None)
1013            .expect_err("overflow must fail");
1014
1015        assert_eq!(
1016            err,
1017            AllocationReservationError::GenerationOverflow {
1018                generation: u64::MAX
1019            }
1020        );
1021    }
1022
1023    #[test]
1024    fn stage_reservation_generation_rejects_invalid_schema_metadata() {
1025        let reservations = vec![declaration_with_invalid_schema(
1026            "ic_memory.generation_log.v1",
1027            1,
1028        )];
1029
1030        let err = ledger()
1031            .stage_reservation_generation(&reservations, None)
1032            .expect_err("invalid reservation schema metadata");
1033
1034        assert_eq!(
1035            err,
1036            AllocationReservationError::InvalidSchemaMetadata {
1037                stable_key: StableKey::parse("ic_memory.generation_log.v1").expect("stable key"),
1038                error: SchemaMetadataError::InvalidVersion,
1039            }
1040        );
1041    }
1042
1043    #[test]
1044    fn stage_reservation_generation_rejects_invalid_decoded_slot() {
1045        let mut reservation = declaration("ic_memory.generation_log.v1", 1, None);
1046        reservation.slot =
1047            AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1048
1049        let err = ledger()
1050            .stage_reservation_generation(&[reservation], None)
1051            .expect_err("invalid decoded reservation slot");
1052
1053        assert!(matches!(
1054            err,
1055            AllocationReservationError::InvalidDeclaration(
1056                DeclarationSnapshotError::MemoryManagerSlot(
1057                    MemoryManagerSlotError::InvalidMemoryManagerId { id }
1058                )
1059            ) if id == MEMORY_MANAGER_INVALID_ID
1060        ));
1061    }
1062
1063    #[test]
1064    fn stage_reservation_generation_rejects_same_key_different_slot() {
1065        let mut ledger = ledger();
1066        *ledger.allocation_history.records_mut() = vec![
1067            AllocationRecord::reserved(3, declaration("app.future_store.v1", 100, None))
1068                .expect("valid schema metadata"),
1069        ];
1070        let reservations = vec![declaration("app.future_store.v1", 101, None)];
1071
1072        let err = ledger
1073            .stage_reservation_generation(&reservations, None)
1074            .expect_err("reservation key cannot move slots");
1075
1076        assert!(matches!(
1077            err,
1078            AllocationReservationError::StableKeySlotConflict { .. }
1079        ));
1080    }
1081
1082    #[test]
1083    fn stage_reservation_generation_rejects_same_slot_different_key() {
1084        let mut ledger = ledger();
1085        *ledger.allocation_history.records_mut() = vec![
1086            AllocationRecord::reserved(3, declaration("app.future_store.v1", 100, None))
1087                .expect("valid schema metadata"),
1088        ];
1089        let reservations = vec![declaration("app.other_future_store.v1", 100, None)];
1090
1091        let err = ledger
1092            .stage_reservation_generation(&reservations, None)
1093            .expect_err("reservation slot cannot be reused by another key");
1094
1095        assert!(matches!(
1096            err,
1097            AllocationReservationError::SlotStableKeyConflict { .. }
1098        ));
1099    }
1100
1101    #[test]
1102    fn stage_reservation_generation_rejects_active_allocation() {
1103        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1104        let staged = ledger()
1105            .stage_validated_generation(&active, None)
1106            .expect("active generation");
1107        let reservations = vec![declaration("app.users.v1", 100, None)];
1108
1109        let err = staged
1110            .stage_reservation_generation(&reservations, None)
1111            .expect_err("active cannot become reserved");
1112
1113        assert!(matches!(
1114            err,
1115            AllocationReservationError::ActiveAllocation { .. }
1116        ));
1117    }
1118
1119    #[test]
1120    fn stage_reservation_generation_rejects_retired_allocation() {
1121        let mut ledger = ledger();
1122        let mut record = active_record("app.users.v1", 100);
1123        record.state = AllocationState::Retired { generation: 3 };
1124        *ledger.allocation_history.records_mut() = vec![record];
1125        let reservations = vec![declaration("app.users.v1", 100, None)];
1126
1127        let err = ledger
1128            .stage_reservation_generation(&reservations, None)
1129            .expect_err("retired cannot revive");
1130
1131        assert!(matches!(
1132            err,
1133            AllocationReservationError::RetiredAllocation { .. }
1134        ));
1135    }
1136
1137    #[test]
1138    fn stage_validated_generation_activates_reserved_record() {
1139        let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1140        let staged = ledger()
1141            .stage_reservation_generation(&reservations, None)
1142            .expect("reserved generation");
1143        let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
1144
1145        let staged = staged
1146            .stage_validated_generation(&active, None)
1147            .expect("active generation");
1148        let record = &staged.allocation_history.records()[0];
1149
1150        assert_eq!(record.state, AllocationState::Active);
1151        assert_eq!(record.first_generation, 4);
1152        assert_eq!(record.last_seen_generation, 5);
1153        assert_eq!(record.schema_history.len(), 2);
1154    }
1155
1156    #[test]
1157    fn stage_retirement_generation_tombstones_named_allocation() {
1158        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1159        let staged = ledger()
1160            .stage_validated_generation(&active, None)
1161            .expect("active generation");
1162        let retirement = AllocationRetirement::new(
1163            "app.users.v1",
1164            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1165        )
1166        .expect("retirement");
1167
1168        let staged = staged
1169            .stage_retirement_generation(&retirement, Some(42))
1170            .expect("retired generation");
1171        let record = &staged.allocation_history.records()[0];
1172
1173        assert_eq!(staged.current_generation, 5);
1174        assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1175        assert_eq!(
1176            staged.allocation_history.generations()[1].declaration_count,
1177            0
1178        );
1179    }
1180
1181    #[test]
1182    fn stage_retirement_generation_tombstones_reserved_allocation() {
1183        let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1184        let staged = ledger()
1185            .stage_reservation_generation(&reservations, None)
1186            .expect("reserved generation");
1187        let retirement = AllocationRetirement::new(
1188            "app.future_store.v1",
1189            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1190        )
1191        .expect("retirement");
1192
1193        let staged = staged
1194            .stage_retirement_generation(&retirement, Some(42))
1195            .expect("reserved retirement generation");
1196        let record = &staged.allocation_history.records()[0];
1197
1198        assert_eq!(staged.current_generation, 5);
1199        assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1200        assert_eq!(record.first_generation, 4);
1201        assert_eq!(
1202            staged.allocation_history.generations()[1].declaration_count(),
1203            0
1204        );
1205    }
1206
1207    #[test]
1208    fn stage_retirement_generation_rejects_generation_overflow() {
1209        let mut ledger = ledger();
1210        ledger.current_generation = u64::MAX;
1211        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1212        let retirement = AllocationRetirement::new(
1213            "app.users.v1",
1214            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1215        )
1216        .expect("retirement");
1217
1218        let err = ledger
1219            .stage_retirement_generation(&retirement, None)
1220            .expect_err("overflow must fail");
1221
1222        assert_eq!(
1223            err,
1224            AllocationRetirementError::GenerationOverflow {
1225                generation: u64::MAX
1226            }
1227        );
1228    }
1229
1230    #[test]
1231    fn stage_retirement_generation_requires_matching_slot() {
1232        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1233        let staged = ledger()
1234            .stage_validated_generation(&active, None)
1235            .expect("active generation");
1236        let retirement = AllocationRetirement::new(
1237            "app.users.v1",
1238            AllocationSlotDescriptor::memory_manager(101).expect("usable slot"),
1239        )
1240        .expect("retirement");
1241
1242        let err = staged
1243            .stage_retirement_generation(&retirement, None)
1244            .expect_err("slot mismatch");
1245
1246        assert!(matches!(
1247            err,
1248            AllocationRetirementError::SlotMismatch { .. }
1249        ));
1250    }
1251
1252    #[test]
1253    fn allocation_retirement_constructor_rejects_invalid_slot() {
1254        let err = AllocationRetirement::new(
1255            "app.users.v1",
1256            AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID),
1257        )
1258        .expect_err("invalid retirement slot must fail at construction");
1259
1260        assert!(matches!(
1261            err,
1262            AllocationRetirementError::MemoryManagerSlot(
1263                MemoryManagerSlotError::InvalidMemoryManagerId { id }
1264            ) if id == MEMORY_MANAGER_INVALID_ID
1265        ));
1266    }
1267
1268    #[test]
1269    fn stage_retirement_generation_rejects_invalid_decoded_slot() {
1270        let mut ledger = ledger();
1271        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1272        let mut retirement = AllocationRetirement::new(
1273            "app.users.v1",
1274            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1275        )
1276        .expect("retirement");
1277        retirement.slot =
1278            AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1279
1280        let err = ledger
1281            .stage_retirement_generation(&retirement, None)
1282            .expect_err("decoded invalid retirement must fail at the boundary");
1283
1284        assert!(matches!(
1285            err,
1286            AllocationRetirementError::MemoryManagerSlot(
1287                MemoryManagerSlotError::InvalidMemoryManagerId { id }
1288            ) if id == MEMORY_MANAGER_INVALID_ID
1289        ));
1290    }
1291
1292    #[test]
1293    fn stage_retirement_generation_rejects_invalid_decoded_stable_key() {
1294        let mut ledger = ledger();
1295        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1296        let retirement = AllocationRetirement::new(
1297            "app.users.v1",
1298            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1299        )
1300        .expect("retirement");
1301        let mut value = crate::test_cbor::to_value(retirement).expect("retirement value");
1302        *map_field_mut(value_map_mut(&mut value), "stable_key") =
1303            crate::test_cbor::Value::Text("App.users.v1".to_string());
1304        let bytes = crate::test_cbor::to_vec(&value).expect("retirement bytes");
1305        let retirement: AllocationRetirement =
1306            crate::test_cbor::from_slice(&bytes).expect("decoded DTO");
1307
1308        let err = ledger
1309            .stage_retirement_generation(&retirement, None)
1310            .expect_err("decoded invalid stable key must fail at the boundary");
1311
1312        assert!(matches!(err, AllocationRetirementError::Key(_)));
1313    }
1314
1315    #[test]
1316    fn snapshot_can_feed_validated_generation() {
1317        let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1318            .expect("snapshot");
1319        let (declarations, runtime_fingerprint) = snapshot.into_parts();
1320        let validated =
1321            crate::capability::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1322
1323        let staged = ledger()
1324            .stage_validated_generation(&validated, None)
1325            .expect("validated generation");
1326
1327        assert_eq!(staged.allocation_history.records().len(), 1);
1328    }
1329
1330    #[test]
1331    fn stage_validated_generation_records_runtime_fingerprint() {
1332        let validated = crate::capability::ValidatedAllocations::new(
1333            3,
1334            vec![declaration("app.users.v1", 100, None)],
1335            Some("wasm:abc123".to_string()),
1336        );
1337
1338        let staged = ledger()
1339            .stage_validated_generation(&validated, None)
1340            .expect("validated generation");
1341
1342        assert_eq!(
1343            staged.allocation_history.generations()[0].runtime_fingerprint,
1344            Some("wasm:abc123".to_string())
1345        );
1346    }
1347
1348    #[test]
1349    fn strict_committed_integrity_accepts_full_lifecycle() {
1350        let mut ledger = committed_ledger(0);
1351        ledger
1352            .validate_committed_integrity()
1353            .expect("genesis ledger with no history");
1354
1355        ledger = ledger
1356            .stage_validated_generation(
1357                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1358                Some(1),
1359            )
1360            .expect("first real commit after genesis");
1361        ledger
1362            .validate_committed_integrity()
1363            .expect("first real commit");
1364
1365        ledger = ledger
1366            .stage_validated_generation(
1367                &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1368                Some(2),
1369            )
1370            .expect("repeated active declaration");
1371        ledger
1372            .validate_committed_integrity()
1373            .expect("repeated active declaration");
1374        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1375
1376        ledger = ledger
1377            .stage_validated_generation(
1378                &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1379                Some(3),
1380            )
1381            .expect("schema drift");
1382        ledger
1383            .validate_committed_integrity()
1384            .expect("schema metadata drift");
1385        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1386
1387        ledger = ledger
1388            .stage_reservation_generation(
1389                &[declaration("app.future_store.v1", 101, Some(1))],
1390                Some(4),
1391            )
1392            .expect("reservation-only generation");
1393        ledger
1394            .validate_committed_integrity()
1395            .expect("reservation-only generation");
1396        assert_eq!(
1397            record(&ledger, "app.future_store.v1").state,
1398            AllocationState::Reserved
1399        );
1400
1401        ledger = ledger
1402            .stage_validated_generation(
1403                &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1404                Some(5),
1405            )
1406            .expect("reservation activation");
1407        ledger
1408            .validate_committed_integrity()
1409            .expect("reservation activation");
1410        assert_eq!(
1411            record(&ledger, "app.future_store.v1").state,
1412            AllocationState::Active
1413        );
1414
1415        let retirement = AllocationRetirement::new(
1416            "app.users.v1",
1417            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1418        )
1419        .expect("retirement");
1420        ledger = ledger
1421            .stage_retirement_generation(&retirement, Some(6))
1422            .expect("retirement generation");
1423        ledger
1424            .validate_committed_integrity()
1425            .expect("retirement generation");
1426        assert_eq!(ledger.current_generation, 6);
1427        assert_eq!(
1428            record(&ledger, "app.users.v1").state,
1429            AllocationState::Retired { generation: 6 }
1430        );
1431        assert_eq!(
1432            record(&ledger, "app.future_store.v1").last_seen_generation,
1433            5
1434        );
1435    }
1436
1437    #[derive(Clone, Copy, Debug)]
1438    enum Transition {
1439        DeclareUsers,
1440        DeclareOrders,
1441        ReserveAudit,
1442        ActivateAudit,
1443        RetireUsers,
1444        RetireAudit,
1445        EmptyValidated,
1446        EmptyReservation,
1447    }
1448
1449    const TRANSITIONS: [Transition; 8] = [
1450        Transition::DeclareUsers,
1451        Transition::DeclareOrders,
1452        Transition::ReserveAudit,
1453        Transition::ActivateAudit,
1454        Transition::RetireUsers,
1455        Transition::RetireAudit,
1456        Transition::EmptyValidated,
1457        Transition::EmptyReservation,
1458    ];
1459
1460    fn apply_transition(
1461        ledger: &AllocationLedger,
1462        transition: Transition,
1463        committed_at: u64,
1464    ) -> Result<AllocationLedger, String> {
1465        match transition {
1466            Transition::DeclareUsers => ledger
1467                .stage_validated_generation(
1468                    &validated(
1469                        ledger.current_generation,
1470                        vec![declaration("app.users.v1", 100, Some(1))],
1471                    ),
1472                    Some(committed_at),
1473                )
1474                .map_err(|err| err.to_string()),
1475            Transition::DeclareOrders => ledger
1476                .stage_validated_generation(
1477                    &validated(
1478                        ledger.current_generation,
1479                        vec![declaration("app.orders.v1", 101, Some(1))],
1480                    ),
1481                    Some(committed_at),
1482                )
1483                .map_err(|err| err.to_string()),
1484            Transition::ReserveAudit => ledger
1485                .stage_reservation_generation(
1486                    &[declaration("app.audit.v1", 102, Some(1))],
1487                    Some(committed_at),
1488                )
1489                .map_err(|err| err.to_string()),
1490            Transition::ActivateAudit => ledger
1491                .stage_validated_generation(
1492                    &validated(
1493                        ledger.current_generation,
1494                        vec![declaration("app.audit.v1", 102, Some(1))],
1495                    ),
1496                    Some(committed_at),
1497                )
1498                .map_err(|err| err.to_string()),
1499            Transition::RetireUsers => ledger
1500                .stage_retirement_generation(
1501                    &AllocationRetirement::new(
1502                        "app.users.v1",
1503                        AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1504                    )
1505                    .expect("retirement"),
1506                    Some(committed_at),
1507                )
1508                .map_err(|err| err.to_string()),
1509            Transition::RetireAudit => ledger
1510                .stage_retirement_generation(
1511                    &AllocationRetirement::new(
1512                        "app.audit.v1",
1513                        AllocationSlotDescriptor::memory_manager(102).expect("usable slot"),
1514                    )
1515                    .expect("retirement"),
1516                    Some(committed_at),
1517                )
1518                .map_err(|err| err.to_string()),
1519            Transition::EmptyValidated => ledger
1520                .stage_validated_generation(
1521                    &validated(ledger.current_generation, Vec::new()),
1522                    Some(committed_at),
1523                )
1524                .map_err(|err| err.to_string()),
1525            Transition::EmptyReservation => ledger
1526                .stage_reservation_generation(&[], Some(committed_at))
1527                .map_err(|err| err.to_string()),
1528        }
1529    }
1530
1531    fn check_transition_sequences(
1532        ledger: AllocationLedger,
1533        depth: usize,
1534        sequence: &mut Vec<Transition>,
1535    ) {
1536        ledger
1537            .validate_committed_integrity()
1538            .unwrap_or_else(|err| panic!("sequence {sequence:?} violated integrity: {err}"));
1539        if depth == 0 {
1540            return;
1541        }
1542
1543        for transition in TRANSITIONS {
1544            sequence.push(transition);
1545            let next = apply_transition(&ledger, transition, sequence.len() as u64)
1546                .unwrap_or_else(|_| ledger.clone());
1547            check_transition_sequences(next, depth - 1, sequence);
1548            sequence.pop();
1549        }
1550    }
1551
1552    #[test]
1553    fn transition_matrix_preserves_committed_invariants() {
1554        check_transition_sequences(committed_ledger(0), 4, &mut Vec::new());
1555    }
1556
1557    #[test]
1558    fn new_committed_requires_strict_generation_history() {
1559        let structurally_valid =
1560            AllocationLedger::new(3, AllocationHistory::default()).expect("structurally valid DTO");
1561
1562        assert_eq!(structurally_valid.current_generation, 3);
1563
1564        let err = AllocationLedger::new_committed(3, AllocationHistory::default())
1565            .expect_err("committed ledger needs generation history");
1566
1567        assert_eq!(
1568            err,
1569            LedgerIntegrityError::MissingCurrentGenerationRecord {
1570                current_generation: 3
1571            }
1572        );
1573    }
1574
1575    #[test]
1576    fn new_committed_preserves_structural_rejection_before_history_checks() {
1577        let history = AllocationHistory::from_parts(
1578            vec![
1579                active_record("app.users.v1", 100),
1580                active_record("app.users.v1", 100),
1581            ],
1582            Vec::new(),
1583        );
1584
1585        // Duplicate keys take precedence over duplicate slots and the missing
1586        // current-generation record, even with only one integrity pass.
1587        assert_eq!(
1588            AllocationLedger::new_committed(3, history).expect_err("duplicate records"),
1589            LedgerIntegrityError::DuplicateStableKey {
1590                stable_key: StableKey::parse("app.users.v1").unwrap(),
1591            }
1592        );
1593
1594        let valid = active_committed_ledger();
1595        assert_eq!(
1596            AllocationLedger::new_committed(
1597                valid.current_generation(),
1598                valid.allocation_history().clone(),
1599            )
1600            .expect("valid committed history"),
1601            valid
1602        );
1603    }
1604
1605    #[test]
1606    fn validate_integrity_rejects_duplicate_stable_keys() {
1607        let mut ledger = ledger();
1608        *ledger.allocation_history.records_mut() = vec![
1609            active_record("app.users.v1", 100),
1610            active_record("app.users.v1", 101),
1611        ];
1612
1613        let err = ledger.validate_integrity().expect_err("duplicate key");
1614
1615        assert_eq!(
1616            err,
1617            LedgerIntegrityError::DuplicateStableKey {
1618                stable_key: StableKey::parse("app.users.v1").unwrap(),
1619            }
1620        );
1621    }
1622
1623    #[test]
1624    fn validate_integrity_rejects_duplicate_slots() {
1625        let mut ledger = ledger();
1626        *ledger.allocation_history.records_mut() = vec![
1627            active_record("app.users.v1", 100),
1628            active_record("app.orders.v1", 100),
1629        ];
1630
1631        let err = ledger.validate_integrity().expect_err("duplicate slot");
1632
1633        assert_eq!(
1634            err,
1635            LedgerIntegrityError::DuplicateSlot {
1636                slot: Box::new(AllocationSlotDescriptor::memory_manager(100).unwrap()),
1637            }
1638        );
1639    }
1640
1641    #[test]
1642    fn validate_committed_integrity_rejects_decoded_invalid_stable_key() {
1643        let mut ledger = committed_ledger(1);
1644        ledger
1645            .allocation_history
1646            .records
1647            .push(active_record("app.users.v1", 100));
1648        let mut bytes = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
1649        let key_start = bytes
1650            .windows(b"app.users.v1".len())
1651            .position(|window| window == b"app.users.v1")
1652            .expect("encoded stable key");
1653        bytes[key_start] = b'A';
1654        let decoded: AllocationLedger =
1655            crate::test_cbor::from_slice(&bytes).expect("decode ledger");
1656
1657        let err = decoded
1658            .validate_committed_integrity()
1659            .expect_err("invalid decoded key must fail");
1660
1661        assert!(matches!(err, LedgerIntegrityError::InvalidStableKey(_)));
1662    }
1663
1664    #[test]
1665    fn validate_committed_integrity_rejects_decoded_invalid_memory_manager_slot() {
1666        let mut ledger = committed_ledger(1);
1667        let mut record = active_record("app.users.v1", 100);
1668        record.slot = AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1669        ledger.allocation_history.records.push(record);
1670
1671        let err = ledger
1672            .validate_committed_integrity()
1673            .expect_err("invalid decoded slot must fail");
1674
1675        assert!(matches!(
1676            err,
1677            LedgerIntegrityError::InvalidSlotDescriptor(
1678                MemoryManagerSlotError::InvalidMemoryManagerId { id }
1679            ) if id == MEMORY_MANAGER_INVALID_ID
1680        ));
1681    }
1682
1683    #[test]
1684    fn validate_integrity_rejects_retirement_before_last_observation() {
1685        let mut ledger = committed_ledger(5);
1686        let mut record = active_record("app.users.v1", 100);
1687        record.last_seen_generation = 4;
1688        record.state = AllocationState::Retired { generation: 3 };
1689        *ledger.allocation_history.records_mut() = vec![record];
1690
1691        let err = ledger
1692            .validate_committed_integrity()
1693            .expect_err("retirement must follow the last observation");
1694
1695        assert!(matches!(
1696            err,
1697            LedgerIntegrityError::RetirementNotAfterLastSeen { .. }
1698        ));
1699    }
1700
1701    #[test]
1702    fn validate_integrity_rejects_retirement_before_allocation() {
1703        let mut ledger = committed_ledger(2);
1704        let mut record = active_record("app.users.v1", 100);
1705        record.state = AllocationState::Retired { generation: 0 };
1706        *ledger.allocation_history.records_mut() = vec![record];
1707
1708        let err = ledger
1709            .validate_committed_integrity()
1710            .expect_err("retirement cannot predate allocation");
1711
1712        assert!(matches!(
1713            err,
1714            LedgerIntegrityError::RetiredBeforeFirstGeneration { .. }
1715        ));
1716    }
1717
1718    #[test]
1719    fn validate_integrity_requires_schema_history_at_first_generation() {
1720        let mut ledger = committed_ledger(2);
1721        let mut record = active_record("app.users.v1", 100);
1722        record.last_seen_generation = 2;
1723        record.schema_history[0].generation = 2;
1724        *ledger.allocation_history.records_mut() = vec![record];
1725
1726        let err = ledger
1727            .validate_committed_integrity()
1728            .expect_err("schema history must begin with the allocation");
1729
1730        assert!(matches!(
1731            err,
1732            LedgerIntegrityError::SchemaHistoryStartMismatch { .. }
1733        ));
1734    }
1735
1736    #[test]
1737    fn validate_integrity_rejects_schema_history_after_last_observation() {
1738        let mut ledger = committed_ledger(3);
1739        let mut record = active_record("app.users.v1", 100);
1740        record.last_seen_generation = 2;
1741        record.schema_history.push(
1742            SchemaMetadataRecord::new(3, SchemaMetadata::new(Some(2)).expect("schema"))
1743                .expect("schema record"),
1744        );
1745        *ledger.allocation_history.records_mut() = vec![record];
1746
1747        let err = ledger
1748            .validate_committed_integrity()
1749            .expect_err("schema metadata cannot postdate the last observation");
1750
1751        assert!(matches!(
1752            err,
1753            LedgerIntegrityError::SchemaHistoryAfterLastSeen { .. }
1754        ));
1755    }
1756
1757    #[test]
1758    fn validate_integrity_rejects_non_increasing_schema_history() {
1759        let mut ledger = ledger();
1760        let mut record = active_record("app.users.v1", 100);
1761        record.schema_history.push(SchemaMetadataRecord {
1762            generation: 1,
1763            schema: SchemaMetadata::default(),
1764        });
1765        *ledger.allocation_history.records_mut() = vec![record];
1766
1767        let err = ledger
1768            .validate_integrity()
1769            .expect_err("non-increasing schema history");
1770
1771        assert!(matches!(
1772            err,
1773            LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1774        ));
1775    }
1776
1777    #[test]
1778    fn validate_integrity_rejects_invalid_schema_metadata_history() {
1779        let mut ledger = committed_ledger(1);
1780        let mut record = active_record("app.users.v1", 100);
1781        record.schema_history[0].schema = invalid_schema_metadata();
1782        *ledger.allocation_history.records_mut() = vec![record];
1783
1784        let err = ledger
1785            .validate_committed_integrity()
1786            .expect_err("invalid committed schema metadata");
1787
1788        assert_eq!(
1789            err,
1790            LedgerIntegrityError::InvalidSchemaMetadata {
1791                stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1792                generation: 1,
1793                error: SchemaMetadataError::InvalidVersion,
1794            }
1795        );
1796    }
1797
1798    #[test]
1799    fn validate_committed_integrity_requires_current_generation_record() {
1800        let err = ledger()
1801            .validate_committed_integrity()
1802            .expect_err("missing current generation");
1803
1804        assert_eq!(
1805            err,
1806            LedgerIntegrityError::MissingCurrentGenerationRecord {
1807                current_generation: 3
1808            }
1809        );
1810    }
1811
1812    #[test]
1813    fn committed_integrity_rejects_allocation_references_to_genesis() {
1814        for generation in [0, 1] {
1815            let mut ledger = committed_ledger(generation);
1816            ledger.allocation_history.push_record(
1817                AllocationRecord::active(0, declaration("app.genesis.v1", 100, None)).unwrap(),
1818            );
1819            assert_eq!(
1820                ledger.validate_committed_integrity(),
1821                Err(LedgerIntegrityError::UnknownRecordGeneration {
1822                    stable_key: StableKey::parse("app.genesis.v1").unwrap(),
1823                    generation: 0,
1824                })
1825            );
1826        }
1827    }
1828
1829    #[test]
1830    fn validate_committed_integrity_rejects_generation_history_gaps() {
1831        let mut ledger = committed_ledger(3);
1832        ledger.allocation_history.generations_mut().remove(1);
1833
1834        let err = ledger
1835            .validate_committed_integrity()
1836            .expect_err("generation history gap");
1837
1838        assert!(matches!(
1839            err,
1840            LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1841        ));
1842    }
1843
1844    #[test]
1845    fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1846        let mut store = LedgerCommitStore::default();
1847        let mut invalid = ledger();
1848        *invalid.allocation_history.records_mut() = vec![
1849            active_record("app.users.v1", 100),
1850            active_record("app.orders.v1", 100),
1851        ];
1852
1853        let err = store.commit(&invalid).expect_err("invalid ledger");
1854
1855        assert!(matches!(
1856            err,
1857            LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1858        ));
1859        assert!(store.physical().is_uninitialized());
1860    }
1861
1862    #[test]
1863    fn ledger_commit_store_recovers_latest_committed_ledger() {
1864        let mut store = LedgerCommitStore::default();
1865        let first = committed_ledger(1);
1866        let second = committed_ledger(2);
1867
1868        store.commit(&first).expect("first commit");
1869        store.commit(&second).expect("second commit");
1870        let recovered = store.recover().expect("recovered ledger");
1871
1872        assert_eq!(recovered.current_generation(), 2);
1873        assert_eq!(recovered.physical_generation(), 2);
1874    }
1875
1876    #[test]
1877    fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1878        let mut store = LedgerCommitStore::default();
1879        let ledger = committed_ledger(1);
1880
1881        store.commit(&ledger).expect("commit");
1882
1883        let committed = store.physical().authoritative().expect("authoritative");
1884        let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1885        assert_eq!(
1886            envelope.payload(),
1887            CborLedgerCodec::encode(&ledger).expect("payload")
1888        );
1889    }
1890
1891    #[test]
1892    fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1893        let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1894        let store = LedgerCommitStore {
1895            physical: DualCommitStore {
1896                slot0: Some(committed),
1897                slot1: None,
1898            },
1899        };
1900
1901        let err = store.recover().expect_err("bad envelope");
1902
1903        assert!(matches!(
1904            err,
1905            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1906        ));
1907    }
1908
1909    #[test]
1910    fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1911        let mut payload = enveloped_payload(&committed_ledger(1));
1912        payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1913        let store = LedgerCommitStore {
1914            physical: DualCommitStore {
1915                slot0: Some(CommittedGenerationBytes::new(1, payload)),
1916                slot1: None,
1917            },
1918        };
1919
1920        let err = store
1921            .recover()
1922            .expect_err("unknown ledger format version must fail");
1923
1924        assert_eq!(
1925            err,
1926            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1927                marker: *b"ICMF",
1928                version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1929            })
1930        );
1931    }
1932
1933    #[test]
1934    fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
1935        let genesis = enveloped_payload(&committed_ledger(0));
1936        let committed = CommittedGenerationBytes::new(0, genesis.clone());
1937        let mut corrupt = committed.clone();
1938        corrupt.checksum ^= 1;
1939        let mut unsupported = genesis.clone();
1940        unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1941        let cases = [
1942            ("empty", None, None),
1943            ("one slot", Some(committed.clone()), None),
1944            (
1945                "identical slots",
1946                Some(committed.clone()),
1947                Some(committed.clone()),
1948            ),
1949            (
1950                "corrupt inactive",
1951                Some(committed.clone()),
1952                Some(corrupt.clone()),
1953            ),
1954            ("corrupt active", Some(corrupt), Some(committed.clone())),
1955            (
1956                "ambiguous",
1957                Some(committed),
1958                Some(CommittedGenerationBytes::new(0, vec![1])),
1959            ),
1960            (
1961                "unsupported format",
1962                Some(CommittedGenerationBytes::new(0, unsupported)),
1963                None,
1964            ),
1965            (
1966                "invalid CBOR",
1967                Some(CommittedGenerationBytes::new(
1968                    0,
1969                    LedgerPayloadEnvelope::current(vec![255])
1970                        .try_encode()
1971                        .unwrap(),
1972                )),
1973                None,
1974            ),
1975            (
1976                "generation mismatch",
1977                Some(CommittedGenerationBytes::new(1, genesis)),
1978                None,
1979            ),
1980            (
1981                "invalid committed integrity",
1982                Some(CommittedGenerationBytes::new(
1983                    3,
1984                    enveloped_payload(&ledger()),
1985                )),
1986                None,
1987            ),
1988        ];
1989
1990        for (label, slot0, slot1) in cases {
1991            let empty = slot0.is_none() && slot1.is_none();
1992            let store = LedgerCommitStore {
1993                physical: DualCommitStore { slot0, slot1 },
1994            };
1995            let before = store.clone();
1996            let (recovered, diagnostic) = store.recover_with_diagnostic();
1997            assert_eq!(recovered, store.recover(), "{label}");
1998            assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
1999            assert_eq!(store, before, "{label}");
2000
2001            let mut initializing = store.clone();
2002            let initialized = initializing.recover_or_initialize(&committed_ledger(0));
2003            if empty {
2004                let initialized = initialized.expect("empty store accepts genesis");
2005                assert_eq!(initialized.current_generation(), 0);
2006                assert_eq!(initialized.physical_generation(), 0);
2007                assert_eq!(initializing.recover().unwrap(), initialized);
2008            } else {
2009                assert_eq!(initialized, recovered, "{label}");
2010                assert_eq!(initializing, before, "{label}");
2011            }
2012
2013            if let Err(LedgerCommitError::Recovery(error)) = recovered {
2014                assert_eq!(diagnostic.recovery, Err(error), "{label}");
2015            } else {
2016                assert!(diagnostic.recovery.is_ok(), "{label}");
2017            }
2018        }
2019    }
2020
2021    #[test]
2022    fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
2023        let mut store = LedgerCommitStore::default();
2024        let genesis = committed_ledger(0);
2025
2026        let recovered = store
2027            .recover_or_initialize(&genesis)
2028            .expect("current-format genesis ledger");
2029        assert_eq!(recovered.current_generation(), 0);
2030        assert_eq!(recovered.physical_generation(), 0);
2031        assert_eq!(recovered.ledger().allocation_history().generations(), []);
2032        assert_eq!(recovered, store.recover().unwrap());
2033
2034        let first = recovered
2035            .ledger()
2036            .stage_validated_generation(
2037                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2038                None,
2039            )
2040            .expect("first real generation");
2041        let recovered = store.commit(&first).expect("first commit");
2042
2043        assert_eq!(recovered, store.recover().unwrap());
2044        assert_eq!(recovered.current_generation(), 1);
2045        assert_eq!(recovered.physical_generation(), 1);
2046        assert_eq!(
2047            recovered.ledger().allocation_history().generations()[0].generation,
2048            1
2049        );
2050        assert_eq!(
2051            record(recovered.ledger(), "app.users.v1").first_generation,
2052            1
2053        );
2054    }
2055
2056    #[test]
2057    fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
2058        let mut store = LedgerCommitStore::default();
2059        let genesis = committed_ledger(0);
2060        store.commit(&genesis).expect("genesis commit");
2061        let first = genesis
2062            .stage_validated_generation(
2063                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2064                None,
2065            )
2066            .expect("first generation");
2067        let first = store.commit(&first).expect("first commit");
2068        let second = first
2069            .ledger()
2070            .stage_validated_generation(
2071                &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
2072                None,
2073            )
2074            .expect("second generation");
2075
2076        store.commit(&second).expect("second commit");
2077        store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
2078
2079        let err = store
2080            .recover()
2081            .expect_err("corrupt latest must not roll back");
2082
2083        assert_eq!(
2084            err,
2085            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2086                slot0_invalid: true,
2087                slot1_invalid: false,
2088            })
2089        );
2090    }
2091
2092    #[test]
2093    fn ledger_commit_store_recovers_identical_duplicate_slots() {
2094        let ledger = committed_ledger(0)
2095            .stage_validated_generation(
2096                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2097                None,
2098            )
2099            .expect("first generation");
2100        let payload = enveloped_payload(&ledger);
2101        let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
2102        let store = LedgerCommitStore {
2103            physical: DualCommitStore {
2104                slot0: Some(committed.clone()),
2105                slot1: Some(committed),
2106            },
2107        };
2108
2109        let recovered = store.recover().expect("recovered");
2110
2111        assert_eq!(recovered.ledger(), &ledger);
2112    }
2113
2114    #[test]
2115    fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
2116        let mut store = LedgerCommitStore::default();
2117        let first = committed_ledger(1);
2118        let second = committed_ledger(2);
2119
2120        store.commit(&first).expect("first commit");
2121        store
2122            .write_corrupt_inactive_ledger(&second)
2123            .expect("corrupt write");
2124        let err = store.recover().expect_err("corrupt slot");
2125
2126        assert_eq!(
2127            err,
2128            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2129                slot0_invalid: false,
2130                slot1_invalid: true,
2131            })
2132        );
2133    }
2134
2135    #[test]
2136    fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
2137        let store = LedgerCommitStore {
2138            physical: DualCommitStore {
2139                slot0: Some(CommittedGenerationBytes::new(
2140                    7,
2141                    enveloped_payload(&committed_ledger(6)),
2142                )),
2143                slot1: None,
2144            },
2145        };
2146        let err = store.recover().expect_err("mismatch");
2147
2148        assert_eq!(
2149            err,
2150            LedgerCommitError::PhysicalLogicalGenerationMismatch {
2151                physical_generation: 7,
2152                logical_generation: 6
2153            }
2154        );
2155    }
2156
2157    #[test]
2158    fn ledger_commit_store_rejects_non_next_logical_generation() {
2159        let mut store = LedgerCommitStore::default();
2160        store.commit(&committed_ledger(1)).expect("first commit");
2161
2162        let err = store
2163            .commit(&committed_ledger(3))
2164            .expect_err("skipped generation");
2165
2166        assert_eq!(
2167            err,
2168            LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2169                expected: 2,
2170                actual: 3
2171            })
2172        );
2173    }
2174
2175    #[test]
2176    fn ledger_commit_store_initializes_empty_store_explicitly() {
2177        let mut store = LedgerCommitStore::default();
2178        let genesis = committed_ledger(3);
2179
2180        let recovered = store
2181            .recover_or_initialize(&genesis)
2182            .expect("initialized ledger");
2183
2184        assert_eq!(recovered.current_generation(), 3);
2185        assert_eq!(recovered.physical_generation(), 3);
2186        assert!(!store.physical().is_uninitialized());
2187    }
2188
2189    #[test]
2190    fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2191        let mut store = LedgerCommitStore::default();
2192        store
2193            .write_corrupt_inactive_ledger(&ledger())
2194            .expect("corrupt write");
2195
2196        let err = store
2197            .recover_or_initialize(&ledger())
2198            .expect_err("corrupt state");
2199
2200        assert!(matches!(
2201            err,
2202            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2203                slot0_invalid: true,
2204                slot1_invalid: false,
2205            })
2206        ));
2207    }
2208    #[test]
2209    fn history_boundary_round_trips_and_rejects_next_generation_without_mutation() {
2210        let ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2211        let mut store = LedgerCommitStore::default();
2212        let recovered = store.commit(&ledger).expect("boundary is admissible");
2213        let record = crate::StableCellLedgerRecord::new(store.clone());
2214        let bytes = crate::test_cbor::to_vec(&record).unwrap();
2215        assert_eq!(record.encoded_size(), bytes.len());
2216        let recovered_record = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2217        assert_eq!(
2218            recovered_record.store().recover().unwrap().ledger(),
2219            &ledger
2220        );
2221        let before = store.clone();
2222        let err = crate::AllocationBootstrap::new(&mut store)
2223            .validate_and_commit(
2224                DeclarationSnapshot::new(Vec::new()).unwrap(),
2225                &crate::GenericRangePolicy,
2226                None,
2227            )
2228            .unwrap_err();
2229        assert!(matches!(
2230            err,
2231            crate::BootstrapError::Staging(AllocationStageError::Integrity(
2232                LedgerIntegrityError::LimitExceeded { .. }
2233            ))
2234        ));
2235        assert_eq!(store, before);
2236        assert_eq!(
2237            recovered.current_generation(),
2238            crate::constants::MAX_LEDGER_GENERATIONS as u64
2239        );
2240        let oversize = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 + 1);
2241        assert!(matches!(
2242            store.commit(&oversize),
2243            Err(LedgerCommitError::Integrity(
2244                LedgerIntegrityError::LimitExceeded { .. }
2245            ))
2246        ));
2247        assert_eq!(store, before);
2248    }
2249
2250    #[test]
2251    fn record_count_boundary_and_malicious_history_reject_on_recovery() {
2252        let mut ledger = committed_ledger(1);
2253        ledger.allocation_history.records = (0..255_u8)
2254            .map(|id| active_record(&format!("app.slot{id}.v1"), id))
2255            .collect();
2256        let mut store = LedgerCommitStore::default();
2257        store.commit(&ledger).unwrap();
2258        assert_eq!(
2259            store
2260                .recover()
2261                .unwrap()
2262                .ledger()
2263                .allocation_history()
2264                .records()
2265                .len(),
2266            255
2267        );
2268        ledger
2269            .allocation_history
2270            .records
2271            .push(active_record("app.extra.v1", 254));
2272        assert!(matches!(
2273            store.commit(&ledger),
2274            Err(LedgerCommitError::Integrity(
2275                LedgerIntegrityError::LimitExceeded { .. }
2276            ))
2277        ));
2278        let payload = enveloped_payload(&ledger);
2279        let mut invalid = LedgerCommitStore {
2280            physical: DualCommitStore {
2281                slot0: Some(CommittedGenerationBytes::new(1, payload)),
2282                slot1: None,
2283            },
2284        };
2285        assert!(invalid.recover().is_err());
2286        assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
2287        for bytes in [
2288            vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2289            vec![0x81; 34],
2290            vec![0xa1],
2291        ] {
2292            let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
2293            let invalid = LedgerCommitStore {
2294                physical: DualCommitStore {
2295                    slot0: Some(CommittedGenerationBytes::new(1, payload)),
2296                    slot1: None,
2297                },
2298            };
2299            assert!(matches!(
2300                invalid.recover(),
2301                Err(LedgerCommitError::Codec(_))
2302            ));
2303        }
2304    }
2305    #[test]
2306    fn encoded_byte_limit_rejects_before_commit_mutation() {
2307        let mut store = LedgerCommitStore::default();
2308        store.commit(&committed_ledger(0)).unwrap();
2309        let before = store.clone();
2310        let mut ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2311        for generation in ledger.allocation_history.generations_mut() {
2312            generation.runtime_fingerprint =
2313                Some("x".repeat(crate::constants::DIAGNOSTIC_STRING_MAX_BYTES));
2314        }
2315        assert!(matches!(
2316            store.commit(&ledger),
2317            Err(LedgerCommitError::Integrity(
2318                LedgerIntegrityError::LimitExceeded {
2319                    resource: "ledger bytes",
2320                    ..
2321                }
2322            ))
2323        ));
2324        assert_eq!(store, before);
2325        let mut envelope = LedgerPayloadEnvelope::current(Vec::new())
2326            .try_encode()
2327            .unwrap();
2328        envelope[16..24]
2329            .copy_from_slice(&((crate::constants::MAX_LEDGER_BYTES + 1) as u64).to_le_bytes());
2330        assert!(matches!(
2331            LedgerPayloadEnvelope::decode(&envelope),
2332            Err(LedgerPayloadEnvelopeError::PayloadTooLarge { .. })
2333        ));
2334    }
2335}