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