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
28struct 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#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
66#[serde(deny_unknown_fields)]
67pub struct LedgerCommitStore {
68 physical: DualCommitStore,
70}
71
72impl LedgerCommitStore {
73 #[must_use]
75 pub const fn physical(&self) -> &DualCommitStore {
76 &self.physical
77 }
78
79 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 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 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 #[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}