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