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