1use crate::{
2 InternalError, InternalErrorOrigin,
3 cdk::{types::Principal, utils::hash::wasm_hash},
4 format::byte_size,
5 ids::CanisterRole,
6 ops::runtime::metrics::wasm_store::{
7 WasmStoreMetricOperation, WasmStoreMetricOutcome, WasmStoreMetricReason,
8 WasmStoreMetricSource, WasmStoreMetrics,
9 },
10};
11use async_trait::async_trait;
12use std::{
13 borrow::Cow,
14 collections::BTreeMap,
15 sync::{Mutex, OnceLock},
16};
17
18#[derive(Clone, Debug, Eq, PartialEq)]
23pub enum ApprovedModulePayload {
24 Chunked {
25 source_canister: Principal,
26 chunk_hashes: Vec<Vec<u8>>,
27 },
28 Embedded {
29 wasm_module: Cow<'static, [u8]>,
30 },
31}
32
33#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ApprovedModuleSource {
39 source_label: String,
40 module_hash: Vec<u8>,
41 payload_size_bytes: u64,
42 payload: ApprovedModulePayload,
43}
44
45impl ApprovedModuleSource {
46 #[must_use]
48 pub const fn chunked(
49 source_canister: Principal,
50 source_label: String,
51 module_hash: Vec<u8>,
52 chunk_hashes: Vec<Vec<u8>>,
53 payload_size_bytes: u64,
54 ) -> Self {
55 Self {
56 source_label,
57 module_hash,
58 payload_size_bytes,
59 payload: ApprovedModulePayload::Chunked {
60 source_canister,
61 chunk_hashes,
62 },
63 }
64 }
65
66 #[must_use]
68 pub fn embedded(source_label: String, wasm_module: &'static [u8]) -> Self {
69 let payload_size_bytes = wasm_module.len() as u64;
70
71 Self {
72 source_label,
73 module_hash: wasm_hash(wasm_module),
74 payload_size_bytes,
75 payload: ApprovedModulePayload::Embedded {
76 wasm_module: Cow::Borrowed(wasm_module),
77 },
78 }
79 }
80
81 #[must_use]
83 pub fn source_label(&self) -> &str {
84 &self.source_label
85 }
86
87 #[must_use]
89 pub fn module_hash(&self) -> &[u8] {
90 &self.module_hash
91 }
92
93 #[must_use]
95 pub fn payload_size(&self) -> String {
96 byte_size(self.payload_size_bytes)
97 }
98
99 #[must_use]
101 pub const fn payload_size_bytes(&self) -> u64 {
102 self.payload_size_bytes
103 }
104
105 #[must_use]
107 pub const fn chunk_count(&self) -> usize {
108 match &self.payload {
109 ApprovedModulePayload::Chunked { chunk_hashes, .. } => chunk_hashes.len(),
110 ApprovedModulePayload::Embedded { .. } => 0,
111 }
112 }
113
114 #[must_use]
116 pub const fn payload(&self) -> &ApprovedModulePayload {
117 &self.payload
118 }
119}
120
121#[async_trait]
126pub trait ModuleSourceResolver: Send + Sync {
127 async fn approved_module_source(
129 &self,
130 role: &CanisterRole,
131 ) -> Result<ApprovedModuleSource, InternalError>;
132}
133
134static MODULE_SOURCE_RESOLVER: OnceLock<&'static dyn ModuleSourceResolver> = OnceLock::new();
135static EMBEDDED_MODULE_SOURCES: OnceLock<Mutex<BTreeMap<CanisterRole, ApprovedModuleSource>>> =
136 OnceLock::new();
137
138pub struct ModuleSourceRuntimeApi;
143
144impl ModuleSourceRuntimeApi {
145 pub fn register_embedded_module_source(role: CanisterRole, source: ApprovedModuleSource) {
147 let sources = EMBEDDED_MODULE_SOURCES.get_or_init(|| Mutex::new(BTreeMap::new()));
148 let mut sources = sources
149 .lock()
150 .unwrap_or_else(std::sync::PoisonError::into_inner);
151
152 match sources.get(&role) {
153 Some(existing) if existing == &source => {}
154 Some(existing) => {
155 panic!(
156 "embedded module source for role '{role}' was already registered with a different payload: existing='{}' new='{}'",
157 existing.source_label(),
158 source.source_label()
159 );
160 }
161 None => {
162 sources.insert(role, source);
163 }
164 }
165 }
166
167 pub fn register_embedded_module_wasm(
169 role: CanisterRole,
170 source_label: impl Into<String>,
171 wasm_module: &'static [u8],
172 ) {
173 Self::register_embedded_module_source(
174 role,
175 ApprovedModuleSource::embedded(source_label.into(), wasm_module),
176 );
177 }
178
179 pub fn register_module_source_resolver(resolver: &'static dyn ModuleSourceResolver) {
181 let _ = MODULE_SOURCE_RESOLVER.set(resolver);
182 }
183
184 #[must_use]
186 pub fn has_embedded_module_source(role: &CanisterRole) -> bool {
187 EMBEDDED_MODULE_SOURCES.get().is_some_and(|sources| {
188 let sources = sources
189 .lock()
190 .unwrap_or_else(std::sync::PoisonError::into_inner);
191 sources.contains_key(role)
192 })
193 }
194
195 pub(crate) async fn approved_module_source(
197 role: &CanisterRole,
198 ) -> Result<ApprovedModuleSource, InternalError> {
199 if let Some(source) = EMBEDDED_MODULE_SOURCES.get().and_then(|sources| {
200 let sources = sources
201 .lock()
202 .unwrap_or_else(std::sync::PoisonError::into_inner);
203 sources.get(role).cloned()
204 }) {
205 WasmStoreMetrics::record(
206 WasmStoreMetricOperation::SourceResolve,
207 WasmStoreMetricSource::Embedded,
208 WasmStoreMetricOutcome::Completed,
209 WasmStoreMetricReason::Ok,
210 );
211 return Ok(source);
212 }
213
214 let resolver = MODULE_SOURCE_RESOLVER.get().ok_or_else(|| {
215 WasmStoreMetrics::record(
216 WasmStoreMetricOperation::SourceResolve,
217 WasmStoreMetricSource::Resolver,
218 WasmStoreMetricOutcome::Failed,
219 WasmStoreMetricReason::InvalidState,
220 );
221 InternalError::workflow(
222 InternalErrorOrigin::Workflow,
223 "module source resolver is not registered; root/control-plane install flows are unavailable".to_string(),
224 )
225 })?;
226
227 match resolver.approved_module_source(role).await {
228 Ok(source) => {
229 WasmStoreMetrics::record(
230 WasmStoreMetricOperation::SourceResolve,
231 WasmStoreMetricSource::Resolver,
232 WasmStoreMetricOutcome::Completed,
233 WasmStoreMetricReason::Ok,
234 );
235 Ok(source)
236 }
237 Err(err) => {
238 WasmStoreMetrics::record(
239 WasmStoreMetricOperation::SourceResolve,
240 WasmStoreMetricSource::Resolver,
241 WasmStoreMetricOutcome::Failed,
242 WasmStoreMetricReason::StoreCall,
243 );
244 Err(err)
245 }
246 }
247 }
248}