ln_gateway/
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::derivable_secret::{ChildId, DerivableSecret};
9use fedimint_client::module::init::ClientModuleInitRegistry;
10use fedimint_client::secret::{PlainRootSecretStrategy, RootSecretStrategy};
11use fedimint_client::{Client, ClientBuilder};
12use fedimint_core::config::FederationId;
13use fedimint_core::core::ModuleKind;
14use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
15use fedimint_core::encoding::Encodable;
16use fedimint_core::module::registry::ModuleDecoderRegistry;
17
18use crate::db::FederationConfig;
19use crate::error::AdminGatewayError;
20use crate::gateway_module_v2::GatewayClientInitV2;
21use crate::state_machine::GatewayClientInit;
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    /// Reads a plain root secret from a database to construct a database.
45    /// Only used for "legacy" federations before v0.5.0
46    async fn client_plainrootsecret(&self, db: &Database) -> AdminResult<DerivableSecret> {
47        let client_secret = Client::load_decodable_client_secret::<[u8; 64]>(db)
48            .await
49            .map_err(AdminGatewayError::ClientCreationError)?;
50        Ok(PlainRootSecretStrategy::to_root_secret(&client_secret))
51    }
52
53    /// Constructs the client builder with the modules, database, and connector
54    /// used to create clients for connected federations.
55    async fn create_client_builder(
56        &self,
57        db: Database,
58        federation_config: &FederationConfig,
59        gateway: Arc<Gateway>,
60    ) -> AdminResult<ClientBuilder> {
61        let FederationConfig {
62            federation_index,
63            timelock_delta,
64            connector,
65            ..
66        } = federation_config.to_owned();
67
68        let mut registry = self.registry.clone();
69
70        if gateway.is_running_lnv1() {
71            registry.attach(GatewayClientInit {
72                timelock_delta,
73                federation_index,
74                gateway: gateway.clone(),
75            });
76        }
77
78        if gateway.is_running_lnv2() {
79            registry.attach(GatewayClientInitV2 {
80                gateway: gateway.clone(),
81            });
82        }
83
84        let mut client_builder = Client::builder(db)
85            .await
86            .map_err(AdminGatewayError::ClientCreationError)?;
87        client_builder.with_module_inits(registry);
88        client_builder.with_primary_module_kind(self.primary_module_kind.clone());
89        client_builder.with_connector(connector);
90        Ok(client_builder)
91    }
92
93    /// Recovers a client with the provided mnemonic. This function will wait
94    /// for the recoveries to finish, but a new client must be created
95    /// afterwards and waited on until the state machines have finished
96    /// for a balance to be present.
97    pub async fn recover(
98        &self,
99        config: FederationConfig,
100        gateway: Arc<Gateway>,
101        mnemonic: &Mnemonic,
102    ) -> AdminResult<()> {
103        let client_config = config
104            .connector
105            .download_from_invite_code(&config.invite_code)
106            .await
107            .map_err(AdminGatewayError::ClientCreationError)?;
108        let federation_id = config.invite_code.federation_id();
109        let db = gateway
110            .gateway_db
111            .with_prefix(federation_id.consensus_encode_to_vec());
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                .map_err(AdminGatewayError::ClientCreationError)?;
156            let db = Database::new(rocksdb, ModuleDecoderRegistry::default());
157            let root_secret = self.client_plainrootsecret(&db).await?;
158            (db, root_secret)
159        } else {
160            let db = gateway
161                .gateway_db
162                .with_prefix(federation_id.consensus_encode_to_vec());
163            let secret = Self::derive_federation_secret(mnemonic, &federation_id);
164            (db, secret)
165        };
166
167        Self::verify_client_config(&db, federation_id).await?;
168
169        let client_builder = self.create_client_builder(db, &config, gateway).await?;
170
171        if Client::is_initialized(client_builder.db_no_decoders()).await {
172            client_builder.open(root_secret).await
173        } else {
174            let client_config = config
175                .connector
176                .download_from_invite_code(&invite_code)
177                .await
178                .map_err(AdminGatewayError::ClientCreationError)?;
179            client_builder
180                .join(root_secret, client_config.clone(), invite_code.api_secret())
181                .await
182        }
183        .map(Arc::new)
184        .map_err(AdminGatewayError::ClientCreationError)
185    }
186
187    /// Verifies that the saved `ClientConfig` contains the expected
188    /// federation's config.
189    async fn verify_client_config(db: &Database, federation_id: FederationId) -> AdminResult<()> {
190        let mut dbtx = db.begin_transaction_nc().await;
191        if let Some(config) = dbtx.get_value(&ClientConfigKey).await {
192            if config.calculate_federation_id() != federation_id {
193                return Err(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
194                    "Federation Id did not match saved federation ID".to_string()
195                )));
196            }
197        }
198        Ok(())
199    }
200
201    /// Derives a per-federation secret according to Fedimint's multi-federation
202    /// secret derivation policy.
203    fn derive_federation_secret(
204        mnemonic: &Mnemonic,
205        federation_id: &FederationId,
206    ) -> DerivableSecret {
207        let global_root_secret = Bip39RootSecretStrategy::<12>::to_root_secret(mnemonic);
208        let multi_federation_root_secret = global_root_secret.child_key(ChildId(0));
209        let federation_root_secret = multi_federation_root_secret.federation_key(federation_id);
210        let federation_wallet_root_secret = federation_root_secret.child_key(ChildId(0));
211        federation_wallet_root_secret.child_key(ChildId(0))
212    }
213
214    /// Returns a vector of "legacy" federations which did not derive their
215    /// client secret's from the gateway's mnemonic.
216    pub fn legacy_federations(&self, all_federations: BTreeSet<FederationId>) -> Vec<FederationId> {
217        all_federations
218            .into_iter()
219            .filter(|federation_id| {
220                let db_path = self.work_dir.join(format!("{federation_id}.db"));
221                db_path.exists()
222            })
223            .collect::<Vec<FederationId>>()
224    }
225}