1use crate::{
8 InternalError, InternalErrorOrigin,
9 cdk::{types::Principal, utils::hash::wasm_hash},
10 domain::metrics::{
11 WasmStoreMetricOperation, WasmStoreMetricOutcome, WasmStoreMetricReason,
12 WasmStoreMetricSource,
13 },
14 format::byte_size,
15 ids::CanisterRole,
16 ops::runtime::metrics::wasm_store::WasmStoreMetrics,
17};
18use async_trait::async_trait;
19use std::{
20 borrow::Cow,
21 collections::BTreeMap,
22 sync::{Mutex, OnceLock},
23};
24
25#[derive(Clone, Debug, Eq, PartialEq)]
32pub enum ApprovedModulePayload {
33 Chunked {
34 source_canister: Principal,
35 chunk_hashes: Vec<Vec<u8>>,
36 },
37 Embedded {
38 wasm_module: Cow<'static, [u8]>,
39 },
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct ApprovedModuleSource {
50 source_label: String,
51 module_hash: Vec<u8>,
52 payload_size_bytes: u64,
53 payload: ApprovedModulePayload,
54}
55
56impl ApprovedModuleSource {
57 #[must_use]
59 pub const fn chunked(
60 source_canister: Principal,
61 source_label: String,
62 module_hash: Vec<u8>,
63 chunk_hashes: Vec<Vec<u8>>,
64 payload_size_bytes: u64,
65 ) -> Self {
66 Self {
67 source_label,
68 module_hash,
69 payload_size_bytes,
70 payload: ApprovedModulePayload::Chunked {
71 source_canister,
72 chunk_hashes,
73 },
74 }
75 }
76
77 #[must_use]
79 pub fn embedded(source_label: String, wasm_module: &'static [u8]) -> Self {
80 let payload_size_bytes = wasm_module.len() as u64;
81
82 Self {
83 source_label,
84 module_hash: wasm_hash(wasm_module),
85 payload_size_bytes,
86 payload: ApprovedModulePayload::Embedded {
87 wasm_module: Cow::Borrowed(wasm_module),
88 },
89 }
90 }
91
92 #[must_use]
94 pub fn source_label(&self) -> &str {
95 &self.source_label
96 }
97
98 #[must_use]
100 pub fn module_hash(&self) -> &[u8] {
101 &self.module_hash
102 }
103
104 #[must_use]
106 pub fn payload_size(&self) -> String {
107 byte_size(self.payload_size_bytes)
108 }
109
110 #[must_use]
112 pub const fn payload_size_bytes(&self) -> u64 {
113 self.payload_size_bytes
114 }
115
116 #[must_use]
118 pub const fn chunk_count(&self) -> usize {
119 match &self.payload {
120 ApprovedModulePayload::Chunked { chunk_hashes, .. } => chunk_hashes.len(),
121 ApprovedModulePayload::Embedded { .. } => 0,
122 }
123 }
124
125 #[must_use]
127 pub const fn payload(&self) -> &ApprovedModulePayload {
128 &self.payload
129 }
130}
131
132#[async_trait]
139pub trait ModuleSourceResolver: Send + Sync {
140 async fn approved_module_source(
142 &self,
143 role: &CanisterRole,
144 ) -> Result<ApprovedModuleSource, InternalError>;
145}
146
147static MODULE_SOURCE_RESOLVER: OnceLock<&'static dyn ModuleSourceResolver> = OnceLock::new();
148static EMBEDDED_MODULE_SOURCES: OnceLock<Mutex<BTreeMap<CanisterRole, ApprovedModuleSource>>> =
149 OnceLock::new();
150
151pub struct ModuleSourceRuntimeApi;
158
159impl ModuleSourceRuntimeApi {
160 pub fn register_embedded_module_source(role: CanisterRole, source: ApprovedModuleSource) {
167 let sources = EMBEDDED_MODULE_SOURCES.get_or_init(|| Mutex::new(BTreeMap::new()));
168 let mut sources = sources
169 .lock()
170 .unwrap_or_else(std::sync::PoisonError::into_inner);
171
172 match sources.get(&role) {
173 Some(existing) if existing == &source => {}
174 Some(existing) => {
175 panic!(
176 "embedded module source for role '{role}' was already registered with a different payload: existing='{}' new='{}'",
177 existing.source_label(),
178 source.source_label()
179 );
180 }
181 None => {
182 sources.insert(role, source);
183 }
184 }
185 }
186
187 pub fn register_embedded_module_wasm(
189 role: CanisterRole,
190 source_label: impl Into<String>,
191 wasm_module: &'static [u8],
192 ) {
193 Self::register_embedded_module_source(
194 role,
195 ApprovedModuleSource::embedded(source_label.into(), wasm_module),
196 );
197 }
198
199 pub fn register_module_source_resolver(resolver: &'static dyn ModuleSourceResolver) {
201 let _ = MODULE_SOURCE_RESOLVER.set(resolver);
202 }
203
204 #[must_use]
206 pub fn has_embedded_module_source(role: &CanisterRole) -> bool {
207 EMBEDDED_MODULE_SOURCES.get().is_some_and(|sources| {
208 let sources = sources
209 .lock()
210 .unwrap_or_else(std::sync::PoisonError::into_inner);
211 sources.contains_key(role)
212 })
213 }
214
215 pub(crate) async fn approved_module_source(
217 role: &CanisterRole,
218 ) -> Result<ApprovedModuleSource, InternalError> {
219 if let Some(source) = EMBEDDED_MODULE_SOURCES.get().and_then(|sources| {
220 let sources = sources
221 .lock()
222 .unwrap_or_else(std::sync::PoisonError::into_inner);
223 sources.get(role).cloned()
224 }) {
225 WasmStoreMetrics::record(
226 WasmStoreMetricOperation::SourceResolve,
227 WasmStoreMetricSource::Embedded,
228 WasmStoreMetricOutcome::Completed,
229 WasmStoreMetricReason::Ok,
230 );
231 return Ok(source);
232 }
233
234 let resolver = MODULE_SOURCE_RESOLVER.get().ok_or_else(|| {
235 WasmStoreMetrics::record(
236 WasmStoreMetricOperation::SourceResolve,
237 WasmStoreMetricSource::Resolver,
238 WasmStoreMetricOutcome::Failed,
239 WasmStoreMetricReason::InvalidState,
240 );
241 InternalError::workflow(
242 InternalErrorOrigin::Workflow,
243 "module source resolver is not registered; root/control-plane install flows are unavailable".to_string(),
244 )
245 })?;
246
247 match resolver.approved_module_source(role).await {
248 Ok(source) => {
249 WasmStoreMetrics::record(
250 WasmStoreMetricOperation::SourceResolve,
251 WasmStoreMetricSource::Resolver,
252 WasmStoreMetricOutcome::Completed,
253 WasmStoreMetricReason::Ok,
254 );
255 Ok(source)
256 }
257 Err(err) => {
258 WasmStoreMetrics::record(
259 WasmStoreMetricOperation::SourceResolve,
260 WasmStoreMetricSource::Resolver,
261 WasmStoreMetricOutcome::Failed,
262 WasmStoreMetricReason::StoreCall,
263 );
264 Err(err)
265 }
266 }
267 }
268}