1use crate::{
2 AllocationBootstrap, AllocationDeclaration, AllocationHistory, AllocationLedger,
3 AllocationPolicy, AllocationSlotDescriptor, CommittedAllocations, DeclarationSnapshot,
4 DefaultMemoryManagerDoctorReport, DiagnosticCheck, DiagnosticCode, DiagnosticDeclaration,
5 DiagnosticExport, DiagnosticFailure, DiagnosticMemorySize, DiagnosticRangeAuthority,
6 DiagnosticStableCell, DiagnosticStableCellStatus, LedgerCommitError,
7 LedgerPayloadEnvelopeError, STABLE_CELL_VALUE_OFFSET, StableCellLedgerError,
8 StableCellLedgerRecord, StableKey,
9 physical::CommitStoreDiagnostic,
10 registry::{
11 StaticMemoryDeclaration, StaticMemoryDeclarationError, StaticMemoryRangeDeclaration,
12 seal_static_memory_registry, static_memory_declarations, static_memory_range_declarations,
13 },
14 slot::{
15 IC_MEMORY_AUTHORITY_OWNER, IC_MEMORY_AUTHORITY_PURPOSE, IC_MEMORY_LEDGER_LABEL,
16 IC_MEMORY_LEDGER_STABLE_KEY, MEMORY_MANAGER_LEDGER_ID, MemoryManagerAuthorityRecord,
17 MemoryManagerIdRange, MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError,
18 MemoryManagerRangeMode, MemoryManagerSlotError,
19 },
20 stable_cell::decode_stable_cell_ledger_record_from_memory,
21};
22use ic_stable_structures::{
23 Cell, DefaultMemoryImpl, Memory, Storable,
24 memory_manager::{MemoryId, MemoryManager, VirtualMemory},
25};
26use std::{
27 cell::RefCell,
28 collections::BTreeMap,
29 convert::Infallible,
30 sync::{
31 Mutex,
32 atomic::{AtomicBool, Ordering},
33 },
34};
35
36type DefaultLedgerCell = Cell<StableCellLedgerRecord, VirtualMemory<DefaultMemoryImpl>>;
37
38thread_local! {
39 static DEFAULT_MEMORY_MANAGER: MemoryManager<DefaultMemoryImpl> =
40 MemoryManager::init(DefaultMemoryImpl::default());
41 static DEFAULT_LEDGER_CELL: RefCell<Option<DefaultLedgerCell>> = const {
42 RefCell::new(None)
43 };
44}
45
46static EAGER_INIT_HOOKS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
47static COMMITTED_ALLOCATIONS: Mutex<Option<CommittedAllocations>> = Mutex::new(None);
48static BOOTSTRAPPED: AtomicBool = AtomicBool::new(false);
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51struct RuntimeLockPoisoned;
52
53impl RuntimeLockPoisoned {
54 const MESSAGE: &'static str = "ic-memory runtime lock poisoned";
55}
56
57#[non_exhaustive]
62#[derive(Debug, thiserror::Error)]
63pub enum RuntimeBootstrapError<P> {
64 #[error(transparent)]
66 Registry(#[from] StaticMemoryDeclarationError),
67 #[error(transparent)]
69 Range(#[from] MemoryManagerRangeAuthorityError),
70 #[error(transparent)]
72 LedgerIntegrity(#[from] crate::LedgerIntegrityError),
73 #[error(transparent)]
75 LedgerCommit(#[from] crate::LedgerCommitError),
76 #[error(transparent)]
78 StableCellLedger(#[from] StableCellLedgerError),
79 #[error("stable-cell ledger record size {value_size} cannot be written to stable memory")]
81 StableCellLedgerWriteTooLarge {
82 value_size: usize,
84 },
85 #[error(transparent)]
87 Validation(#[from] crate::AllocationValidationError<RuntimePolicyError<P>>),
88 #[error(transparent)]
90 Staging(#[from] crate::AllocationStageError),
91 #[error("ic-memory runtime lock poisoned")]
93 RuntimeLockPoisoned,
94}
95
96#[non_exhaustive]
101#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
102pub enum RuntimeOpenError {
103 #[error("ic-memory runtime has not completed bootstrap validation")]
105 NotBootstrapped,
106 #[error("ic-memory runtime lock poisoned")]
108 RuntimeLockPoisoned,
109 #[error(transparent)]
111 StableKey(#[from] crate::StableKeyError),
112 #[error("stable key '{0}' was not committed by ic-memory runtime bootstrap")]
114 StableKeyNotCommitted(String),
115 #[error("stable key '{stable_key}' is reserved for ic-memory runtime governance")]
117 ReservedStableKey {
118 stable_key: String,
120 },
121 #[error(transparent)]
123 MemoryManagerSlot(#[from] MemoryManagerSlotError),
124 #[error(
126 "stable key '{stable_key}' is committed for MemoryManager ID {committed_id}, not requested ID {requested_id}"
127 )]
128 MemoryIdMismatch {
129 stable_key: String,
131 committed_id: u8,
133 requested_id: u8,
135 },
136}
137
138#[non_exhaustive]
145#[derive(Debug, thiserror::Error)]
146pub enum RuntimeDiagnosticError {
147 #[error("ic-memory runtime has not completed bootstrap validation")]
149 NotBootstrapped,
150 #[error(transparent)]
152 LedgerCommit(#[from] LedgerCommitError),
153 #[error(transparent)]
155 StableCellLedger(#[from] StableCellLedgerError),
156 #[error(transparent)]
158 MemoryManagerSlot(#[from] MemoryManagerSlotError),
159}
160
161#[non_exhaustive]
166#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
167pub enum RuntimePolicyError<P> {
168 #[error(transparent)]
170 Range(#[from] MemoryManagerRangeAuthorityError),
171 #[error("runtime declaration metadata is missing for stable key '{0}'")]
173 MissingDeclarationMetadata(String),
174 #[error("stable key '{stable_key}' is reserved to authority '{expected_authority}'")]
176 ReservedStableKeyAuthority {
177 stable_key: String,
179 expected_authority: &'static str,
181 },
182 #[error(transparent)]
184 Custom(P),
185}
186
187#[doc(hidden)]
189pub fn defer_eager_init(f: fn()) {
190 assert!(
191 !is_default_memory_manager_bootstrapped(),
192 "ic-memory eager-init registration attempted after runtime bootstrap"
193 );
194 EAGER_INIT_HOOKS
195 .lock()
196 .expect("ic-memory eager-init queue poisoned")
197 .push(f);
198}
199
200#[must_use]
202pub fn is_default_memory_manager_bootstrapped() -> bool {
203 BOOTSTRAPPED.load(Ordering::SeqCst)
204}
205
206pub fn committed_allocations() -> Result<CommittedAllocations, RuntimeOpenError> {
208 if !is_default_memory_manager_bootstrapped() {
209 return Err(RuntimeOpenError::NotBootstrapped);
210 }
211 COMMITTED_ALLOCATIONS
212 .lock()
213 .map_err(|_| RuntimeOpenError::RuntimeLockPoisoned)?
214 .clone()
215 .ok_or(RuntimeOpenError::NotBootstrapped)
216}
217
218pub fn bootstrap_default_memory_manager()
220-> Result<CommittedAllocations, RuntimeBootstrapError<Infallible>> {
221 bootstrap_default_memory_manager_with_policy(&NoopPolicy)
222}
223
224pub fn bootstrap_default_memory_manager_with_policy<P: AllocationPolicy>(
239 policy: &P,
240) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
241 if let Ok(committed) = committed_allocations() {
242 return Ok(committed);
243 }
244
245 run_eager_init_hooks().map_err(|_err| RuntimeBootstrapError::RuntimeLockPoisoned)?;
246
247 let registered_declarations = static_memory_declarations()?;
248 let registered_ranges = static_memory_range_declarations()?;
249 let user_ranges_registered = !registered_ranges.is_empty();
250 let declaration_metadata = declaration_metadata(®istered_declarations);
251 let range_authority = range_authority(registered_ranges)?;
252 let snapshot = declaration_snapshot(registered_declarations)?;
253 seal_static_memory_registry()?;
254 let policy = RuntimeMemoryManagerPolicy {
255 range_authority,
256 user_ranges_registered,
257 declaration_metadata,
258 custom_policy: policy,
259 };
260 let genesis = AllocationLedger::new(0, AllocationHistory::default())?;
261
262 let committed = with_default_ledger_cell(
263 |cell| -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
264 let mut record = cell.get().clone();
265 let mut bootstrap = AllocationBootstrap::new(record.store_mut());
266 let commit = bootstrap
267 .initialize_validate_and_commit(&genesis, snapshot, &policy, None)
268 .map_err(runtime_bootstrap_error_from_bootstrap)?;
269 let (ledger, validated) = commit.into_parts();
270 set_default_ledger_cell(cell, record)?;
271 Ok(external_runtime_allocations(
272 validated.confirm_persisted(ledger.current_generation()),
273 ))
274 },
275 )?;
276
277 publish_committed_allocations(committed.clone())?;
278 BOOTSTRAPPED.store(true, Ordering::SeqCst);
279 Ok(committed)
280}
281
282pub fn open_default_memory_manager_memory(
284 stable_key: &str,
285 id: u8,
286) -> Result<VirtualMemory<DefaultMemoryImpl>, RuntimeOpenError> {
287 let key = StableKey::parse(stable_key)?;
288 if crate::is_ic_memory_stable_key(key.as_str()) {
289 return Err(RuntimeOpenError::ReservedStableKey {
290 stable_key: stable_key.to_string(),
291 });
292 }
293 let committed = committed_allocations()?;
294 let slot = committed
295 .slot_for(&key)
296 .ok_or_else(|| RuntimeOpenError::StableKeyNotCommitted(stable_key.to_string()))?;
297 let committed_id = slot.memory_manager_id()?;
298 if committed_id != id {
299 return Err(RuntimeOpenError::MemoryIdMismatch {
300 stable_key: stable_key.to_string(),
301 committed_id,
302 requested_id: id,
303 });
304 }
305 Ok(default_memory_manager_memory(id))
306}
307
308pub fn default_memory_manager_diagnostic_export() -> Result<DiagnosticExport, RuntimeDiagnosticError>
315{
316 let record = default_ledger_record_for_diagnostics()?;
317 let recovered = record.store().recover()?;
318 let ledger = recovered.ledger();
319 let memory_sizes = default_memory_manager_memory_sizes(ledger)?;
320
321 Ok(
322 DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
323 ledger,
324 AllocationSlotDescriptor::memory_manager(MEMORY_MANAGER_LEDGER_ID)?,
325 Some(record.store().physical().diagnostic()),
326 memory_sizes,
327 ),
328 )
329}
330
331pub fn default_memory_manager_commit_recovery_diagnostic()
338-> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
339 let record = default_ledger_record_from_memory()?;
340 Ok(record.store().physical().diagnostic())
341}
342
343#[must_use]
351pub fn default_memory_manager_doctor_report() -> DefaultMemoryManagerDoctorReport {
352 let bootstrapped = is_default_memory_manager_bootstrapped();
353 let eager_init_error = if bootstrapped {
354 None
355 } else {
356 run_eager_init_hooks().err().map(|_err| {
357 DiagnosticFailure::new(
358 DiagnosticCode::EagerInit,
359 format!("eager-init hooks: {}", RuntimeLockPoisoned::MESSAGE),
360 )
361 })
362 };
363
364 let stable_cell = default_memory_manager_stable_cell_diagnostic();
365 let commit_recovery = stable_cell
366 .record
367 .as_ref()
368 .map(|record| record.store().physical().diagnostic());
369 let recovered = stable_cell
370 .record
371 .as_ref()
372 .map(|record| record.store().recover());
373 let recovered_for_export = recovered.as_ref().and_then(|result| result.as_ref().ok());
374 let ledger_anchor = default_ledger_anchor_descriptor();
375 let ledger = recovered_for_export.map(|recovered| {
376 DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
377 recovered.ledger(),
378 ledger_anchor.clone(),
379 commit_recovery,
380 default_memory_manager_memory_sizes_lossy(recovered.ledger()),
381 )
382 });
383
384 let registered_declarations = static_memory_declarations();
385 let registered_ranges = static_memory_range_declarations();
386 let diagnostic_declarations = registered_declarations
387 .as_ref()
388 .map(|declarations| {
389 declarations
390 .iter()
391 .map(|registration| {
392 DiagnosticDeclaration::new(
393 registration.authority(),
394 registration.declaration().clone(),
395 )
396 })
397 .collect()
398 })
399 .unwrap_or_default();
400 let range_authority = diagnostic_range_authority(®istered_ranges);
401 let validation = eager_init_error.map_or_else(
402 || {
403 diagnostic_validation(
404 ®istered_declarations,
405 ®istered_ranges,
406 stable_cell.record.as_ref(),
407 recovered.as_ref(),
408 )
409 },
410 |failure| DiagnosticCheck::failed(failure.code, failure.message),
411 );
412
413 DefaultMemoryManagerDoctorReport {
414 bootstrapped: BOOTSTRAPPED.load(Ordering::SeqCst),
415 ledger_anchor,
416 stable_cell: stable_cell.diagnostic,
417 commit_recovery,
418 ledger,
419 registered_declarations: diagnostic_declarations,
420 range_authority,
421 validation,
422 }
423}
424
425fn run_eager_init_hooks() -> Result<(), RuntimeLockPoisoned> {
426 let hooks = {
427 let mut hooks = EAGER_INIT_HOOKS.lock().map_err(|_| RuntimeLockPoisoned)?;
428 std::mem::take(&mut *hooks)
429 };
430
431 for hook in hooks {
432 hook();
433 }
434 Ok(())
435}
436
437fn with_default_ledger_cell<P, T>(
438 op: impl FnOnce(&mut DefaultLedgerCell) -> Result<T, RuntimeBootstrapError<P>>,
439) -> Result<T, RuntimeBootstrapError<P>> {
440 DEFAULT_LEDGER_CELL.with(|cell| {
441 let mut cell = cell.borrow_mut();
442 if cell.is_none() {
443 let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
444 crate::validate_stable_cell_ledger_memory(&memory)?;
445 *cell = Some(Cell::init(memory, StableCellLedgerRecord::default()));
446 }
447 let Some(cell) = cell.as_mut() else {
448 return Err(RuntimeBootstrapError::RuntimeLockPoisoned);
449 };
450 op(cell)
451 })
452}
453
454fn set_default_ledger_cell<P>(
455 cell: &mut DefaultLedgerCell,
456 record: StableCellLedgerRecord,
457) -> Result<(), RuntimeBootstrapError<P>> {
458 ensure_default_ledger_cell_capacity(&record)?;
459 let _previous = cell.set(record);
460 Ok(())
461}
462
463fn ensure_default_ledger_cell_capacity<P>(
464 record: &StableCellLedgerRecord,
465) -> Result<(), RuntimeBootstrapError<P>> {
466 let encoded = record.to_bytes();
467 let value_size = encoded.len();
468 if value_size > u32::MAX as usize {
469 return Err(RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size });
470 }
471
472 let value_size_u32 = u32::try_from(value_size)
473 .map_err(|_| RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size })?;
474 let value_size_u64 = u64::from(value_size_u32);
475 let required_bytes = STABLE_CELL_VALUE_OFFSET
476 .checked_add(value_size_u64)
477 .ok_or(RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size })?;
478 let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
479 let available_bytes = memory.size().saturating_mul(crate::WASM_PAGE_SIZE_BYTES);
480 if required_bytes <= available_bytes {
481 return Ok(());
482 }
483
484 let grow_by = required_bytes
485 .saturating_sub(available_bytes)
486 .div_ceil(crate::WASM_PAGE_SIZE_BYTES);
487 if memory.grow(grow_by) < 0 {
488 return Err(RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size });
489 }
490 Ok(())
491}
492
493fn external_runtime_allocations(committed: CommittedAllocations) -> CommittedAllocations {
494 committed.without_stable_key_prefix(crate::IC_MEMORY_STABLE_KEY_PREFIX)
495}
496
497fn default_memory_manager_memory(id: u8) -> VirtualMemory<DefaultMemoryImpl> {
498 DEFAULT_MEMORY_MANAGER.with(|manager| manager.get(MemoryId::new(id)))
499}
500
501const fn default_ledger_anchor_descriptor() -> AllocationSlotDescriptor {
502 AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
503}
504
505fn default_ledger_record_for_diagnostics() -> Result<StableCellLedgerRecord, RuntimeDiagnosticError>
506{
507 if !is_default_memory_manager_bootstrapped() {
508 return Err(RuntimeDiagnosticError::NotBootstrapped);
509 }
510
511 default_ledger_record_from_memory().map_err(RuntimeDiagnosticError::StableCellLedger)
512}
513
514fn default_ledger_record_from_memory() -> Result<StableCellLedgerRecord, StableCellLedgerError> {
515 let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
516 decode_stable_cell_ledger_record_from_memory(&memory)
517}
518
519fn default_memory_manager_memory_sizes(
520 ledger: &AllocationLedger,
521) -> Result<Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)>, RuntimeDiagnosticError> {
522 ledger
523 .allocation_history()
524 .records()
525 .iter()
526 .map(|record| {
527 let id = record.slot().memory_manager_id()?;
528 let memory = default_memory_manager_memory(id);
529 Ok((
530 record.slot().clone(),
531 DiagnosticMemorySize::from_wasm_pages(memory.size()),
532 ))
533 })
534 .collect()
535}
536
537fn default_memory_manager_memory_sizes_lossy(
538 ledger: &AllocationLedger,
539) -> Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)> {
540 default_memory_manager_memory_sizes(ledger).unwrap_or_default()
541}
542
543struct DefaultStableCellDiagnostic {
544 diagnostic: DiagnosticStableCell,
545 record: Option<StableCellLedgerRecord>,
546}
547
548fn default_memory_manager_stable_cell_diagnostic() -> DefaultStableCellDiagnostic {
549 let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
550 let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
551 if memory.size() == 0 {
552 return DefaultStableCellDiagnostic {
553 diagnostic: DiagnosticStableCell::new(DiagnosticStableCellStatus::Empty, memory_size),
554 record: Some(StableCellLedgerRecord::default()),
555 };
556 }
557
558 let record = decode_stable_cell_ledger_record_from_memory(&memory);
559 match record {
560 Ok(record) => DefaultStableCellDiagnostic {
561 diagnostic: DiagnosticStableCell::new(
562 DiagnosticStableCellStatus::Readable,
563 memory_size,
564 ),
565 record: Some(record),
566 },
567 Err(err) => DefaultStableCellDiagnostic {
568 diagnostic: DiagnosticStableCell::new(
569 DiagnosticStableCellStatus::Corrupt {
570 failure: DiagnosticFailure::new(DiagnosticCode::StableCell, err.to_string()),
571 },
572 memory_size,
573 ),
574 record: None,
575 },
576 }
577}
578
579fn diagnostic_range_authority(
580 registered_ranges: &Result<Vec<StaticMemoryRangeDeclaration>, StaticMemoryDeclarationError>,
581) -> DiagnosticRangeAuthority {
582 match registered_ranges {
583 Ok(ranges) => {
584 let registered_records = ranges
585 .iter()
586 .map(|registration| registration.record().clone())
587 .collect();
588 match range_authority(ranges.clone()) {
589 Ok(authority) => DiagnosticRangeAuthority::new(registered_records, Ok(authority)),
590 Err(err) => DiagnosticRangeAuthority::new(
591 registered_records,
592 Err(DiagnosticFailure::new(
593 DiagnosticCode::RangeAuthority,
594 err.to_string(),
595 )),
596 ),
597 }
598 }
599 Err(err) => DiagnosticRangeAuthority::new(
600 Vec::new(),
601 Err(DiagnosticFailure::new(
602 DiagnosticCode::RangeRegistry,
603 err.to_string(),
604 )),
605 ),
606 }
607}
608
609fn diagnostic_validation(
610 registered_declarations: &Result<Vec<StaticMemoryDeclaration>, StaticMemoryDeclarationError>,
611 registered_ranges: &Result<Vec<StaticMemoryRangeDeclaration>, StaticMemoryDeclarationError>,
612 stable_cell_record: Option<&StableCellLedgerRecord>,
613 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
614) -> DiagnosticCheck {
615 let registered_declarations = match registered_declarations {
616 Ok(declarations) => declarations.clone(),
617 Err(err) => {
618 return DiagnosticCheck::failed(
619 DiagnosticCode::DeclarationRegistry,
620 format!("declaration registry: {err}"),
621 );
622 }
623 };
624 let registered_ranges = match registered_ranges {
625 Ok(ranges) => ranges.clone(),
626 Err(err) => {
627 return DiagnosticCheck::failed(
628 DiagnosticCode::RangeRegistry,
629 format!("range registry: {err}"),
630 );
631 }
632 };
633 let range_authority = match range_authority(registered_ranges.clone()) {
634 Ok(authority) => authority,
635 Err(err) => {
636 return DiagnosticCheck::failed(
637 DiagnosticCode::RangeAuthority,
638 format!("range authority: {err}"),
639 );
640 }
641 };
642 let snapshot = match declaration_snapshot(registered_declarations.clone()) {
643 Ok(snapshot) => snapshot,
644 Err(err) => {
645 return DiagnosticCheck::failed(
646 DiagnosticCode::DeclarationSnapshot,
647 format!("declaration snapshot: {err}"),
648 );
649 }
650 };
651 let recovered = match diagnostic_validation_ledger(stable_cell_record, recovered) {
652 Ok(recovered) => recovered,
653 Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
654 };
655 let policy = RuntimeMemoryManagerPolicy {
656 range_authority,
657 user_ranges_registered: !registered_ranges.is_empty(),
658 declaration_metadata: declaration_metadata(®istered_declarations),
659 custom_policy: &NoopPolicy,
660 };
661
662 match crate::validate_allocations(&recovered, snapshot, &policy) {
663 Ok(_) => DiagnosticCheck::passed(),
664 Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
665 }
666}
667
668fn diagnostic_validation_ledger(
669 stable_cell_record: Option<&StableCellLedgerRecord>,
670 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
671) -> Result<crate::RecoveredLedger, DiagnosticFailure> {
672 if let Some(Ok(recovered)) = recovered {
673 return Ok(recovered.clone());
674 }
675 if let Some(Err(err)) = recovered {
676 if stable_cell_record.is_some_and(|record| record.store().physical().is_uninitialized()) {
677 return diagnostic_genesis_recovered_ledger();
678 }
679 let code = if matches!(
680 err,
681 LedgerCommitError::PayloadEnvelope(
682 LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
683 )
684 ) {
685 DiagnosticCode::UnsupportedFormat
686 } else {
687 DiagnosticCode::LedgerRecovery
688 };
689 return Err(DiagnosticFailure::new(
690 code,
691 format!("protected ledger recovery: {err}"),
692 ));
693 }
694 if stable_cell_record.is_some() {
695 return diagnostic_genesis_recovered_ledger();
696 }
697 Err(DiagnosticFailure::new(
698 DiagnosticCode::StableCell,
699 "stable-cell ledger record is not readable",
700 ))
701}
702
703fn diagnostic_genesis_recovered_ledger() -> Result<crate::RecoveredLedger, DiagnosticFailure> {
704 AllocationLedger::new(0, AllocationHistory::default())
705 .map(|ledger| crate::RecoveredLedger::from_trusted_parts(ledger, 0))
706 .map_err(|err| {
707 DiagnosticFailure::new(
708 DiagnosticCode::GenesisLedger,
709 format!("genesis ledger: {err}"),
710 )
711 })
712}
713
714fn publish_committed_allocations<P>(
715 committed: CommittedAllocations,
716) -> Result<(), RuntimeBootstrapError<P>> {
717 *COMMITTED_ALLOCATIONS
718 .lock()
719 .map_err(|_| RuntimeBootstrapError::RuntimeLockPoisoned)? = Some(committed);
720 Ok(())
721}
722
723fn declaration_snapshot(
724 registrations: Vec<StaticMemoryDeclaration>,
725) -> Result<DeclarationSnapshot, StaticMemoryDeclarationError> {
726 let mut declarations = Vec::with_capacity(registrations.len() + 1);
727 declarations.push(internal_ledger_declaration()?);
728 declarations.extend(
729 registrations
730 .into_iter()
731 .map(StaticMemoryDeclaration::into_declaration),
732 );
733 DeclarationSnapshot::new(declarations).map_err(StaticMemoryDeclarationError::Declaration)
734}
735
736fn declaration_metadata(
737 registrations: &[StaticMemoryDeclaration],
738) -> BTreeMap<String, RuntimeDeclarationAuthority> {
739 let mut metadata = BTreeMap::new();
740 metadata.insert(
741 IC_MEMORY_LEDGER_STABLE_KEY.to_string(),
742 RuntimeDeclarationAuthority::Internal,
743 );
744 for registration in registrations {
745 metadata.insert(
746 registration.declaration().stable_key().as_str().to_string(),
747 RuntimeDeclarationAuthority::External(registration.authority().to_string()),
748 );
749 }
750 metadata
751}
752
753fn range_authority(
754 registrations: Vec<StaticMemoryRangeDeclaration>,
755) -> Result<MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError> {
756 let mut records = Vec::with_capacity(registrations.len() + 1);
757 records.push(internal_ledger_range()?);
758 records.extend(
759 registrations
760 .into_iter()
761 .map(StaticMemoryRangeDeclaration::into_record),
762 );
763 MemoryManagerRangeAuthority::from_records(records)
764}
765
766fn internal_ledger_declaration() -> Result<AllocationDeclaration, crate::DeclarationSnapshotError> {
767 AllocationDeclaration::memory_manager(
768 IC_MEMORY_LEDGER_STABLE_KEY,
769 MEMORY_MANAGER_LEDGER_ID,
770 IC_MEMORY_LEDGER_LABEL,
771 )
772}
773
774fn internal_ledger_range() -> Result<MemoryManagerAuthorityRecord, MemoryManagerRangeAuthorityError>
775{
776 MemoryManagerAuthorityRecord::new(
777 MemoryManagerIdRange::new(
778 MEMORY_MANAGER_LEDGER_ID,
779 crate::MEMORY_MANAGER_GOVERNANCE_MAX_ID,
780 )?,
781 IC_MEMORY_AUTHORITY_OWNER,
782 MemoryManagerRangeMode::Reserved,
783 Some(IC_MEMORY_AUTHORITY_PURPOSE.to_string()),
784 )
785}
786
787fn runtime_bootstrap_error_from_bootstrap<P>(
788 err: crate::BootstrapError<RuntimePolicyError<P>>,
789) -> RuntimeBootstrapError<P> {
790 match err {
791 crate::BootstrapError::Ledger(err) => RuntimeBootstrapError::LedgerCommit(err),
792 crate::BootstrapError::Validation(err) => RuntimeBootstrapError::Validation(err),
793 crate::BootstrapError::Staging(err) => RuntimeBootstrapError::Staging(err),
794 }
795}
796
797struct RuntimeMemoryManagerPolicy<'a, P> {
798 range_authority: MemoryManagerRangeAuthority,
799 user_ranges_registered: bool,
800 declaration_metadata: BTreeMap<String, RuntimeDeclarationAuthority>,
801 custom_policy: &'a P,
802}
803
804enum RuntimeDeclarationAuthority {
805 Internal,
806 External(String),
807}
808
809impl<P: AllocationPolicy> AllocationPolicy for RuntimeMemoryManagerPolicy<'_, P> {
810 type Error = RuntimePolicyError<P::Error>;
811
812 fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
813 let authority = self.declaration_authority(key)?;
814 if matches!(authority, RuntimeDeclarationAuthority::Internal) {
815 return Ok(());
816 }
817 if crate::is_ic_memory_stable_key(key.as_str()) {
818 return Err(RuntimePolicyError::ReservedStableKeyAuthority {
819 stable_key: key.as_str().to_string(),
820 expected_authority: IC_MEMORY_AUTHORITY_OWNER,
821 });
822 }
823 self.custom_policy
824 .validate_key(key)
825 .map_err(RuntimePolicyError::Custom)
826 }
827
828 fn validate_slot(
829 &self,
830 key: &StableKey,
831 slot: &AllocationSlotDescriptor,
832 ) -> Result<(), Self::Error> {
833 self.validate_runtime_range(key, slot)?;
834 if matches!(
835 self.declaration_authority(key)?,
836 RuntimeDeclarationAuthority::Internal
837 ) {
838 return Ok(());
839 }
840 self.custom_policy
841 .validate_slot(key, slot)
842 .map_err(RuntimePolicyError::Custom)
843 }
844
845 fn validate_reserved_slot(
846 &self,
847 key: &StableKey,
848 slot: &AllocationSlotDescriptor,
849 ) -> Result<(), Self::Error> {
850 self.validate_runtime_range(key, slot)?;
851 if matches!(
852 self.declaration_authority(key)?,
853 RuntimeDeclarationAuthority::Internal
854 ) {
855 return Ok(());
856 }
857 self.custom_policy
858 .validate_reserved_slot(key, slot)
859 .map_err(RuntimePolicyError::Custom)
860 }
861}
862
863impl<P: AllocationPolicy> RuntimeMemoryManagerPolicy<'_, P> {
864 fn declaration_authority(
865 &self,
866 key: &StableKey,
867 ) -> Result<&RuntimeDeclarationAuthority, RuntimePolicyError<P::Error>> {
868 self.declaration_metadata
869 .get(key.as_str())
870 .ok_or_else(|| RuntimePolicyError::MissingDeclarationMetadata(key.as_str().to_string()))
871 }
872
873 fn validate_runtime_range(
874 &self,
875 key: &StableKey,
876 slot: &AllocationSlotDescriptor,
877 ) -> Result<(), RuntimePolicyError<P::Error>> {
878 let authority = self.declaration_authority(key)?;
879 if matches!(authority, RuntimeDeclarationAuthority::Internal) {
885 self.range_authority
886 .validate_slot_authority(slot, IC_MEMORY_AUTHORITY_OWNER)?;
887 return Ok(());
888 }
889
890 let RuntimeDeclarationAuthority::External(authority) = authority else {
891 return Err(RuntimePolicyError::MissingDeclarationMetadata(
892 key.as_str().to_string(),
893 ));
894 };
895 if self.user_ranges_registered {
896 self.range_authority
897 .validate_slot_authority(slot, authority)?;
898 return Ok(());
899 }
900
901 let id = slot
902 .memory_manager_id()
903 .map_err(MemoryManagerRangeAuthorityError::Slot)?;
904 if self
905 .range_authority
906 .authority_for_id(id)
907 .map_err(RuntimePolicyError::Range)?
908 .is_some()
909 {
910 self.range_authority
911 .validate_slot_authority(slot, authority)?;
912 }
913 Ok(())
914 }
915}
916
917struct NoopPolicy;
918
919impl AllocationPolicy for NoopPolicy {
920 type Error = Infallible;
921
922 fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
923 Ok(())
924 }
925
926 fn validate_slot(
927 &self,
928 _key: &StableKey,
929 _slot: &AllocationSlotDescriptor,
930 ) -> Result<(), Self::Error> {
931 Ok(())
932 }
933
934 fn validate_reserved_slot(
935 &self,
936 _key: &StableKey,
937 _slot: &AllocationSlotDescriptor,
938 ) -> Result<(), Self::Error> {
939 Ok(())
940 }
941}
942
943#[cfg(test)]
944pub fn reset_for_tests() {
945 crate::registry::reset_static_memory_declarations_for_tests();
946 EAGER_INIT_HOOKS
947 .lock()
948 .expect("ic-memory eager-init queue poisoned")
949 .clear();
950 *COMMITTED_ALLOCATIONS
951 .lock()
952 .expect("ic-memory runtime validation state poisoned") = None;
953 BOOTSTRAPPED.store(false, Ordering::SeqCst);
954 DEFAULT_LEDGER_CELL.with_borrow_mut(|cell| {
955 *cell = None;
956 });
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962 use crate::registry::{
963 TEST_REGISTRY_LOCK, register_static_memory_manager_declaration,
964 register_static_memory_manager_range,
965 };
966 use std::sync::atomic::{AtomicBool, Ordering};
967
968 static EAGER_INIT_RAN: AtomicBool = AtomicBool::new(false);
969
970 struct ExternalOnlyPolicy;
971
972 impl AllocationPolicy for ExternalOnlyPolicy {
973 type Error = &'static str;
974
975 fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
976 if crate::is_ic_memory_stable_key(key.as_str()) {
977 return Err("internal key reached external policy");
978 }
979 Ok(())
980 }
981
982 fn validate_slot(
983 &self,
984 _key: &StableKey,
985 slot: &AllocationSlotDescriptor,
986 ) -> Result<(), Self::Error> {
987 if slot
988 .memory_manager_id()
989 .is_ok_and(|id| id <= crate::MEMORY_MANAGER_GOVERNANCE_MAX_ID)
990 {
991 return Err("internal slot reached external policy");
992 }
993 Ok(())
994 }
995
996 fn validate_reserved_slot(
997 &self,
998 key: &StableKey,
999 slot: &AllocationSlotDescriptor,
1000 ) -> Result<(), Self::Error> {
1001 self.validate_slot(key, slot)
1002 }
1003 }
1004
1005 fn register_crate_a() {
1006 register_static_memory_manager_range(
1007 100,
1008 109,
1009 "crate_a",
1010 MemoryManagerRangeMode::Reserved,
1011 None,
1012 )
1013 .expect("crate A range");
1014 register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1015 .expect("crate A memory");
1016 }
1017
1018 fn register_crate_b() {
1019 register_static_memory_manager_range(
1020 110,
1021 119,
1022 "crate_b",
1023 MemoryManagerRangeMode::Reserved,
1024 None,
1025 )
1026 .expect("crate B range");
1027 register_static_memory_manager_declaration(110, "crate_b", "orders", "crate_b.orders.v1")
1028 .expect("crate B memory");
1029 }
1030
1031 fn mark_eager_init() {
1032 EAGER_INIT_RAN.store(true, Ordering::SeqCst);
1033 register_static_memory_manager_declaration(101, "crate_a", "audit", "crate_a.audit.v1")
1034 .expect("eager-init declaration");
1035 }
1036
1037 #[test]
1038 fn multi_crate_declarations_compose_into_one_bootstrap() {
1039 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1040 reset_for_tests();
1041 register_crate_a();
1042 register_crate_b();
1043
1044 let validated = bootstrap_default_memory_manager().expect("bootstrap");
1045
1046 assert_eq!(validated.declarations().len(), 2);
1047 assert!(
1048 validated
1049 .declarations()
1050 .iter()
1051 .any(|declaration| declaration.stable_key().as_str() == "crate_a.users.v1")
1052 );
1053 assert!(
1054 validated
1055 .declarations()
1056 .iter()
1057 .any(|declaration| declaration.stable_key().as_str() == "crate_b.orders.v1")
1058 );
1059 }
1060
1061 #[test]
1062 fn default_runtime_keeps_internal_ledger_slot_private() {
1063 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1064 reset_for_tests();
1065
1066 let validated = bootstrap_default_memory_manager().expect("bootstrap");
1067
1068 assert!(validated.declarations().is_empty());
1069 assert!(
1070 committed_allocations()
1071 .expect("published allocations")
1072 .declarations()
1073 .is_empty()
1074 );
1075 let Err(err) = open_default_memory_manager_memory(
1076 IC_MEMORY_LEDGER_STABLE_KEY,
1077 MEMORY_MANAGER_LEDGER_ID,
1078 ) else {
1079 panic!("internal ledger slot must stay private");
1080 };
1081 assert!(matches!(err, RuntimeOpenError::ReservedStableKey { .. }));
1082 }
1083
1084 #[test]
1085 fn custom_policy_validates_external_declarations_only() {
1086 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1087 reset_for_tests();
1088 register_static_memory_manager_range(
1089 244,
1090 244,
1091 "external_policy",
1092 MemoryManagerRangeMode::Reserved,
1093 None,
1094 )
1095 .expect("external range");
1096 register_static_memory_manager_declaration(
1097 244,
1098 "external_policy",
1099 "users",
1100 "external_policy.users.v1",
1101 )
1102 .expect("external declaration");
1103
1104 let committed = bootstrap_default_memory_manager_with_policy(&ExternalOnlyPolicy)
1105 .expect("external-only policy bootstrap");
1106
1107 assert_eq!(committed.declarations().len(), 1);
1108 assert_eq!(
1109 committed.declarations()[0].stable_key().as_str(),
1110 "external_policy.users.v1"
1111 );
1112 }
1113
1114 #[test]
1115 fn conflicting_ranges_fail() {
1116 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1117 reset_for_tests();
1118 register_static_memory_manager_range(
1119 100,
1120 110,
1121 "crate_a",
1122 MemoryManagerRangeMode::Reserved,
1123 None,
1124 )
1125 .expect("crate A range");
1126 register_static_memory_manager_range(
1127 105,
1128 119,
1129 "crate_b",
1130 MemoryManagerRangeMode::Reserved,
1131 None,
1132 )
1133 .expect("crate B range");
1134
1135 let err = bootstrap_default_memory_manager().expect_err("overlap must fail");
1136 assert!(matches!(
1137 err,
1138 RuntimeBootstrapError::Range(
1139 MemoryManagerRangeAuthorityError::OverlappingRanges { .. }
1140 )
1141 ));
1142 }
1143
1144 #[test]
1145 fn duplicate_stable_keys_fail() {
1146 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1147 reset_for_tests();
1148 register_static_memory_manager_declaration(100, "crate_a", "users", "app.users.v1")
1149 .expect("first declaration");
1150 register_static_memory_manager_declaration(101, "crate_b", "users", "app.users.v1")
1151 .expect("second declaration");
1152
1153 let err = bootstrap_default_memory_manager().expect_err("duplicate key must fail");
1154 assert!(matches!(
1155 err,
1156 RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
1157 crate::DeclarationSnapshotError::DuplicateStableKey(_)
1158 ))
1159 ));
1160 }
1161
1162 #[test]
1163 fn duplicate_memory_manager_ids_fail() {
1164 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1165 reset_for_tests();
1166 register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1167 .expect("first declaration");
1168 register_static_memory_manager_declaration(100, "crate_b", "orders", "crate_b.orders.v1")
1169 .expect("second declaration");
1170
1171 let err = bootstrap_default_memory_manager().expect_err("duplicate slot must fail");
1172 assert!(matches!(
1173 err,
1174 RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
1175 crate::DeclarationSnapshotError::DuplicateSlot(_)
1176 ))
1177 ));
1178 }
1179
1180 #[test]
1181 fn out_of_range_memory_declaration_fails_when_ranges_are_declared() {
1182 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1183 reset_for_tests();
1184 register_static_memory_manager_range(
1185 100,
1186 109,
1187 "crate_a",
1188 MemoryManagerRangeMode::Reserved,
1189 None,
1190 )
1191 .expect("crate A range");
1192 register_static_memory_manager_declaration(120, "crate_a", "users", "crate_a.users.v1")
1193 .expect("out-of-range declaration");
1194
1195 let err = bootstrap_default_memory_manager().expect_err("out of range must fail");
1196 assert!(matches!(
1197 err,
1198 RuntimeBootstrapError::Validation(crate::AllocationValidationError::Policy(
1199 RuntimePolicyError::Range(MemoryManagerRangeAuthorityError::UnclaimedId {
1200 id: 120
1201 })
1202 ))
1203 ));
1204 }
1205
1206 #[test]
1207 fn late_registration_after_bootstrap_fails() {
1208 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1209 reset_for_tests();
1210 register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1211 .expect("declaration");
1212 bootstrap_default_memory_manager().expect("bootstrap");
1213
1214 let err = register_static_memory_manager_declaration(
1215 101,
1216 "crate_a",
1217 "orders",
1218 "crate_a.orders.v1",
1219 )
1220 .expect_err("late registration must fail");
1221 assert_eq!(err, StaticMemoryDeclarationError::RegistrySealed);
1222 }
1223
1224 #[test]
1225 fn late_eager_init_registration_after_bootstrap_fails() {
1226 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1227 reset_for_tests();
1228 register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1229 .expect("declaration");
1230 bootstrap_default_memory_manager().expect("bootstrap");
1231
1232 let err = std::panic::catch_unwind(|| defer_eager_init(mark_eager_init))
1233 .expect_err("late eager-init registration must fail");
1234
1235 let message = err
1236 .downcast_ref::<String>()
1237 .map(String::as_str)
1238 .or_else(|| err.downcast_ref::<&str>().copied())
1239 .expect("panic message");
1240 assert!(message.contains("after runtime bootstrap"));
1241 }
1242
1243 #[test]
1244 fn eager_init_runs_before_snapshot_seal() {
1245 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1246 reset_for_tests();
1247 EAGER_INIT_RAN.store(false, Ordering::SeqCst);
1248 register_static_memory_manager_range(
1249 100,
1250 109,
1251 "crate_a",
1252 MemoryManagerRangeMode::Reserved,
1253 None,
1254 )
1255 .expect("crate A range");
1256 defer_eager_init(mark_eager_init);
1257
1258 let validated = bootstrap_default_memory_manager().expect("bootstrap");
1259
1260 assert!(EAGER_INIT_RAN.load(Ordering::SeqCst));
1261 assert!(
1262 validated
1263 .declarations()
1264 .iter()
1265 .any(|declaration| declaration.stable_key().as_str() == "crate_a.audit.v1")
1266 );
1267 }
1268
1269 #[test]
1270 fn direct_user_can_bootstrap_and_open_without_canic() {
1271 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1272 reset_for_tests();
1273 register_static_memory_manager_range(
1274 120,
1275 129,
1276 "icydb",
1277 MemoryManagerRangeMode::Reserved,
1278 None,
1279 )
1280 .expect("icydb range");
1281 register_static_memory_manager_declaration(120, "icydb", "users", "icydb.users.data.v1")
1282 .expect("icydb declaration");
1283
1284 bootstrap_default_memory_manager().expect("bootstrap");
1285 open_default_memory_manager_memory("icydb.users.data.v1", 120).expect("open memory");
1286 }
1287
1288 #[test]
1289 fn diagnostic_export_reports_default_memory_manager_sizes() {
1290 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1291 reset_for_tests();
1292 register_static_memory_manager_range(
1293 130,
1294 139,
1295 "diagnostics",
1296 MemoryManagerRangeMode::Reserved,
1297 None,
1298 )
1299 .expect("diagnostics range");
1300 register_static_memory_manager_declaration(
1301 130,
1302 "diagnostics",
1303 "users",
1304 "diagnostics.users.v1",
1305 )
1306 .expect("diagnostics declaration");
1307
1308 bootstrap_default_memory_manager().expect("bootstrap");
1309 let memory =
1310 open_default_memory_manager_memory("diagnostics.users.v1", 130).expect("open memory");
1311 let old_size = memory.size();
1312 memory.grow(2);
1313
1314 let export = default_memory_manager_diagnostic_export().expect("diagnostic export");
1315 let recovery =
1316 default_memory_manager_commit_recovery_diagnostic().expect("recovery diagnostic");
1317 let record = export
1318 .records
1319 .iter()
1320 .find(|record| record.allocation.stable_key().as_str() == "diagnostics.users.v1")
1321 .expect("diagnostic allocation");
1322
1323 assert_eq!(recovery.recovery, Ok(export.current_generation));
1324 assert_eq!(
1325 record.memory_size,
1326 Some(DiagnosticMemorySize::from_wasm_pages(old_size + 2))
1327 );
1328 }
1329
1330 #[test]
1331 fn doctor_report_preflights_before_bootstrap() {
1332 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1333 reset_for_tests();
1334 register_static_memory_manager_range(
1335 240,
1336 240,
1337 "doctor_preflight",
1338 MemoryManagerRangeMode::Reserved,
1339 None,
1340 )
1341 .expect("doctor range");
1342 register_static_memory_manager_declaration(
1343 240,
1344 "doctor_preflight",
1345 "users",
1346 "doctor_preflight.users.v1",
1347 )
1348 .expect("doctor declaration");
1349
1350 let report = default_memory_manager_doctor_report();
1351
1352 assert!(!report.bootstrapped);
1353 assert_eq!(report.registered_declarations.len(), 1);
1354 assert!(report.range_authority.effective_authority.is_ok());
1355 assert_eq!(report.validation, crate::DiagnosticCheck::Passed);
1356 assert!(report.commit_recovery.is_some());
1357 assert!(matches!(
1358 report.stable_cell.status,
1359 crate::DiagnosticStableCellStatus::Empty | crate::DiagnosticStableCellStatus::Readable
1360 ));
1361 }
1362
1363 #[test]
1364 fn doctor_report_includes_recovered_ledger_and_memory_sizes_after_bootstrap() {
1365 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1366 reset_for_tests();
1367 register_static_memory_manager_range(
1368 241,
1369 241,
1370 "doctor_runtime",
1371 MemoryManagerRangeMode::Reserved,
1372 None,
1373 )
1374 .expect("doctor range");
1375 register_static_memory_manager_declaration(
1376 241,
1377 "doctor_runtime",
1378 "orders",
1379 "doctor_runtime.orders.v1",
1380 )
1381 .expect("doctor declaration");
1382
1383 bootstrap_default_memory_manager().expect("bootstrap");
1384 let memory = open_default_memory_manager_memory("doctor_runtime.orders.v1", 241)
1385 .expect("open memory");
1386 let old_size = memory.size();
1387 memory.grow(1);
1388
1389 let report = default_memory_manager_doctor_report();
1390 let ledger = report.ledger.expect("recovered ledger export");
1391 let record = ledger
1392 .records
1393 .iter()
1394 .find(|record| record.allocation.stable_key().as_str() == "doctor_runtime.orders.v1")
1395 .expect("doctor allocation");
1396
1397 assert!(report.bootstrapped);
1398 assert_eq!(
1399 report.stable_cell.status,
1400 crate::DiagnosticStableCellStatus::Readable
1401 );
1402 assert_eq!(report.validation, crate::DiagnosticCheck::Passed);
1403 assert_eq!(
1404 record.memory_size,
1405 Some(DiagnosticMemorySize::from_wasm_pages(old_size + 1))
1406 );
1407 }
1408
1409 #[test]
1410 fn doctor_report_captures_validation_failure() {
1411 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1412 reset_for_tests();
1413 register_static_memory_manager_declaration(
1414 242,
1415 "doctor_failure_a",
1416 "users",
1417 "doctor_failure.users.v1",
1418 )
1419 .expect("first declaration");
1420 register_static_memory_manager_declaration(
1421 243,
1422 "doctor_failure_b",
1423 "orders",
1424 "doctor_failure.users.v1",
1425 )
1426 .expect("second declaration");
1427
1428 let report = default_memory_manager_doctor_report();
1429
1430 let crate::DiagnosticCheck::Failed { code, message } = report.validation else {
1431 panic!("validation must fail");
1432 };
1433 assert_eq!(code, crate::DiagnosticCode::DeclarationSnapshot);
1434 assert!(message.contains("declared more than once"));
1435 }
1436
1437 #[test]
1438 fn validation_diagnostic_preserves_unsupported_format_code() {
1439 let recovered = Err(LedgerCommitError::PayloadEnvelope(
1440 LedgerPayloadEnvelopeError::UnsupportedFormat {
1441 marker: *b"ICMF",
1442 version: Some(crate::LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1443 },
1444 ));
1445
1446 let failure = diagnostic_validation_ledger(None, Some(&recovered))
1447 .expect_err("unsupported format must block validation");
1448
1449 assert_eq!(failure.code, DiagnosticCode::UnsupportedFormat);
1450 assert!(
1451 failure
1452 .message
1453 .contains("unsupported ic-memory ledger payload format")
1454 );
1455 }
1456}