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::validate_reservation_declaration;
30pub use stage::{
31    stage_reservation_generation, stage_retirement_generation, stage_validated_generation,
32};
33
34fn decode_ledger(bytes: &[u8]) -> Result<AllocationLedger, String> {
35    crate::cbor::from_slice_exact(bytes).map_err(|err| err.to_string())
36}
37
38///
39/// LedgerCommitStore
40///
41/// Generation-scoped allocation ledger commit store.
42///
43/// This type owns the logical commit lifecycle and the crate-owned CBOR ledger
44/// encoding. It deliberately does not own stable-memory IO; that remains the
45/// substrate or framework owner's responsibility.
46///
47/// This store commits allocation ledger generations. It does not open
48/// stable-memory handles and does not allocate application slots.
49///
50#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
51#[serde(deny_unknown_fields)]
52pub struct LedgerCommitStore {
53    /// Protected physical commit slots.
54    physical: DualCommitStore,
55}
56
57impl LedgerCommitStore {
58    /// Borrow the protected physical commit store for diagnostics.
59    #[must_use]
60    pub const fn physical(&self) -> &DualCommitStore {
61        &self.physical
62    }
63
64    /// Recover the authoritative allocation ledger using the native CBOR ledger codec.
65    pub fn recover(&self) -> Result<RecoveredLedger, LedgerCommitError> {
66        let committed = self
67            .physical
68            .authoritative()
69            .map_err(LedgerCommitError::Recovery)?;
70        Self::recover_committed(committed)
71    }
72
73    pub(crate) fn recover_with_diagnostic(
74        &self,
75    ) -> (
76        Result<RecoveredLedger, LedgerCommitError>,
77        CommitStoreDiagnostic,
78    ) {
79        let (committed, diagnostic) = self.physical.authoritative_with_diagnostic();
80        let recovered = committed
81            .map_err(LedgerCommitError::Recovery)
82            .and_then(Self::recover_committed);
83        (recovered, diagnostic)
84    }
85
86    fn recover_committed(
87        committed: &CommittedGenerationBytes,
88    ) -> Result<RecoveredLedger, LedgerCommitError> {
89        let payload = LedgerPayloadEnvelope::decode_payload(committed.payload())
90            .map_err(LedgerCommitError::PayloadEnvelope)?;
91        let ledger = decode_ledger(payload).map_err(LedgerCommitError::Codec)?;
92        if committed.generation() != ledger.current_generation {
93            return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
94                physical_generation: committed.generation(),
95                logical_generation: ledger.current_generation,
96            });
97        }
98        ledger
99            .validate_committed_integrity()
100            .map_err(LedgerCommitError::Integrity)?;
101        Ok(RecoveredLedger::from_trusted_ledger(ledger))
102    }
103
104    /// Recover the authoritative ledger, or explicitly initialize an empty store.
105    ///
106    /// Initialization is allowed only when no physical commit slot has ever
107    /// been written. Corrupt or partially written stores fail closed even when
108    /// a genesis ledger is supplied.
109    ///
110    /// Supplying a non-empty `genesis` is a privileged import/migration action.
111    /// Normal runtime bootstraps should seed an empty current-format ledger.
112    pub fn recover_or_initialize(
113        &mut self,
114        genesis: &AllocationLedger,
115    ) -> Result<RecoveredLedger, LedgerCommitError> {
116        match self.recover() {
117            Ok(ledger) => Ok(ledger),
118            // Physical selection returns this error only when both slots are absent.
119            Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)) => {
120                self.commit(genesis)
121            }
122            Err(err) => Err(err),
123        }
124    }
125
126    /// Commit one logical allocation ledger generation through the native CBOR ledger codec.
127    ///
128    /// # Panics
129    ///
130    /// Panics only if the private concrete ledger-encoding invariant is broken.
131    pub fn commit(
132        &mut self,
133        ledger: &AllocationLedger,
134    ) -> Result<RecoveredLedger, LedgerCommitError> {
135        self.commit_generation(ledger)?;
136        // The current writer encoded this integrity-checked ledger, and the
137        // physical commit checked its predecessor and established this generation.
138        // Existing persisted bytes still cross the full recovery boundary.
139        Ok(RecoveredLedger::from_trusted_ledger(ledger.clone()))
140    }
141
142    // Bootstrap already owns its staged ledger. Share the checked mutation
143    // without cloning that ledger into a proof only to unwrap it immediately.
144    pub(crate) fn commit_generation(
145        &mut self,
146        ledger: &AllocationLedger,
147    ) -> Result<(), LedgerCommitError> {
148        ledger
149            .validate_committed_integrity()
150            .map_err(LedgerCommitError::Integrity)?;
151        let payload =
152            LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
153        self.physical
154            .commit_payload_at_generation(ledger.current_generation, payload)
155            .map(|_| ())
156            .map_err(LedgerCommitError::Recovery)
157    }
158
159    /// Simulate corruption of a logical ledger payload in the inactive slot.
160    #[cfg(test)]
161    pub fn write_corrupt_inactive_ledger(
162        &mut self,
163        ledger: &AllocationLedger,
164    ) -> Result<(), LedgerCommitError> {
165        let payload =
166            LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
167        self.physical
168            .write_corrupt_inactive_slot(ledger.current_generation, payload);
169        Ok(())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::test_cbor::hex_fixture;
177    use crate::{
178        declaration::{AllocationDeclaration, DeclarationSnapshot, DeclarationSnapshotError},
179        key::StableKey,
180        physical::CommittedGenerationBytes,
181        schema::SchemaMetadata,
182        slot::MemoryManagerSlot,
183    };
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_invalid_decoded_label() {
962        let mut value = serde_json::to_value(declaration("app.future.v1", 100, Some(1))).unwrap();
963        value["label"] = "".into();
964        let reservation = serde_json::from_value(value).unwrap();
965        let source = ledger();
966        assert_eq!(
967            source.stage_reservation_generation(&[reservation], None),
968            Err(AllocationReservationError::InvalidDeclaration(
969                DeclarationSnapshotError::EmptyLabel,
970            ))
971        );
972        assert_eq!(source, ledger());
973    }
974
975    #[test]
976    fn stage_reservation_generation_rejects_same_key_different_slot() {
977        let mut ledger = ledger();
978        ledger.allocation_history.records = vec![AllocationRecord::reserved(
979            3,
980            &declaration("app.future_store.v1", 100, None),
981        )];
982        let reservations = vec![declaration("app.future_store.v1", 101, None)];
983
984        let err = ledger
985            .stage_reservation_generation(&reservations, None)
986            .expect_err("reservation key cannot move slots");
987
988        assert!(matches!(
989            err,
990            AllocationReservationError::StableKeySlotConflict { .. }
991        ));
992    }
993
994    #[test]
995    fn stage_reservation_generation_rejects_same_slot_different_key() {
996        let mut ledger = ledger();
997        ledger.allocation_history.records = vec![AllocationRecord::reserved(
998            3,
999            &declaration("app.future_store.v1", 100, None),
1000        )];
1001        let reservations = vec![declaration("app.other_future_store.v1", 100, None)];
1002
1003        let err = ledger
1004            .stage_reservation_generation(&reservations, None)
1005            .expect_err("reservation slot cannot be reused by another key");
1006
1007        assert!(matches!(
1008            err,
1009            AllocationReservationError::SlotStableKeyConflict { .. }
1010        ));
1011    }
1012
1013    #[test]
1014    fn stage_reservation_generation_rejects_active_allocation() {
1015        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1016        let staged = ledger()
1017            .stage_validated_generation(&active, None)
1018            .expect("active generation");
1019        let reservations = vec![declaration("app.users.v1", 100, None)];
1020
1021        let err = staged
1022            .stage_reservation_generation(&reservations, None)
1023            .expect_err("active cannot become reserved");
1024
1025        assert!(matches!(
1026            err,
1027            AllocationReservationError::ActiveAllocation { .. }
1028        ));
1029    }
1030
1031    #[test]
1032    fn stage_reservation_generation_rejects_retired_allocation() {
1033        let mut ledger = ledger();
1034        let mut record = active_record("app.users.v1", 100);
1035        record.state = AllocationState::Retired { generation: 3 };
1036        ledger.allocation_history.records = vec![record];
1037        let reservations = vec![declaration("app.users.v1", 100, None)];
1038
1039        let err = ledger
1040            .stage_reservation_generation(&reservations, None)
1041            .expect_err("retired cannot revive");
1042
1043        assert!(matches!(
1044            err,
1045            AllocationReservationError::RetiredAllocation { .. }
1046        ));
1047    }
1048
1049    #[test]
1050    fn stage_validated_generation_activates_reserved_record() {
1051        let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1052        let staged = ledger()
1053            .stage_reservation_generation(&reservations, None)
1054            .expect("reserved generation");
1055        let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
1056
1057        let staged = staged
1058            .stage_validated_generation(&active, None)
1059            .expect("active generation");
1060        let record = &staged.allocation_history.records()[0];
1061
1062        assert_eq!(record.state, AllocationState::Active);
1063        assert_eq!(record.first_generation, 4);
1064        assert_eq!(record.last_seen_generation, 5);
1065        assert_eq!(record.schema_history.len(), 2);
1066    }
1067
1068    #[test]
1069    fn stage_retirement_generation_tombstones_named_allocation() {
1070        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1071        let staged = ledger()
1072            .stage_validated_generation(&active, None)
1073            .expect("active generation");
1074        let retirement = AllocationRetirement::new(
1075            "app.users.v1",
1076            MemoryManagerSlot::new(100).expect("usable slot"),
1077        )
1078        .expect("retirement");
1079
1080        let staged = staged
1081            .stage_retirement_generation(&retirement, Some(42))
1082            .expect("retired generation");
1083        let record = &staged.allocation_history.records()[0];
1084
1085        assert_eq!(staged.current_generation, 5);
1086        assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1087        assert_eq!(
1088            staged.allocation_history.generations()[1].declaration_count,
1089            0
1090        );
1091    }
1092
1093    #[test]
1094    fn stage_retirement_generation_tombstones_reserved_allocation() {
1095        let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1096        let staged = ledger()
1097            .stage_reservation_generation(&reservations, None)
1098            .expect("reserved generation");
1099        let retirement = AllocationRetirement::new(
1100            "app.future_store.v1",
1101            MemoryManagerSlot::new(100).expect("usable slot"),
1102        )
1103        .expect("retirement");
1104
1105        let staged = staged
1106            .stage_retirement_generation(&retirement, Some(42))
1107            .expect("reserved retirement generation");
1108        let record = &staged.allocation_history.records()[0];
1109
1110        assert_eq!(staged.current_generation, 5);
1111        assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1112        assert_eq!(record.first_generation, 4);
1113        assert_eq!(
1114            staged.allocation_history.generations()[1].declaration_count(),
1115            0
1116        );
1117    }
1118
1119    #[test]
1120    fn stage_retirement_generation_rejects_generation_overflow() {
1121        let mut ledger = ledger();
1122        ledger.current_generation = u64::MAX;
1123        ledger.allocation_history.records = vec![active_record("app.users.v1", 100)];
1124        let retirement = AllocationRetirement::new(
1125            "app.users.v1",
1126            MemoryManagerSlot::new(100).expect("usable slot"),
1127        )
1128        .expect("retirement");
1129
1130        let err = ledger
1131            .stage_retirement_generation(&retirement, None)
1132            .expect_err("overflow must fail");
1133
1134        assert_eq!(
1135            err,
1136            AllocationRetirementError::GenerationOverflow {
1137                generation: u64::MAX
1138            }
1139        );
1140    }
1141
1142    #[test]
1143    fn stage_retirement_generation_requires_matching_slot() {
1144        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1145        let staged = ledger()
1146            .stage_validated_generation(&active, None)
1147            .expect("active generation");
1148        let retirement = AllocationRetirement::new(
1149            "app.users.v1",
1150            MemoryManagerSlot::new(101).expect("usable slot"),
1151        )
1152        .expect("retirement");
1153
1154        let err = staged
1155            .stage_retirement_generation(&retirement, None)
1156            .expect_err("slot mismatch");
1157
1158        assert!(matches!(
1159            err,
1160            AllocationRetirementError::SlotMismatch { .. }
1161        ));
1162    }
1163
1164    #[test]
1165    fn retirement_rejections_preserve_source_and_slot_error_precedence() {
1166        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1167        let source = ledger().stage_validated_generation(&active, None).unwrap();
1168        let retirement =
1169            AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
1170                .unwrap();
1171        let source = source
1172            .stage_retirement_generation(&retirement, None)
1173            .unwrap();
1174        let before = source.clone();
1175
1176        assert!(matches!(
1177            source.stage_retirement_generation(&retirement, None),
1178            Err(AllocationRetirementError::AlreadyRetired { .. })
1179        ));
1180        let wrong_slot =
1181            AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(101).unwrap())
1182                .unwrap();
1183        assert!(matches!(
1184            source.stage_retirement_generation(&wrong_slot, None),
1185            Err(AllocationRetirementError::SlotMismatch { .. })
1186        ));
1187        let unknown =
1188            AllocationRetirement::new("app.unknown.v1", MemoryManagerSlot::new(100).unwrap())
1189                .unwrap();
1190        assert!(matches!(
1191            source.stage_retirement_generation(&unknown, None),
1192            Err(AllocationRetirementError::UnknownStableKey(_))
1193        ));
1194        assert_eq!(source, before);
1195    }
1196
1197    #[test]
1198    fn allocation_retirement_constructor_rejects_invalid_key() {
1199        let err = AllocationRetirement::new("App.users.v1", MemoryManagerSlot::new(100).unwrap())
1200            .expect_err("invalid key must fail at construction");
1201        assert!(matches!(err, AllocationRetirementError::Key(_)));
1202    }
1203
1204    #[test]
1205    fn retirement_decode_rejects_unusable_memory_manager_slot() {
1206        let retirement =
1207            AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
1208                .unwrap();
1209        let mut value = serde_json::to_value(retirement).unwrap();
1210        value["slot"]["slot"]["MemoryManagerId"] = serde_json::json!(255);
1211        assert!(serde_json::from_value::<AllocationRetirement>(value).is_err());
1212    }
1213
1214    #[test]
1215    fn retirement_decode_rejects_invalid_stable_key() {
1216        let retirement = AllocationRetirement::new(
1217            "app.users.v1",
1218            MemoryManagerSlot::new(100).expect("usable slot"),
1219        )
1220        .expect("retirement");
1221        let mut value = crate::test_cbor::to_value(retirement).expect("retirement value");
1222        *map_field_mut(value_map_mut(&mut value), "stable_key") =
1223            crate::test_cbor::Value::Text("App.users.v1".to_string());
1224        let bytes = crate::test_cbor::to_vec(&value).expect("retirement bytes");
1225        assert!(crate::test_cbor::from_slice::<AllocationRetirement>(&bytes).is_err());
1226    }
1227
1228    #[test]
1229    fn snapshot_can_feed_validated_generation() {
1230        let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1231            .expect("snapshot");
1232        let (declarations, runtime_fingerprint) = snapshot.into_parts();
1233        let validated =
1234            crate::capability::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1235
1236        let staged = ledger()
1237            .stage_validated_generation(&validated, None)
1238            .expect("validated generation");
1239
1240        assert_eq!(staged.allocation_history.records().len(), 1);
1241    }
1242
1243    #[test]
1244    fn stage_validated_generation_records_runtime_fingerprint() {
1245        let validated = crate::capability::ValidatedAllocations::new(
1246            3,
1247            vec![declaration("app.users.v1", 100, None)],
1248            Some("wasm:abc123".to_string()),
1249        );
1250
1251        let staged = ledger()
1252            .stage_validated_generation(&validated, None)
1253            .expect("validated generation");
1254
1255        assert_eq!(
1256            staged.allocation_history.generations()[0].runtime_fingerprint,
1257            Some("wasm:abc123".to_string())
1258        );
1259    }
1260
1261    #[test]
1262    fn strict_committed_integrity_accepts_full_lifecycle() {
1263        let mut ledger = committed_ledger(0);
1264        ledger
1265            .validate_committed_integrity()
1266            .expect("genesis ledger with no history");
1267
1268        ledger = ledger
1269            .stage_validated_generation(
1270                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1271                Some(1),
1272            )
1273            .expect("first real commit after genesis");
1274        ledger
1275            .validate_committed_integrity()
1276            .expect("first real commit");
1277
1278        ledger = ledger
1279            .stage_validated_generation(
1280                &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1281                Some(2),
1282            )
1283            .expect("repeated active declaration");
1284        ledger
1285            .validate_committed_integrity()
1286            .expect("repeated active declaration");
1287        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1288
1289        ledger = ledger
1290            .stage_validated_generation(
1291                &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1292                Some(3),
1293            )
1294            .expect("schema drift");
1295        ledger
1296            .validate_committed_integrity()
1297            .expect("schema metadata drift");
1298        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1299
1300        ledger = ledger
1301            .stage_reservation_generation(
1302                &[declaration("app.future_store.v1", 101, Some(1))],
1303                Some(4),
1304            )
1305            .expect("reservation-only generation");
1306        ledger
1307            .validate_committed_integrity()
1308            .expect("reservation-only generation");
1309        assert_eq!(
1310            record(&ledger, "app.future_store.v1").state,
1311            AllocationState::Reserved
1312        );
1313
1314        ledger = ledger
1315            .stage_validated_generation(
1316                &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1317                Some(5),
1318            )
1319            .expect("reservation activation");
1320        ledger
1321            .validate_committed_integrity()
1322            .expect("reservation activation");
1323        assert_eq!(
1324            record(&ledger, "app.future_store.v1").state,
1325            AllocationState::Active
1326        );
1327
1328        let retirement = AllocationRetirement::new(
1329            "app.users.v1",
1330            MemoryManagerSlot::new(100).expect("usable slot"),
1331        )
1332        .expect("retirement");
1333        ledger = ledger
1334            .stage_retirement_generation(&retirement, Some(6))
1335            .expect("retirement generation");
1336        ledger
1337            .validate_committed_integrity()
1338            .expect("retirement generation");
1339        assert_eq!(ledger.current_generation, 6);
1340        assert_eq!(
1341            record(&ledger, "app.users.v1").state,
1342            AllocationState::Retired { generation: 6 }
1343        );
1344        assert_eq!(
1345            record(&ledger, "app.future_store.v1").last_seen_generation,
1346            5
1347        );
1348    }
1349
1350    #[derive(Clone, Copy, Debug)]
1351    enum Transition {
1352        DeclareUsers,
1353        DeclareOrders,
1354        ReserveAudit,
1355        ActivateAudit,
1356        RetireUsers,
1357        RetireAudit,
1358        EmptyValidated,
1359        EmptyReservation,
1360    }
1361
1362    const TRANSITIONS: [Transition; 8] = [
1363        Transition::DeclareUsers,
1364        Transition::DeclareOrders,
1365        Transition::ReserveAudit,
1366        Transition::ActivateAudit,
1367        Transition::RetireUsers,
1368        Transition::RetireAudit,
1369        Transition::EmptyValidated,
1370        Transition::EmptyReservation,
1371    ];
1372
1373    fn apply_transition(
1374        ledger: &AllocationLedger,
1375        transition: Transition,
1376        committed_at: u64,
1377    ) -> Result<AllocationLedger, String> {
1378        match transition {
1379            Transition::DeclareUsers => ledger
1380                .stage_validated_generation(
1381                    &validated(
1382                        ledger.current_generation,
1383                        vec![declaration("app.users.v1", 100, Some(1))],
1384                    ),
1385                    Some(committed_at),
1386                )
1387                .map_err(|err| err.to_string()),
1388            Transition::DeclareOrders => ledger
1389                .stage_validated_generation(
1390                    &validated(
1391                        ledger.current_generation,
1392                        vec![declaration("app.orders.v1", 101, Some(1))],
1393                    ),
1394                    Some(committed_at),
1395                )
1396                .map_err(|err| err.to_string()),
1397            Transition::ReserveAudit => ledger
1398                .stage_reservation_generation(
1399                    &[declaration("app.audit.v1", 102, Some(1))],
1400                    Some(committed_at),
1401                )
1402                .map_err(|err| err.to_string()),
1403            Transition::ActivateAudit => ledger
1404                .stage_validated_generation(
1405                    &validated(
1406                        ledger.current_generation,
1407                        vec![declaration("app.audit.v1", 102, Some(1))],
1408                    ),
1409                    Some(committed_at),
1410                )
1411                .map_err(|err| err.to_string()),
1412            Transition::RetireUsers => ledger
1413                .stage_retirement_generation(
1414                    &AllocationRetirement::new(
1415                        "app.users.v1",
1416                        MemoryManagerSlot::new(100).expect("usable slot"),
1417                    )
1418                    .expect("retirement"),
1419                    Some(committed_at),
1420                )
1421                .map_err(|err| err.to_string()),
1422            Transition::RetireAudit => ledger
1423                .stage_retirement_generation(
1424                    &AllocationRetirement::new(
1425                        "app.audit.v1",
1426                        MemoryManagerSlot::new(102).expect("usable slot"),
1427                    )
1428                    .expect("retirement"),
1429                    Some(committed_at),
1430                )
1431                .map_err(|err| err.to_string()),
1432            Transition::EmptyValidated => ledger
1433                .stage_validated_generation(
1434                    &validated(ledger.current_generation, Vec::new()),
1435                    Some(committed_at),
1436                )
1437                .map_err(|err| err.to_string()),
1438            Transition::EmptyReservation => ledger
1439                .stage_reservation_generation(&[], Some(committed_at))
1440                .map_err(|err| err.to_string()),
1441        }
1442    }
1443
1444    fn check_transition_sequences(
1445        ledger: AllocationLedger,
1446        depth: usize,
1447        sequence: &mut Vec<Transition>,
1448    ) {
1449        ledger
1450            .validate_committed_integrity()
1451            .unwrap_or_else(|err| panic!("sequence {sequence:?} violated integrity: {err}"));
1452        if depth == 0 {
1453            return;
1454        }
1455
1456        for transition in TRANSITIONS {
1457            sequence.push(transition);
1458            let next = apply_transition(&ledger, transition, sequence.len() as u64)
1459                .unwrap_or_else(|_| ledger.clone());
1460            check_transition_sequences(next, depth - 1, sequence);
1461            sequence.pop();
1462        }
1463    }
1464
1465    #[test]
1466    fn transition_matrix_preserves_committed_invariants() {
1467        check_transition_sequences(committed_ledger(0), 4, &mut Vec::new());
1468    }
1469
1470    #[test]
1471    fn new_committed_requires_strict_generation_history() {
1472        let structurally_valid =
1473            AllocationLedger::new(3, AllocationHistory::default()).expect("structurally valid DTO");
1474
1475        assert_eq!(structurally_valid.current_generation, 3);
1476
1477        let err = AllocationLedger::new_committed(3, AllocationHistory::default())
1478            .expect_err("committed ledger needs generation history");
1479
1480        assert_eq!(
1481            err,
1482            LedgerIntegrityError::MissingCurrentGenerationRecord {
1483                current_generation: 3
1484            }
1485        );
1486    }
1487
1488    #[test]
1489    fn new_committed_preserves_structural_rejection_before_history_checks() {
1490        let history = AllocationHistory::from_parts(
1491            vec![
1492                active_record("app.users.v1", 100),
1493                active_record("app.users.v1", 100),
1494            ],
1495            Vec::new(),
1496        );
1497
1498        // Duplicate keys take precedence over duplicate slots and the missing
1499        // current-generation record, even with only one integrity pass.
1500        assert_eq!(
1501            AllocationLedger::new_committed(3, history).expect_err("duplicate records"),
1502            LedgerIntegrityError::DuplicateStableKey {
1503                stable_key: StableKey::parse("app.users.v1").unwrap(),
1504            }
1505        );
1506
1507        let valid = active_committed_ledger();
1508        assert_eq!(
1509            AllocationLedger::new_committed(
1510                valid.current_generation(),
1511                valid.allocation_history().clone(),
1512            )
1513            .expect("valid committed history"),
1514            valid
1515        );
1516    }
1517
1518    #[test]
1519    fn validate_integrity_rejects_duplicate_stable_keys() {
1520        let mut ledger = ledger();
1521        for second_id in [101, 100] {
1522            ledger.allocation_history.records = vec![
1523                active_record("app.users.v1", 100),
1524                active_record("app.users.v1", second_id),
1525            ];
1526
1527            let err = ledger
1528                .validate_integrity()
1529                .expect_err("duplicate key precedes duplicate slot");
1530
1531            assert_eq!(
1532                err,
1533                LedgerIntegrityError::DuplicateStableKey {
1534                    stable_key: StableKey::parse("app.users.v1").unwrap(),
1535                }
1536            );
1537        }
1538    }
1539
1540    #[test]
1541    fn validate_integrity_rejects_duplicate_slots() {
1542        let mut ledger = ledger();
1543        ledger.allocation_history.records = vec![
1544            active_record("app.users.v1", 100),
1545            active_record("app.orders.v1", 100),
1546        ];
1547
1548        let err = ledger.validate_integrity().expect_err("duplicate slot");
1549
1550        assert_eq!(
1551            err,
1552            LedgerIntegrityError::DuplicateSlot {
1553                slot: MemoryManagerSlot::new(100).unwrap(),
1554            }
1555        );
1556    }
1557
1558    #[test]
1559    fn recovery_rejects_invalid_stable_key_during_ledger_decode() {
1560        let mut ledger = committed_ledger(1);
1561        ledger
1562            .allocation_history
1563            .records
1564            .push(active_record("app.users.v1", 100));
1565        let mut bytes = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
1566        let key_start = bytes
1567            .windows(b"app.users.v1".len())
1568            .position(|window| window == b"app.users.v1")
1569            .expect("encoded stable key");
1570        bytes[key_start] = b'A';
1571        assert!(crate::test_cbor::from_slice::<AllocationLedger>(&bytes).is_err());
1572        let payload = LedgerPayloadEnvelope::current(bytes).encode();
1573        let mut physical = DualCommitStore::default();
1574        physical.commit_payload_at_generation(1, payload).unwrap();
1575        let store = LedgerCommitStore { physical };
1576        let before = store.clone();
1577        assert!(matches!(store.recover(), Err(LedgerCommitError::Codec(_))));
1578        assert_eq!(store, before, "malformed-key recovery must preserve slots");
1579    }
1580
1581    #[test]
1582    fn ledger_decode_rejects_unusable_memory_manager_slot() {
1583        let mut ledger = committed_ledger(1);
1584        ledger
1585            .allocation_history
1586            .records
1587            .push(active_record("app.users.v1", 100));
1588        let mut value = serde_json::to_value(ledger).unwrap();
1589        value["allocation_history"]["records"][0]["slot"]["slot"]["MemoryManagerId"] =
1590            serde_json::json!(255);
1591        let bytes = crate::test_cbor::to_vec(&value).unwrap();
1592        assert!(decode_ledger(&bytes).is_err());
1593    }
1594
1595    #[test]
1596    fn validate_integrity_rejects_retirement_before_last_observation() {
1597        let mut ledger = committed_ledger(5);
1598        let mut record = active_record("app.users.v1", 100);
1599        record.last_seen_generation = 4;
1600        record.state = AllocationState::Retired { generation: 3 };
1601        ledger.allocation_history.records = vec![record];
1602
1603        let err = ledger
1604            .validate_committed_integrity()
1605            .expect_err("retirement must follow the last observation");
1606
1607        assert!(matches!(
1608            err,
1609            LedgerIntegrityError::RetirementNotAfterLastSeen { .. }
1610        ));
1611    }
1612
1613    #[test]
1614    fn validate_integrity_rejects_retirement_before_allocation() {
1615        let mut ledger = committed_ledger(2);
1616        let mut record = active_record("app.users.v1", 100);
1617        record.state = AllocationState::Retired { generation: 0 };
1618        ledger.allocation_history.records = vec![record];
1619
1620        let err = ledger
1621            .validate_committed_integrity()
1622            .expect_err("retirement cannot predate allocation");
1623
1624        assert!(matches!(
1625            err,
1626            LedgerIntegrityError::RetiredBeforeFirstGeneration { .. }
1627        ));
1628    }
1629
1630    #[test]
1631    fn validate_integrity_requires_schema_history_at_first_generation() {
1632        let mut ledger = committed_ledger(2);
1633        let mut record = active_record("app.users.v1", 100);
1634        record.last_seen_generation = 2;
1635        record.schema_history[0].generation = 2;
1636        ledger.allocation_history.records = vec![record];
1637
1638        let err = ledger
1639            .validate_committed_integrity()
1640            .expect_err("schema history must begin with the allocation");
1641
1642        assert!(matches!(
1643            err,
1644            LedgerIntegrityError::SchemaHistoryStartMismatch { .. }
1645        ));
1646    }
1647
1648    #[test]
1649    fn validate_integrity_rejects_schema_history_after_last_observation() {
1650        for generation in [3, 4] {
1651            let mut ledger = committed_ledger(3);
1652            let mut record = active_record("app.users.v1", 100);
1653            record.last_seen_generation = 2;
1654            record.schema_history.push(SchemaMetadataRecord::new(
1655                generation,
1656                SchemaMetadata::new(Some(2)).expect("schema"),
1657            ));
1658            ledger.allocation_history.records = vec![record];
1659
1660            let err = ledger
1661                .validate_committed_integrity()
1662                .expect_err("schema metadata cannot postdate the last observation");
1663
1664            assert!(matches!(
1665                (generation, err),
1666                (
1667                    3,
1668                    LedgerIntegrityError::SchemaHistoryAfterLastSeen {
1669                        generation: 3,
1670                        last_seen_generation: 2,
1671                        ..
1672                    }
1673                ) | (
1674                    4,
1675                    LedgerIntegrityError::SchemaHistoryOutOfBounds { generation: 4, .. }
1676                )
1677            ));
1678        }
1679    }
1680
1681    #[test]
1682    fn validate_integrity_rejects_non_increasing_schema_history() {
1683        for generation in [0, 1] {
1684            let mut ledger = ledger();
1685            let mut record = active_record("app.users.v1", 100);
1686            record.schema_history.push(SchemaMetadataRecord {
1687                generation,
1688                schema: SchemaMetadata::default(),
1689            });
1690            ledger.allocation_history.records = vec![record];
1691
1692            let err = ledger
1693                .validate_integrity()
1694                .expect_err("non-increasing schema history");
1695
1696            assert!(matches!(
1697                err,
1698                LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1699            ));
1700        }
1701    }
1702
1703    #[test]
1704    fn recovery_rejects_zero_schema_version_during_ledger_decode() {
1705        let mut ledger = committed_ledger(1);
1706        ledger
1707            .allocation_history
1708            .records
1709            .push(AllocationRecord::active(
1710                1,
1711                &declaration("app.users.v1", 100, Some(1)),
1712            ));
1713        let mut value = serde_json::to_value(&ledger).unwrap();
1714        value["allocation_history"]["records"][0]["schema_history"][0]["schema"]["schema_version"] =
1715            0.into();
1716        let bytes = crate::test_cbor::to_vec(&value).unwrap();
1717        assert!(crate::test_cbor::from_slice::<AllocationLedger>(&bytes).is_err());
1718        let payload = LedgerPayloadEnvelope::current(bytes).encode();
1719        let mut physical = DualCommitStore::default();
1720        physical.commit_payload_at_generation(1, payload).unwrap();
1721        let store = LedgerCommitStore { physical };
1722        let before = store.clone();
1723        assert!(matches!(store.recover(), Err(LedgerCommitError::Codec(_))));
1724        assert_eq!(
1725            store, before,
1726            "malformed-schema recovery must preserve slots"
1727        );
1728    }
1729
1730    #[test]
1731    fn validate_committed_integrity_requires_current_generation_record() {
1732        let err = ledger()
1733            .validate_committed_integrity()
1734            .expect_err("missing current generation");
1735
1736        assert_eq!(
1737            err,
1738            LedgerIntegrityError::MissingCurrentGenerationRecord {
1739                current_generation: 3
1740            }
1741        );
1742    }
1743
1744    #[test]
1745    fn committed_integrity_rejects_allocation_references_to_genesis() {
1746        for generation in [0, 1, 2] {
1747            let mut ledger = committed_ledger(generation);
1748            let mut record = AllocationRecord::active(0, &declaration("app.genesis.v1", 100, None));
1749            if generation == 2 {
1750                record.last_seen_generation = 1;
1751                record
1752                    .schema_history
1753                    .push(SchemaMetadataRecord::new(1, SchemaMetadata::default()));
1754                record.state = AllocationState::Retired { generation: 2 };
1755            }
1756            ledger.allocation_history.records.push(record);
1757            ledger.validate_integrity().expect("structurally valid DTO");
1758            assert_eq!(
1759                ledger.validate_committed_integrity(),
1760                Err(LedgerIntegrityError::UnknownRecordGeneration {
1761                    stable_key: StableKey::parse("app.genesis.v1").unwrap(),
1762                    generation: 0,
1763                })
1764            );
1765        }
1766    }
1767
1768    #[test]
1769    fn validate_committed_integrity_rejects_generation_history_gaps() {
1770        let mut ledger = committed_ledger(3);
1771        ledger.allocation_history.generations.remove(1);
1772        ledger
1773            .allocation_history
1774            .records
1775            .push(AllocationRecord::active(
1776                0,
1777                &declaration("app.genesis.v1", 100, None),
1778            ));
1779
1780        let err = ledger
1781            .validate_committed_integrity()
1782            .expect_err("generation history gap takes precedence over genesis reference");
1783
1784        assert!(matches!(
1785            err,
1786            LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1787        ));
1788    }
1789
1790    #[test]
1791    fn committed_integrity_rejects_a_skipped_parent_in_contiguous_history() {
1792        let mut ledger = committed_ledger(3);
1793        ledger.allocation_history.generations[2].parent_generation = 1;
1794        ledger
1795            .validate_integrity()
1796            .expect("structurally valid history");
1797
1798        assert_eq!(
1799            ledger.validate_committed_integrity(),
1800            Err(LedgerIntegrityError::BrokenGenerationChain {
1801                generation: 3,
1802                expected_parent: 2,
1803                actual_parent: 1,
1804            })
1805        );
1806    }
1807
1808    #[test]
1809    fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1810        let mut store = LedgerCommitStore::default();
1811        let mut invalid = ledger();
1812        invalid.allocation_history.records = vec![
1813            active_record("app.users.v1", 100),
1814            active_record("app.orders.v1", 100),
1815        ];
1816
1817        let err = store.commit(&invalid).expect_err("invalid ledger");
1818
1819        assert!(matches!(
1820            err,
1821            LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1822        ));
1823        assert!(store.physical().is_uninitialized());
1824    }
1825
1826    #[test]
1827    fn ledger_commit_store_recovers_latest_committed_ledger() {
1828        let mut store = LedgerCommitStore::default();
1829        let first = committed_ledger(1);
1830        let second = committed_ledger(2);
1831
1832        store.commit(&first).expect("first commit");
1833        store.commit(&second).expect("second commit");
1834        let recovered = store.recover().expect("recovered ledger");
1835
1836        assert_eq!(recovered.current_generation(), 2);
1837        assert_eq!(recovered.physical_generation(), 2);
1838    }
1839
1840    #[test]
1841    fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1842        let mut store = LedgerCommitStore::default();
1843        let ledger = committed_ledger(1);
1844
1845        store.commit(&ledger).expect("commit");
1846
1847        let committed = store.physical().authoritative().expect("authoritative");
1848        assert_eq!(
1849            committed.payload(),
1850            LedgerPayloadEnvelope::current(crate::test_cbor::to_vec(&ledger).unwrap())
1851                .try_encode()
1852                .unwrap(),
1853            "direct commit encoding preserves the complete public envelope bytes"
1854        );
1855        let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1856        assert_eq!(
1857            envelope.payload(),
1858            crate::test_cbor::to_vec(&ledger).expect("encode ledger")
1859        );
1860    }
1861
1862    #[test]
1863    fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1864        let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1865        let store = LedgerCommitStore {
1866            physical: DualCommitStore {
1867                slot0: Some(committed),
1868                slot1: None,
1869            },
1870        };
1871
1872        let err = store.recover().expect_err("bad envelope");
1873
1874        assert!(matches!(
1875            err,
1876            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1877        ));
1878    }
1879
1880    #[test]
1881    fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1882        let mut payload = enveloped_payload(&committed_ledger(1));
1883        payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1884        let store = LedgerCommitStore {
1885            physical: DualCommitStore {
1886                slot0: Some(CommittedGenerationBytes::new(1, payload)),
1887                slot1: None,
1888            },
1889        };
1890
1891        let err = store
1892            .recover()
1893            .expect_err("unknown ledger format version must fail");
1894
1895        assert_eq!(
1896            err,
1897            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1898                marker: *b"ICMF",
1899                version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1900            })
1901        );
1902    }
1903
1904    #[test]
1905    fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
1906        let genesis = enveloped_payload(&committed_ledger(0));
1907        let committed = CommittedGenerationBytes::new(0, genesis.clone());
1908        let mut corrupt = committed.clone();
1909        corrupt.checksum ^= 1;
1910        let mut unsupported = genesis.clone();
1911        unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1912        let cases = [
1913            ("empty", None, None),
1914            ("one slot", Some(committed.clone()), None),
1915            (
1916                "identical slots",
1917                Some(committed.clone()),
1918                Some(committed.clone()),
1919            ),
1920            (
1921                "corrupt inactive",
1922                Some(committed.clone()),
1923                Some(corrupt.clone()),
1924            ),
1925            ("corrupt active", Some(corrupt), Some(committed.clone())),
1926            (
1927                "ambiguous",
1928                Some(committed),
1929                Some(CommittedGenerationBytes::new(0, vec![1])),
1930            ),
1931            (
1932                "unsupported format",
1933                Some(CommittedGenerationBytes::new(0, unsupported)),
1934                None,
1935            ),
1936            (
1937                "invalid CBOR",
1938                Some(CommittedGenerationBytes::new(
1939                    0,
1940                    LedgerPayloadEnvelope::current(vec![255])
1941                        .try_encode()
1942                        .unwrap(),
1943                )),
1944                None,
1945            ),
1946            (
1947                "generation mismatch",
1948                Some(CommittedGenerationBytes::new(1, genesis)),
1949                None,
1950            ),
1951            (
1952                "invalid committed integrity",
1953                Some(CommittedGenerationBytes::new(
1954                    3,
1955                    enveloped_payload(&ledger()),
1956                )),
1957                None,
1958            ),
1959        ];
1960
1961        for (label, slot0, slot1) in cases {
1962            let empty = slot0.is_none() && slot1.is_none();
1963            let store = LedgerCommitStore {
1964                physical: DualCommitStore { slot0, slot1 },
1965            };
1966            let before = store.clone();
1967            let (recovered, diagnostic) = store.recover_with_diagnostic();
1968            assert_eq!(recovered, store.recover(), "{label}");
1969            assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
1970            assert_eq!(store, before, "{label}");
1971
1972            let mut initializing = store.clone();
1973            let initialized = initializing.recover_or_initialize(&committed_ledger(0));
1974            if empty {
1975                let initialized = initialized.expect("empty store accepts genesis");
1976                assert_eq!(initialized.current_generation(), 0);
1977                assert_eq!(initialized.physical_generation(), 0);
1978                assert_eq!(initializing.recover().unwrap(), initialized);
1979            } else {
1980                assert_eq!(initialized, recovered, "{label}");
1981                assert_eq!(initializing, before, "{label}");
1982            }
1983
1984            if let Err(LedgerCommitError::Recovery(error)) = recovered {
1985                assert_eq!(diagnostic.recovery, Err(error), "{label}");
1986            } else {
1987                assert!(diagnostic.recovery.is_ok(), "{label}");
1988            }
1989        }
1990    }
1991
1992    #[test]
1993    fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
1994        let mut store = LedgerCommitStore::default();
1995        let genesis = committed_ledger(0);
1996
1997        let recovered = store
1998            .recover_or_initialize(&genesis)
1999            .expect("current-format genesis ledger");
2000        assert_eq!(recovered.current_generation(), 0);
2001        assert_eq!(recovered.physical_generation(), 0);
2002        assert_eq!(recovered.ledger().allocation_history().generations(), []);
2003        assert_eq!(recovered, store.recover().unwrap());
2004
2005        let first = recovered
2006            .ledger()
2007            .stage_validated_generation(
2008                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2009                None,
2010            )
2011            .expect("first real generation");
2012        let recovered = store.commit(&first).expect("first commit");
2013
2014        assert_eq!(recovered, store.recover().unwrap());
2015        assert_eq!(recovered.current_generation(), 1);
2016        assert_eq!(recovered.physical_generation(), 1);
2017        assert_eq!(
2018            recovered.ledger().allocation_history().generations()[0].generation,
2019            1
2020        );
2021        assert_eq!(
2022            record(recovered.ledger(), "app.users.v1").first_generation,
2023            1
2024        );
2025    }
2026
2027    #[test]
2028    fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
2029        let mut store = LedgerCommitStore::default();
2030        let genesis = committed_ledger(0);
2031        store.commit(&genesis).expect("genesis commit");
2032        let first = genesis
2033            .stage_validated_generation(
2034                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2035                None,
2036            )
2037            .expect("first generation");
2038        let first = store.commit(&first).expect("first commit");
2039        let second = first
2040            .ledger()
2041            .stage_validated_generation(
2042                &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
2043                None,
2044            )
2045            .expect("second generation");
2046
2047        store.commit(&second).expect("second commit");
2048        store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
2049
2050        let err = store
2051            .recover()
2052            .expect_err("corrupt latest must not roll back");
2053
2054        assert_eq!(
2055            err,
2056            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2057                slot0_invalid: true,
2058                slot1_invalid: false,
2059            })
2060        );
2061    }
2062
2063    #[test]
2064    fn ledger_commit_store_recovers_identical_duplicate_slots() {
2065        let ledger = committed_ledger(0)
2066            .stage_validated_generation(
2067                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2068                None,
2069            )
2070            .expect("first generation");
2071        let payload = enveloped_payload(&ledger);
2072        let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
2073        let store = LedgerCommitStore {
2074            physical: DualCommitStore {
2075                slot0: Some(committed.clone()),
2076                slot1: Some(committed),
2077            },
2078        };
2079
2080        let recovered = store.recover().expect("recovered");
2081
2082        assert_eq!(recovered.ledger(), &ledger);
2083    }
2084
2085    #[test]
2086    fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
2087        let mut store = LedgerCommitStore::default();
2088        let first = committed_ledger(1);
2089        let second = committed_ledger(2);
2090
2091        store.commit(&first).expect("first commit");
2092        store
2093            .write_corrupt_inactive_ledger(&second)
2094            .expect("corrupt write");
2095        let err = store.recover().expect_err("corrupt slot");
2096
2097        assert_eq!(
2098            err,
2099            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2100                slot0_invalid: false,
2101                slot1_invalid: true,
2102            })
2103        );
2104    }
2105
2106    #[test]
2107    fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
2108        let store = LedgerCommitStore {
2109            physical: DualCommitStore {
2110                slot0: Some(CommittedGenerationBytes::new(
2111                    7,
2112                    enveloped_payload(&committed_ledger(6)),
2113                )),
2114                slot1: None,
2115            },
2116        };
2117        let err = store.recover().expect_err("mismatch");
2118
2119        assert_eq!(
2120            err,
2121            LedgerCommitError::PhysicalLogicalGenerationMismatch {
2122                physical_generation: 7,
2123                logical_generation: 6
2124            }
2125        );
2126    }
2127
2128    #[test]
2129    fn ledger_commit_store_rejects_non_next_logical_generation() {
2130        let mut store = LedgerCommitStore::default();
2131        store.commit(&committed_ledger(1)).expect("first commit");
2132
2133        let err = store
2134            .commit(&committed_ledger(3))
2135            .expect_err("skipped generation");
2136
2137        assert_eq!(
2138            err,
2139            LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2140                expected: 2,
2141                actual: 3
2142            })
2143        );
2144    }
2145
2146    #[test]
2147    fn ledger_commit_store_initializes_empty_store_explicitly() {
2148        let mut store = LedgerCommitStore::default();
2149        let genesis = committed_ledger(3);
2150
2151        let recovered = store
2152            .recover_or_initialize(&genesis)
2153            .expect("initialized ledger");
2154
2155        assert_eq!(recovered.current_generation(), 3);
2156        assert_eq!(recovered.physical_generation(), 3);
2157        assert!(!store.physical().is_uninitialized());
2158    }
2159
2160    #[test]
2161    fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2162        let mut store = LedgerCommitStore::default();
2163        store
2164            .write_corrupt_inactive_ledger(&ledger())
2165            .expect("corrupt write");
2166
2167        let err = store
2168            .recover_or_initialize(&ledger())
2169            .expect_err("corrupt state");
2170
2171        assert!(matches!(
2172            err,
2173            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2174                slot0_invalid: true,
2175                slot1_invalid: false,
2176            })
2177        ));
2178    }
2179    #[test]
2180    fn history_boundary_round_trips_and_rejects_next_generation_without_mutation() {
2181        let mut source = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 - 1);
2182        source
2183            .allocation_history
2184            .records
2185            .push(active_record("app.users.v1", 100));
2186        let retirement =
2187            AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
2188                .unwrap();
2189        let retired = source
2190            .stage_retirement_generation(&retirement, None)
2191            .unwrap();
2192        retired.validate_committed_integrity().unwrap();
2193        assert_eq!(
2194            retired.allocation_history.generations().len(),
2195            crate::constants::MAX_LEDGER_GENERATIONS
2196        );
2197        assert_eq!(
2198            retired.allocation_history.records()[0].schema_history,
2199            source.allocation_history.records()[0].schema_history
2200        );
2201        assert_eq!(
2202            source.allocation_history.records()[0].state,
2203            AllocationState::Active
2204        );
2205        assert!(matches!(
2206            retired.stage_retirement_generation(&retirement, None),
2207            Err(AllocationRetirementError::Integrity(
2208                LedgerIntegrityError::LimitExceeded {
2209                    resource: "generation history",
2210                    ..
2211                }
2212            ))
2213        ));
2214
2215        let ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2216        let mut store = LedgerCommitStore::default();
2217        let recovered = store.commit(&ledger).expect("boundary is admissible");
2218        let record = crate::StableCellLedgerRecord::new(store.clone());
2219        let bytes = crate::test_cbor::to_vec(&record).unwrap();
2220        assert_eq!(record.encoded_size(), bytes.len());
2221        let recovered_record = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2222        assert_eq!(
2223            recovered_record.store().recover().unwrap().ledger(),
2224            &ledger
2225        );
2226        let before = store.clone();
2227        let err = crate::AllocationBootstrap::new(&mut store)
2228            .validate_and_commit(
2229                DeclarationSnapshot::new(Vec::new()).unwrap(),
2230                &crate::GenericRangePolicy,
2231                None,
2232            )
2233            .unwrap_err();
2234        assert!(matches!(
2235            err,
2236            crate::BootstrapError::Staging(AllocationStageError::Integrity(
2237                LedgerIntegrityError::LimitExceeded { .. }
2238            ))
2239        ));
2240        assert_eq!(store, before);
2241        assert_eq!(
2242            recovered.current_generation(),
2243            crate::constants::MAX_LEDGER_GENERATIONS as u64
2244        );
2245        let oversize = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 + 1);
2246        assert!(matches!(
2247            store.commit(&oversize),
2248            Err(LedgerCommitError::Integrity(
2249                LedgerIntegrityError::LimitExceeded { .. }
2250            ))
2251        ));
2252        assert_eq!(store, before);
2253    }
2254
2255    #[test]
2256    fn record_count_boundary_and_malicious_history_reject_on_recovery() {
2257        let mut ledger = committed_ledger(1);
2258        ledger.allocation_history.records = (0..255_u8)
2259            .map(|id| active_record(&format!("app.slot{id}.v1"), id))
2260            .collect();
2261        let mut store = LedgerCommitStore::default();
2262        store.commit(&ledger).unwrap();
2263        assert_eq!(
2264            store
2265                .recover()
2266                .unwrap()
2267                .ledger()
2268                .allocation_history()
2269                .records()
2270                .len(),
2271            255
2272        );
2273        ledger
2274            .allocation_history
2275            .records
2276            .push(active_record("app.extra.v1", 254));
2277        assert!(matches!(
2278            store.commit(&ledger),
2279            Err(LedgerCommitError::Integrity(
2280                LedgerIntegrityError::LimitExceeded { .. }
2281            ))
2282        ));
2283        let payload = enveloped_payload(&ledger);
2284        let mut invalid = LedgerCommitStore {
2285            physical: DualCommitStore {
2286                slot0: Some(CommittedGenerationBytes::new(1, payload)),
2287                slot1: None,
2288            },
2289        };
2290        assert!(invalid.recover().is_err());
2291        assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
2292        for bytes in [
2293            vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2294            vec![0x81; 34],
2295            vec![0xa1],
2296        ] {
2297            let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
2298            let invalid = LedgerCommitStore {
2299                physical: DualCommitStore {
2300                    slot0: Some(CommittedGenerationBytes::new(1, payload)),
2301                    slot1: None,
2302                },
2303            };
2304            assert!(matches!(
2305                invalid.recover(),
2306                Err(LedgerCommitError::Codec(_))
2307            ));
2308        }
2309    }
2310    #[test]
2311    fn encoded_byte_limit_rejects_before_commit_mutation() {
2312        let mut store = LedgerCommitStore::default();
2313        store.commit(&committed_ledger(0)).unwrap();
2314        let before = store.clone();
2315        let mut ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2316        for generation in &mut ledger.allocation_history.generations {
2317            generation.runtime_fingerprint =
2318                Some("x".repeat(crate::constants::DIAGNOSTIC_STRING_MAX_BYTES));
2319        }
2320        assert!(matches!(
2321            store.commit(&ledger),
2322            Err(LedgerCommitError::Integrity(
2323                LedgerIntegrityError::LimitExceeded {
2324                    resource: "ledger bytes",
2325                    ..
2326                }
2327            ))
2328        ));
2329        assert_eq!(store, before);
2330        let mut envelope = LedgerPayloadEnvelope::current(Vec::new())
2331            .try_encode()
2332            .unwrap();
2333        envelope[16..24]
2334            .copy_from_slice(&((crate::constants::MAX_LEDGER_BYTES + 1) as u64).to_le_bytes());
2335        assert!(matches!(
2336            LedgerPayloadEnvelope::decode(&envelope),
2337            Err(LedgerPayloadEnvelopeError::PayloadTooLarge { .. })
2338        ));
2339    }
2340}