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