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