Skip to main content

ic_memory/ledger/
mod.rs

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