Skip to main content

ic_memory/ledger/
mod.rs

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