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