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        let err = AllocationRetirement::new(
1268            "App.users.v1",
1269            AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID),
1270        )
1271        .expect_err("invalid key precedes invalid slot");
1272        assert!(matches!(err, AllocationRetirementError::Key(_)));
1273    }
1274
1275    #[test]
1276    fn stage_retirement_generation_rejects_invalid_decoded_slot() {
1277        let mut ledger = ledger();
1278        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1279        let mut retirement = AllocationRetirement::new(
1280            "app.users.v1",
1281            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1282        )
1283        .expect("retirement");
1284        retirement.slot =
1285            AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1286
1287        let err = ledger
1288            .stage_retirement_generation(&retirement, None)
1289            .expect_err("decoded invalid retirement must fail at the boundary");
1290
1291        assert!(matches!(
1292            err,
1293            AllocationRetirementError::MemoryManagerSlot(
1294                MemoryManagerSlotError::InvalidMemoryManagerId { id }
1295            ) if id == MEMORY_MANAGER_INVALID_ID
1296        ));
1297    }
1298
1299    #[test]
1300    fn stage_retirement_generation_rejects_invalid_decoded_stable_key() {
1301        let mut ledger = ledger();
1302        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1303        let retirement = AllocationRetirement::new(
1304            "app.users.v1",
1305            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1306        )
1307        .expect("retirement");
1308        let mut value = crate::test_cbor::to_value(retirement).expect("retirement value");
1309        *map_field_mut(value_map_mut(&mut value), "stable_key") =
1310            crate::test_cbor::Value::Text("App.users.v1".to_string());
1311        let bytes = crate::test_cbor::to_vec(&value).expect("retirement bytes");
1312        let retirement: AllocationRetirement =
1313            crate::test_cbor::from_slice(&bytes).expect("decoded DTO");
1314
1315        let err = ledger
1316            .stage_retirement_generation(&retirement, None)
1317            .expect_err("decoded invalid stable key must fail at the boundary");
1318
1319        assert!(matches!(err, AllocationRetirementError::Key(_)));
1320    }
1321
1322    #[test]
1323    fn snapshot_can_feed_validated_generation() {
1324        let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1325            .expect("snapshot");
1326        let (declarations, runtime_fingerprint) = snapshot.into_parts();
1327        let validated =
1328            crate::capability::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1329
1330        let staged = ledger()
1331            .stage_validated_generation(&validated, None)
1332            .expect("validated generation");
1333
1334        assert_eq!(staged.allocation_history.records().len(), 1);
1335    }
1336
1337    #[test]
1338    fn stage_validated_generation_records_runtime_fingerprint() {
1339        let validated = crate::capability::ValidatedAllocations::new(
1340            3,
1341            vec![declaration("app.users.v1", 100, None)],
1342            Some("wasm:abc123".to_string()),
1343        );
1344
1345        let staged = ledger()
1346            .stage_validated_generation(&validated, None)
1347            .expect("validated generation");
1348
1349        assert_eq!(
1350            staged.allocation_history.generations()[0].runtime_fingerprint,
1351            Some("wasm:abc123".to_string())
1352        );
1353    }
1354
1355    #[test]
1356    fn strict_committed_integrity_accepts_full_lifecycle() {
1357        let mut ledger = committed_ledger(0);
1358        ledger
1359            .validate_committed_integrity()
1360            .expect("genesis ledger with no history");
1361
1362        ledger = ledger
1363            .stage_validated_generation(
1364                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1365                Some(1),
1366            )
1367            .expect("first real commit after genesis");
1368        ledger
1369            .validate_committed_integrity()
1370            .expect("first real commit");
1371
1372        ledger = ledger
1373            .stage_validated_generation(
1374                &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1375                Some(2),
1376            )
1377            .expect("repeated active declaration");
1378        ledger
1379            .validate_committed_integrity()
1380            .expect("repeated active declaration");
1381        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1382
1383        ledger = ledger
1384            .stage_validated_generation(
1385                &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1386                Some(3),
1387            )
1388            .expect("schema drift");
1389        ledger
1390            .validate_committed_integrity()
1391            .expect("schema metadata drift");
1392        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1393
1394        ledger = ledger
1395            .stage_reservation_generation(
1396                &[declaration("app.future_store.v1", 101, Some(1))],
1397                Some(4),
1398            )
1399            .expect("reservation-only generation");
1400        ledger
1401            .validate_committed_integrity()
1402            .expect("reservation-only generation");
1403        assert_eq!(
1404            record(&ledger, "app.future_store.v1").state,
1405            AllocationState::Reserved
1406        );
1407
1408        ledger = ledger
1409            .stage_validated_generation(
1410                &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1411                Some(5),
1412            )
1413            .expect("reservation activation");
1414        ledger
1415            .validate_committed_integrity()
1416            .expect("reservation activation");
1417        assert_eq!(
1418            record(&ledger, "app.future_store.v1").state,
1419            AllocationState::Active
1420        );
1421
1422        let retirement = AllocationRetirement::new(
1423            "app.users.v1",
1424            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1425        )
1426        .expect("retirement");
1427        ledger = ledger
1428            .stage_retirement_generation(&retirement, Some(6))
1429            .expect("retirement generation");
1430        ledger
1431            .validate_committed_integrity()
1432            .expect("retirement generation");
1433        assert_eq!(ledger.current_generation, 6);
1434        assert_eq!(
1435            record(&ledger, "app.users.v1").state,
1436            AllocationState::Retired { generation: 6 }
1437        );
1438        assert_eq!(
1439            record(&ledger, "app.future_store.v1").last_seen_generation,
1440            5
1441        );
1442    }
1443
1444    #[derive(Clone, Copy, Debug)]
1445    enum Transition {
1446        DeclareUsers,
1447        DeclareOrders,
1448        ReserveAudit,
1449        ActivateAudit,
1450        RetireUsers,
1451        RetireAudit,
1452        EmptyValidated,
1453        EmptyReservation,
1454    }
1455
1456    const TRANSITIONS: [Transition; 8] = [
1457        Transition::DeclareUsers,
1458        Transition::DeclareOrders,
1459        Transition::ReserveAudit,
1460        Transition::ActivateAudit,
1461        Transition::RetireUsers,
1462        Transition::RetireAudit,
1463        Transition::EmptyValidated,
1464        Transition::EmptyReservation,
1465    ];
1466
1467    fn apply_transition(
1468        ledger: &AllocationLedger,
1469        transition: Transition,
1470        committed_at: u64,
1471    ) -> Result<AllocationLedger, String> {
1472        match transition {
1473            Transition::DeclareUsers => ledger
1474                .stage_validated_generation(
1475                    &validated(
1476                        ledger.current_generation,
1477                        vec![declaration("app.users.v1", 100, Some(1))],
1478                    ),
1479                    Some(committed_at),
1480                )
1481                .map_err(|err| err.to_string()),
1482            Transition::DeclareOrders => ledger
1483                .stage_validated_generation(
1484                    &validated(
1485                        ledger.current_generation,
1486                        vec![declaration("app.orders.v1", 101, Some(1))],
1487                    ),
1488                    Some(committed_at),
1489                )
1490                .map_err(|err| err.to_string()),
1491            Transition::ReserveAudit => ledger
1492                .stage_reservation_generation(
1493                    &[declaration("app.audit.v1", 102, Some(1))],
1494                    Some(committed_at),
1495                )
1496                .map_err(|err| err.to_string()),
1497            Transition::ActivateAudit => ledger
1498                .stage_validated_generation(
1499                    &validated(
1500                        ledger.current_generation,
1501                        vec![declaration("app.audit.v1", 102, Some(1))],
1502                    ),
1503                    Some(committed_at),
1504                )
1505                .map_err(|err| err.to_string()),
1506            Transition::RetireUsers => ledger
1507                .stage_retirement_generation(
1508                    &AllocationRetirement::new(
1509                        "app.users.v1",
1510                        AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1511                    )
1512                    .expect("retirement"),
1513                    Some(committed_at),
1514                )
1515                .map_err(|err| err.to_string()),
1516            Transition::RetireAudit => ledger
1517                .stage_retirement_generation(
1518                    &AllocationRetirement::new(
1519                        "app.audit.v1",
1520                        AllocationSlotDescriptor::memory_manager(102).expect("usable slot"),
1521                    )
1522                    .expect("retirement"),
1523                    Some(committed_at),
1524                )
1525                .map_err(|err| err.to_string()),
1526            Transition::EmptyValidated => ledger
1527                .stage_validated_generation(
1528                    &validated(ledger.current_generation, Vec::new()),
1529                    Some(committed_at),
1530                )
1531                .map_err(|err| err.to_string()),
1532            Transition::EmptyReservation => ledger
1533                .stage_reservation_generation(&[], Some(committed_at))
1534                .map_err(|err| err.to_string()),
1535        }
1536    }
1537
1538    fn check_transition_sequences(
1539        ledger: AllocationLedger,
1540        depth: usize,
1541        sequence: &mut Vec<Transition>,
1542    ) {
1543        ledger
1544            .validate_committed_integrity()
1545            .unwrap_or_else(|err| panic!("sequence {sequence:?} violated integrity: {err}"));
1546        if depth == 0 {
1547            return;
1548        }
1549
1550        for transition in TRANSITIONS {
1551            sequence.push(transition);
1552            let next = apply_transition(&ledger, transition, sequence.len() as u64)
1553                .unwrap_or_else(|_| ledger.clone());
1554            check_transition_sequences(next, depth - 1, sequence);
1555            sequence.pop();
1556        }
1557    }
1558
1559    #[test]
1560    fn transition_matrix_preserves_committed_invariants() {
1561        check_transition_sequences(committed_ledger(0), 4, &mut Vec::new());
1562    }
1563
1564    #[test]
1565    fn new_committed_requires_strict_generation_history() {
1566        let structurally_valid =
1567            AllocationLedger::new(3, AllocationHistory::default()).expect("structurally valid DTO");
1568
1569        assert_eq!(structurally_valid.current_generation, 3);
1570
1571        let err = AllocationLedger::new_committed(3, AllocationHistory::default())
1572            .expect_err("committed ledger needs generation history");
1573
1574        assert_eq!(
1575            err,
1576            LedgerIntegrityError::MissingCurrentGenerationRecord {
1577                current_generation: 3
1578            }
1579        );
1580    }
1581
1582    #[test]
1583    fn new_committed_preserves_structural_rejection_before_history_checks() {
1584        let history = AllocationHistory::from_parts(
1585            vec![
1586                active_record("app.users.v1", 100),
1587                active_record("app.users.v1", 100),
1588            ],
1589            Vec::new(),
1590        );
1591
1592        // Duplicate keys take precedence over duplicate slots and the missing
1593        // current-generation record, even with only one integrity pass.
1594        assert_eq!(
1595            AllocationLedger::new_committed(3, history).expect_err("duplicate records"),
1596            LedgerIntegrityError::DuplicateStableKey {
1597                stable_key: StableKey::parse("app.users.v1").unwrap(),
1598            }
1599        );
1600
1601        let valid = active_committed_ledger();
1602        assert_eq!(
1603            AllocationLedger::new_committed(
1604                valid.current_generation(),
1605                valid.allocation_history().clone(),
1606            )
1607            .expect("valid committed history"),
1608            valid
1609        );
1610    }
1611
1612    #[test]
1613    fn validate_integrity_rejects_duplicate_stable_keys() {
1614        let mut ledger = ledger();
1615        for second_id in [101, 100] {
1616            *ledger.allocation_history.records_mut() = vec![
1617                active_record("app.users.v1", 100),
1618                active_record("app.users.v1", second_id),
1619            ];
1620
1621            let err = ledger
1622                .validate_integrity()
1623                .expect_err("duplicate key precedes duplicate slot");
1624
1625            assert_eq!(
1626                err,
1627                LedgerIntegrityError::DuplicateStableKey {
1628                    stable_key: StableKey::parse("app.users.v1").unwrap(),
1629                }
1630            );
1631        }
1632    }
1633
1634    #[test]
1635    fn validate_integrity_rejects_duplicate_slots() {
1636        let mut ledger = ledger();
1637        *ledger.allocation_history.records_mut() = vec![
1638            active_record("app.users.v1", 100),
1639            active_record("app.orders.v1", 100),
1640        ];
1641
1642        let err = ledger.validate_integrity().expect_err("duplicate slot");
1643
1644        assert_eq!(
1645            err,
1646            LedgerIntegrityError::DuplicateSlot {
1647                slot: AllocationSlotDescriptor::memory_manager(100).unwrap(),
1648            }
1649        );
1650
1651        ledger.allocation_history.records_mut()[1].stable_key =
1652            serde_json::from_str("\"App.orders.v1\"").expect("decoded unvalidated key");
1653        assert_eq!(
1654            ledger.validate_integrity().unwrap_err(),
1655            LedgerIntegrityError::DuplicateSlot {
1656                slot: AllocationSlotDescriptor::memory_manager(100).unwrap(),
1657            },
1658            "duplicate slot precedes the second record's key validation"
1659        );
1660    }
1661
1662    #[test]
1663    fn validate_committed_integrity_rejects_decoded_invalid_stable_key() {
1664        let mut ledger = committed_ledger(1);
1665        ledger
1666            .allocation_history
1667            .records
1668            .push(active_record("app.users.v1", 100));
1669        let mut bytes = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
1670        let key_start = bytes
1671            .windows(b"app.users.v1".len())
1672            .position(|window| window == b"app.users.v1")
1673            .expect("encoded stable key");
1674        bytes[key_start] = b'A';
1675        let decoded: AllocationLedger =
1676            crate::test_cbor::from_slice(&bytes).expect("decode ledger");
1677
1678        let err = decoded
1679            .validate_committed_integrity()
1680            .expect_err("invalid decoded key must fail");
1681
1682        assert!(matches!(err, LedgerIntegrityError::InvalidStableKey(_)));
1683    }
1684
1685    #[test]
1686    fn validate_committed_integrity_rejects_decoded_invalid_memory_manager_slot() {
1687        let mut ledger = committed_ledger(1);
1688        let mut record = active_record("app.users.v1", 100);
1689        record.slot = AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1690        ledger.allocation_history.records.push(record);
1691
1692        let err = ledger
1693            .validate_committed_integrity()
1694            .expect_err("invalid decoded slot must fail");
1695
1696        assert!(matches!(
1697            err,
1698            LedgerIntegrityError::InvalidSlotDescriptor(
1699                MemoryManagerSlotError::InvalidMemoryManagerId { id }
1700            ) if id == MEMORY_MANAGER_INVALID_ID
1701        ));
1702    }
1703
1704    #[test]
1705    fn validate_integrity_rejects_retirement_before_last_observation() {
1706        let mut ledger = committed_ledger(5);
1707        let mut record = active_record("app.users.v1", 100);
1708        record.last_seen_generation = 4;
1709        record.state = AllocationState::Retired { generation: 3 };
1710        *ledger.allocation_history.records_mut() = vec![record];
1711
1712        let err = ledger
1713            .validate_committed_integrity()
1714            .expect_err("retirement must follow the last observation");
1715
1716        assert!(matches!(
1717            err,
1718            LedgerIntegrityError::RetirementNotAfterLastSeen { .. }
1719        ));
1720    }
1721
1722    #[test]
1723    fn validate_integrity_rejects_retirement_before_allocation() {
1724        let mut ledger = committed_ledger(2);
1725        let mut record = active_record("app.users.v1", 100);
1726        record.state = AllocationState::Retired { generation: 0 };
1727        *ledger.allocation_history.records_mut() = vec![record];
1728
1729        let err = ledger
1730            .validate_committed_integrity()
1731            .expect_err("retirement cannot predate allocation");
1732
1733        assert!(matches!(
1734            err,
1735            LedgerIntegrityError::RetiredBeforeFirstGeneration { .. }
1736        ));
1737    }
1738
1739    #[test]
1740    fn validate_integrity_requires_schema_history_at_first_generation() {
1741        let mut ledger = committed_ledger(2);
1742        let mut record = active_record("app.users.v1", 100);
1743        record.last_seen_generation = 2;
1744        record.schema_history[0].generation = 2;
1745        *ledger.allocation_history.records_mut() = vec![record];
1746
1747        let err = ledger
1748            .validate_committed_integrity()
1749            .expect_err("schema history must begin with the allocation");
1750
1751        assert!(matches!(
1752            err,
1753            LedgerIntegrityError::SchemaHistoryStartMismatch { .. }
1754        ));
1755    }
1756
1757    #[test]
1758    fn validate_integrity_rejects_schema_history_after_last_observation() {
1759        let mut ledger = committed_ledger(3);
1760        let mut record = active_record("app.users.v1", 100);
1761        record.last_seen_generation = 2;
1762        record.schema_history.push(
1763            SchemaMetadataRecord::new(3, SchemaMetadata::new(Some(2)).expect("schema"))
1764                .expect("schema record"),
1765        );
1766        *ledger.allocation_history.records_mut() = vec![record];
1767
1768        let err = ledger
1769            .validate_committed_integrity()
1770            .expect_err("schema metadata cannot postdate the last observation");
1771
1772        assert!(matches!(
1773            err,
1774            LedgerIntegrityError::SchemaHistoryAfterLastSeen { .. }
1775        ));
1776    }
1777
1778    #[test]
1779    fn validate_integrity_rejects_non_increasing_schema_history() {
1780        let mut ledger = ledger();
1781        let mut record = active_record("app.users.v1", 100);
1782        record.schema_history.push(SchemaMetadataRecord {
1783            generation: 1,
1784            schema: SchemaMetadata::default(),
1785        });
1786        *ledger.allocation_history.records_mut() = vec![record];
1787
1788        let err = ledger
1789            .validate_integrity()
1790            .expect_err("non-increasing schema history");
1791
1792        assert!(matches!(
1793            err,
1794            LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1795        ));
1796    }
1797
1798    #[test]
1799    fn validate_integrity_rejects_invalid_schema_metadata_history() {
1800        let mut ledger = committed_ledger(1);
1801        let mut record = active_record("app.users.v1", 100);
1802        record.schema_history[0].schema = invalid_schema_metadata();
1803        *ledger.allocation_history.records_mut() = vec![record];
1804
1805        let err = ledger
1806            .validate_committed_integrity()
1807            .expect_err("invalid committed schema metadata");
1808
1809        assert_eq!(
1810            err,
1811            LedgerIntegrityError::InvalidSchemaMetadata {
1812                stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1813                generation: 1,
1814                error: SchemaMetadataError::InvalidVersion,
1815            }
1816        );
1817    }
1818
1819    #[test]
1820    fn validate_committed_integrity_requires_current_generation_record() {
1821        let err = ledger()
1822            .validate_committed_integrity()
1823            .expect_err("missing current generation");
1824
1825        assert_eq!(
1826            err,
1827            LedgerIntegrityError::MissingCurrentGenerationRecord {
1828                current_generation: 3
1829            }
1830        );
1831    }
1832
1833    #[test]
1834    fn committed_integrity_rejects_allocation_references_to_genesis() {
1835        for generation in [0, 1, 2] {
1836            let mut ledger = committed_ledger(generation);
1837            let mut record = AllocationRecord::active(0, declaration("app.genesis.v1", 100, None));
1838            if generation == 2 {
1839                record.last_seen_generation = 1;
1840                record
1841                    .schema_history
1842                    .push(SchemaMetadataRecord::new(1, SchemaMetadata::default()).unwrap());
1843                record.state = AllocationState::Retired { generation: 2 };
1844            }
1845            ledger.allocation_history.push_record(record);
1846            ledger.validate_integrity().expect("structurally valid DTO");
1847            assert_eq!(
1848                ledger.validate_committed_integrity(),
1849                Err(LedgerIntegrityError::UnknownRecordGeneration {
1850                    stable_key: StableKey::parse("app.genesis.v1").unwrap(),
1851                    generation: 0,
1852                })
1853            );
1854        }
1855    }
1856
1857    #[test]
1858    fn validate_committed_integrity_rejects_generation_history_gaps() {
1859        let mut ledger = committed_ledger(3);
1860        ledger.allocation_history.generations_mut().remove(1);
1861        ledger
1862            .allocation_history
1863            .push_record(AllocationRecord::active(
1864                0,
1865                declaration("app.genesis.v1", 100, None),
1866            ));
1867
1868        let err = ledger
1869            .validate_committed_integrity()
1870            .expect_err("generation history gap takes precedence over genesis reference");
1871
1872        assert!(matches!(
1873            err,
1874            LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1875        ));
1876    }
1877
1878    #[test]
1879    fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1880        let mut store = LedgerCommitStore::default();
1881        let mut invalid = ledger();
1882        *invalid.allocation_history.records_mut() = vec![
1883            active_record("app.users.v1", 100),
1884            active_record("app.orders.v1", 100),
1885        ];
1886
1887        let err = store.commit(&invalid).expect_err("invalid ledger");
1888
1889        assert!(matches!(
1890            err,
1891            LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1892        ));
1893        assert!(store.physical().is_uninitialized());
1894    }
1895
1896    #[test]
1897    fn ledger_commit_store_recovers_latest_committed_ledger() {
1898        let mut store = LedgerCommitStore::default();
1899        let first = committed_ledger(1);
1900        let second = committed_ledger(2);
1901
1902        store.commit(&first).expect("first commit");
1903        store.commit(&second).expect("second commit");
1904        let recovered = store.recover().expect("recovered ledger");
1905
1906        assert_eq!(recovered.current_generation(), 2);
1907        assert_eq!(recovered.physical_generation(), 2);
1908    }
1909
1910    #[test]
1911    fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1912        let mut store = LedgerCommitStore::default();
1913        let ledger = committed_ledger(1);
1914
1915        store.commit(&ledger).expect("commit");
1916
1917        let committed = store.physical().authoritative().expect("authoritative");
1918        let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1919        assert_eq!(envelope.payload(), CborLedgerCodec::encode(&ledger));
1920    }
1921
1922    #[test]
1923    fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1924        let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1925        let store = LedgerCommitStore {
1926            physical: DualCommitStore {
1927                slot0: Some(committed),
1928                slot1: None,
1929            },
1930        };
1931
1932        let err = store.recover().expect_err("bad envelope");
1933
1934        assert!(matches!(
1935            err,
1936            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1937        ));
1938    }
1939
1940    #[test]
1941    fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1942        let mut payload = enveloped_payload(&committed_ledger(1));
1943        payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1944        let store = LedgerCommitStore {
1945            physical: DualCommitStore {
1946                slot0: Some(CommittedGenerationBytes::new(1, payload)),
1947                slot1: None,
1948            },
1949        };
1950
1951        let err = store
1952            .recover()
1953            .expect_err("unknown ledger format version must fail");
1954
1955        assert_eq!(
1956            err,
1957            LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1958                marker: *b"ICMF",
1959                version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1960            })
1961        );
1962    }
1963
1964    #[test]
1965    fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
1966        let genesis = enveloped_payload(&committed_ledger(0));
1967        let committed = CommittedGenerationBytes::new(0, genesis.clone());
1968        let mut corrupt = committed.clone();
1969        corrupt.checksum ^= 1;
1970        let mut unsupported = genesis.clone();
1971        unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1972        let cases = [
1973            ("empty", None, None),
1974            ("one slot", Some(committed.clone()), None),
1975            (
1976                "identical slots",
1977                Some(committed.clone()),
1978                Some(committed.clone()),
1979            ),
1980            (
1981                "corrupt inactive",
1982                Some(committed.clone()),
1983                Some(corrupt.clone()),
1984            ),
1985            ("corrupt active", Some(corrupt), Some(committed.clone())),
1986            (
1987                "ambiguous",
1988                Some(committed),
1989                Some(CommittedGenerationBytes::new(0, vec![1])),
1990            ),
1991            (
1992                "unsupported format",
1993                Some(CommittedGenerationBytes::new(0, unsupported)),
1994                None,
1995            ),
1996            (
1997                "invalid CBOR",
1998                Some(CommittedGenerationBytes::new(
1999                    0,
2000                    LedgerPayloadEnvelope::current(vec![255])
2001                        .try_encode()
2002                        .unwrap(),
2003                )),
2004                None,
2005            ),
2006            (
2007                "generation mismatch",
2008                Some(CommittedGenerationBytes::new(1, genesis)),
2009                None,
2010            ),
2011            (
2012                "invalid committed integrity",
2013                Some(CommittedGenerationBytes::new(
2014                    3,
2015                    enveloped_payload(&ledger()),
2016                )),
2017                None,
2018            ),
2019        ];
2020
2021        for (label, slot0, slot1) in cases {
2022            let empty = slot0.is_none() && slot1.is_none();
2023            let store = LedgerCommitStore {
2024                physical: DualCommitStore { slot0, slot1 },
2025            };
2026            let before = store.clone();
2027            let (recovered, diagnostic) = store.recover_with_diagnostic();
2028            assert_eq!(recovered, store.recover(), "{label}");
2029            assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
2030            assert_eq!(store, before, "{label}");
2031
2032            let mut initializing = store.clone();
2033            let initialized = initializing.recover_or_initialize(&committed_ledger(0));
2034            if empty {
2035                let initialized = initialized.expect("empty store accepts genesis");
2036                assert_eq!(initialized.current_generation(), 0);
2037                assert_eq!(initialized.physical_generation(), 0);
2038                assert_eq!(initializing.recover().unwrap(), initialized);
2039            } else {
2040                assert_eq!(initialized, recovered, "{label}");
2041                assert_eq!(initializing, before, "{label}");
2042            }
2043
2044            if let Err(LedgerCommitError::Recovery(error)) = recovered {
2045                assert_eq!(diagnostic.recovery, Err(error), "{label}");
2046            } else {
2047                assert!(diagnostic.recovery.is_ok(), "{label}");
2048            }
2049        }
2050    }
2051
2052    #[test]
2053    fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
2054        let mut store = LedgerCommitStore::default();
2055        let genesis = committed_ledger(0);
2056
2057        let recovered = store
2058            .recover_or_initialize(&genesis)
2059            .expect("current-format genesis ledger");
2060        assert_eq!(recovered.current_generation(), 0);
2061        assert_eq!(recovered.physical_generation(), 0);
2062        assert_eq!(recovered.ledger().allocation_history().generations(), []);
2063        assert_eq!(recovered, store.recover().unwrap());
2064
2065        let first = recovered
2066            .ledger()
2067            .stage_validated_generation(
2068                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2069                None,
2070            )
2071            .expect("first real generation");
2072        let recovered = store.commit(&first).expect("first commit");
2073
2074        assert_eq!(recovered, store.recover().unwrap());
2075        assert_eq!(recovered.current_generation(), 1);
2076        assert_eq!(recovered.physical_generation(), 1);
2077        assert_eq!(
2078            recovered.ledger().allocation_history().generations()[0].generation,
2079            1
2080        );
2081        assert_eq!(
2082            record(recovered.ledger(), "app.users.v1").first_generation,
2083            1
2084        );
2085    }
2086
2087    #[test]
2088    fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
2089        let mut store = LedgerCommitStore::default();
2090        let genesis = committed_ledger(0);
2091        store.commit(&genesis).expect("genesis commit");
2092        let first = genesis
2093            .stage_validated_generation(
2094                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2095                None,
2096            )
2097            .expect("first generation");
2098        let first = store.commit(&first).expect("first commit");
2099        let second = first
2100            .ledger()
2101            .stage_validated_generation(
2102                &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
2103                None,
2104            )
2105            .expect("second generation");
2106
2107        store.commit(&second).expect("second commit");
2108        store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
2109
2110        let err = store
2111            .recover()
2112            .expect_err("corrupt latest must not roll back");
2113
2114        assert_eq!(
2115            err,
2116            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2117                slot0_invalid: true,
2118                slot1_invalid: false,
2119            })
2120        );
2121    }
2122
2123    #[test]
2124    fn ledger_commit_store_recovers_identical_duplicate_slots() {
2125        let ledger = committed_ledger(0)
2126            .stage_validated_generation(
2127                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2128                None,
2129            )
2130            .expect("first generation");
2131        let payload = enveloped_payload(&ledger);
2132        let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
2133        let store = LedgerCommitStore {
2134            physical: DualCommitStore {
2135                slot0: Some(committed.clone()),
2136                slot1: Some(committed),
2137            },
2138        };
2139
2140        let recovered = store.recover().expect("recovered");
2141
2142        assert_eq!(recovered.ledger(), &ledger);
2143    }
2144
2145    #[test]
2146    fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
2147        let mut store = LedgerCommitStore::default();
2148        let first = committed_ledger(1);
2149        let second = committed_ledger(2);
2150
2151        store.commit(&first).expect("first commit");
2152        store
2153            .write_corrupt_inactive_ledger(&second)
2154            .expect("corrupt write");
2155        let err = store.recover().expect_err("corrupt slot");
2156
2157        assert_eq!(
2158            err,
2159            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2160                slot0_invalid: false,
2161                slot1_invalid: true,
2162            })
2163        );
2164    }
2165
2166    #[test]
2167    fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
2168        let store = LedgerCommitStore {
2169            physical: DualCommitStore {
2170                slot0: Some(CommittedGenerationBytes::new(
2171                    7,
2172                    enveloped_payload(&committed_ledger(6)),
2173                )),
2174                slot1: None,
2175            },
2176        };
2177        let err = store.recover().expect_err("mismatch");
2178
2179        assert_eq!(
2180            err,
2181            LedgerCommitError::PhysicalLogicalGenerationMismatch {
2182                physical_generation: 7,
2183                logical_generation: 6
2184            }
2185        );
2186    }
2187
2188    #[test]
2189    fn ledger_commit_store_rejects_non_next_logical_generation() {
2190        let mut store = LedgerCommitStore::default();
2191        store.commit(&committed_ledger(1)).expect("first commit");
2192
2193        let err = store
2194            .commit(&committed_ledger(3))
2195            .expect_err("skipped generation");
2196
2197        assert_eq!(
2198            err,
2199            LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2200                expected: 2,
2201                actual: 3
2202            })
2203        );
2204    }
2205
2206    #[test]
2207    fn ledger_commit_store_initializes_empty_store_explicitly() {
2208        let mut store = LedgerCommitStore::default();
2209        let genesis = committed_ledger(3);
2210
2211        let recovered = store
2212            .recover_or_initialize(&genesis)
2213            .expect("initialized ledger");
2214
2215        assert_eq!(recovered.current_generation(), 3);
2216        assert_eq!(recovered.physical_generation(), 3);
2217        assert!(!store.physical().is_uninitialized());
2218    }
2219
2220    #[test]
2221    fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2222        let mut store = LedgerCommitStore::default();
2223        store
2224            .write_corrupt_inactive_ledger(&ledger())
2225            .expect("corrupt write");
2226
2227        let err = store
2228            .recover_or_initialize(&ledger())
2229            .expect_err("corrupt state");
2230
2231        assert!(matches!(
2232            err,
2233            LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2234                slot0_invalid: true,
2235                slot1_invalid: false,
2236            })
2237        ));
2238    }
2239    #[test]
2240    fn history_boundary_round_trips_and_rejects_next_generation_without_mutation() {
2241        let ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2242        let mut store = LedgerCommitStore::default();
2243        let recovered = store.commit(&ledger).expect("boundary is admissible");
2244        let record = crate::StableCellLedgerRecord::new(store.clone());
2245        let bytes = crate::test_cbor::to_vec(&record).unwrap();
2246        assert_eq!(record.encoded_size(), bytes.len());
2247        let recovered_record = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2248        assert_eq!(
2249            recovered_record.store().recover().unwrap().ledger(),
2250            &ledger
2251        );
2252        let before = store.clone();
2253        let err = crate::AllocationBootstrap::new(&mut store)
2254            .validate_and_commit(
2255                DeclarationSnapshot::new(Vec::new()).unwrap(),
2256                &crate::GenericRangePolicy,
2257                None,
2258            )
2259            .unwrap_err();
2260        assert!(matches!(
2261            err,
2262            crate::BootstrapError::Staging(AllocationStageError::Integrity(
2263                LedgerIntegrityError::LimitExceeded { .. }
2264            ))
2265        ));
2266        assert_eq!(store, before);
2267        assert_eq!(
2268            recovered.current_generation(),
2269            crate::constants::MAX_LEDGER_GENERATIONS as u64
2270        );
2271        let oversize = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 + 1);
2272        assert!(matches!(
2273            store.commit(&oversize),
2274            Err(LedgerCommitError::Integrity(
2275                LedgerIntegrityError::LimitExceeded { .. }
2276            ))
2277        ));
2278        assert_eq!(store, before);
2279    }
2280
2281    #[test]
2282    fn record_count_boundary_and_malicious_history_reject_on_recovery() {
2283        let mut ledger = committed_ledger(1);
2284        ledger.allocation_history.records = (0..255_u8)
2285            .map(|id| active_record(&format!("app.slot{id}.v1"), id))
2286            .collect();
2287        let mut store = LedgerCommitStore::default();
2288        store.commit(&ledger).unwrap();
2289        assert_eq!(
2290            store
2291                .recover()
2292                .unwrap()
2293                .ledger()
2294                .allocation_history()
2295                .records()
2296                .len(),
2297            255
2298        );
2299        ledger
2300            .allocation_history
2301            .records
2302            .push(active_record("app.extra.v1", 254));
2303        assert!(matches!(
2304            store.commit(&ledger),
2305            Err(LedgerCommitError::Integrity(
2306                LedgerIntegrityError::LimitExceeded { .. }
2307            ))
2308        ));
2309        let payload = enveloped_payload(&ledger);
2310        let mut invalid = LedgerCommitStore {
2311            physical: DualCommitStore {
2312                slot0: Some(CommittedGenerationBytes::new(1, payload)),
2313                slot1: None,
2314            },
2315        };
2316        assert!(invalid.recover().is_err());
2317        assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
2318        for bytes in [
2319            vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2320            vec![0x81; 34],
2321            vec![0xa1],
2322        ] {
2323            let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
2324            let invalid = LedgerCommitStore {
2325                physical: DualCommitStore {
2326                    slot0: Some(CommittedGenerationBytes::new(1, payload)),
2327                    slot1: None,
2328                },
2329            };
2330            assert!(matches!(
2331                invalid.recover(),
2332                Err(LedgerCommitError::Codec(_))
2333            ));
2334        }
2335    }
2336    #[test]
2337    fn encoded_byte_limit_rejects_before_commit_mutation() {
2338        let mut store = LedgerCommitStore::default();
2339        store.commit(&committed_ledger(0)).unwrap();
2340        let before = store.clone();
2341        let mut ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2342        for generation in ledger.allocation_history.generations_mut() {
2343            generation.runtime_fingerprint =
2344                Some("x".repeat(crate::constants::DIAGNOSTIC_STRING_MAX_BYTES));
2345        }
2346        assert!(matches!(
2347            store.commit(&ledger),
2348            Err(LedgerCommitError::Integrity(
2349                LedgerIntegrityError::LimitExceeded {
2350                    resource: "ledger bytes",
2351                    ..
2352                }
2353            ))
2354        ));
2355        assert_eq!(store, before);
2356        let mut envelope = LedgerPayloadEnvelope::current(Vec::new())
2357            .try_encode()
2358            .unwrap();
2359        envelope[16..24]
2360            .copy_from_slice(&((crate::constants::MAX_LEDGER_BYTES + 1) as u64).to_le_bytes());
2361        assert!(matches!(
2362            LedgerPayloadEnvelope::decode(&envelope),
2363            Err(LedgerPayloadEnvelopeError::PayloadTooLarge { .. })
2364        ));
2365    }
2366}