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