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