namada_apps_lib/wasm_loader/
mod.rs

1//! A module for loading WASM files and downloading pre-built WASMs.
2use 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/// A hash map where keys are simple file names and values their full file name
32/// including SHA256 hash
33#[derive(Debug, Serialize, Deserialize)]
34#[serde(transparent)]
35pub struct Checksums(pub HashMap<String, String>);
36
37impl Checksums {
38    /// Read WASM checksums from the given path
39    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    /// Read WASM checksums from "checksums.json" in the given directory
70    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                // Ignoring the result, next step will fail if not read
87                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
133/// Validate wasm artifacts
134pub async fn validate_wasm_artifacts(wasm_directory: impl AsRef<Path>) {
135    // load json with wasm hashes
136    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        // Async check and download (if needed) each file
142        tokio::spawn(async move {
143            let wasm_path = wasm_directory.join(&full_name);
144            match tokio::fs::read(&wasm_path).await {
145                // if the file exist, check the hash
146                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                // if the doesn't file exist, download it.
160                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    // load json with wasm hashes
179    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}