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