diff --git a/parameters/src/canary/mod.rs b/parameters/src/canary/mod.rs index f36fd587d7..13ff4dd771 100644 --- a/parameters/src/canary/mod.rs +++ b/parameters/src/canary/mod.rs @@ -19,6 +19,8 @@ pub use genesis::*; /// The restrictions list as a JSON-compatible string. pub const RESTRICTIONS_LIST: &str = include_str!("./resources/restrictions.json"); +/// The remote URLs to fetch parameters from. +#[cfg_attr(not(any(feature = "filesystem", feature = "wasm")), allow(dead_code))] const REMOTE_URLS: [&str; 2] = ["https://parameters.provable.com/canary", "https://s3.us-west-1.amazonaws.com/canary.parameters"]; // BondPublic diff --git a/parameters/src/macros.rs b/parameters/src/macros.rs index 6203a322b8..4933c7c032 100644 --- a/parameters/src/macros.rs +++ b/parameters/src/macros.rs @@ -13,45 +13,86 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[macro_export] -macro_rules! checksum { - ($bytes: expr) => {{ - use sha2::Digest; - hex::encode(&sha2::Sha256::digest($bytes)) - }}; +/// Computes a SHA-256 digest of the given byte slice and returns it as a lowercase hex string. +/// Used to verify the integrity of downloaded or loaded parameter files. +#[inline(always)] +pub(crate) fn checksum(bytes: impl AsRef<[u8]>) -> String { + use sha2::Digest; + hex::encode(sha2::Sha256::digest(bytes)) } -#[macro_export] -macro_rules! checksum_error { - ($expected: expr, $candidate: expr) => { - Err($crate::errors::ParameterError::ChecksumMismatch($expected, $candidate)) - }; +/// Constructs a `ParameterError::ChecksumMismatch` error from an expected and a computed checksum. +/// Used to report integrity failures when a loaded or downloaded file does not match its metadata. +/// Generic over `T` so it can be returned from any `Result`-returning function. +#[cfg(any(feature = "filesystem", feature = "wasm"))] +#[inline(always)] +pub(crate) fn checksum_error(expected: String, candidate: String) -> Result { + Err(crate::errors::ParameterError::ChecksumMismatch(expected, candidate)) } -#[macro_export] -macro_rules! remove_file { - ($filepath:expr) => { - // Safely remove the corrupt file, if it exists. - #[cfg(not(feature = "wasm"))] - if std::path::PathBuf::from(&$filepath).exists() { - match std::fs::remove_file(&$filepath) { - Ok(()) => println!("Removed {:?}. Please retry the command.", $filepath), - Err(err) => eprintln!("Failed to remove {:?}: {err}", $filepath), +/// Removes a parameter file from disk (on targets where the filesystem is available) +/// when it is found to be corrupt or mismatched. +/// Prints a message on success so the user knows to retry; logs a warning on failure. +#[inline(always)] +pub(crate) fn remove_file(filepath: impl AsRef) { + cfg_if::cfg_if! { + if #[cfg(feature="wasm")] { + // No-op on wasm targets where filesystem access is unavailable. + let _ = filepath; + } else { + let filepath = filepath.as_ref(); + if filepath.exists() { + match std::fs::remove_file(filepath) { + Ok(()) => println!("Removed {:?}. Please retry the command.", filepath), + Err(err) => eprintln!("Failed to remove {:?}: {err}", filepath), + } } } - }; + } +} + +/// Validates a locally loaded byte buffer against an expected size and checksum, then returns it. +/// On a size mismatch the cached file is removed. On a checksum mismatch a `ChecksumMismatch` +/// error is returned without removing the file. +/// Used inside `impl_local!` after reading a compile-time embedded parameter file. +#[inline(always)] +pub(crate) fn load_bytes_local( + filepath: &str, + buffer: &[u8], + expected_size: usize, + expected_checksum: &str, +) -> Result, crate::errors::ParameterError> { + if expected_size != buffer.len() { + remove_file(filepath); + return Err(crate::errors::ParameterError::SizeMismatch(expected_size, buffer.len())); + } + let candidate_checksum = checksum(buffer); + if expected_checksum != candidate_checksum { + return Err(crate::errors::ParameterError::ChecksumMismatch(expected_checksum.to_string(), candidate_checksum)); + } + Ok(buffer.to_vec()) } +/// Injects `store_bytes` and `remote_fetch` helper methods into the enclosing `impl` block. +/// +/// - `store_bytes` writes a parameter buffer to a local path, creating any missing directories. +/// Disabled on wasm targets. +/// - `remote_fetch` downloads a parameter file from a URL into a provided buffer. On native +/// targets it uses a blocking `reqwest` client with per-host retry logic; on wasm it uses a +/// synchronous `XmlHttpRequest` with ISO-8859-5 encoding to preserve raw bytes. Disabled on +/// SGX targets. +/// +/// Used internally by `impl_remote!` to provide download and caching support. macro_rules! impl_store_and_remote_fetch { () => { - #[cfg(not(feature = "wasm"))] + #[cfg(all(feature = "filesystem", not(feature = "wasm")))] fn store_bytes(buffer: &[u8], file_path: &std::path::Path) -> Result<(), $crate::errors::ParameterError> { use snarkvm_utilities::Write; #[cfg(not(feature = "no_std_out"))] { use colored::*; - let output = format!("{:>15} - Storing file in {:?}", "Installation", file_path); + let output = format!("{:>15} - Storing file in \"{}\"", "Installation", file_path.display()); println!("{}", output.dimmed()); } @@ -68,14 +109,14 @@ macro_rules! impl_store_and_remote_fetch { Ok(()) } - #[cfg(all(not(feature = "wasm"), not(target_env = "sgx")))] + #[cfg(all(feature = "filesystem", not(feature = "wasm"), not(target_env = "sgx")))] fn remote_fetch(buffer: &mut Vec, url: &str) -> Result<(), $crate::errors::ParameterError> { use std::io::Read; #[cfg(not(feature = "no_std_out"))] { use colored::*; - let output = format!("{:>15} - Downloading \"{}\"", "Installation", url); + let output = format!("{:>15} - Downloading \"{url}\"", "Installation"); println!("{}", output.dimmed()); } @@ -101,7 +142,7 @@ macro_rules! impl_store_and_remote_fetch { { use colored::*; let size_in_megabytes = buffer.len() as u64 / 1_048_576; - let output = format!("{:>15} - Download complete ({} MB)", "Installation", size_in_megabytes); + let output = format!("{:>15} - Download complete ({size_in_megabytes} MB)", "Installation"); println!("{}", output.dimmed()); } @@ -154,28 +195,92 @@ macro_rules! impl_store_and_remote_fetch { }; } -macro_rules! impl_load_bytes_logic_local { - ($filepath: expr, $buffer: expr, $expected_size: expr, $expected_checksum: expr) => { - // Ensure the size matches. - if $expected_size != $buffer.len() { - remove_file!($filepath); - return Err($crate::errors::ParameterError::SizeMismatch($expected_size, $buffer.len())); - } - - // Ensure the checksum matches. - let candidate_checksum = checksum!($buffer); - if $expected_checksum != candidate_checksum { - return checksum_error!($expected_checksum, candidate_checksum); - } - - return Ok($buffer.to_vec()); - }; -} - +/// Implements the full remote-load flow for a parameter file. +/// +/// On native `filesystem` targets, it serves from the local cache directory if present; otherwise +/// iterates through `$remote_urls` in order, retrying on transient errors, and caches the first +/// verified download to disk. +/// +/// On WASM targets, it terates through `$remote_urls` using `XmlHttpRequest`, verifying the +/// checksum after each attempt and returning on the first success. +/// In all cases the buffer is validated against `$expected_size` and `$expected_checksum` before +/// being returned. +/// +/// On SGX targets: it will attempt to load the paramtes for local directory. +/// The function will return an error in cases where the `filesystem` feature is disabled or no parameter file exists locally, as remote fetch is not supported on SGX. +/// +/// Used inside `impl_remote!`. macro_rules! impl_load_bytes_logic_remote { ($remote_urls: expr, $local_dir: expr, $filename: expr, $metadata: expr, $expected_checksum: expr, $expected_size: expr) => { cfg_if::cfg_if! { - if #[cfg(all(feature = "filesystem", not(feature="wasm")))] { + if #[cfg(feature = "wasm")] { + // Try each URL in order, falling back to the next if one fails. + let remote_urls: &[&str] = &$remote_urls; + let mut buffer = vec![]; + let mut last_error: Option<$crate::errors::ParameterError> = None; + + for base_url in remote_urls.iter() { + let url = format!("{base_url}/{}", $filename); + + match Self::remote_fetch(&url) { + Ok(fetched_buffer) => { + // Ensure the checksum matches. + let candidate_checksum = $crate::macros::checksum(&fetched_buffer); + if $expected_checksum == candidate_checksum { + buffer = fetched_buffer; + last_error = None; + break; + } else { + last_error = Some($crate::errors::ParameterError::ChecksumMismatch( + $expected_checksum.to_string(), + candidate_checksum, + )); + } + } + Err(e) => { + last_error = Some(e); + } + } + } + + // If all URLs failed, return the last error. + if let Some(e) = last_error { + return Err(e); + } + + // Ensure the size matches. + if $expected_size != buffer.len() { + return Err($crate::errors::ParameterError::SizeMismatch($expected_size, buffer.len())); + } + + return Ok(buffer) + } else if #[cfg(all(feature = "filesystem", target_env="sgx"))] { + // Compose the correct file path for the parameter file. + let mut file_path = aleo_std::aleo_dir(); + file_path.push($local_dir); + file_path.push($filename); + + let buffer = if file_path.exists() { + // Attempts to load the parameter file locally with an absolute path. + std::fs::read(&file_path)? + } else { + // Cannot remote fetch on SGX. + return Err($crate::errors::ParameterError::RemoteFetchDisabled); + }; + + // Ensure the size matches. + if $expected_size != buffer.len() { + $crate::macros::remove_file(&file_path); + return Err($crate::errors::ParameterError::SizeMismatch($expected_size, buffer.len())); + } + + // Ensure the checksum matches. + let candidate_checksum = $crate::macros::checksum(buffer.as_slice()); + if $expected_checksum != candidate_checksum { + return $crate::macros::checksum_error($expected_checksum, candidate_checksum) + } + return Ok(buffer); + } else if #[cfg(feature="filesystem")] { // Compose the correct file path for the parameter file. let mut file_path = aleo_std::aleo_dir(); file_path.push($local_dir); @@ -189,144 +294,109 @@ macro_rules! impl_load_bytes_logic_remote { #[cfg(not(feature = "no_std_out"))] { use colored::*; - let path = format!("(in {:?})", file_path); + let path = format!("(in \"{}\")", file_path.display()); eprintln!( "\n⚠️ \"{}\" does not exist. Downloading and storing it {}.\n", $filename, path.dimmed() ); } - // Load remote file - cfg_if::cfg_if!{ - if #[cfg(all(not(feature = "wasm"), not(target_env = "sgx")))] { - // Try each URL in order, falling back to the next if one fails. - let remote_urls: &[&str] = &$remote_urls; - let mut buffer = vec![]; - let mut last_error: Option<($crate::errors::ParameterError, &str)> = None; - - for base_url in remote_urls.iter() { - // Remove the previous error (if any). - cfg_if::cfg_if!{ - if #[cfg(feature = "no_std_out")] { - last_error = None; - } else { - use colored::Colorize; - // If this is a retry, print the previous error as warning. - if let Some((err, url)) = last_error.take() { - eprintln!("{:>15} - {err}", "Warning".yellow()); - eprintln!("{:>15} - Failed to fetch from \"{url}\". Trying next source...", "Warning".yellow()); - } - } - } + // -- Load remote file -- + // Try each URL in order, falling back to the next if one fails. + let remote_urls: &[&str] = &$remote_urls; + let mut buffer = vec![]; + let mut last_error: Option<($crate::errors::ParameterError, &str)> = None; + + for base_url in remote_urls.iter() { + // Remove the previous error (if any). + cfg_if::cfg_if!{ + if #[cfg(feature = "no_std_out")] { + last_error = None; + } else { + use colored::Colorize; + // If this is a retry, print the previous error as warning. + if let Some((err, url)) = last_error.take() { + eprintln!("{:>15} - {err}", "Warning".yellow()); + eprintln!("{:>15} - Failed to fetch from \"{url}\". Trying next source...", "Warning".yellow()); + } + } + } - let url = format!("{}/{}", base_url, $filename); - buffer.clear(); - - match Self::remote_fetch(&mut buffer, &url) { - Ok(()) => { - // Ensure the checksum matches. - let candidate_checksum = checksum!(&buffer); - if $expected_checksum == candidate_checksum { - // Success - break out of the loop - break; - } else { - last_error = Some(($crate::errors::ParameterError::ChecksumMismatch( - $expected_checksum.to_string(), - candidate_checksum, - ), base_url)); - } - } - Err(err) => { - last_error = Some((err, base_url)); - } + let url = format!("{base_url}/{}", $filename); + buffer.clear(); + + match Self::remote_fetch(&mut buffer, &url) { + Ok(()) => { + // Ensure the checksum matches. + let candidate_checksum = $crate::macros::checksum(&buffer); + if $expected_checksum == candidate_checksum { + // Success - break out of the loop + break; + } else { + last_error = Some(($crate::errors::ParameterError::ChecksumMismatch( + $expected_checksum.to_string(), + candidate_checksum, + ), base_url)); } } - - // If all URLs failed, return the last error. - if let Some((err, _)) = last_error { - return Err(err); + Err(err) => { + last_error = Some((err, base_url)); } + } + } - match Self::store_bytes(&buffer, &file_path) { - Ok(()) => buffer, - Err(_) => { - eprintln!( - "\n❗ Error - Failed to store \"{}\" locally. Please download this file manually and ensure it is stored in {:?}.\n", - $filename, file_path - ); - buffer - } - } - } else { - return Err($crate::errors::ParameterError::RemoteFetchDisabled); + // If all URLs failed, return the last error. + if let Some((err, _)) = last_error { + return Err(err); + } + + match Self::store_bytes(&buffer, &file_path) { + Ok(()) => buffer, + Err(_) => { + eprintln!( + "\n❗ Error - Failed to store \"{}\" locally. Please download this file manually and ensure it is stored in {:?}.\n", + $filename, file_path + ); + buffer } } }; // Ensure the size matches. if $expected_size != buffer.len() { - remove_file!(file_path); + $crate::macros::remove_file(&file_path); return Err($crate::errors::ParameterError::SizeMismatch($expected_size, buffer.len())); } // Ensure the checksum matches. - let candidate_checksum = checksum!(buffer.as_slice()); + let candidate_checksum = $crate::macros::checksum(buffer.as_slice()); if $expected_checksum != candidate_checksum { - return checksum_error!($expected_checksum, candidate_checksum) + return $crate::macros::checksum_error($expected_checksum, candidate_checksum) } return Ok(buffer); } else { - cfg_if::cfg_if! { - if #[cfg(feature = "wasm")] { - // Try each URL in order, falling back to the next if one fails. - let remote_urls: &[&str] = &$remote_urls; - let mut buffer = vec![]; - let mut last_error: Option<$crate::errors::ParameterError> = None; - - for base_url in remote_urls.iter() { - let url = format!("{}/{}", base_url, $filename); - - match Self::remote_fetch(&url) { - Ok(fetched_buffer) => { - // Ensure the checksum matches. - let candidate_checksum = checksum!(&fetched_buffer); - if $expected_checksum == candidate_checksum { - buffer = fetched_buffer; - last_error = None; - break; - } else { - last_error = Some($crate::errors::ParameterError::ChecksumMismatch( - $expected_checksum.to_string(), - candidate_checksum, - )); - } - } - Err(e) => { - last_error = Some(e); - } - } - } - - // If all URLs failed, return the last error. - if let Some(e) = last_error { - return Err(e); - } - - // Ensure the size matches. - if $expected_size != buffer.len() { - return Err($crate::errors::ParameterError::SizeMismatch($expected_size, buffer.len())); - } - - return Ok(buffer) - } else { - return Err($crate::errors::ParameterError::FilesystemDisabled); - } - } + // We need either the `filesystem` or `wasm` feature to load parameters. + return Err($crate::errors::ParameterError::FilesystemDisabled); } } } } +/// Generates a parameter struct whose bytes are embedded at compile time via `include_bytes!`. +/// +/// Two variants: +/// - `($name, $local_dir, $fname, "usrs")` — for universal reference string (`.usrs`) files. +/// Metadata is read from `$local_dir/$fname.metadata`. +/// - `($name, $local_dir, $fname, $ftype, $credits_version)` — for versioned parameter files. +/// Metadata is read from `$local_dir/$credits_version/$fname.metadata`, and the checksum/size +/// keys in the metadata are prefixed with `$ftype_`. +/// +/// Both variants expose: +/// - `METADATA: &'static str` — the raw JSON metadata string. +/// - `load_bytes() -> Result, ParameterError>` — returns the embedded bytes after +/// verifying size and checksum. +/// +/// A compile-time test is also generated to ensure the embedded bytes load successfully. #[macro_export] macro_rules! impl_local { ($name: ident, $local_dir: expr, $fname: tt, "usrs") => { @@ -341,10 +411,10 @@ macro_rules! impl_local { let expected_checksum: String = metadata["checksum"].as_str().expect("Failed to parse checksum").to_string(); let expected_size: usize = metadata["size"].to_string().parse().expect("Failed to retrieve the file size"); - let _filepath = concat!($local_dir, $fname, ".", "usrs"); + let filepath = concat!($local_dir, $fname, ".", "usrs"); let buffer = include_bytes!(concat!($local_dir, $fname, ".", "usrs")); - impl_load_bytes_logic_local!(_filepath, buffer, expected_size, expected_checksum); + $crate::macros::load_bytes_local(filepath, buffer, expected_size, &expected_checksum) } } @@ -373,10 +443,10 @@ macro_rules! impl_local { let expected_size: usize = metadata[concat!($ftype, "_size")].to_string().parse().expect("Failed to retrieve the file size"); - let _filepath = concat!($local_dir, $credits_version, "/", $fname, ".", $ftype); + let filepath = concat!($local_dir, $credits_version, "/", $fname, ".", $ftype); let buffer = include_bytes!(concat!($local_dir, $credits_version, "/", $fname, ".", $ftype)); - impl_load_bytes_logic_local!(_filepath, buffer, expected_size, expected_checksum); + $crate::macros::load_bytes_local(filepath, buffer, expected_size, &expected_checksum) } } @@ -392,6 +462,23 @@ macro_rules! impl_local { }; } +/// Generates a parameter struct whose bytes are fetched from a remote URL and cached locally. +/// +/// Two variants: +/// - `($name, $remote_url, $local_dir, $fname, "usrs")` — for universal reference string +/// (`.usrs`) files. Metadata lives at `$local_dir/$fname.metadata`. +/// - `($name, $remote_url, $local_dir, $fname, $ftype, $credits_version)` — for versioned +/// parameter files. Metadata lives at `$local_dir/$credits_version/$fname.metadata`. +/// On wasm this variant also exposes `verify_bytes(buffer)` to validate externally supplied +/// bytes against the embedded metadata without performing a download. +/// +/// Both variants expose: +/// - `METADATA: &'static str` — the raw JSON metadata string. +/// - `load_bytes() -> Result, ParameterError>` — serves from the local cache when +/// available, otherwise downloads from `$remote_url`, caches the result, and verifies size and +/// checksum before returning. +/// +/// A test is also generated that calls `load_bytes()` to verify the download path. #[macro_export] macro_rules! impl_remote { ($name: ident, $remote_url: expr, $local_dir: expr, $fname: tt, "usrs") => { @@ -412,6 +499,7 @@ macro_rules! impl_remote { Some(sum) => format!("{}.{}.{}", $fname, "usrs", sum), _ => format!("{}.{}", $fname, "usrs"), }; + let _ = (&expected_size, &filename); impl_load_bytes_logic_remote!($remote_url, $local_dir, &filename, metadata, expected_checksum, expected_size); } @@ -444,6 +532,7 @@ macro_rules! impl_remote { Some(sum) => format!("{}.{}.{}", $fname, $ftype, sum), _ => format!("{}.{}", $fname, $ftype), }; + let _ = (&expected_size, &filename); impl_load_bytes_logic_remote!($remote_url, $local_dir, &filename, metadata, expected_checksum, expected_size); } @@ -463,9 +552,9 @@ macro_rules! impl_remote { } // Ensure the checksum matches. - let candidate_checksum = checksum!(buffer); + let candidate_checksum = $crate::macros::checksum(buffer); if expected_checksum != candidate_checksum { - return checksum_error!(expected_checksum, candidate_checksum); + return $crate::macros::checksum_error(expected_checksum, candidate_checksum); } Ok(()) } diff --git a/parameters/src/mainnet/mod.rs b/parameters/src/mainnet/mod.rs index b9f5591c5f..20e552f8cf 100644 --- a/parameters/src/mainnet/mod.rs +++ b/parameters/src/mainnet/mod.rs @@ -22,6 +22,8 @@ pub use powers::*; /// The restrictions list as a JSON-compatible string. pub const RESTRICTIONS_LIST: &str = include_str!("./resources/restrictions.json"); +/// The remote URLs to fetch parameters from. +#[cfg_attr(not(any(feature = "filesystem", feature = "wasm")), allow(dead_code))] const REMOTE_URLS: [&str; 2] = ["https://parameters.provable.com/mainnet", "https://s3.us-west-1.amazonaws.com/mainnet.parameters"]; diff --git a/parameters/src/testnet/mod.rs b/parameters/src/testnet/mod.rs index fdfe8942f3..16a8d3cc90 100644 --- a/parameters/src/testnet/mod.rs +++ b/parameters/src/testnet/mod.rs @@ -19,6 +19,8 @@ pub use genesis::*; /// The restrictions list as a JSON-compatible string. pub const RESTRICTIONS_LIST: &str = include_str!("./resources/restrictions.json"); +/// The remote URLs to fetch parameters from. +#[cfg_attr(not(any(feature = "filesystem", feature = "wasm")), allow(dead_code))] const REMOTE_URLS: [&str; 2] = ["https://parameters.provable.com/testnet", "https://s3.us-west-1.amazonaws.com/testnet.parameters"];