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