namada_apps_lib/wasm_loader/
mod.rs1use std::fs;
3use std::path::Path;
4
5use data_encoding::HEXLOWER;
6use eyre::{WrapErr, eyre};
7use futures::future::join_all;
8use namada_sdk::collections::HashMap;
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11use thiserror::Error;
12use tokio::io::AsyncReadExt;
13
14use crate::cli::safe_exit;
15use crate::config::DEFAULT_WASM_CHECKSUMS_FILE;
16
17#[derive(Error, Debug)]
18pub enum Error {
19 #[error("Not able to download {0}, failed with {1}")]
20 Download(String, reqwest::Error),
21 #[error("Error writing to {0}")]
22 FileWrite(String),
23 #[error("Cannot download {0}")]
24 WasmNotFound(String),
25 #[error("Error while downloading {0}: {1}")]
26 ServerError(String, String),
27 #[error("Checksum mismatch in downloaded wasm: {0}")]
28 ChecksumMismatch(String),
29}
30
31#[derive(Debug, Serialize, Deserialize)]
34#[serde(transparent)]
35pub struct Checksums(pub HashMap<String, String>);
36
37impl Checksums {
38 pub fn read_checksums_file(
40 checksums_path: impl AsRef<Path>,
41 ) -> Result<Self, eyre::Error> {
42 match fs::File::open(&checksums_path) {
43 Ok(file) => match serde_json::from_reader(file) {
44 Ok(result) => Ok(result),
45 Err(_) => {
46 eprintln!(
47 "Can't read checksums from {}",
48 checksums_path.as_ref().to_string_lossy()
49 );
50 Err(eyre!(
51 "Can't read checksums from {}",
52 checksums_path.as_ref().to_string_lossy()
53 ))
54 }
55 },
56 Err(_) => {
57 eprintln!(
58 "Can't find checksums at {}",
59 checksums_path.as_ref().to_string_lossy()
60 );
61 Err(eyre!(
62 "Can't find checksums at {}",
63 checksums_path.as_ref().to_string_lossy()
64 ))
65 }
66 }
67 }
68
69 pub fn read_checksums(
71 wasm_directory: impl AsRef<Path>,
72 ) -> Result<Self, eyre::Error> {
73 let checksums_path =
74 wasm_directory.as_ref().join(DEFAULT_WASM_CHECKSUMS_FILE);
75 Self::read_checksums_file(checksums_path)
76 }
77
78 pub async fn read_checksums_async(
79 wasm_directory: impl AsRef<Path>,
80 ) -> Self {
81 let checksums_path =
82 wasm_directory.as_ref().join(DEFAULT_WASM_CHECKSUMS_FILE);
83 match tokio::fs::File::open(&checksums_path).await {
84 Ok(mut file) => {
85 let mut contents = vec![];
86 let _ = file.read_to_end(&mut contents).await;
88 match serde_json::from_slice(&contents[..]) {
89 Ok(checksums) => checksums,
90 Err(err) => {
91 eprintln!(
92 "Failed decoding WASM checksums from {}. Failed \
93 with {}",
94 checksums_path.to_string_lossy(),
95 err
96 );
97 safe_exit(1);
98 }
99 }
100 }
101 Err(err) => {
102 eprintln!(
103 "Unable to read WASM checksums from {}. Failed with {}",
104 checksums_path.to_string_lossy(),
105 err
106 );
107 safe_exit(1);
108 }
109 }
110 }
111}
112
113fn valid_wasm_checksum(
114 wasm_payload: &[u8],
115 name: &str,
116 full_name: &str,
117) -> Result<(), String> {
118 let mut hasher = Sha256::new();
119 hasher.update(wasm_payload);
120 let result = HEXLOWER.encode(&hasher.finalize());
121 let derived_name = format!(
122 "{}.{}.wasm",
123 &name.split('.').collect::<Vec<&str>>()[0],
124 result
125 );
126 if full_name == derived_name {
127 Ok(())
128 } else {
129 Err(derived_name)
130 }
131}
132
133pub async fn validate_wasm_artifacts(wasm_directory: impl AsRef<Path>) {
135 let checksums = Checksums::read_checksums_async(&wasm_directory).await;
137
138 join_all(checksums.0.into_iter().map(|(name, full_name)| {
139 let wasm_directory = wasm_directory.as_ref().to_owned();
140
141 tokio::spawn(async move {
143 let wasm_path = wasm_directory.join(&full_name);
144 match tokio::fs::read(&wasm_path).await {
145 Ok(bytes) => {
147 if let Err(derived_name) =
148 valid_wasm_checksum(&bytes, &name, &full_name)
149 {
150 tracing::info!(
151 "WASM checksum mismatch: Got {}, expected {}. \
152 Check your wasms artifacts.",
153 derived_name,
154 full_name
155 );
156 safe_exit(1);
157 }
158 }
159 Err(err) => {
161 eprintln!(
162 "Can't read {}: {}",
163 wasm_path.as_os_str().to_string_lossy(),
164 err
165 );
166 safe_exit(1);
167 }
168 }
169 })
170 }))
171 .await;
172}
173
174pub fn read_wasm(
175 wasm_directory: impl AsRef<Path>,
176 file_path: impl AsRef<Path>,
177) -> eyre::Result<Vec<u8>> {
178 let checksums = Checksums::read_checksums(&wasm_directory)?;
180
181 if let Some(os_name) = file_path.as_ref().file_name() {
182 if let Some(name) = os_name.to_str() {
183 let wasm_path = match checksums.0.get(name) {
184 Some(wasm_filename) => {
185 wasm_directory.as_ref().join(wasm_filename)
186 }
187 None => {
188 if !file_path.as_ref().is_absolute() {
189 wasm_directory.as_ref().join(file_path.as_ref())
190 } else {
191 file_path.as_ref().to_path_buf()
192 }
193 }
194 };
195 return fs::read(&wasm_path).wrap_err_with(|| {
196 format!(
197 "Failed to read WASM from {}",
198 &wasm_path.to_string_lossy()
199 )
200 });
201 }
202 }
203 Err(eyre!(
204 "Could not read {}",
205 file_path.as_ref().to_string_lossy()
206 ))
207}
208
209pub fn read_wasm_or_exit(
210 wasm_directory: impl AsRef<Path>,
211 file_path: impl AsRef<Path>,
212) -> Vec<u8> {
213 match read_wasm(wasm_directory, file_path) {
214 Ok(wasm) => wasm,
215 Err(err) => {
216 eprintln!("Error reading wasm: {}", err);
217 safe_exit(1);
218 }
219 }
220}