diff --git a/mutiny-core/src/fedimint.rs b/mutiny-core/src/fedimint.rs new file mode 100644 index 000000000..b803921e0 --- /dev/null +++ b/mutiny-core/src/fedimint.rs @@ -0,0 +1,139 @@ +use crate::{error::MutinyError, logging::MutinyLogger, storage::MutinyStorage}; +use bitcoin::{secp256k1::Secp256k1, util::bip32::ExtendedPrivKey}; +use bitcoin::{ + util::bip32::{ChildNumber, DerivationPath}, + Network, +}; +use fedimint_client::{derivable_secret::DerivableSecret, ClientArc, FederationInfo}; +use fedimint_core::db::mem_impl::MemDatabase; +use fedimint_core::{api::InviteCode, config::FederationId}; +use fedimint_ln_client::LightningClientInit; +use fedimint_mint_client::MintClientInit; +use fedimint_wallet_client::WalletClientInit; +use lightning::log_info; +use lightning::util::logger::Logger; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + str::FromStr, + sync::{atomic::AtomicBool, Arc, RwLock}, +}; + +const FEDIMINT_CLIENT_NONCE: &[u8] = b"Fedimint Client Salt"; + +// This is the FedimintStorage object saved to the DB +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct FedimintStorage { + pub fedimints: HashMap, + #[serde(default)] + pub version: u32, +} + +// This is the FedimintIdentity that refer to a specific node +// Used for public facing identification. +pub struct FedimintIdentity { + pub uuid: String, + pub federation_id: FederationId, +} + +// This is the FedimintIndex reference that is saved to the DB +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] +pub struct FedimintIndex { + pub child_index: u32, + pub federation_code: String, + pub archived: Option, +} + +impl FedimintIndex { + pub fn is_archived(&self) -> bool { + self.archived.unwrap_or(false) + } +} + +pub(crate) struct FedimintClient { + pub _uuid: String, + pub child_index: u32, + pub federation_code: String, + pub fedimint_client: ClientArc, + stopped_components: Arc>>, + storage: S, + network: Network, + pub(crate) logger: Arc, + stop: Arc, +} + +impl FedimintClient { + #[allow(clippy::too_many_arguments)] + pub(crate) async fn new( + uuid: String, + fedimint_index: &FedimintIndex, + federation_code: String, + xprivkey: ExtendedPrivKey, + storage: S, + network: Network, + logger: Arc, + ) -> Result { + log_info!(logger, "initializing a new fedimint client: {uuid}"); + + // a list of components that need to be stopped and whether or not they are stopped + let stopped_components = Arc::new(RwLock::new(vec![])); + + let stop = Arc::new(AtomicBool::new(false)); + + log_info!(logger, "Joining federation {}", federation_code); + + let invite_code = InviteCode::from_str(&federation_code) + .map_err(|_| MutinyError::InvalidArgumentsError)?; + + let mut client_builder = fedimint_client::Client::builder(); + client_builder.with_module(WalletClientInit(None)); + client_builder.with_module(MintClientInit); + client_builder.with_module(LightningClientInit); + client_builder.with_database(MemDatabase::new().into()); // TODO not in memory + client_builder.with_primary_module(1); + client_builder.with_federation_info(FederationInfo::from_invite_code(invite_code).await?); + + let secret = create_fedimint_secret(xprivkey, fedimint_index)?; + + let fedimint_client = client_builder.build(secret).await?; + + Ok(FedimintClient { + _uuid: uuid, + child_index: fedimint_index.child_index, + federation_code, + fedimint_client, + stopped_components, + storage, + network, + logger, + stop, + }) + } + + pub fn fedimint_index(&self) -> FedimintIndex { + FedimintIndex { + child_index: self.child_index, + federation_code: self.federation_code.clone(), + archived: Some(false), + } + } +} + +// A fedimint private key will be derived from `m/1'/X'`, where X is the index of a specific fedimint. +// Fedimint will derive further keys from there. +fn create_fedimint_secret( + xprivkey: ExtendedPrivKey, + fedimint_index: &FedimintIndex, +) -> Result { + let context = Secp256k1::new(); + let xpriv = xprivkey.derive_priv( + &context, + &DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(1)?, + ChildNumber::from_hardened_idx(fedimint_index.child_index)?, + ]), + )?; + let secret = + DerivableSecret::new_root(&xpriv.private_key.secret_bytes(), FEDIMINT_CLIENT_NONCE); + Ok(secret) +} diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index e83d763c5..92e3d43ec 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -18,6 +18,7 @@ pub mod encrypt; pub mod error; pub mod esplora; mod event; +pub mod fedimint; mod fees; mod gossip; mod keymanager; diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 118152565..970d78ebe 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -1,6 +1,3 @@ -use crate::lnurlauth::AuthManager; -use crate::logging::LOGGING_KEY; -use crate::multiesplora::MultiEsploraClient; use crate::redshift::{RedshiftManager, RedshiftStatus, RedshiftStorage}; use crate::storage::{MutinyStorage, DEVICE_ID_KEY, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY}; use crate::utils::{sleep, spawn}; @@ -23,6 +20,12 @@ use crate::{ event::{HTLCStatus, PaymentInfo}, lnurlauth::make_lnurl_auth_connection, }; +use crate::{fedimint::FedimintIdentity, multiesplora::MultiEsploraClient}; +use crate::{fedimint::FedimintStorage, lnurlauth::AuthManager}; +use crate::{ + fedimint::{FedimintClient, FedimintIndex}, + logging::LOGGING_KEY, +}; use crate::{gossip::*, scorer::HubPreferentialScorer}; use crate::{labels::LabelStorage, subscription::MutinySubscriptionClient}; use anyhow::anyhow; @@ -36,6 +39,7 @@ use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{Address, Network, OutPoint, Transaction, Txid}; use core::time::Duration; use esplora_client::Builder; +use fedimint_core::config::FederationId; use futures::{future::join_all, lock::Mutex}; use lightning::chain::Confirm; use lightning::events::ClosureReason; @@ -505,6 +509,8 @@ pub struct NodeManager { pub(crate) storage: S, pub(crate) node_storage: Mutex, pub(crate) nodes: Arc>>>>, + pub(crate) fedimint_storage: Mutex, + pub(crate) fedimints: Arc>>>>, auth: AuthManager, lnurl_client: Arc, pub(crate) lsp_clients: Vec, @@ -708,6 +714,35 @@ impl NodeManager { Arc::new(Mutex::new(nodes_map)) }; + let fedimint_storage = storage.get_fedimints()?; + + let unarchived_fedimints = fedimint_storage + .clone() + .fedimints + .into_iter() + .filter(|(_, n)| !n.is_archived()); + + let mut fedimint_map = HashMap::new(); + + for fedimint_item in unarchived_fedimints { + let fedimint = FedimintClient::new( + fedimint_item.0, + &fedimint_item.1, + fedimint_item.1.federation_code.clone(), + c.xprivkey, + storage.clone(), + c.network, + logger.clone(), + ) + .await?; + + let id = fedimint.fedimint_client.federation_id(); + + fedimint_map.insert(id, Arc::new(fedimint)); + } + + let fedimints = Arc::new(Mutex::new(fedimint_map)); + let lnurl_client = Arc::new( lnurl::Builder::default() .build_async() @@ -749,6 +784,8 @@ impl NodeManager { storage, node_storage: Mutex::new(node_storage), nodes, + fedimint_storage: Mutex::new(fedimint_storage), + fedimints, #[cfg(target_arch = "wasm32")] websocket_proxy_addr, user_rgs_url: c.user_rgs_url, @@ -2200,6 +2237,54 @@ impl NodeManager { Ok(storage_peers) } + /// Lists the federation id's of the fedimint clients in the manager. + pub async fn list_federations(&self) -> Result, MutinyError> { + let fedimints = self.fedimints.lock().await; + let federation_ids = fedimints + .iter() + .map(|(_, n)| n.fedimint_client.federation_id()) + .collect(); + Ok(federation_ids) + } + + /// Add a federation based on it's federation code + pub async fn new_federation( + &self, + federation_code: String, + ) -> Result { + create_new_fedimint_from_node_manager(self, federation_code).await + } + + /// Removes a federation by setting its archived status to true, based on the FederationId. + pub async fn remove_federation(&self, federation_id: FederationId) -> Result<(), MutinyError> { + // Lock the fedimints to find the federation client + let mut fedimints_guard = self.fedimints.lock().await; + + if let Some(fedimint_client) = fedimints_guard.get(&federation_id) { + let uuid = &fedimint_client._uuid; + + // Lock the fedimint storage to safely modify the federation storage + let mut fedimint_storage_guard = self.fedimint_storage.lock().await; + + if let Some(fedimint) = fedimint_storage_guard.fedimints.get_mut(uuid) { + fedimint.archived = Some(true); + + // Save the updated storage + self.storage + .insert_fedimints(fedimint_storage_guard.clone())?; + } else { + return Err(MutinyError::NotFound); + } + + // Remove the federation from the fedimints hashmap + fedimints_guard.remove(&federation_id); + } else { + return Err(MutinyError::NotFound); + } + + Ok(()) + } + /// Checks whether or not the user is subscribed to Mutiny+. /// /// Returns None if there's no subscription at all. @@ -2531,6 +2616,80 @@ pub(crate) async fn create_new_node_from_node_manager( }) } +// This will create a new federation with a node manager and return the PublicKey of the node created. +pub(crate) async fn create_new_fedimint_from_node_manager( + node_manager: &NodeManager, + federation_code: String, +) -> Result { + // Begin with a mutex lock so that nothing else can + // save or alter the node list while it is about to + // be saved. + let mut fedimint_mutex = node_manager.fedimint_storage.lock().await; + + // Get the current fedimints and their bip32 indices + // so that we can create another federation with the next. + // Always get it from our storage, the fedimint_mutex is + // mostly for read only and locking. + let mut existing_fedimints = node_manager.storage.get_fedimints()?; + let next_fedimint_index = match existing_fedimints + .fedimints + .iter() + .max_by_key(|(_, v)| v.child_index) + { + None => 0, + Some((_, v)) => v.child_index + 1, + }; + + // Create and save a new fedimint using the next child index + let next_fedimint_uuid = Uuid::new_v4().to_string(); + + let next_fedimint = FedimintIndex { + child_index: next_fedimint_index, + federation_code: federation_code.clone(), + archived: Some(false), + }; + + existing_fedimints.version += 1; + existing_fedimints + .fedimints + .insert(next_fedimint_uuid.clone(), next_fedimint.clone()); + + node_manager + .storage + .insert_fedimints(existing_fedimints.clone())?; + fedimint_mutex.fedimints = existing_fedimints.fedimints.clone(); + + // now create the node process and init it + let new_fedimint_res = FedimintClient::new( + next_fedimint_uuid.clone(), + &next_fedimint, + federation_code, + node_manager.xprivkey, + node_manager.storage.clone(), + node_manager.network, + node_manager.logger.clone(), + ) + .await; + + let new_fedimint = match new_fedimint_res { + Ok(new_fedimint) => new_fedimint, + Err(e) => return Err(e), + }; + + let federation_id = new_fedimint.fedimint_client.federation_id(); + node_manager + .fedimints + .clone() + .lock() + .await + .insert(federation_id, Arc::new(new_fedimint)); + + Ok(FedimintIdentity { + uuid: next_fedimint_uuid.clone(), + federation_id, + }) +} + #[cfg(test)] mod tests { use crate::{ diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index a39810451..4e30ac8d2 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -1,9 +1,12 @@ -use crate::encrypt::{decrypt_with_password, encrypt, encryption_key_from_pass, Cipher}; use crate::error::{MutinyError, MutinyStorageError}; use crate::ldkstorage::CHANNEL_MANAGER_KEY; use crate::nodemanager::{NodeStorage, DEVICE_LOCK_INTERVAL_SECS}; use crate::utils::{now, spawn}; use crate::vss::{MutinyVssClient, VssKeyValueItem}; +use crate::{ + encrypt::{decrypt_with_password, encrypt, encryption_key_from_pass, Cipher}, + fedimint::FedimintStorage, +}; use bdk::chain::{Append, PersistBackend}; use bip39::Mnemonic; use lightning::log_error; @@ -18,6 +21,7 @@ pub const KEYCHAIN_STORE_KEY: &str = "bdk_keychain"; pub const MNEMONIC_KEY: &str = "mnemonic"; pub(crate) const NEED_FULL_SYNC_KEY: &str = "needs_full_sync"; pub const NODES_KEY: &str = "nodes"; +pub const FEDIMINTS_KEY: &str = "fedimints"; const FEE_ESTIMATES_KEY: &str = "fee_estimates"; pub const BITCOIN_PRICE_CACHE_KEY: &str = "bitcoin_price_cache"; const FIRST_SYNC_KEY: &str = "first_sync"; @@ -345,6 +349,21 @@ pub trait MutinyStorage: Clone + Sized + 'static { self.set_data(NODES_KEY, nodes, version) } + /// Gets the fedimint indexes from storage + fn get_fedimints(&self) -> Result { + let res: Option = self.get_data(FEDIMINTS_KEY)?; + match res { + Some(f) => Ok(f), + None => Ok(FedimintStorage::default()), + } + } + + /// Inserts the fedimint indexes into storage + fn insert_fedimints(&self, fedimints: FedimintStorage) -> Result<(), MutinyError> { + let version = Some(fedimints.version); + self.set_data(FEDIMINTS_KEY, fedimints, version) + } + /// Get the current fee estimates from storage /// The key is block target, the value is the fee in satoshis per byte fn get_fee_estimates(&self) -> Result>, MutinyError> {