1
// Copyright 2024 Moonbeam foundation
2
// This file is part of Moonbeam.
3

            
4
// Moonbeam is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Moonbeam is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16

            
17
//! # WASM Local Blob-Override
18
//!
19
//! WASM Local blob override provides tools to replace on-chain WASM with custom WASM.
20
//! These customized WASM blobs may include functionality that is not included in the
21
//! on-chain WASM, such as tracing or debugging information. This extra information is especially
22
//! useful in external scenarios, like exchanges or archive nodes.
23
//!
24
//! ## Usage
25
//!
26
//! WASM overrides may be enabled with the `--wasm-runtime-overrides` argument. The argument
27
//! expects a path to a directory that holds custom WASM.
28
//!
29
//! Any file ending in '.wasm' will be scraped and instantiated as a WASM blob. WASM can be built by
30
//! compiling the required runtime with the changes needed. For example, compiling a runtime with
31
//! tracing enabled would produce a WASM blob that can used.
32
//!
33
//! A custom WASM blob will override on-chain WASM if the spec version matches. If it is
34
//! required to overrides multiple runtimes, multiple WASM blobs matching each of the spec versions
35
//! needed must be provided in the given directory.
36

            
37
use sc_executor::RuntimeVersionOf;
38
use sp_blockchain::Result;
39
use sp_core::traits::{FetchRuntimeCode, RuntimeCode, WrappedRuntimeCode};
40
use sp_state_machine::BasicExternalities;
41
use sp_version::RuntimeVersion;
42
use std::{
43
	collections::{hash_map::DefaultHasher, HashMap},
44
	fs,
45
	hash::Hasher as _,
46
	path::{Path, PathBuf},
47
	time::{Duration, Instant},
48
};
49

            
50
/// The interval in that we will print a warning when a wasm blob `spec_name`
51
/// doesn't match with the on-chain `spec_name`.
52
const WARN_INTERVAL: Duration = Duration::from_secs(30);
53

            
54
/// Auxiliary structure that holds a wasm blob and its hash.
55
#[derive(Debug)]
56
struct WasmBlob {
57
	/// The actual wasm blob, aka the code.
58
	code: Vec<u8>,
59
	/// The hash of [`Self::code`].
60
	hash: Vec<u8>,
61
	/// The path where this blob was found.
62
	path: PathBuf,
63
	/// The runtime version of this blob.
64
	version: RuntimeVersion,
65
	/// When was the last time we have warned about the wasm blob having
66
	/// a wrong `spec_name`?
67
	last_warn: parking_lot::Mutex<Option<Instant>>,
68
}
69

            
70
impl WasmBlob {
71
	fn new(code: Vec<u8>, hash: Vec<u8>, path: PathBuf, version: RuntimeVersion) -> Self {
72
		Self {
73
			code,
74
			hash,
75
			path,
76
			version,
77
			last_warn: Default::default(),
78
		}
79
	}
80

            
81
	fn runtime_code(&self, heap_pages: Option<u64>) -> RuntimeCode {
82
		RuntimeCode {
83
			code_fetcher: self,
84
			hash: self.hash.clone(),
85
			heap_pages,
86
		}
87
	}
88
}
89

            
90
/// Make a hash out of a byte string using the default rust hasher
91
fn make_hash<K: std::hash::Hash + ?Sized>(val: &K) -> Vec<u8> {
92
	let mut state = DefaultHasher::new();
93
	val.hash(&mut state);
94
	state.finish().to_le_bytes().to_vec()
95
}
96

            
97
impl FetchRuntimeCode for WasmBlob {
98
	fn fetch_runtime_code(&self) -> Option<std::borrow::Cow<[u8]>> {
99
		Some(self.code.as_slice().into())
100
	}
101
}
102

            
103
#[derive(Debug, thiserror::Error)]
104
#[allow(missing_docs)]
105
pub enum WasmOverrideError {
106
	#[error("Failed to get runtime version: {0}")]
107
	VersionInvalid(String),
108

            
109
	#[error("WASM override IO error")]
110
	Io(PathBuf, #[source] std::io::Error),
111

            
112
	#[error("Overwriting WASM requires a directory where local \
113
	WASM is stored. {} is not a directory", .0.display())]
114
	NotADirectory(PathBuf),
115

            
116
	#[error("Duplicate WASM Runtimes found: \n{}\n", .0.join("\n") )]
117
	DuplicateRuntime(Vec<String>),
118
}
119

            
120
impl From<WasmOverrideError> for sp_blockchain::Error {
121
	fn from(err: WasmOverrideError) -> Self {
122
		Self::Application(Box::new(err))
123
	}
124
}
125

            
126
/// Scrapes WASM from a folder and returns WASM from that folder
127
/// if the runtime spec version matches.
128
#[derive(Debug)]
129
pub struct WasmOverride {
130
	// Map of runtime spec version -> Wasm Blob
131
	overrides: HashMap<u32, WasmBlob>,
132
}
133

            
134
impl WasmOverride {
135
	pub fn new<P, E>(path: P, executor: &E) -> Result<Self>
136
	where
137
		P: AsRef<Path>,
138
		E: RuntimeVersionOf,
139
	{
140
		let overrides = Self::scrape_overrides(path.as_ref(), executor)?;
141
		Ok(Self { overrides })
142
	}
143

            
144
	/// Gets an override by it's runtime spec version.
145
	///
146
	/// Returns `None` if an override for a spec version does not exist.
147
	pub fn get<'a, 'b: 'a>(
148
		&'b self,
149
		spec: &u32,
150
		pages: Option<u64>,
151
		spec_name: &str,
152
	) -> Option<(RuntimeCode<'a>, RuntimeVersion)> {
153
		self.overrides.get(spec).and_then(|w| {
154
			if spec_name == &*w.version.spec_name {
155
				Some((w.runtime_code(pages), w.version.clone()))
156
			} else {
157
				let mut last_warn = w.last_warn.lock();
158
				let now = Instant::now();
159

            
160
				if last_warn.map_or(true, |l| l + WARN_INTERVAL <= now) {
161
					*last_warn = Some(now);
162

            
163
					tracing::warn!(
164
						target = "wasm_overrides",
165
						on_chain_spec_name = %spec_name,
166
						override_spec_name = %w.version,
167
						spec_version = %spec,
168
						wasm_file = %w.path.display(),
169
						"On chain and override `spec_name` do not match! Ignoring override.",
170
					);
171
				}
172

            
173
				None
174
			}
175
		})
176
	}
177

            
178
	/// Scrapes a folder for WASM runtimes.
179
	/// Returns a hashmap of the runtime version and wasm runtime code.
180
	fn scrape_overrides<E>(dir: &Path, executor: &E) -> Result<HashMap<u32, WasmBlob>>
181
	where
182
		E: RuntimeVersionOf,
183
	{
184
		let handle_err = |e: std::io::Error| -> sp_blockchain::Error {
185
			WasmOverrideError::Io(dir.to_owned(), e).into()
186
		};
187

            
188
		if !dir.is_dir() {
189
			return Err(WasmOverrideError::NotADirectory(dir.to_owned()).into());
190
		}
191

            
192
		let mut overrides = HashMap::new();
193
		let mut duplicates = Vec::new();
194
		for entry in fs::read_dir(dir).map_err(handle_err)? {
195
			let entry = entry.map_err(handle_err)?;
196
			let path = entry.path();
197
			if let Some("wasm") = path.extension().and_then(|e| e.to_str()) {
198
				let code = fs::read(&path).map_err(handle_err)?;
199
				let code_hash = make_hash(&code);
200
				let version = Self::runtime_version(executor, &code, &code_hash, Some(128))?;
201
				tracing::info!(
202
					target: "wasm_overrides",
203
					version = %version,
204
					file = %path.display(),
205
					"Found wasm override.",
206
				);
207

            
208
				let wasm = WasmBlob::new(code, code_hash, path.clone(), version.clone());
209

            
210
				if let Some(other) = overrides.insert(version.spec_version, wasm) {
211
					tracing::info!(
212
						target: "wasm_overrides",
213
						first = %other.path.display(),
214
						second = %path.display(),
215
						%version,
216
						"Found duplicate spec version for runtime.",
217
					);
218
					duplicates.push(path.display().to_string());
219
				}
220
			}
221
		}
222

            
223
		if !duplicates.is_empty() {
224
			return Err(WasmOverrideError::DuplicateRuntime(duplicates).into());
225
		}
226

            
227
		Ok(overrides)
228
	}
229

            
230
	fn runtime_version<E>(
231
		executor: &E,
232
		code: &[u8],
233
		code_hash: &[u8],
234
		heap_pages: Option<u64>,
235
	) -> Result<RuntimeVersion>
236
	where
237
		E: RuntimeVersionOf,
238
	{
239
		let mut ext = BasicExternalities::default();
240
		executor
241
			.runtime_version(
242
				&mut ext,
243
				&RuntimeCode {
244
					code_fetcher: &WrappedRuntimeCode(code.into()),
245
					heap_pages,
246
					hash: code_hash.into(),
247
				},
248
			)
249
			.map_err(|e| WasmOverrideError::VersionInvalid(e.to_string()).into())
250
	}
251
}