diff --git a/.github/workflows/deploy_package.yml b/.github/workflows/deploy_package.yml index 53530896..d2de0c2b 100644 --- a/.github/workflows/deploy_package.yml +++ b/.github/workflows/deploy_package.yml @@ -117,7 +117,7 @@ jobs: sudo chown -R miden /opt/miden; \ sudo /usr/bin/miden-node init -c /etc/miden/miden-node.toml -g /opt/miden/miden-node/genesis.toml; \ sudo /usr/bin/miden-node make-genesis -i /opt/miden/miden-node/genesis.toml -o /opt/miden/miden-node/genesis.dat --force; \ - sudo /usr/bin/miden-faucet init -c /opt/miden/miden-faucet/miden-faucet.toml + sudo /usr/bin/miden-faucet init -c /opt/miden/miden-faucet/miden-faucet.toml -f /opt/miden/miden-node/accounts/faucet.mac - name: Start miden node service uses: ./.github/actions/ssm_execute diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b1d07d9..ed2ecf83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - [BREAKING] Improved store API errors (return "not found" instead of "internal error" status if requested account(s) not found) (#518). - [BREAKING] Migrated to v0.11 version of Miden VM (#528). - Reduce cloning in the store's `apply_block` (#532). +- [BREAKING] Changed faucet storage type in the genesis to public. Using faucet from the genesis for faucet web app. Added support for faucet restarting without blockchain restarting (#517). ## 0.5.1 (2024-09-12) diff --git a/Cargo.lock b/Cargo.lock index ee1f4a6e..67049c24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1469,6 +1469,7 @@ dependencies = [ name = "miden-faucet" version = "0.6.0" dependencies = [ + "anyhow", "axum", "clap", "figment", @@ -1568,6 +1569,7 @@ dependencies = [ "miden-node-store", "miden-node-utils", "miden-objects", + "rand", "rand_chacha", "serde", "tokio", @@ -1686,6 +1688,7 @@ dependencies = [ "figment", "itertools 0.12.1", "miden-objects", + "rand", "serde", "thiserror", "tonic", diff --git a/Cargo.toml b/Cargo.toml index dfc3349c..9e0d53b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ miden-processor = { git = "https://github.com/0xPolygonMiden/miden-vm.git", bran miden-stdlib = { git = "https://github.com/0xPolygonMiden/miden-vm.git", branch = "next", default-features = false } miden-tx = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } prost = { version = "0.13" } +rand = { version = "0.8" } thiserror = { version = "1.0" } tokio = { version = "1.40", features = ["rt-multi-thread"] } tokio-stream = { version = "0.1" } diff --git a/bin/faucet/Cargo.toml b/bin/faucet/Cargo.toml index fc03a81a..7d299e3e 100644 --- a/bin/faucet/Cargo.toml +++ b/bin/faucet/Cargo.toml @@ -17,6 +17,7 @@ repository.workspace = true testing = ["miden-objects/testing", "miden-lib/testing"] [dependencies] +anyhow = "1.0" axum = { version = "0.7", features = ["tokio"] } clap = { version = "4.5", features = ["derive", "string"] } figment = { version = "0.10", features = ["toml", "env"] } @@ -28,12 +29,12 @@ miden-node-utils = { workspace = true } miden-objects = { workspace = true , features = ["concurrent"] } miden-tx = { workspace = true, features = ["concurrent"] } mime = "0.3" -rand = { version = "0.8" } +rand = { workspace = true } rand_chacha = "0.3" serde = { version = "1.0", features = ["derive"] } static-files = "0.2" thiserror = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["fs"] } toml = { version = "0.8" } tonic = { workspace = true } tower = "0.5" diff --git a/bin/faucet/README.md b/bin/faucet/README.md index 2d9fe8ed..2d7b15e5 100644 --- a/bin/faucet/README.md +++ b/bin/faucet/README.md @@ -2,8 +2,12 @@ This crate contains a binary for running a Miden rollup faucet. -## Running the faucet -1. Run a local node, for example using the docker image. From the "miden-node" repo root run the following commands: +## Running the faucet in testing mode + +> [!TIP] +> Miden account generation uses a proof-of-work puzzle to prevent DoS attacks. These puzzles can be quite expensive, especially for test purposes. You can lower the difficulty of the puzzle by appending `--features testing` to the `cargo install ..` invocation. + +1. Run a local node with the "testing" feature, for example using the docker image. From the "miden-node" repo root run the following commands: ```bash make docker-build-node make docker-run-node @@ -14,12 +18,24 @@ make docker-run-node make install-faucet-testing ``` -3. Create the default faucet configuration file: +3. [Optional] Create faucet account (skip this step if you want to use an account from the genesis). This will generate authentication keypair and generate and write public faucet account data with its keypair into the file specified in `output-path`: + +```bash +miden-faucet create-faucet-account \ + --output-path \ + --token-symbol POL \ + --decimals 9 \ + --max-supply 1000000000 +``` +> [!TIP] +> This account will not be created on chain yet, creation on chain will happen on the first minting transaction. + +4. Create the default faucet configuration file. Specify the path to the faucet account file created on the previous step in the `-f` flag (if you want to use an account from the genesis, specify the path to the `faucet.mac` file generated by the [make-genesis](../../README.md#setup) command of the Miden node): ```bash -miden-faucet init +miden-faucet init -f ``` -4. Start the faucet server: +5. Start the faucet server: ```bash miden-faucet start ``` diff --git a/bin/faucet/src/client.rs b/bin/faucet/src/client.rs index ae5d55a9..ce76a37e 100644 --- a/bin/faucet/src/client.rs +++ b/bin/faucet/src/client.rs @@ -1,41 +1,39 @@ -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; +use std::{sync::Arc, time::Duration}; -use miden_lib::{ - accounts::faucets::create_basic_fungible_faucet, notes::create_p2id_note, - transaction::TransactionKernel, AuthScheme, -}; +use anyhow::Context; +use miden_lib::{notes::create_p2id_note, transaction::TransactionKernel}; use miden_node_proto::generated::{ - requests::{GetBlockHeaderByNumberRequest, SubmitProvenTransactionRequest}, + requests::{ + GetAccountDetailsRequest, GetBlockHeaderByNumberRequest, SubmitProvenTransactionRequest, + }, rpc::api_client::ApiClient, }; use miden_objects::{ - accounts::{Account, AccountDelta, AccountId, AccountStorageMode, AuthSecretKey}, - assets::{FungibleAsset, TokenSymbol}, + accounts::{Account, AccountData, AccountId, AuthSecretKey}, + assets::FungibleAsset, crypto::{ - dsa::rpo_falcon512::SecretKey, merkle::{MmrPeaks, PartialMmr}, rand::RpoRandomCoin, }, - notes::{Note, NoteId, NoteType}, - transaction::{ChainMmr, ExecutedTransaction, InputNotes, TransactionArgs, TransactionScript}, + notes::{Note, NoteType}, + transaction::{ChainMmr, ExecutedTransaction, TransactionArgs, TransactionScript}, + utils::Deserializable, vm::AdviceMap, - BlockHeader, Felt, Word, + BlockHeader, Felt, }; use miden_tx::{ - auth::BasicAuthenticator, utils::Serializable, DataStore, DataStoreError, - LocalTransactionProver, ProvingOptions, TransactionExecutor, TransactionInputs, - TransactionProver, + auth::BasicAuthenticator, utils::Serializable, LocalTransactionProver, ProvingOptions, + TransactionExecutor, TransactionProver, }; -use rand::{rngs::StdRng, thread_rng, Rng}; -use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; +use rand::{random, rngs::StdRng}; use tonic::transport::Channel; +use tracing::info; use crate::{ config::FaucetConfig, - errors::{InitError, ProcessError}, + errors::{ClientError, ImplError}, + store::FaucetDataStore, + COMPONENT, }; pub const DISTRIBUTE_FUNGIBLE_ASSET_SCRIPT: &str = @@ -54,31 +52,68 @@ pub struct FaucetClient { rng: RpoRandomCoin, } +// TODO: Remove this once https://github.com/0xPolygonMiden/miden-base/issues/909 is resolved unsafe impl Send for FaucetClient {} -unsafe impl Sync for FaucetClient {} impl FaucetClient { - pub async fn new(config: FaucetConfig) -> Result { - let (rpc_api, root_block_header, root_chain_mmr) = - initialize_faucet_client(config.clone()).await?; - - let (faucet_account, account_seed, secret) = build_account(config.clone())?; - let id = faucet_account.id(); + /// Fetches the latest faucet account state from the node and creates a new faucet client. + /// + /// # Note + /// If the faucet account is not found on chain, it will be created on submission of the first + /// minting transaction. + pub async fn new(config: &FaucetConfig) -> Result { + let (mut rpc_api, root_block_header, root_chain_mmr) = + initialize_faucet_client(config).await?; + + let faucet_account_data = AccountData::read(&config.faucet_account_path) + .context("Failed to load faucet account from file")?; + + let id = faucet_account_data.account.id(); + + info!(target: COMPONENT, "Requesting account state from the node..."); + let faucet_account = match request_account_state(&mut rpc_api, id).await { + Ok(account) => { + info!( + target: COMPONENT, + hash = %account.hash(), + nonce = %account.nonce(), + "Received faucet account state from the node", + ); + + account + }, + + Err(err) => match err { + ClientError::RequestError(status) if status.code() == tonic::Code::NotFound => { + info!(target: COMPONENT, "Faucet account not found in the node"); + + faucet_account_data.account + }, + _ => { + return Err(err); + }, + }, + }; let data_store = Arc::new(FaucetDataStore::new( faucet_account, - account_seed, + faucet_account_data.account_seed, root_block_header, root_chain_mmr, )); - let authenticator = Arc::new(BasicAuthenticator::::new(&[( - secret.public_key().into(), - AuthSecretKey::RpoFalcon512(secret), - )])); - let executor = TransactionExecutor::new(data_store.clone(), Some(authenticator)); - - let mut rng = thread_rng(); - let coin_seed: [u64; 4] = rng.gen(); + + let public_key = match &faucet_account_data.auth_secret_key { + AuthSecretKey::RpoFalcon512(secret) => secret.public_key(), + }; + + let authenticator = BasicAuthenticator::::new(&[( + public_key.into(), + faucet_account_data.auth_secret_key, + )]); + + let executor = TransactionExecutor::new(data_store.clone(), Some(Arc::new(authenticator))); + + let coin_seed: [u64; 4] = random(); let rng = RpoRandomCoin::new(coin_seed.map(Felt::new)); Ok(Self { data_store, rpc_api, executor, id, rng }) @@ -92,9 +127,9 @@ impl FaucetClient { target_account_id: AccountId, is_private_note: bool, asset_amount: u64, - ) -> Result<(ExecutedTransaction, Note), ProcessError> { - let asset = FungibleAsset::new(self.id, asset_amount) - .map_err(|err| ProcessError::InternalServerError(err.to_string()))?; + ) -> Result<(ExecutedTransaction, Note), ClientError> { + let asset = + FungibleAsset::new(self.id, asset_amount).context("Failed to create fungible asset")?; let note_type = if is_private_note { NoteType::Private @@ -110,16 +145,14 @@ impl FaucetClient { Default::default(), &mut self.rng, ) - .map_err(|err| ProcessError::InternalServerError(err.to_string()))?; + .context("Failed to create P2ID note")?; let transaction_args = build_transaction_arguments(&output_note, note_type, asset)?; let executed_tx = self .executor .execute_transaction(self.id, 0, &[], transaction_args) - .map_err(|err| { - ProcessError::InternalServerError(format!("Failed to execute transaction: {err}")) - })?; + .context("Failed to execute transaction")?; Ok((executed_tx, output_note)) } @@ -128,19 +161,16 @@ impl FaucetClient { pub async fn prove_and_submit_transaction( &mut self, executed_tx: ExecutedTransaction, - ) -> Result { - let delta = executed_tx.account_delta().clone(); - + ) -> Result { // Prepare request with proven transaction. // This is needed to be in a separated code block in order to release reference to avoid // borrow checker error. let request = { let transaction_prover = LocalTransactionProver::new(ProvingOptions::default()); - let proven_transaction = - transaction_prover.prove(executed_tx.into()).map_err(|err| { - ProcessError::InternalServerError(format!("Failed to prove transaction: {err}")) - })?; + let proven_transaction = transaction_prover + .prove(executed_tx.into()) + .context("Failed to prove transaction")?; SubmitProvenTransactionRequest { transaction: proven_transaction.to_bytes(), @@ -151,137 +181,50 @@ impl FaucetClient { .rpc_api .submit_proven_transaction(request) .await - .map_err(|err| ProcessError::InternalServerError(err.to_string()))?; - - self.data_store.update_faucet_account(&delta).map_err(|err| { - ProcessError::InternalServerError(format!("Failed to update account: {err}")) - })?; + .context("Failed to submit proven transaction")?; Ok(response.into_inner().block_height) } - pub fn get_faucet_id(&self) -> AccountId { - self.id + /// Returns a reference to the data store. + pub fn data_store(&self) -> &FaucetDataStore { + &self.data_store } -} - -pub struct FaucetDataStore { - faucet_account: Mutex, - seed: Word, - block_header: BlockHeader, - chain_mmr: ChainMmr, -} -// FAUCET DATA STORE -// ================================================================================================ - -impl FaucetDataStore { - pub fn new( - faucet_account: Account, - seed: Word, - root_block_header: BlockHeader, - root_chain_mmr: ChainMmr, - ) -> Self { - Self { - faucet_account: Mutex::new(faucet_account), - seed, - block_header: root_block_header, - chain_mmr: root_chain_mmr, - } - } - - /// Updates the stored faucet account with the provided delta. - fn update_faucet_account(&self, delta: &AccountDelta) -> Result<(), ProcessError> { - self.faucet_account - .lock() - .expect("Poisoned lock") - .apply_delta(delta) - .map_err(|err| ProcessError::InternalServerError(err.to_string())) - } -} - -impl DataStore for FaucetDataStore { - fn get_transaction_inputs( - &self, - account_id: AccountId, - _block_ref: u32, - _notes: &[NoteId], - ) -> Result { - let account = self.faucet_account.lock().expect("Poisoned lock"); - if account_id != account.id() { - return Err(DataStoreError::AccountNotFound(account_id)); - } - - let empty_input_notes = - InputNotes::new(Vec::new()).map_err(DataStoreError::InvalidTransactionInput)?; - - TransactionInputs::new( - account.clone(), - account.is_new().then_some(self.seed), - self.block_header, - self.chain_mmr.clone(), - empty_input_notes, - ) - .map_err(DataStoreError::InvalidTransactionInput) + /// Returns the id of the faucet account. + pub fn get_faucet_id(&self) -> AccountId { + self.id } } // HELPER FUNCTIONS // ================================================================================================ -/// Builds a new faucet account with the provided configuration. -/// -/// Returns the created account, its seed, and the secret key used to sign transactions. -fn build_account(config: FaucetConfig) -> Result<(Account, Word, SecretKey), InitError> { - let token_symbol = TokenSymbol::new(config.token_symbol.as_str()) - .map_err(|err| InitError::AccountCreationError(err.to_string()))?; - - let seed: [u8; 32] = [0; 32]; - - // Instantiate keypair and auth scheme - let mut rng = ChaCha20Rng::from_seed(seed); - let secret = SecretKey::with_rng(&mut rng); - let auth_scheme = AuthScheme::RpoFalcon512 { pub_key: secret.public_key() }; - - let (faucet_account, account_seed) = create_basic_fungible_faucet( - seed, - token_symbol, - config.decimals, - Felt::try_from(config.max_supply) - .map_err(|err| InitError::AccountCreationError(err.to_string()))?, - AccountStorageMode::Private, - auth_scheme, - ) - .map_err(|err| InitError::AccountCreationError(err.to_string()))?; - - Ok((faucet_account, account_seed, secret)) -} - /// Initializes the faucet client by connecting to the node and fetching the root block header. pub async fn initialize_faucet_client( - config: FaucetConfig, -) -> Result<(ApiClient, BlockHeader, ChainMmr), InitError> { + config: &FaucetConfig, +) -> Result<(ApiClient, BlockHeader, ChainMmr), ClientError> { let endpoint = tonic::transport::Endpoint::try_from(config.node_url.clone()) - .map_err(|_| InitError::ClientInitFailed("Failed to connect to node.".to_string()))? + .context("Failed to parse node URL from configuration file")? .timeout(Duration::from_millis(config.timeout_ms)); - let mut rpc_api = ApiClient::connect(endpoint) - .await - .map_err(|err| InitError::ClientInitFailed(err.to_string()))?; + let mut rpc_api = + ApiClient::connect(endpoint).await.context("Failed to connect to the node")?; let request = GetBlockHeaderByNumberRequest { block_num: Some(0), - include_mmr_proof: Some(true), + include_mmr_proof: None, }; let response = rpc_api .get_block_header_by_number(request) .await - .map_err(|err| InitError::ClientInitFailed(format!("Failed to get block header: {err}")))?; - let root_block_header = response.into_inner().block_header.unwrap(); + .context("Failed to get block header")?; + let root_block_header = response + .into_inner() + .block_header + .context("Missing root block header in response")?; - let root_block_header: BlockHeader = root_block_header.try_into().map_err(|err| { - InitError::ClientInitFailed(format!("Failed to parse block header: {err}")) - })?; + let root_block_header = root_block_header.try_into().context("Failed to parse block header")?; let root_chain_mmr = ChainMmr::new( PartialMmr::from_peaks( @@ -294,12 +237,35 @@ pub async fn initialize_faucet_client( Ok((rpc_api, root_block_header, root_chain_mmr)) } +/// Requests account state from the node. +/// +/// The account is expected to be public, otherwise, the error is returned. +async fn request_account_state( + rpc_api: &mut ApiClient, + account_id: AccountId, +) -> Result { + let account_info = rpc_api + .get_account_details(GetAccountDetailsRequest { account_id: Some(account_id.into()) }) + .await? + .into_inner() + .details + .context("Account info field is empty")?; + + let faucet_account_state_bytes = + account_info.details.context("Account details field is empty")?; + + Account::read_from_bytes(&faucet_account_state_bytes) + .map_err(ImplError) + .context("Failed to deserialize faucet account") + .map_err(Into::into) +} + /// Builds transaction arguments for the mint transaction. fn build_transaction_arguments( output_note: &Note, note_type: NoteType, asset: FungibleAsset, -) -> Result { +) -> Result { let recipient = output_note .recipient() .digest() @@ -321,9 +287,7 @@ fn build_transaction_arguments( .replace("{execution_hint}", &Felt::new(execution_hint).to_string()); let script = TransactionScript::compile(script, vec![], TransactionKernel::assembler()) - .map_err(|err| { - ProcessError::InternalServerError(format!("Failed to compile script: {err}")) - })?; + .context("Failed to compile script")?; let mut transaction_args = TransactionArgs::new(Some(script), None, AdviceMap::new()); transaction_args.extend_expected_output_notes(vec![output_note.clone()]); diff --git a/bin/faucet/src/config.rs b/bin/faucet/src/config.rs index 1e443585..d61fe5cb 100644 --- a/bin/faucet/src/config.rs +++ b/bin/faucet/src/config.rs @@ -1,4 +1,7 @@ -use std::fmt::{Display, Formatter}; +use std::{ + fmt::{Display, Formatter}, + path::PathBuf, +}; use miden_node_utils::config::{Endpoint, DEFAULT_FAUCET_SERVER_PORT, DEFAULT_NODE_RPC_PORT}; use serde::{Deserialize, Serialize}; @@ -6,30 +9,32 @@ use serde::{Deserialize, Serialize}; // Faucet config // ================================================================================================ +/// Default path to the faucet account file +pub const DEFAULT_FAUCET_ACCOUNT_PATH: &str = "accounts/faucet.mac"; + +/// Default timeout for RPC requests +pub const DEFAULT_RPC_TIMEOUT_MS: u64 = 10000; + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct FaucetConfig { /// Endpoint of the faucet pub endpoint: Endpoint, - /// Node RPC gRPC endpoint in the format `http://[:]`. + /// Node RPC gRPC endpoint in the format `http://[:]` pub node_url: String, /// Timeout for RPC requests in milliseconds pub timeout_ms: u64, /// Possible options on the amount of asset that should be dispersed on each faucet request pub asset_amount_options: Vec, - /// Token symbol of the generated fungible asset - pub token_symbol: String, - /// Number of decimals of the generated fungible asset - pub decimals: u8, - /// Maximum supply of the generated fungible asset - pub max_supply: u64, + /// Path to the faucet account file + pub faucet_account_path: PathBuf, } impl Display for FaucetConfig { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( - "{{ endpoint: \"{}\", asset_amount_options: {:?}, token_symbol: {}, decimals: {}, max_supply: {} }}", - self.endpoint, self.asset_amount_options, self.token_symbol, self.decimals, self.max_supply + "{{ endpoint: \"{}\", node_url: \"{}\", timeout_ms: \"{}\", asset_amount_options: {:?}, faucet_account_path: \"{}\" }}", + self.endpoint, self.node_url, self.timeout_ms, self.asset_amount_options, self.faucet_account_path.display() )) } } @@ -39,11 +44,9 @@ impl Default for FaucetConfig { Self { endpoint: Endpoint::localhost(DEFAULT_FAUCET_SERVER_PORT), node_url: Endpoint::localhost(DEFAULT_NODE_RPC_PORT).to_string(), - timeout_ms: 10000, + timeout_ms: DEFAULT_RPC_TIMEOUT_MS, asset_amount_options: vec![100, 500, 1000], - token_symbol: "POL".to_string(), - decimals: 8, - max_supply: 1000000, + faucet_account_path: DEFAULT_FAUCET_ACCOUNT_PATH.into(), } } } diff --git a/bin/faucet/src/errors.rs b/bin/faucet/src/errors.rs index 50777c71..c8705413 100644 --- a/bin/faucet/src/errors.rs +++ b/bin/faucet/src/errors.rs @@ -1,56 +1,61 @@ +use std::fmt::{Debug, Display}; + use axum::{ http::{header, StatusCode}, response::{IntoResponse, Response}, }; use thiserror::Error; +/// Wrapper for implementing `Error` trait for errors, which do not implement it, like +/// [miden_objects::crypto::utils::DeserializationError] and other error types from `miden-base`. #[derive(Debug, Error)] -pub enum InitError { - #[error("Failed to start faucet: {0}")] - FaucetFailedToStart(String), - - #[error("Failed to initialize client: {0}")] - ClientInitFailed(String), +#[error("{0}")] +pub struct ImplError(pub E); - #[error("Failed to configure faucet: {0}")] - ConfigurationError(String), +#[derive(Debug, Error)] +pub enum ClientError { + #[error("Request error: {0}")] + RequestError(#[from] tonic::Status), - #[error("Failed to create Miden account: {0}")] - AccountCreationError(String), + #[error("Client error: {0:#}")] + Other(#[from] anyhow::Error), } #[derive(Debug, Error)] -pub enum ProcessError { +pub enum HandlerError { + #[error("Node client error: {0}")] + ClientError(#[from] ClientError), + + #[error("Server has encountered an internal error: {0:#}")] + Internal(#[from] anyhow::Error), + #[error("Client has submitted a bad request: {0}")] BadRequest(String), - #[error("Server has encountered an internal error: {0}")] - InternalServerError(String), - #[error("Page not found: {0}")] NotFound(String), } -impl ProcessError { +impl HandlerError { fn status_code(&self) -> StatusCode { match *self { Self::BadRequest(_) => StatusCode::BAD_REQUEST, Self::NotFound(_) => StatusCode::NOT_FOUND, - Self::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::ClientError(_) | Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, } } fn message(&self) -> String { match self { Self::BadRequest(msg) => msg, - Self::InternalServerError(_) => "Error processing request", + Self::ClientError(_) | Self::Internal(_) => "Error processing request", Self::NotFound(msg) => msg, } .to_string() } } -impl IntoResponse for ProcessError { +impl IntoResponse for HandlerError { fn into_response(self) -> Response { ( self.status_code(), diff --git a/bin/faucet/src/handlers.rs b/bin/faucet/src/handlers.rs index 5e51e4fd..764f997d 100644 --- a/bin/faucet/src/handlers.rs +++ b/bin/faucet/src/handlers.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use axum::{ extract::{Path, State}, http::{Response, StatusCode}, @@ -15,7 +16,7 @@ use serde::{Deserialize, Serialize}; use tonic::body; use tracing::info; -use crate::{errors::ProcessError, state::FaucetState, COMPONENT}; +use crate::{errors::HandlerError, state::FaucetState, COMPONENT}; #[derive(Deserialize)] pub struct FaucetRequest { @@ -44,7 +45,7 @@ pub async fn get_metadata( pub async fn get_tokens( State(state): State, Json(req): Json, -) -> Result { +) -> Result { info!( target: COMPONENT, account_id = %req.account_id, @@ -55,14 +56,14 @@ pub async fn get_tokens( // Check that the amount is in the asset amount options if !state.config.asset_amount_options.contains(&req.asset_amount) { - return Err(ProcessError::BadRequest("Invalid asset amount".to_string())); + return Err(HandlerError::BadRequest("Invalid asset amount".to_string())); } let mut client = state.client.lock().await; // Receive and hex user account id let target_account_id = AccountId::from_hex(req.account_id.as_str()) - .map_err(|err| ProcessError::BadRequest(err.to_string()))?; + .map_err(|err| HandlerError::BadRequest(err.to_string()))?; // Execute transaction info!(target: COMPONENT, "Executing mint transaction for account."); @@ -72,16 +73,24 @@ pub async fn get_tokens( req.asset_amount, )?; + let mut faucet_account = client.data_store().faucet_account(); + faucet_account + .apply_delta(executed_tx.account_delta()) + .context("Failed to apply faucet account delta")?; + // Run transaction prover & send transaction to node info!(target: COMPONENT, "Proving and submitting transaction."); let block_height = client.prove_and_submit_transaction(executed_tx).await?; + // Update data store with the new faucet state + client.data_store().update_faucet_state(faucet_account).await?; + let note_id: NoteId = created_note.id(); let note_details = NoteDetails::new(created_note.assets().clone(), created_note.recipient().clone()); let note_tag = NoteTag::from_account_id(target_account_id, NoteExecutionMode::Local) - .expect("failed to build note tag for local execution"); + .context("failed to build note tag for local execution")?; // Serialize note into bytes let bytes = NoteFile::NoteDetails { @@ -100,24 +109,26 @@ pub async fn get_tokens( .header(header::CONTENT_DISPOSITION, "attachment; filename=note.mno") .header("Note-Id", note_id.to_string()) .body(body::boxed(Full::from(bytes))) - .map_err(|err| ProcessError::InternalServerError(err.to_string())) + .context("Failed to build response") + .map_err(Into::into) } -pub async fn get_index(state: State) -> Result { +pub async fn get_index(state: State) -> Result { get_static_file(state, Path("index.html".to_string())).await } pub async fn get_static_file( State(state): State, Path(path): Path, -) -> Result { +) -> Result { info!(target: COMPONENT, path, "Serving static file"); - let static_file = state.static_files.get(path.as_str()).ok_or(ProcessError::NotFound(path))?; + let static_file = state.static_files.get(path.as_str()).ok_or(HandlerError::NotFound(path))?; Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, static_file.mime_type) .body(body::boxed(Full::from(static_file.data))) - .map_err(|err| ProcessError::InternalServerError(err.to_string())) + .context("Failed to build response") + .map_err(Into::into) } diff --git a/bin/faucet/src/main.rs b/bin/faucet/src/main.rs index f58f9f18..a172440a 100644 --- a/bin/faucet/src/main.rs +++ b/bin/faucet/src/main.rs @@ -3,16 +3,27 @@ mod config; mod errors; mod handlers; mod state; +mod store; -use std::{fs::File, io::Write, path::PathBuf}; +use std::path::PathBuf; +use anyhow::Context; use axum::{ routing::{get, post}, Router, }; use clap::{Parser, Subcommand}; use http::HeaderValue; -use miden_node_utils::{config::load_config, version::LongVersion}; +use miden_lib::{accounts::faucets::create_basic_fungible_faucet, AuthScheme}; +use miden_node_utils::{config::load_config, crypto::get_rpo_random_coin, version::LongVersion}; +use miden_objects::{ + accounts::{AccountData, AccountStorageMode, AuthSecretKey}, + assets::TokenSymbol, + crypto::dsa::rpo_falcon512::SecretKey, + Felt, +}; +use rand::Rng; +use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; use state::FaucetState; use tokio::net::TcpListener; use tower::ServiceBuilder; @@ -20,10 +31,10 @@ use tower_http::{cors::CorsLayer, set_header::SetResponseHeaderLayer, trace::Tra use tracing::info; use crate::{ - config::FaucetConfig, - errors::InitError, + config::{FaucetConfig, DEFAULT_FAUCET_ACCOUNT_PATH}, handlers::{get_index, get_metadata, get_static_file, get_tokens}, }; + // CONSTANTS // ================================================================================================= @@ -48,10 +59,24 @@ pub enum Command { config: PathBuf, }, - /// Generates default configuration file for the faucet + /// Create a new public faucet account and save to the specified file + CreateFaucetAccount { + #[arg(short, long, value_name = "FILE", default_value = DEFAULT_FAUCET_ACCOUNT_PATH)] + output_path: PathBuf, + #[arg(short, long)] + token_symbol: String, + #[arg(short, long)] + decimals: u8, + #[arg(short, long)] + max_supply: u64, + }, + + /// Generate default configuration file for the faucet Init { #[arg(short, long, default_value = FAUCET_CONFIG_FILE_PATH)] config_path: String, + #[arg(short, long, default_value = DEFAULT_FAUCET_ACCOUNT_PATH)] + faucet_account_path: String, }, } @@ -59,16 +84,15 @@ pub enum Command { // ================================================================================================= #[tokio::main] -async fn main() -> Result<(), InitError> { - miden_node_utils::logging::setup_logging() - .map_err(|err| InitError::FaucetFailedToStart(err.to_string()))?; +async fn main() -> anyhow::Result<()> { + miden_node_utils::logging::setup_logging().context("Failed to initialize logging")?; let cli = Cli::parse(); match &cli.command { Command::Start { config } => { - let config: FaucetConfig = load_config(config) - .map_err(|err| InitError::ConfigurationError(err.to_string()))?; + let config: FaucetConfig = + load_config(config).context("Failed to load configuration file")?; let faucet_state = FaucetState::new(config.clone()).await?; @@ -96,31 +120,67 @@ async fn main() -> Result<(), InitError> { let listener = TcpListener::bind((config.endpoint.host.as_str(), config.endpoint.port)) .await - .map_err(|err| InitError::FaucetFailedToStart(err.to_string()))?; + .context("Failed to bind TCP listener")?; info!(target: COMPONENT, endpoint = %config.endpoint, "Server started"); axum::serve(listener, app).await.unwrap(); }, - Command::Init { config_path } => { - let current_dir = std::env::current_dir().map_err(|err| { - InitError::ConfigurationError(format!("failed to open current directory: {err}")) - })?; + + Command::CreateFaucetAccount { + output_path, + token_symbol, + decimals, + max_supply, + } => { + println!("Generating new faucet account. This may take a few minutes..."); + + let current_dir = + std::env::current_dir().context("Failed to open current directory")?; + + let mut rng = ChaCha20Rng::from_seed(rand::random()); + + let secret = SecretKey::with_rng(&mut get_rpo_random_coin(&mut rng)); + + let (account, account_seed) = create_basic_fungible_faucet( + rng.gen(), + TokenSymbol::try_from(token_symbol.as_str()) + .context("Failed to parse token symbol")?, + *decimals, + Felt::try_from(*max_supply) + .expect("max supply value is greater than or equal to the field modulus"), + AccountStorageMode::Public, + AuthScheme::RpoFalcon512 { pub_key: secret.public_key() }, + ) + .context("Failed to create basic fungible faucet account")?; + + let account_data = + AccountData::new(account, Some(account_seed), AuthSecretKey::RpoFalcon512(secret)); + + let output_path = current_dir.join(output_path); + account_data + .write(&output_path) + .context("Failed to write account data to file")?; + + println!("Faucet account file successfully created at: {output_path:?}"); + }, + + Command::Init { config_path, faucet_account_path } => { + let current_dir = + std::env::current_dir().context("Failed to open current directory")?; let config_file_path = current_dir.join(config_path); - let config = FaucetConfig::default(); - let config_as_toml_string = toml::to_string(&config).map_err(|err| { - InitError::ConfigurationError(format!("Failed to serialize default config: {err}")) - })?; - - let mut file_handle = - File::options().write(true).create_new(true).open(&config_file_path).map_err( - |err| InitError::ConfigurationError(format!("Error opening the file: {err}")), - )?; - - file_handle.write(config_as_toml_string.as_bytes()).map_err(|err| { - InitError::ConfigurationError(format!("Error writing to file: {err}")) - })?; + + let config = FaucetConfig { + faucet_account_path: faucet_account_path.into(), + ..FaucetConfig::default() + }; + + let config_as_toml_string = + toml::to_string(&config).context("Failed to serialize default config")?; + + std::fs::write(&config_file_path, config_as_toml_string) + .context("Error writing config to file")?; println!("Config file successfully created at: {config_file_path:?}"); }, diff --git a/bin/faucet/src/state.rs b/bin/faucet/src/state.rs index 898a5e7b..fb694081 100644 --- a/bin/faucet/src/state.rs +++ b/bin/faucet/src/state.rs @@ -5,9 +5,8 @@ use static_files::Resource; use tokio::sync::Mutex; use tracing::info; -use crate::{ - client::FaucetClient, config::FaucetConfig, errors::InitError, static_resources, COMPONENT, -}; +use crate::{client::FaucetClient, config::FaucetConfig, static_resources, COMPONENT}; + // FAUCET STATE // ================================================================================================ @@ -24,8 +23,8 @@ pub struct FaucetState { } impl FaucetState { - pub async fn new(config: FaucetConfig) -> Result { - let client = FaucetClient::new(config.clone()).await?; + pub async fn new(config: FaucetConfig) -> anyhow::Result { + let client = FaucetClient::new(&config).await?; let id = client.get_faucet_id(); let client = Arc::new(Mutex::new(client)); let static_files = Arc::new(static_resources::generate()); diff --git a/bin/faucet/src/store.rs b/bin/faucet/src/store.rs new file mode 100644 index 00000000..02050765 --- /dev/null +++ b/bin/faucet/src/store.rs @@ -0,0 +1,73 @@ +use std::sync::Mutex; + +use miden_objects::{ + accounts::{Account, AccountId}, + notes::NoteId, + transaction::{ChainMmr, InputNotes, TransactionInputs}, + BlockHeader, Word, +}; +use miden_tx::{DataStore, DataStoreError}; + +use crate::errors::HandlerError; + +pub struct FaucetDataStore { + faucet_account: Mutex, + /// Optional initial seed used for faucet account creation. + init_seed: Option, + block_header: BlockHeader, + chain_mmr: ChainMmr, +} + +// FAUCET DATA STORE +// ================================================================================================ + +impl FaucetDataStore { + pub fn new( + faucet_account: Account, + init_seed: Option, + block_header: BlockHeader, + chain_mmr: ChainMmr, + ) -> Self { + Self { + faucet_account: Mutex::new(faucet_account), + init_seed, + block_header, + chain_mmr, + } + } + + /// Returns the stored faucet account. + pub fn faucet_account(&self) -> Account { + self.faucet_account.lock().expect("Poisoned lock").clone() + } + + /// Updates the stored faucet account with the new one. + pub async fn update_faucet_state(&self, new_faucet_state: Account) -> Result<(), HandlerError> { + *self.faucet_account.lock().expect("Poisoned lock") = new_faucet_state; + + Ok(()) + } +} + +impl DataStore for FaucetDataStore { + fn get_transaction_inputs( + &self, + account_id: AccountId, + _block_ref: u32, + _notes: &[NoteId], + ) -> Result { + let account = self.faucet_account.lock().expect("Poisoned lock"); + if account_id != account.id() { + return Err(DataStoreError::AccountNotFound(account_id)); + } + + TransactionInputs::new( + account.clone(), + account.is_new().then_some(self.init_seed).flatten(), + self.block_header, + self.chain_mmr.clone(), + InputNotes::default(), + ) + .map_err(DataStoreError::InvalidTransactionInput) + } +} diff --git a/bin/node/Cargo.toml b/bin/node/Cargo.toml index d544f285..b855cbe6 100644 --- a/bin/node/Cargo.toml +++ b/bin/node/Cargo.toml @@ -26,6 +26,7 @@ miden-node-rpc = { workspace = true } miden-node-store = { workspace = true } miden-node-utils = { workspace = true } miden-objects = { workspace = true } +rand = { workspace = true } rand_chacha = "0.3" serde = { version = "1.0", features = ["derive"] } tokio = { workspace = true, features = ["rt-multi-thread", "net", "macros"] } diff --git a/bin/node/src/commands/genesis/inputs.rs b/bin/node/src/commands/genesis/inputs.rs index a400fc69..2f93d1d6 100644 --- a/bin/node/src/commands/genesis/inputs.rs +++ b/bin/node/src/commands/genesis/inputs.rs @@ -23,9 +23,7 @@ pub enum AccountInput { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct BasicFungibleFaucetInputs { - pub init_seed: String, pub auth_scheme: AuthSchemeInput, - pub auth_seed: String, pub token_symbol: String, pub decimals: u8, pub max_supply: u64, @@ -46,11 +44,7 @@ impl Default for GenesisInput { .expect("Current timestamp should be greater than unix epoch") .as_secs() as u32, accounts: Some(vec![AccountInput::BasicFungibleFaucet(BasicFungibleFaucetInputs { - init_seed: "0xc123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - .to_string(), auth_scheme: AuthSchemeInput::RpoFalcon512, - auth_seed: "0xd123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - .to_string(), token_symbol: "POL".to_string(), decimals: 12, max_supply: 1000000, diff --git a/bin/node/src/commands/genesis/mod.rs b/bin/node/src/commands/genesis/mod.rs index 3772b120..8e9d7f7f 100644 --- a/bin/node/src/commands/genesis/mod.rs +++ b/bin/node/src/commands/genesis/mod.rs @@ -7,17 +7,15 @@ use anyhow::{anyhow, bail, Context, Result}; pub use inputs::{AccountInput, AuthSchemeInput, GenesisInput}; use miden_lib::{accounts::faucets::create_basic_fungible_faucet, AuthScheme}; use miden_node_store::genesis::GenesisState; -use miden_node_utils::config::load_config; +use miden_node_utils::{config::load_config, crypto::get_rpo_random_coin}; use miden_objects::{ - accounts::{Account, AccountData, AccountStorageMode, AuthSecretKey}, + accounts::{Account, AccountData, AuthSecretKey}, assets::TokenSymbol, - crypto::{ - dsa::rpo_falcon512::SecretKey, - rand::RpoRandomCoin, - utils::{hex_to_bytes, Serializable}, - }, - Digest, Felt, ONE, + crypto::{dsa::rpo_falcon512::SecretKey, utils::Serializable}, + Felt, ONE, }; +use rand::Rng; +use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; use tracing::info; mod inputs; @@ -80,13 +78,9 @@ pub fn make_genesis(inputs_path: &PathBuf, output_path: &PathBuf, force: &bool) })?; info!("Genesis input file: {} has successfully been loaded.", inputs_path.display()); + let accounts_path = parent_path.join(DEFAULT_ACCOUNTS_DIR); let accounts = - create_accounts(&genesis_input.accounts.unwrap_or_default(), parent_path, force)?; - info!( - "Accounts have successfully been created at: {}/{}", - parent_path.display(), - DEFAULT_ACCOUNTS_DIR - ); + create_accounts(&genesis_input.accounts.unwrap_or_default(), &accounts_path, force)?; let genesis_state = GenesisState::new(accounts, genesis_input.version, genesis_input.timestamp); fs::write(output_path, genesis_state.to_bytes()).unwrap_or_else(|_| { @@ -102,13 +96,10 @@ pub fn make_genesis(inputs_path: &PathBuf, output_path: &PathBuf, force: &bool) /// This function also writes the account data files into the default accounts directory. fn create_accounts( accounts: &[AccountInput], - parent_path: &Path, + accounts_path: impl AsRef, force: &bool, ) -> Result> { - let mut accounts_path = PathBuf::from(&parent_path); - accounts_path.push(DEFAULT_ACCOUNTS_DIR); - - if accounts_path.try_exists()? { + if accounts_path.as_ref().try_exists()? { if !force { bail!( "Failed to create accounts directory because it already exists. \ @@ -121,21 +112,19 @@ fn create_accounts( fs::create_dir_all(&accounts_path).context("Failed to create accounts directory")?; let mut final_accounts = Vec::new(); + let mut faucet_count = 0; + let mut rng = ChaCha20Rng::from_seed(rand::random()); for account in accounts { // build offchain account data from account inputs - let mut account_data = match account { + let (mut account_data, name) = match account { AccountInput::BasicFungibleFaucet(inputs) => { info!("Creating fungible faucet account..."); - let init_seed = hex_to_bytes(&inputs.init_seed)?; - - let (auth_scheme, auth_secret_key) = - parse_auth_inputs(inputs.auth_scheme, &inputs.auth_seed)?; - - let storage_mode: AccountStorageMode = inputs.storage_mode.as_str().try_into()?; + let (auth_scheme, auth_secret_key) = gen_auth_keys(inputs.auth_scheme, &mut rng)?; + let storage_mode = inputs.storage_mode.as_str().try_into()?; let (account, account_seed) = create_basic_fungible_faucet( - init_seed, + rng.gen(), TokenSymbol::try_from(inputs.token_symbol.as_str())?, inputs.decimals, Felt::try_from(inputs.max_supply) @@ -144,23 +133,28 @@ fn create_accounts( auth_scheme, )?; - AccountData::new(account, Some(account_seed), auth_secret_key) + let name = format!( + "faucet{}", + (faucet_count > 0).then(|| faucet_count.to_string()).unwrap_or_default() + ); + faucet_count += 1; + + (AccountData::new(account, Some(account_seed), auth_secret_key), name) }, }; // write account data to file - let path = format!("{}/account{}.mac", accounts_path.display(), final_accounts.len()); - let path = Path::new(&path); + let path = accounts_path.as_ref().join(format!("{name}.mac")); - if let Ok(path_exists) = path.try_exists() { - if path_exists && !force { - bail!("Failed to generate account file {} because it already exists. Use the --force flag to overwrite.", path.display()); - } + if !force && matches!(path.try_exists(), Ok(true)) { + bail!("Failed to generate account file {} because it already exists. Use the --force flag to overwrite.", path.display()); } account_data.account.set_nonce(ONE)?; - account_data.write(path)?; + account_data.write(&path)?; + + info!("Account \"{name}\" has successfully been saved to: {}", path.display()); final_accounts.push(account_data.account); } @@ -168,21 +162,18 @@ fn create_accounts( Ok(final_accounts) } -fn parse_auth_inputs( +fn gen_auth_keys( auth_scheme_input: AuthSchemeInput, - auth_seed: &str, + rng: &mut ChaCha20Rng, ) -> Result<(AuthScheme, AuthSecretKey)> { match auth_scheme_input { AuthSchemeInput::RpoFalcon512 => { - let auth_seed: [u8; 32] = hex_to_bytes(auth_seed)?; - let rng_seed = Digest::try_from(&auth_seed)?.into(); - let mut rng = RpoRandomCoin::new(rng_seed); - let secret = SecretKey::with_rng(&mut rng); + let secret = SecretKey::with_rng(&mut get_rpo_random_coin(rng)); - let auth_scheme = AuthScheme::RpoFalcon512 { pub_key: secret.public_key() }; - let auth_secret_key = AuthSecretKey::RpoFalcon512(secret); - - Ok((auth_scheme, auth_secret_key)) + Ok(( + AuthScheme::RpoFalcon512 { pub_key: secret.public_key() }, + AuthSecretKey::RpoFalcon512(secret), + )) }, } } @@ -215,9 +206,7 @@ mod tests { [[accounts]] type = "BasicFungibleFaucet" - init_seed = "0xc123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" auth_scheme = "RpoFalcon512" - auth_seed = "0xd123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" token_symbol = "POL" decimals = 12 max_supply = 1000000 @@ -230,7 +219,7 @@ mod tests { // run make_genesis to generate genesis.dat and accounts folder and files make_genesis(&genesis_inputs_file_path, &genesis_dat_file_path, &true).unwrap(); - let a0_file_path = PathBuf::from("accounts/account0.mac"); + let a0_file_path = PathBuf::from("accounts/faucet.mac"); // assert that the genesis.dat and account files exist assert!(genesis_dat_file_path.exists()); diff --git a/bin/node/src/main.rs b/bin/node/src/main.rs index 1ce93c60..1839d748 100644 --- a/bin/node/src/main.rs +++ b/bin/node/src/main.rs @@ -127,11 +127,8 @@ async fn main() -> anyhow::Result<()> { let current_dir = std::env::current_dir() .map_err(|err| anyhow!("failed to open current directory: {err}"))?; - let mut config = current_dir.clone(); - let mut genesis = current_dir.clone(); - - config.push(config_path); - genesis.push(genesis_path); + let config = current_dir.join(config_path); + let genesis = current_dir.join(genesis_path); init_config_files(config, genesis) }, diff --git a/config/genesis.toml b/config/genesis.toml index 6a77d9a7..8c74a4dd 100644 --- a/config/genesis.toml +++ b/config/genesis.toml @@ -5,9 +5,7 @@ timestamp = 1672531200 [[accounts]] type = "BasicFungibleFaucet" storage_mode = "public" -init_seed = "0xc123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" auth_scheme = "RpoFalcon512" -auth_seed = "0xd123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" token_symbol = "POL" decimals = 12 max_supply = 1000000 diff --git a/config/miden-faucet.toml b/config/miden-faucet.toml index a9fdc142..8124e33d 100644 --- a/config/miden-faucet.toml +++ b/config/miden-faucet.toml @@ -1,9 +1,5 @@ endpoint = { host = "localhost", port = 8080 } node_url = "http://localhost:57291" timeout_ms = 10000 - -# Data used to construct the faucet account of the faucet asset_amount_options = [100, 500, 1000] -token_symbol = "POL" -decimals = 8 -max_supply = 1000000 +faucet_account_path = "accounts/faucet.mac" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index e3b4494d..75f19675 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -20,6 +20,7 @@ anyhow = { version = "1.0" } figment = { version = "0.10", features = ["toml", "env"] } itertools = { version = "0.12" } miden-objects = { workspace = true } +rand = { workspace = true } serde = { version = "1.0", features = ["derive"] } thiserror = { workspace = true } tonic = { workspace = true } diff --git a/crates/utils/src/crypto.rs b/crates/utils/src/crypto.rs new file mode 100644 index 00000000..f8a48aef --- /dev/null +++ b/crates/utils/src/crypto.rs @@ -0,0 +1,13 @@ +use miden_objects::{ + crypto::{hash::rpo::RpoDigest, rand::RpoRandomCoin}, + Felt, +}; +use rand::{Rng, RngCore}; + +/// Creates a new RPO Random Coin with random seed +pub fn get_rpo_random_coin(rng: &mut T) -> RpoRandomCoin { + let auth_seed: [u64; 4] = rng.gen(); + let rng_seed = RpoDigest::from(auth_seed.map(Felt::new)); + + RpoRandomCoin::new(rng_seed.into()) +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index b098bf2d..275ee7e3 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod crypto; pub mod errors; pub mod formatting; pub mod logging;