Skip to main content

ic_memory/ledger/
mod.rs

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