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