fedimint_gateway_server/
client.rs

1use std::collections::BTreeSet;
2use std::fmt::Debug;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use fedimint_bip39::{Bip39RootSecretStrategy, Mnemonic};
7use fedimint_client::db::ClientConfigKey;
8use fedimint_client::module_init::ClientModuleInitRegistry;
9use fedimint_client::{Client, ClientBuilder};
10use fedimint_client_module::secret::{PlainRootSecretStrategy, RootSecretStrategy};
11use fedimint_core::config::FederationId;
12use fedimint_core::core::ModuleKind;
13use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
14use fedimint_core::module::registry::ModuleDecoderRegistry;
15use fedimint_derive_secret::{ChildId, DerivableSecret};
16use fedimint_gateway_common::FederationConfig;
17use fedimint_gateway_server_db::GatewayDbExt as _;
18use fedimint_gw_client::GatewayClientInit;
19use fedimint_gwv2_client::GatewayClientInitV2;
20
21use crate::error::AdminGatewayError;
22use crate::{AdminResult, Gateway};
23
24#[derive(Debug, Clone)]
25pub struct GatewayClientBuilder {
26    work_dir: PathBuf,
27    registry: ClientModuleInitRegistry,
28    primary_module_kind: ModuleKind,
29}
30
31impl GatewayClientBuilder {
32    pub fn new(
33        work_dir: PathBuf,
34        registry: ClientModuleInitRegistry,
35        primary_module_kind: ModuleKind,
36    ) -> Self {
37        Self {
38            work_dir,
39            registry,
40            primary_module_kind,
41        }
42    }
43
44    pub fn data_dir(&self) -> PathBuf {
45        self.work_dir.clone()
46    }
47
48    /// Reads a plain root secret from a database to construct a database.
49    /// Only used for "legacy" federations before v0.5.0
50    async fn client_plainrootsecret(&self, db: &Database) -> AdminResult<DerivableSecret> {
51        let client_secret = Client::load_decodable_client_secret::<[u8; 64]>(db)
52            .await
53            .map_err(AdminGatewayError::ClientCreationError)?;
54        Ok(PlainRootSecretStrategy::to_root_secret(&client_secret))
55    }
56
57    /// Constructs the client builder with the modules, database, and connector
58    /// used to create clients for connected federations.
59    async fn create_client_builder(
60        &self,
61        db: Database,
62        federation_config: &FederationConfig,
63        gateway: Arc<Gateway>,
64    ) -> AdminResult<ClientBuilder> {
65        let FederationConfig {
66            federation_index,
67            connector,
68            ..
69        } = federation_config.to_owned();
70
71        let mut registry = self.registry.clone();
72
73        if gateway.is_running_lnv1() {
74            registry.attach(GatewayClientInit {
75                federation_index,
76                lightning_manager: gateway.clone(),
77            });
78        }
79
80        if gateway.is_running_lnv2() {
81            registry.attach(GatewayClientInitV2 {
82                gateway: gateway.clone(),
83            });
84        }
85
86        let mut client_builder = Client::builder(db)
87            .await
88            .map_err(AdminGatewayError::ClientCreationError)?;
89        client_builder.with_module_inits(registry);
90        client_builder.with_primary_module_kind(self.primary_module_kind.clone());
91        client_builder.with_connector(connector);
92        Ok(client_builder)
93    }
94
95    /// Recovers a client with the provided mnemonic. This function will wait
96    /// for the recoveries to finish, but a new client must be created
97    /// afterwards and waited on until the state machines have finished
98    /// for a balance to be present.
99    pub async fn recover(
100        &self,
101        config: FederationConfig,
102        gateway: Arc<Gateway>,
103        mnemonic: &Mnemonic,
104    ) -> AdminResult<()> {
105        let client_config = config
106            .connector
107            .download_from_invite_code(&config.invite_code)
108            .await
109            .map_err(AdminGatewayError::ClientCreationError)?;
110        let federation_id = config.invite_code.federation_id();
111        let db = gateway.gateway_db.get_client_database(&federation_id);
112        let client_builder = self
113            .create_client_builder(db, &config, gateway.clone())
114            .await?;
115        let secret = Self::derive_federation_secret(mnemonic, &federation_id);
116        let backup = client_builder
117            .download_backup_from_federation(
118                &secret,
119                &client_config,
120                config.invite_code.api_secret(),
121            )
122            .await
123            .map_err(AdminGatewayError::ClientCreationError)?;
124        let client = client_builder
125            .recover(
126                secret.clone(),
127                client_config,
128                config.invite_code.api_secret(),
129                backup,
130            )
131            .await
132            .map(Arc::new)
133            .map_err(AdminGatewayError::ClientCreationError)?;
134        client
135            .wait_for_all_recoveries()
136            .await
137            .map_err(AdminGatewayError::ClientCreationError)?;
138        Ok(())
139    }
140
141    /// Builds a new client with the provided `FederationConfig` and `Mnemonic`.
142    /// Only used for newly joined federations.
143    pub async fn build(
144        &self,
145        config: FederationConfig,
146        gateway: Arc<Gateway>,
147        mnemonic: &Mnemonic,
148    ) -> AdminResult<fedimint_client::ClientHandleArc> {
149        let invite_code = config.invite_code.clone();
150        let federation_id = invite_code.federation_id();
151        let db_path = self.work_dir.join(format!("{federation_id}.db"));
152
153        let (db, root_secret) = if db_path.exists() {
154            let rocksdb = fedimint_rocksdb::RocksDb::open(db_path.clone())
155                .await
156                .map_err(AdminGatewayError::ClientCreationError)?;
157            let db = Database::new(rocksdb, ModuleDecoderRegistry::default());
158            let root_secret = self.client_plainrootsecret(&db).await?;
159            (db, root_secret)
160        } else {
161            let db = gateway.gateway_db.get_client_database(&federation_id);
162            let secret = Self::derive_federation_secret(mnemonic, &federation_id);
163            (db, secret)
164        };
165
166        Self::verify_client_config(&db, federation_id).await?;
167
168        let client_builder = self.create_client_builder(db, &config, gateway).await?;
169
170        if Client::is_initialized(client_builder.db_no_decoders()).await {
171            client_builder.open(root_secret).await
172        } else {
173            let client_config = config
174                .connector
175                .download_from_invite_code(&invite_code)
176                .await
177                .map_err(AdminGatewayError::ClientCreationError)?;
178            client_builder
179                .join(root_secret, client_config.clone(), invite_code.api_secret())
180                .await
181        }
182        .map(Arc::new)
183        .map_err(AdminGatewayError::ClientCreationError)
184    }
185
186    /// Verifies that the saved `ClientConfig` contains the expected
187    /// federation's config.
188    async fn verify_client_config(db: &Database, federation_id: FederationId) -> AdminResult<()> {
189        let mut dbtx = db.begin_transaction_nc().await;
190        if let Some(config) = dbtx.get_value(&ClientConfigKey).await {
191            if config.calculate_federation_id() != federation_id {
192                return Err(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
193                    "Federation Id did not match saved federation ID".to_string()
194                )));
195            }
196        }
197        Ok(())
198    }
199
200    /// Derives a per-federation secret according to Fedimint's multi-federation
201    /// secret derivation policy.
202    fn derive_federation_secret(
203        mnemonic: &Mnemonic,
204        federation_id: &FederationId,
205    ) -> DerivableSecret {
206        let global_root_secret = Bip39RootSecretStrategy::<12>::to_root_secret(mnemonic);
207        let multi_federation_root_secret = global_root_secret.child_key(ChildId(0));
208        let federation_root_secret = multi_federation_root_secret.federation_key(federation_id);
209        let federation_wallet_root_secret = federation_root_secret.child_key(ChildId(0));
210        federation_wallet_root_secret.child_key(ChildId(0))
211    }
212
213    /// Returns a vector of "legacy" federations which did not derive their
214    /// client secret's from the gateway's mnemonic.
215    pub fn legacy_federations(&self, all_federations: BTreeSet<FederationId>) -> Vec<FederationId> {
216        all_federations
217            .into_iter()
218            .filter(|federation_id| {
219                let db_path = self.work_dir.join(format!("{federation_id}.db"));
220                db_path.exists()
221            })
222            .collect::<Vec<FederationId>>()
223    }
224}