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