diff --git a/.gitignore b/.gitignore index 8cf6843..4364d09 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ fm_client_db .DS_Store fm_db fm_db_dir +fm_db_mnemonic result /vendor diff --git a/Cargo.lock b/Cargo.lock index 4bcb3b8..c926a28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -530,11 +530,13 @@ dependencies = [ [[package]] name = "bip39" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" +checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387" dependencies = [ - "bitcoin_hashes 0.11.0", + "bitcoin_hashes 0.13.0", + "rand", + "rand_core", "serde", "unicode-normalization", ] @@ -1288,6 +1290,18 @@ dependencies = [ "webpki-roots 0.26.6", ] +[[package]] +name = "fedimint-bip39" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0b2301eb60061096ab7f82bf1b8d5a98980cf27ea5fef07cbc47a1d58e2afa" +dependencies = [ + "bip39", + "fedimint-client", + "fedimint-core", + "rand", +] + [[package]] name = "fedimint-bitcoind" version = "0.4.2" @@ -1373,7 +1387,7 @@ dependencies = [ "lnurl-rs 0.5.0", "metrics", "metrics-exporter-prometheus", - "multimint 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "multimint 0.4.0", "reqwest 0.12.7", "serde", "serde_json", @@ -2690,7 +2704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3002,7 +3016,9 @@ name = "multimint" version = "0.4.0" dependencies = [ "anyhow", + "bip39", "fedimint-api-client", + "fedimint-bip39", "fedimint-client", "fedimint-core", "fedimint-ln-client", diff --git a/Cargo.toml b/Cargo.toml index da30e6f..5a9b2b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ fedimint-mint-client = "0.4.2" fedimint-ln-client = "0.4.2" fedimint-ln-common = "0.4.2" fedimint-rocksdb = "0.4.2" +fedimint-bip39 = "0.4.2" # Config for 'cargo dist' [workspace.metadata.dist] diff --git a/example.env b/example.env index 23cb907..77533cd 100644 --- a/example.env +++ b/example.env @@ -8,3 +8,4 @@ FEDIMINT_CLIENTD_DB_PATH='/absolute/path/to/fm_db_dir' FEDIMINT_CLIENTD_PASSWORD='password' FEDIMINT_CLIENTD_BASE_URL='http://127.0.0.1:3333' FEDIMINT_CLIENTD_ADDR='127.0.0.1:3333' +MULTIMINT_MNEMONIC_ENV='ivory put armed include entire report oblige mystery ivory reunion siren actor' diff --git a/fedimint-clientd/Cargo.toml b/fedimint-clientd/Cargo.toml index c7d126c..0472b75 100644 --- a/fedimint-clientd/Cargo.toml +++ b/fedimint-clientd/Cargo.toml @@ -35,8 +35,8 @@ time = { version = "0.3.25", features = ["formatting"] } chrono = "0.4.31" futures-util = "0.3.30" clap = { version = "3", features = ["derive", "env"] } -multimint = { version = "0.4.0" } -# multimint = { path = "../multimint" } +# multimint = { version = "0.4.0" } +multimint = { path = "../multimint" } hex = "0.4.3" futures = "0.3" diff --git a/fedimint-clientd/src/main.rs b/fedimint-clientd/src/main.rs index f405738..d30fa49 100644 --- a/fedimint-clientd/src/main.rs +++ b/fedimint-clientd/src/main.rs @@ -97,20 +97,9 @@ async fn main() -> Result<()> { let mut state = AppState::new(cli.db_path).await?; - let manual_secret = match cli.manual_secret { - Some(secret) => Some(secret), - None => match std::env::var("FEDIMINT_CLIENTD_MANUAL_SECRET") { - Ok(secret) => Some(secret), - Err(_) => None, - }, - }; - match InviteCode::from_str(&cli.invite_code) { Ok(invite_code) => { - let federation_id = state - .multimint - .register_new(invite_code, manual_secret) - .await?; + let federation_id = state.multimint.register_new(invite_code).await?; info!("Created client for federation id: {:?}", federation_id); } Err(e) => { diff --git a/fedimint-clientd/src/router/handlers/admin/join.rs b/fedimint-clientd/src/router/handlers/admin/join.rs index 6fe9bca..599b959 100644 --- a/fedimint-clientd/src/router/handlers/admin/join.rs +++ b/fedimint-clientd/src/router/handlers/admin/join.rs @@ -1,5 +1,3 @@ -use std::env; - use anyhow::{anyhow, Error}; use axum::extract::State; use axum::http::StatusCode; @@ -17,7 +15,6 @@ use crate::state::AppState; #[serde(rename_all = "camelCase")] pub struct JoinRequest { pub invite_code: InviteCode, - pub use_manual_secret: bool, } #[derive(Debug, Serialize)] @@ -28,22 +25,7 @@ pub struct JoinResponse { } async fn _join(mut multimint: MultiMint, req: JoinRequest) -> Result { - let manual_secret = if req.use_manual_secret { - match env::var("FEDIMINT_CLIENTD_MANUAL_SECRET") { - Ok(secret) => Some(secret), - Err(_) => { - return Err(anyhow!( - "FEDIMINT_CLIENTD_MANUAL_SECRET must be set to join with manual secret" - )) - } - } - } else { - None - }; - - let this_federation_id = multimint - .register_new(req.invite_code.clone(), manual_secret) - .await?; + let this_federation_id = multimint.register_new(req.invite_code.clone()).await?; let federation_ids = multimint.ids().await.into_iter().collect::>(); diff --git a/multimint/Cargo.toml b/multimint/Cargo.toml index 8030238..20ee517 100644 --- a/multimint/Cargo.toml +++ b/multimint/Cargo.toml @@ -25,7 +25,9 @@ fedimint-mint-client = { workspace = true } fedimint-ln-client = { workspace = true } fedimint-ln-common = { workspace = true } fedimint-rocksdb = { workspace = true } +fedimint-bip39 = { workspace = true } futures-util = "0.3.30" rand = "0.8.5" tracing = "0.1.40" hex = "0.4.3" +bip39 = "2.1.0" diff --git a/multimint/src/client.rs b/multimint/src/client.rs index b8c409c..f24af5d 100644 --- a/multimint/src/client.rs +++ b/multimint/src/client.rs @@ -3,32 +3,36 @@ use std::collections::BTreeMap; use std::fmt::Debug; -use std::path::PathBuf; use std::sync::Arc; use anyhow::Result; -use fedimint_client::secret::{PlainRootSecretStrategy, RootSecretStrategy}; -use fedimint_client::Client; +use bip39::Mnemonic; +use fedimint_bip39::Bip39RootSecretStrategy; +use fedimint_client::db::ClientConfigKey; +use fedimint_client::derivable_secret::{ChildId, DerivableSecret}; +use fedimint_client::module::init::ClientModuleInitRegistry; +use fedimint_client::secret::RootSecretStrategy; +use fedimint_client::{Client, ClientBuilder}; +use fedimint_core::config::FederationId; use fedimint_core::db::{ Committable, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped, }; +use fedimint_core::encoding::Encodable; use fedimint_ln_client::LightningClientInit; use fedimint_mint_client::MintClientInit; use fedimint_wallet_client::WalletClientInit; use futures_util::StreamExt; -use rand::thread_rng; -use tracing::info; use crate::db::{FederationConfig, FederationIdKey, FederationIdKeyPrefix}; #[derive(Debug, Clone)] pub struct LocalClientBuilder { - work_dir: PathBuf, + mnemonic: Mnemonic, } impl LocalClientBuilder { - pub fn new(work_dir: PathBuf) -> Self { - Self { work_dir } + pub fn new(mnemonic: Mnemonic) -> Self { + Self { mnemonic } } } @@ -37,48 +41,23 @@ impl LocalClientBuilder { #[allow(clippy::too_many_arguments)] pub async fn build( &self, + db: &Database, config: FederationConfig, - manual_secret: Option<[u8; 64]>, ) -> Result { let federation_id = config.invite_code.federation_id(); + let db = db.with_prefix(federation_id.consensus_encode_to_vec()); + let secret = self.derive_federation_secret(&federation_id); + Self::verify_client_config(&db, federation_id).await?; - let db_path = self.work_dir.join(format!("{federation_id}.db")); + let client_builder = self.create_client_builder(db.clone()).await?; - let db = Database::new( - fedimint_rocksdb::RocksDb::open(db_path.clone())?, - Default::default(), - ); - - let mut client_builder = Client::builder(db.clone()).await?; - client_builder.with_module(WalletClientInit(None)); - client_builder.with_module(MintClientInit); - client_builder.with_module(LightningClientInit::default()); - client_builder.with_primary_module(1); - - let client_secret = match Client::load_decodable_client_secret::<[u8; 64]>(&db).await { - Ok(secret) => secret, - Err(_) => { - if let Some(manual_secret) = manual_secret { - info!("Using manual secret provided by user and writing to client storage"); - Client::store_encodable_client_secret(&db, manual_secret).await?; - manual_secret - } else { - info!("Generating new secret and writing to client storage"); - let secret = PlainRootSecretStrategy::random(&mut thread_rng()); - Client::store_encodable_client_secret(&db, secret).await?; - secret - } - } - }; - - let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret); let client_res = if Client::is_initialized(&db).await { - client_builder.open(root_secret).await + client_builder.open(secret).await } else { let client_config = fedimint_api_client::download_from_invite_code(&config.invite_code).await?; client_builder - .join(root_secret, client_config.to_owned(), None) + .join(secret, client_config.to_owned(), None) .await }?; @@ -107,4 +86,37 @@ impl LocalClientBuilder { .cloned() .collect::>() } + + pub fn derive_federation_secret(&self, federation_id: &FederationId) -> DerivableSecret { + let global_root_secret = Bip39RootSecretStrategy::<12>::to_root_secret(&self.mnemonic); + let multi_federation_root_secret = global_root_secret.child_key(ChildId(0)); + let federation_root_secret = multi_federation_root_secret.federation_key(federation_id); + let federation_wallet_root_secret = federation_root_secret.child_key(ChildId(0)); + federation_wallet_root_secret.child_key(ChildId(0)) + } + + /// Verifies that the saved `ClientConfig` contains the expected + /// federation's config. + async fn verify_client_config(db: &Database, federation_id: FederationId) -> Result<()> { + let mut dbtx = db.begin_transaction_nc().await; + if let Some(config) = dbtx.get_value(&ClientConfigKey).await { + if config.calculate_federation_id() != federation_id { + anyhow::bail!("Federation Id did not match saved federation ID") + } + } + Ok(()) + } + + /// Constructs the client builder with the modules, database, and connector + /// used to create clients for connected federations. + async fn create_client_builder(&self, db: Database) -> Result { + let mut registry = ClientModuleInitRegistry::new(); + registry.attach(WalletClientInit::default()); + registry.attach(MintClientInit); + registry.attach(LightningClientInit::default()); + let mut client_builder = Client::builder(db).await?; + client_builder.with_module_inits(registry); + client_builder.with_primary_module(1); + Ok(client_builder) + } } diff --git a/multimint/src/lib.rs b/multimint/src/lib.rs index ba1fbb8..e3899ef 100644 --- a/multimint/src/lib.rs +++ b/multimint/src/lib.rs @@ -67,7 +67,10 @@ use std::str::FromStr; use std::sync::Arc; use anyhow::Result; -use fedimint_client::ClientHandleArc; +use bip39::Mnemonic; +use fedimint_bip39::Bip39RootSecretStrategy; +use fedimint_client::secret::RootSecretStrategy; +use fedimint_client::{Client, ClientHandleArc}; use fedimint_core::config::{FederationId, FederationIdPrefix, JsonClientConfig}; use fedimint_core::db::Database; use fedimint_core::invite_code::InviteCode; @@ -75,8 +78,9 @@ use fedimint_core::Amount; use fedimint_ln_client::LightningClientModule; use fedimint_mint_client::MintClientModule; use fedimint_wallet_client::WalletClientModule; +use rand::thread_rng; use tokio::sync::Mutex; -use tracing::warn; +use tracing::{info, warn}; use types::InfoResponse; // Reexport all the fedimint crates for ease of use pub use { @@ -144,8 +148,9 @@ impl MultiMint { fedimint_rocksdb::RocksDb::open(work_dir.join("multimint.db"))?, Default::default(), ); + let mnemonic = load_or_generate_mnemonic(&db).await?; - let client_builder = LocalClientBuilder::new(work_dir); + let client_builder = LocalClientBuilder::new(mnemonic); let clients = Arc::new(Mutex::new(BTreeMap::new())); @@ -172,7 +177,7 @@ impl MultiMint { for config in configs { let federation_id = config.invite_code.federation_id(); - if let Ok(client) = client_builder.build(config.clone(), None).await { + if let Ok(client) = client_builder.build(&db, config.clone()).await { clients.insert(federation_id, client); } else { warn!("Failed to load client for federation: {federation_id}"); @@ -189,22 +194,7 @@ impl MultiMint { /// You can provide a manual secret to use for the client's keypair. If you /// don't provide a secret, a 64 byte random secret will be generated, which /// you can extract from the client if needed. - pub async fn register_new( - &mut self, - invite_code: InviteCode, - manual_secret: Option, - ) -> Result { - let manual_secret: Option<[u8; 64]> = match manual_secret { - Some(manual_secret) => { - let bytes = hex::decode(manual_secret)?; - Some( - bytes - .try_into() - .map_err(|_| anyhow::anyhow!("Manual secret must be 64 bytes long"))?, - ) - } - None => None, - }; + pub async fn register_new(&mut self, invite_code: InviteCode) -> Result { let federation_id = invite_code.federation_id(); if self .clients @@ -224,7 +214,7 @@ impl MultiMint { let client = self .client_builder - .build(client_cfg.clone(), manual_secret) + .build(&self.db, client_cfg.clone()) .await?; self.clients.lock().await.insert(federation_id, client); @@ -385,3 +375,22 @@ impl MultiMint { Ok(()) } } + +async fn load_or_generate_mnemonic(db: &Database) -> Result { + Ok( + if let Ok(entropy) = Client::load_decodable_client_secret::>(db).await { + Mnemonic::from_entropy(&entropy)? + } else { + let mnemonic = if let Ok(words) = std::env::var("MULTIMINT_MNEMONIC_ENV") { + info!("Using provided mnemonic from environment variable"); + Mnemonic::parse_in_normalized(bip39::Language::English, words.as_str())? + } else { + info!("Generating mnemonic and writing entropy to client storage"); + Bip39RootSecretStrategy::<12>::random(&mut thread_rng()) + }; + + Client::store_encodable_client_secret(&db, mnemonic.to_entropy()).await?; + mnemonic + }, + ) +}