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