Skip to content

Commit

Permalink
Add, remove, and list federations
Browse files Browse the repository at this point in the history
  • Loading branch information
TonyGiorgio committed Nov 24, 2023
1 parent b6cfcaa commit d65918f
Show file tree
Hide file tree
Showing 3 changed files with 349 additions and 2 deletions.
139 changes: 139 additions & 0 deletions mutiny-core/src/fedimint.rs
Original file line number Diff line number Diff line change
@@ -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<String, FedimintIndex>,
#[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: bool,
}

impl FedimintIndex {
pub fn is_archived(&self) -> bool {
self.archived
}
}

pub(crate) struct FedimintClient<S: MutinyStorage> {
pub _uuid: String,
pub fedimint_index: FedimintIndex,
pub federation_code: String,
pub fedimint_client: ClientArc,
stopped_components: Arc<RwLock<Vec<bool>>>,
storage: S,
network: Network,
pub(crate) logger: Arc<MutinyLogger>,
stop: Arc<AtomicBool>,
}

impl<S: MutinyStorage> FedimintClient<S> {
#[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<MutinyLogger>,
) -> Result<Self, MutinyError> {
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,
fedimint_index: fedimint_index.clone(),
federation_code,
fedimint_client,
stopped_components,
storage,
network,
logger,
stop,
})
}

pub fn fedimint_index(&self) -> FedimintIndex {
FedimintIndex {
child_index: self.fedimint_index.child_index,
federation_code: self.federation_code.clone(),
archived: self.fedimint_index.archived,
}
}
}

// 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<DerivableSecret, MutinyError> {
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)
}
191 changes: 190 additions & 1 deletion mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod encrypt;
pub mod error;
pub mod esplora;
mod event;
pub mod fedimint;
mod fees;
mod gossip;
mod keymanager;
Expand Down Expand Up @@ -48,14 +49,20 @@ pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY
pub use crate::keymanager::generate_seed;
pub use crate::ldkstorage::{CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY};

use crate::labels::{get_contact_key, Contact, LabelStorage};
use crate::fedimint::FedimintClient;
use crate::fedimint::FedimintIdentity;
use crate::nostr::nwc::{
BudgetPeriod, BudgetedSpendingConditions, NwcProfileTag, SpendingConditions,
};
use crate::nostr::MUTINY_PLUS_SUBSCRIPTION_LABEL;
use crate::storage::{MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY};
use crate::{auth::MutinyAuthClient, logging::MutinyLogger};
use crate::{error::MutinyError, nostr::ReservedProfile};
use crate::{
fedimint::FedimintIndex,
fedimint::FedimintStorage,
labels::{get_contact_key, Contact, LabelStorage},
};
use crate::{nodemanager::NodeManager, nostr::ProfileType};
use crate::{nostr::NostrManager, utils::sleep};
use ::nostr::key::XOnlyPublicKey;
Expand All @@ -64,7 +71,9 @@ use bip39::Mnemonic;
use bitcoin::secp256k1::PublicKey;
use bitcoin::util::bip32::ExtendedPrivKey;
use bitcoin::Network;
use fedimint_core::config::FederationId;
use futures::{pin_mut, select, FutureExt};
use futures_util::lock::Mutex;
use lightning::{log_debug, util::logger::Logger};
use lightning::{log_error, log_info, log_warn};
use lightning_invoice::Bolt11Invoice;
Expand All @@ -73,6 +82,7 @@ use serde_json::{json, Value};
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::{collections::HashMap, sync::atomic::AtomicBool};
use uuid::Uuid;

#[derive(Clone)]
pub struct MutinyWalletConfig {
Expand Down Expand Up @@ -145,6 +155,8 @@ pub struct MutinyWallet<S: MutinyStorage> {
pub storage: S,
pub node_manager: Arc<NodeManager<S>>,
pub nostr: Arc<NostrManager<S>>,
pub fedimint_storage: Arc<Mutex<FedimintStorage>>,
pub fedimints: Arc<Mutex<HashMap<FederationId, Arc<FedimintClient<S>>>>>,
pub stop: Arc<AtomicBool>,
pub logger: Arc<MutinyLogger>,
}
Expand Down Expand Up @@ -190,6 +202,12 @@ impl<S: MutinyStorage> MutinyWallet<S> {
node_manager.logger.clone(),
)?);

// create fedimint library
let (fedimint_storage, fedimints) =
create_fedimint(&storage, &config, &logger).await.unwrap();
let fedimint_storage = Arc::new(Mutex::new(fedimint_storage));
let fedimints = fedimints;

if !config.skip_hodl_invoices {
log_warn!(
node_manager.logger,
Expand All @@ -202,6 +220,8 @@ impl<S: MutinyStorage> MutinyWallet<S> {
storage,
node_manager,
nostr,
fedimint_storage,
fedimints,
stop,
logger,
};
Expand Down Expand Up @@ -654,6 +674,175 @@ impl<S: MutinyStorage> MutinyWallet<S> {
storage.set_data(DEVICE_ID_KEY, device_id, None)?;
Ok(())
}

pub async fn new_federation(
&self,
federation_code: String,
) -> Result<FedimintIdentity, MutinyError> {
create_new_fedimint(
self.config.xprivkey.clone(),
self.storage.clone(),
self.config.network,
self.logger.clone(),
self.fedimint_storage.clone(),
self.fedimints.clone(),
federation_code,
)
.await
}

/// Lists the federation id's of the fedimint clients in the manager.
pub async fn list_federations(&self) -> Result<Vec<FederationId>, MutinyError> {
let fedimints = self.fedimints.lock().await;
let federation_ids = fedimints
.iter()
.map(|(_, n)| n.fedimint_client.federation_id())
.collect();
Ok(federation_ids)
}

/// 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 = 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(())
}
}

async fn create_fedimint<S: MutinyStorage>(
storage: &S,
c: &MutinyWalletConfig,
logger: &Arc<MutinyLogger>,
) -> Result<
(
FedimintStorage,
Arc<Mutex<HashMap<FederationId, Arc<FedimintClient<S>>>>>,
),
MutinyError,
> {
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));
Ok((fedimint_storage, fedimints))
}

// This will create a new federation and returns the Federation ID of the client created.
pub(crate) async fn create_new_fedimint<S: MutinyStorage>(
xprivkey: ExtendedPrivKey,
storage: S,
network: Network,
logger: Arc<MutinyLogger>,
fedimint_storage: Arc<Mutex<FedimintStorage>>,
fedimints: Arc<Mutex<HashMap<FederationId, Arc<FedimintClient<S>>>>>,
federation_code: String,
) -> Result<FedimintIdentity, MutinyError> {
// 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 = 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 = 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: false,
};

existing_fedimints.version += 1;
existing_fedimints
.fedimints
.insert(next_fedimint_uuid.clone(), next_fedimint.clone());

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,
xprivkey,
storage.clone(),
network,
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();
fedimints
.clone()
.lock()
.await
.insert(federation_id, Arc::new(new_fedimint));

Ok(FedimintIdentity {
uuid: next_fedimint_uuid.clone(),
federation_id,
})
}

#[cfg(test)]
Expand Down

0 comments on commit d65918f

Please sign in to comment.