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