1use sc_executor::RuntimeVersionOf;
40use sp_blockchain::Result;
41use sp_core::traits::{FetchRuntimeCode, RuntimeCode, WrappedRuntimeCode};
42use sp_state_machine::BasicExternalities;
43use sp_version::RuntimeVersion;
44use std::{
45 collections::{hash_map::DefaultHasher, HashMap},
46 fs,
47 hash::Hasher as _,
48 path::{Path, PathBuf},
49 time::{Duration, Instant},
50};
51
52const WARN_INTERVAL: Duration = Duration::from_secs(30);
55
56#[derive(Debug)]
58struct WasmBlob {
59 code: Vec<u8>,
61 hash: Vec<u8>,
63 path: PathBuf,
65 version: RuntimeVersion,
67 last_warn: parking_lot::Mutex<Option<Instant>>,
70}
71
72impl WasmBlob {
73 fn new(code: Vec<u8>, hash: Vec<u8>, path: PathBuf, version: RuntimeVersion) -> Self {
74 Self { code, hash, path, version, last_warn: Default::default() }
75 }
76
77 fn runtime_code(&self, heap_pages: Option<u64>) -> RuntimeCode {
78 RuntimeCode { code_fetcher: self, hash: self.hash.clone(), heap_pages }
79 }
80}
81
82fn make_hash<K: std::hash::Hash + ?Sized>(val: &K) -> Vec<u8> {
84 let mut state = DefaultHasher::new();
85 val.hash(&mut state);
86 state.finish().to_le_bytes().to_vec()
87}
88
89impl FetchRuntimeCode for WasmBlob {
90 fn fetch_runtime_code(&self) -> Option<std::borrow::Cow<[u8]>> {
91 Some(self.code.as_slice().into())
92 }
93}
94
95#[derive(Debug, thiserror::Error)]
96#[allow(missing_docs)]
97pub enum WasmOverrideError {
98 #[error("Failed to get runtime version: {0}")]
99 VersionInvalid(String),
100
101 #[error("WASM override IO error")]
102 Io(PathBuf, #[source] std::io::Error),
103
104 #[error("Overwriting WASM requires a directory where local \
105 WASM is stored. {} is not a directory", .0.display())]
106 NotADirectory(PathBuf),
107
108 #[error("Duplicate WASM Runtimes found: \n{}\n", .0.join("\n") )]
109 DuplicateRuntime(Vec<String>),
110}
111
112impl From<WasmOverrideError> for sp_blockchain::Error {
113 fn from(err: WasmOverrideError) -> Self {
114 Self::Application(Box::new(err))
115 }
116}
117
118#[derive(Debug)]
121pub struct WasmOverride {
122 overrides: HashMap<u32, WasmBlob>,
124}
125
126impl WasmOverride {
127 pub fn new<P, E>(path: P, executor: &E) -> Result<Self>
128 where
129 P: AsRef<Path>,
130 E: RuntimeVersionOf,
131 {
132 let overrides = Self::scrape_overrides(path.as_ref(), executor)?;
133 Ok(Self { overrides })
134 }
135
136 pub fn get<'a, 'b: 'a>(
140 &'b self,
141 spec: &u32,
142 pages: Option<u64>,
143 spec_name: &str,
144 ) -> Option<(RuntimeCode<'a>, RuntimeVersion)> {
145 self.overrides.get(spec).and_then(|w| {
146 if spec_name == &*w.version.spec_name {
147 Some((w.runtime_code(pages), w.version.clone()))
148 } else {
149 let mut last_warn = w.last_warn.lock();
150 let now = Instant::now();
151
152 if last_warn.map_or(true, |l| l + WARN_INTERVAL <= now) {
153 *last_warn = Some(now);
154
155 tracing::warn!(
156 target = "wasm_overrides",
157 on_chain_spec_name = %spec_name,
158 override_spec_name = %w.version,
159 spec_version = %spec,
160 wasm_file = %w.path.display(),
161 "On chain and override `spec_name` do not match! Ignoring override.",
162 );
163 }
164
165 None
166 }
167 })
168 }
169
170 fn scrape_overrides<E>(dir: &Path, executor: &E) -> Result<HashMap<u32, WasmBlob>>
173 where
174 E: RuntimeVersionOf,
175 {
176 let handle_err = |e: std::io::Error| -> sp_blockchain::Error {
177 WasmOverrideError::Io(dir.to_owned(), e).into()
178 };
179
180 if !dir.is_dir() {
181 return Err(WasmOverrideError::NotADirectory(dir.to_owned()).into())
182 }
183
184 let mut overrides = HashMap::new();
185 let mut duplicates = Vec::new();
186 for entry in fs::read_dir(dir).map_err(handle_err)? {
187 let entry = entry.map_err(handle_err)?;
188 let path = entry.path();
189 if let Some("wasm") = path.extension().and_then(|e| e.to_str()) {
190 let code = fs::read(&path).map_err(handle_err)?;
191 let code_hash = make_hash(&code);
192 let version = Self::runtime_version(executor, &code, &code_hash, Some(128))?;
193 tracing::info!(
194 target: "wasm_overrides",
195 version = %version,
196 file = %path.display(),
197 "Found wasm override.",
198 );
199
200 let wasm = WasmBlob::new(code, code_hash, path.clone(), version.clone());
201
202 if let Some(other) = overrides.insert(version.spec_version, wasm) {
203 tracing::info!(
204 target: "wasm_overrides",
205 first = %other.path.display(),
206 second = %path.display(),
207 %version,
208 "Found duplicate spec version for runtime.",
209 );
210 duplicates.push(path.display().to_string());
211 }
212 }
213 }
214
215 if !duplicates.is_empty() {
216 return Err(WasmOverrideError::DuplicateRuntime(duplicates).into())
217 }
218
219 Ok(overrides)
220 }
221
222 fn runtime_version<E>(
223 executor: &E,
224 code: &[u8],
225 code_hash: &[u8],
226 heap_pages: Option<u64>,
227 ) -> Result<RuntimeVersion>
228 where
229 E: RuntimeVersionOf,
230 {
231 let mut ext = BasicExternalities::default();
232 executor
233 .runtime_version(
234 &mut ext,
235 &RuntimeCode {
236 code_fetcher: &WrappedRuntimeCode(code.into()),
237 heap_pages,
238 hash: code_hash.into(),
239 },
240 )
241 .map_err(|e| WasmOverrideError::VersionInvalid(e.to_string()).into())
242 }
243}
244
245#[cfg(test)]
247pub fn dummy_overrides() -> WasmOverride {
248 let version = RuntimeVersion { spec_name: "test".into(), ..Default::default() };
249 let mut overrides = HashMap::new();
250 overrides.insert(
251 0,
252 WasmBlob::new(vec![0, 0, 0, 0, 0, 0, 0, 0], vec![0], PathBuf::new(), version.clone()),
253 );
254 overrides.insert(
255 1,
256 WasmBlob::new(vec![1, 1, 1, 1, 1, 1, 1, 1], vec![1], PathBuf::new(), version.clone()),
257 );
258 overrides
259 .insert(2, WasmBlob::new(vec![2, 2, 2, 2, 2, 2, 2, 2], vec![2], PathBuf::new(), version));
260
261 WasmOverride { overrides }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use sc_executor::{HeapAllocStrategy, WasmExecutor};
268 use std::fs::{self, File};
269
270 fn executor() -> WasmExecutor {
271 WasmExecutor::builder()
272 .with_onchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 128 })
273 .with_offchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 128 })
274 .with_max_runtime_instances(1)
275 .with_runtime_cache_size(2)
276 .build()
277 }
278
279 fn wasm_test<F>(fun: F)
280 where
281 F: Fn(&Path, &[u8], &WasmExecutor),
282 {
283 let exec = executor();
284 let bytes = substrate_test_runtime::wasm_binary_unwrap();
285 let dir = tempfile::tempdir().expect("Create a temporary directory");
286 fun(dir.path(), bytes, &exec);
287 dir.close().expect("Temporary Directory should close");
288 }
289
290 #[test]
291 fn should_get_runtime_version() {
292 let executor = executor();
293
294 let version = WasmOverride::runtime_version(
295 &executor,
296 substrate_test_runtime::wasm_binary_unwrap(),
297 &[1],
298 Some(128),
299 )
300 .expect("should get the `RuntimeVersion` of the test-runtime wasm blob");
301 assert_eq!(version.spec_version, 2);
302 }
303
304 #[test]
305 fn should_scrape_wasm() {
306 wasm_test(|dir, wasm_bytes, exec| {
307 fs::write(dir.join("test.wasm"), wasm_bytes).expect("Create test file");
308 let overrides =
309 WasmOverride::scrape_overrides(dir, exec).expect("HashMap of u32 and WasmBlob");
310 let wasm = overrides.get(&2).expect("WASM binary");
311 assert_eq!(wasm.code, substrate_test_runtime::wasm_binary_unwrap().to_vec())
312 });
313 }
314
315 #[test]
316 fn should_check_for_duplicates() {
317 wasm_test(|dir, wasm_bytes, exec| {
318 fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file");
319 fs::write(dir.join("test1.wasm"), wasm_bytes).expect("Create test file");
320 let scraped = WasmOverride::scrape_overrides(dir, exec);
321
322 match scraped {
323 Err(sp_blockchain::Error::Application(e)) => {
324 match e.downcast_ref::<WasmOverrideError>() {
325 Some(WasmOverrideError::DuplicateRuntime(duplicates)) => {
326 assert_eq!(duplicates.len(), 1);
327 },
328 _ => panic!("Test should end with Msg Error Variant"),
329 }
330 },
331 _ => panic!("Test should end in error"),
332 }
333 });
334 }
335
336 #[test]
337 fn should_ignore_non_wasm() {
338 wasm_test(|dir, wasm_bytes, exec| {
339 File::create(dir.join("README.md")).expect("Create test file");
340 File::create(dir.join("LICENSE")).expect("Create a test file");
341 fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file");
342 let scraped =
343 WasmOverride::scrape_overrides(dir, exec).expect("HashMap of u32 and WasmBlob");
344 assert_eq!(scraped.len(), 1);
345 });
346 }
347}