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