Skip to main content

ic_memory/ledger/
mod.rs

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