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