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)]
440pub enum DiagnosticMemorySizeOutcome {
441 Measured(DiagnosticMemorySize),
443 Failed(DiagnosticFailure),
445}
446
447#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
454#[serde(deny_unknown_fields)]
455pub struct DiagnosticMemorySize {
456 pub wasm_pages: u64,
458 pub bytes: u64,
460}
461
462impl DiagnosticMemorySize {
463 #[must_use]
465 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
466 Self {
467 wasm_pages,
468 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
469 }
470 }
471}
472
473#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
478#[serde(deny_unknown_fields)]
479pub struct DiagnosticGeneration {
480 pub generation: GenerationRecord,
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use crate::{
488 declaration::AllocationDeclaration,
489 ledger::{AllocationHistory, AllocationRecord},
490 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
491 schema::SchemaMetadata,
492 };
493
494 #[test]
495 fn diagnostic_export_copies_ledger_records() {
496 let declaration = AllocationDeclaration::new(
497 "app.users.v1",
498 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
499 None,
500 SchemaMetadata::default(),
501 )
502 .expect("declaration");
503 let ledger = AllocationLedger {
504 current_generation: 3,
505 allocation_history: AllocationHistory::from_parts(
506 vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
507 vec![GenerationRecord {
508 generation: 3,
509 parent_generation: 2,
510 runtime_fingerprint: Some("wasm:abc123".to_string()),
511 declaration_count: 1,
512 committed_at: None,
513 }],
514 ),
515 };
516
517 let export = DiagnosticExport::from_ledger(
518 &ledger,
519 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
520 );
521
522 assert_eq!(export.current_generation, 3);
523 assert_eq!(export.records.len(), 1);
524 assert_eq!(export.records[0].memory_size, None);
525 assert_eq!(export.generations.len(), 1);
526 assert_eq!(
527 export.ledger_anchor,
528 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
529 );
530 assert_eq!(export.commit_recovery, None);
531 }
532
533 #[test]
534 fn diagnostic_export_rejects_unknown_top_level_fields() {
535 use crate::test_cbor::Value;
536
537 let export = DiagnosticExport {
538 current_generation: 0,
539 ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
540 records: Vec::new(),
541 generations: Vec::new(),
542 commit_recovery: None,
543 };
544 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
545 else {
546 panic!("diagnostic export encodes as a map");
547 };
548 crate::test_cbor::map_insert(
549 &mut map,
550 Value::Text("future_field".to_string()),
551 Value::Bool(true),
552 );
553 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
554
555 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
556 .expect_err("unknown diagnostic field must fail closed");
557
558 assert!(err.to_string().contains("future_field"));
559 }
560
561 #[test]
562 fn diagnostic_outcome_states_round_trip() {
563 let stable_cell = DiagnosticStableCell::new(
564 DiagnosticStableCellStatus::Corrupt {
565 failure: DiagnosticFailure::new(
566 DiagnosticCode::StableCell,
567 "bad stable-cell record",
568 ),
569 },
570 DiagnosticMemorySize::from_wasm_pages(1),
571 );
572 let range_authority =
573 DiagnosticRangeAuthority::new(Vec::new(), MemoryManagerRangeAuthority::default());
574 let check = DiagnosticCheck::failed(
575 DiagnosticCode::AllocationValidation,
576 "duplicate declaration",
577 );
578
579 for value in [DiagnosticCheck::passed(), check] {
580 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
581 let decoded: DiagnosticCheck =
582 crate::test_cbor::from_slice(&bytes).expect("check round trip");
583 assert_eq!(decoded, value);
584 }
585
586 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
587 let decoded: DiagnosticStableCell =
588 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
589 assert_eq!(decoded, stable_cell);
590
591 let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
592 let decoded: DiagnosticRangeAuthority =
593 crate::test_cbor::from_slice(&bytes).expect("range round trip");
594 assert_eq!(decoded, range_authority);
595 }
596
597 #[test]
598 fn diagnostic_codes_have_stable_wire_names() {
599 let cases = [
600 (DiagnosticCode::StableCell, "stable_cell"),
601 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
602 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
603 (DiagnosticCode::GenesisLedger, "genesis_ledger"),
604 (
605 DiagnosticCode::AllocationValidation,
606 "allocation_validation",
607 ),
608 (DiagnosticCode::PolicyIdentity, "policy_identity"),
609 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
610 (DiagnosticCode::MemorySize, "memory_size"),
611 ];
612
613 for (code, expected) in cases {
614 assert_eq!(
615 crate::test_cbor::to_value(code).expect("diagnostic code value"),
616 crate::test_cbor::Value::Text(expected.to_string())
617 );
618 }
619 }
620
621 #[test]
622 fn diagnostic_export_can_include_commit_recovery_state() {
623 let ledger = AllocationLedger {
624 current_generation: 3,
625 allocation_history: AllocationHistory::default(),
626 };
627 let commit_recovery = CommitStoreDiagnostic {
628 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
629 slot1: CommitSlotDiagnostic::Empty,
630 recovery: Ok(3),
631 };
632
633 let export = DiagnosticExport::from_ledger_with_commit_recovery(
634 &ledger,
635 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
636 Some(commit_recovery),
637 );
638
639 assert_eq!(export.commit_recovery, Some(commit_recovery));
640 }
641
642 #[test]
643 fn diagnostic_export_can_include_memory_sizes() {
644 let declaration = AllocationDeclaration::new(
645 "app.users.v1",
646 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
647 None,
648 SchemaMetadata::default(),
649 )
650 .expect("declaration");
651 let ledger = AllocationLedger {
652 current_generation: 3,
653 allocation_history: AllocationHistory::from_parts(
654 vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
655 Vec::new(),
656 ),
657 };
658
659 let export = DiagnosticExport::from_ledger_with_memory_sizes(
660 &ledger,
661 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
662 [(
663 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
664 DiagnosticMemorySize::from_wasm_pages(2),
665 )],
666 );
667
668 assert_eq!(
669 export.records[0].memory_size,
670 Some(DiagnosticMemorySizeOutcome::Measured(
671 DiagnosticMemorySize {
672 wasm_pages: 2,
673 bytes: 131_072,
674 }
675 ))
676 );
677 }
678
679 #[test]
680 fn diagnostic_export_preserves_per_slot_size_successes_and_failures() {
681 let users = AllocationDeclaration::memory_manager("app.users.v1", 100, "users")
682 .expect("users declaration");
683 let orders = AllocationDeclaration::memory_manager("app.orders.v1", 101, "orders")
684 .expect("orders declaration");
685 let ledger = AllocationLedger {
686 current_generation: 3,
687 allocation_history: AllocationHistory::from_parts(
688 vec![
689 AllocationRecord::active(3, users).expect("users record"),
690 AllocationRecord::active(3, orders).expect("orders record"),
691 ],
692 Vec::new(),
693 ),
694 };
695 let size_failure =
696 DiagnosticFailure::new(DiagnosticCode::MemorySize, "slot could not be measured");
697
698 let export = DiagnosticExport::from_ledger_with_commit_recovery_and_memory_size_outcomes(
699 &ledger,
700 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
701 None,
702 [
703 (
704 AllocationSlotDescriptor::memory_manager(100).expect("users slot"),
705 DiagnosticMemorySizeOutcome::Measured(DiagnosticMemorySize::from_wasm_pages(2)),
706 ),
707 (
708 AllocationSlotDescriptor::memory_manager(101).expect("orders slot"),
709 DiagnosticMemorySizeOutcome::Failed(size_failure.clone()),
710 ),
711 ],
712 );
713
714 assert_eq!(
715 export.records[0].memory_size,
716 Some(DiagnosticMemorySizeOutcome::Measured(
717 DiagnosticMemorySize::from_wasm_pages(2)
718 ))
719 );
720 assert_eq!(
721 export.records[1].memory_size,
722 Some(DiagnosticMemorySizeOutcome::Failed(size_failure))
723 );
724 }
725
726 #[test]
727 fn diagnostic_export_can_report_recovery_failure() {
728 let ledger = AllocationLedger {
729 current_generation: 0,
730 allocation_history: AllocationHistory::default(),
731 };
732 let commit_recovery = CommitStoreDiagnostic {
733 slot0: CommitSlotDiagnostic::Empty,
734 slot1: CommitSlotDiagnostic::Empty,
735 recovery: Err(CommitRecoveryError::NoValidGeneration),
736 };
737
738 let export = DiagnosticExport::from_ledger_with_commit_recovery(
739 &ledger,
740 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
741 Some(commit_recovery),
742 );
743
744 assert_eq!(
745 export.commit_recovery.expect("commit recovery").recovery,
746 Err(CommitRecoveryError::NoValidGeneration)
747 );
748 }
749}