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