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