1use crate::{
2 constants::WASM_PAGE_SIZE_BYTES,
3 declaration::AllocationDeclaration,
4 ledger::{AllocationLedger, AllocationRecord, GenerationRecord},
5 physical::CommitStoreDiagnostic,
6 policy::PolicyIdentity,
7 registry::SealedDeclarationFingerprint,
8 slot::{AllocationSlotDescriptor, MemoryManagerAuthorityRecord, MemoryManagerRangeAuthority},
9};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18#[serde(deny_unknown_fields)]
19pub struct DiagnosticExport {
20 pub current_generation: u64,
22 pub ledger_anchor: AllocationSlotDescriptor,
24 pub records: Vec<DiagnosticRecord>,
26 pub generations: Vec<DiagnosticGeneration>,
28 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
30 pub commit_recovery: Option<CommitStoreDiagnostic>,
31}
32
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
41#[serde(deny_unknown_fields)]
42pub struct DiagnosticRuntimeBinding {
43 pub policy_identity: PolicyIdentity,
45 pub declaration_fingerprint: SealedDeclarationFingerprint,
47}
48
49impl DiagnosticRuntimeBinding {
50 #[must_use]
52 pub const fn new(
53 policy_identity: PolicyIdentity,
54 declaration_fingerprint: SealedDeclarationFingerprint,
55 ) -> Self {
56 Self {
57 policy_identity,
58 declaration_fingerprint,
59 }
60 }
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
75#[serde(deny_unknown_fields)]
76pub struct MemoryRuntimeDoctorReport {
77 pub bootstrapped: bool,
79 pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
81 pub tested_declaration_fingerprint: SealedDeclarationFingerprint,
83 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
85 pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
86 pub bootstrap_binding: DiagnosticCheck,
89 pub ledger_anchor: AllocationSlotDescriptor,
91 pub stable_cell: DiagnosticStableCell,
93 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
95 pub commit_recovery: Option<CommitStoreDiagnostic>,
96 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
98 pub ledger: Option<DiagnosticExport>,
99 pub registered_declarations: Vec<DiagnosticDeclaration>,
101 pub range_authority: DiagnosticRangeAuthority,
104 pub validation: DiagnosticCheck,
106}
107
108#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
115#[serde(deny_unknown_fields)]
116pub struct DiagnosticDeclaration {
117 pub authority: String,
119 pub declaration: AllocationDeclaration,
121}
122
123impl DiagnosticDeclaration {
124 #[must_use]
126 pub fn new(authority: impl Into<String>, declaration: AllocationDeclaration) -> Self {
127 Self {
128 authority: authority.into(),
129 declaration,
130 }
131 }
132}
133
134#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
141pub enum DiagnosticCode {
142 #[serde(rename = "stable_cell")]
144 StableCell,
145 #[serde(rename = "unsupported_format")]
147 UnsupportedFormat,
148 #[serde(rename = "ledger_recovery")]
150 LedgerRecovery,
151 #[serde(rename = "genesis_ledger")]
153 GenesisLedger,
154 #[serde(rename = "allocation_validation")]
156 AllocationValidation,
157 #[serde(rename = "policy_identity")]
159 PolicyIdentity,
160 #[serde(rename = "runtime_binding")]
162 RuntimeBinding,
163 #[serde(rename = "memory_size")]
165 MemorySize,
166}
167
168#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
175#[serde(deny_unknown_fields)]
176pub struct DiagnosticFailure {
177 pub code: DiagnosticCode,
179 pub message: String,
181}
182
183impl DiagnosticFailure {
184 #[must_use]
186 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
187 Self {
188 code,
189 message: message.into(),
190 }
191 }
192}
193
194#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
201#[serde(deny_unknown_fields)]
202pub struct DiagnosticRangeAuthority {
203 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
205 pub effective_authority: MemoryManagerRangeAuthority,
207}
208
209impl DiagnosticRangeAuthority {
210 #[must_use]
212 pub const fn new(
213 registered_records: Vec<MemoryManagerAuthorityRecord>,
214 effective_authority: MemoryManagerRangeAuthority,
215 ) -> Self {
216 Self {
217 registered_records,
218 effective_authority,
219 }
220 }
221}
222
223#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
230#[serde(deny_unknown_fields)]
231pub struct DiagnosticStableCell {
232 pub status: DiagnosticStableCellStatus,
234 pub memory_size: DiagnosticMemorySize,
236}
237
238impl DiagnosticStableCell {
239 #[must_use]
241 pub const fn new(
242 status: DiagnosticStableCellStatus,
243 memory_size: DiagnosticMemorySize,
244 ) -> Self {
245 Self {
246 status,
247 memory_size,
248 }
249 }
250}
251
252#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
259#[serde(deny_unknown_fields)]
260pub enum DiagnosticStableCellStatus {
261 Empty,
263 Readable,
265 Corrupt {
268 failure: DiagnosticFailure,
270 },
271}
272
273#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
280#[serde(deny_unknown_fields)]
281pub enum DiagnosticCheck {
282 NotRun {
284 code: DiagnosticCode,
286 message: String,
288 },
289 Passed,
291 Failed {
293 code: DiagnosticCode,
295 message: String,
297 },
298}
299
300impl DiagnosticCheck {
301 #[must_use]
303 pub const fn passed() -> Self {
304 Self::Passed
305 }
306
307 #[must_use]
309 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
310 Self::Failed {
311 code,
312 message: message.into(),
313 }
314 }
315
316 #[must_use]
318 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
319 Self::NotRun {
320 code,
321 message: message.into(),
322 }
323 }
324}
325
326impl DiagnosticExport {
327 #[must_use]
329 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
330 Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
331 }
332
333 #[must_use]
335 pub fn from_ledger_with_commit_recovery(
336 ledger: &AllocationLedger,
337 ledger_anchor: AllocationSlotDescriptor,
338 commit_recovery: Option<CommitStoreDiagnostic>,
339 ) -> Self {
340 Self::from_ledger_with_commit_recovery_and_memory_sizes(
341 ledger,
342 ledger_anchor,
343 commit_recovery,
344 std::iter::empty(),
345 )
346 }
347
348 #[must_use]
350 pub fn from_ledger_with_memory_sizes(
351 ledger: &AllocationLedger,
352 ledger_anchor: AllocationSlotDescriptor,
353 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
354 ) -> Self {
355 Self::from_ledger_with_commit_recovery_and_memory_sizes(
356 ledger,
357 ledger_anchor,
358 None,
359 memory_sizes,
360 )
361 }
362
363 #[must_use]
365 pub fn from_ledger_with_commit_recovery_and_memory_sizes(
366 ledger: &AllocationLedger,
367 ledger_anchor: AllocationSlotDescriptor,
368 commit_recovery: Option<CommitStoreDiagnostic>,
369 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
370 ) -> Self {
371 Self::from_ledger_with_commit_recovery_and_memory_size_outcomes(
372 ledger,
373 ledger_anchor,
374 commit_recovery,
375 memory_sizes
376 .into_iter()
377 .map(|(slot, size)| (slot, DiagnosticMemorySizeOutcome::Measured(size))),
378 )
379 }
380
381 pub(crate) fn from_ledger_with_commit_recovery_and_memory_size_outcomes(
382 ledger: &AllocationLedger,
383 ledger_anchor: AllocationSlotDescriptor,
384 commit_recovery: Option<CommitStoreDiagnostic>,
385 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySizeOutcome)>,
386 ) -> Self {
387 let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
388 Self {
389 current_generation: ledger.current_generation,
390 ledger_anchor,
391 records: ledger
392 .allocation_history()
393 .records()
394 .iter()
395 .cloned()
396 .map(|allocation| {
397 let memory_size = memory_sizes.get(allocation.slot()).cloned();
398 DiagnosticRecord {
399 allocation,
400 memory_size,
401 }
402 })
403 .collect(),
404 generations: ledger
405 .allocation_history()
406 .generations()
407 .iter()
408 .cloned()
409 .map(|generation| DiagnosticGeneration { generation })
410 .collect(),
411 commit_recovery,
412 }
413 }
414}
415
416#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
421#[serde(deny_unknown_fields)]
422pub struct DiagnosticRecord {
423 pub allocation: AllocationRecord,
425 #[serde(skip_serializing_if = "Option::is_none")]
430 pub memory_size: Option<DiagnosticMemorySizeOutcome>,
431}
432
433#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
444pub enum DiagnosticMemorySizeOutcome {
445 Measured(DiagnosticMemorySize),
447 Failed(DiagnosticFailure),
449}
450
451#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
458#[serde(deny_unknown_fields)]
459pub struct DiagnosticMemorySize {
460 pub wasm_pages: u64,
462 pub bytes: u64,
464}
465
466impl DiagnosticMemorySize {
467 #[must_use]
469 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
470 Self {
471 wasm_pages,
472 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
473 }
474 }
475}
476
477#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
482#[serde(deny_unknown_fields)]
483pub struct DiagnosticGeneration {
484 pub generation: GenerationRecord,
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use crate::{
492 declaration::AllocationDeclaration,
493 ledger::{AllocationHistory, AllocationRecord},
494 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
495 schema::SchemaMetadata,
496 };
497
498 #[test]
499 fn diagnostic_export_copies_ledger_records() {
500 let declaration = AllocationDeclaration::new(
501 "app.users.v1",
502 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
503 None,
504 SchemaMetadata::default(),
505 )
506 .expect("declaration");
507 let ledger = AllocationLedger {
508 current_generation: 3,
509 allocation_history: AllocationHistory::from_parts(
510 vec![AllocationRecord::active(3, declaration)],
511 vec![GenerationRecord {
512 generation: 3,
513 parent_generation: 2,
514 runtime_fingerprint: Some("wasm:abc123".to_string()),
515 declaration_count: 1,
516 committed_at: None,
517 }],
518 ),
519 };
520
521 let export = DiagnosticExport::from_ledger(
522 &ledger,
523 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
524 );
525
526 assert_eq!(export.current_generation, 3);
527 assert_eq!(export.records.len(), 1);
528 assert_eq!(export.records[0].memory_size, None);
529 assert_eq!(export.generations.len(), 1);
530 assert_eq!(
531 export.ledger_anchor,
532 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
533 );
534 assert_eq!(export.commit_recovery, None);
535 }
536
537 #[test]
538 fn diagnostic_export_rejects_unknown_top_level_fields() {
539 use crate::test_cbor::Value;
540
541 let export = DiagnosticExport {
542 current_generation: 0,
543 ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
544 records: Vec::new(),
545 generations: Vec::new(),
546 commit_recovery: None,
547 };
548 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
549 else {
550 panic!("diagnostic export encodes as a map");
551 };
552 crate::test_cbor::map_insert(
553 &mut map,
554 Value::Text("future_field".to_string()),
555 Value::Bool(true),
556 );
557 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
558
559 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
560 .expect_err("unknown diagnostic field must fail closed");
561
562 assert!(err.to_string().contains("future_field"));
563 }
564
565 #[test]
566 fn diagnostic_outcome_states_round_trip() {
567 let stable_cell = DiagnosticStableCell::new(
568 DiagnosticStableCellStatus::Corrupt {
569 failure: DiagnosticFailure::new(
570 DiagnosticCode::StableCell,
571 "bad stable-cell record",
572 ),
573 },
574 DiagnosticMemorySize::from_wasm_pages(1),
575 );
576 let range_authority =
577 DiagnosticRangeAuthority::new(Vec::new(), MemoryManagerRangeAuthority::default());
578 let check = DiagnosticCheck::failed(
579 DiagnosticCode::AllocationValidation,
580 "duplicate declaration",
581 );
582
583 for value in [DiagnosticCheck::passed(), check] {
584 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
585 let decoded: DiagnosticCheck =
586 crate::test_cbor::from_slice(&bytes).expect("check round trip");
587 assert_eq!(decoded, value);
588 }
589
590 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
591 let decoded: DiagnosticStableCell =
592 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
593 assert_eq!(decoded, stable_cell);
594
595 let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
596 let decoded: DiagnosticRangeAuthority =
597 crate::test_cbor::from_slice(&bytes).expect("range round trip");
598 assert_eq!(decoded, range_authority);
599 }
600
601 #[test]
602 fn diagnostic_codes_have_stable_wire_names() {
603 let cases = [
604 (DiagnosticCode::StableCell, "stable_cell"),
605 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
606 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
607 (DiagnosticCode::GenesisLedger, "genesis_ledger"),
608 (
609 DiagnosticCode::AllocationValidation,
610 "allocation_validation",
611 ),
612 (DiagnosticCode::PolicyIdentity, "policy_identity"),
613 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
614 (DiagnosticCode::MemorySize, "memory_size"),
615 ];
616
617 for (code, expected) in cases {
618 assert_eq!(
619 crate::test_cbor::to_value(code).expect("diagnostic code value"),
620 crate::test_cbor::Value::Text(expected.to_string())
621 );
622 }
623 }
624
625 #[test]
626 fn diagnostic_export_can_include_commit_recovery_state() {
627 let ledger = AllocationLedger {
628 current_generation: 3,
629 allocation_history: AllocationHistory::default(),
630 };
631 let commit_recovery = CommitStoreDiagnostic {
632 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
633 slot1: CommitSlotDiagnostic::Empty,
634 recovery: Ok(3),
635 };
636
637 let export = DiagnosticExport::from_ledger_with_commit_recovery(
638 &ledger,
639 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
640 Some(commit_recovery),
641 );
642
643 assert_eq!(export.commit_recovery, Some(commit_recovery));
644 }
645
646 #[test]
647 fn diagnostic_export_can_include_memory_sizes() {
648 let declaration = AllocationDeclaration::new(
649 "app.users.v1",
650 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
651 None,
652 SchemaMetadata::default(),
653 )
654 .expect("declaration");
655 let ledger = AllocationLedger {
656 current_generation: 3,
657 allocation_history: AllocationHistory::from_parts(
658 vec![AllocationRecord::active(3, declaration)],
659 Vec::new(),
660 ),
661 };
662
663 let export = DiagnosticExport::from_ledger_with_memory_sizes(
664 &ledger,
665 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
666 [(
667 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
668 DiagnosticMemorySize::from_wasm_pages(2),
669 )],
670 );
671
672 assert_eq!(
673 export.records[0].memory_size,
674 Some(DiagnosticMemorySizeOutcome::Measured(
675 DiagnosticMemorySize {
676 wasm_pages: 2,
677 bytes: 131_072,
678 }
679 ))
680 );
681 }
682
683 #[test]
684 fn diagnostic_export_preserves_per_slot_size_successes_and_failures() {
685 let users = AllocationDeclaration::memory_manager("app.users.v1", 100, "users")
686 .expect("users declaration");
687 let orders = AllocationDeclaration::memory_manager("app.orders.v1", 101, "orders")
688 .expect("orders declaration");
689 let ledger = AllocationLedger {
690 current_generation: 3,
691 allocation_history: AllocationHistory::from_parts(
692 vec![
693 AllocationRecord::active(3, users),
694 AllocationRecord::active(3, orders),
695 ],
696 Vec::new(),
697 ),
698 };
699 let size_failure =
700 DiagnosticFailure::new(DiagnosticCode::MemorySize, "slot could not be measured");
701
702 let export = DiagnosticExport::from_ledger_with_commit_recovery_and_memory_size_outcomes(
703 &ledger,
704 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
705 None,
706 [
707 (
708 AllocationSlotDescriptor::memory_manager(100).expect("users slot"),
709 DiagnosticMemorySizeOutcome::Measured(DiagnosticMemorySize::from_wasm_pages(2)),
710 ),
711 (
712 AllocationSlotDescriptor::memory_manager(101).expect("orders slot"),
713 DiagnosticMemorySizeOutcome::Failed(size_failure.clone()),
714 ),
715 ],
716 );
717
718 assert_eq!(
719 export.records[0].memory_size,
720 Some(DiagnosticMemorySizeOutcome::Measured(
721 DiagnosticMemorySize::from_wasm_pages(2)
722 ))
723 );
724 assert_eq!(
725 export.records[1].memory_size,
726 Some(DiagnosticMemorySizeOutcome::Failed(size_failure))
727 );
728 }
729
730 #[test]
731 fn diagnostic_export_can_report_recovery_failure() {
732 let ledger = AllocationLedger {
733 current_generation: 0,
734 allocation_history: AllocationHistory::default(),
735 };
736 let commit_recovery = CommitStoreDiagnostic {
737 slot0: CommitSlotDiagnostic::Empty,
738 slot1: CommitSlotDiagnostic::Empty,
739 recovery: Err(CommitRecoveryError::NoValidGeneration),
740 };
741
742 let export = DiagnosticExport::from_ledger_with_commit_recovery(
743 &ledger,
744 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
745 Some(commit_recovery),
746 );
747
748 assert_eq!(
749 export.commit_recovery.expect("commit recovery").recovery,
750 Err(CommitRecoveryError::NoValidGeneration)
751 );
752 }
753}