1use std::collections::{BTreeMap, BTreeSet};
2use std::iter::once;
3use std::mem::discriminant;
4use std::str::FromStr as _;
5use std::sync::Arc;
6
7use anyhow::{Context, ensure};
8use async_trait::async_trait;
9use fedimint_core::admin_client::{SetLocalParamsRequest, SetupStatus};
10use fedimint_core::base32::FEDIMINT_PREFIX;
11use fedimint_core::config::META_FEDERATION_NAME_KEY;
12use fedimint_core::core::ModuleInstanceId;
13use fedimint_core::db::Database;
14use fedimint_core::endpoint_constants::{
15 ADD_PEER_SETUP_CODE_ENDPOINT, GET_SETUP_CODE_ENDPOINT, RESET_PEER_SETUP_CODES_ENDPOINT,
16 SET_LOCAL_PARAMS_ENDPOINT, SETUP_STATUS_ENDPOINT, START_DKG_ENDPOINT,
17};
18use fedimint_core::envs::{
19 FM_DISABLE_BASE_FEES_ENV, FM_IROH_API_SECRET_KEY_OVERRIDE_ENV,
20 FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV, is_env_var_set,
21};
22use fedimint_core::module::{
23 ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiVersion, api_endpoint,
24};
25use fedimint_core::setup_code::PeerEndpoints;
26use fedimint_core::{PeerId, base32};
27use fedimint_logging::LOG_SERVER;
28use fedimint_server_core::net::check_auth;
29use fedimint_server_core::setup_ui::ISetupApi;
30use iroh::SecretKey;
31use rand::rngs::OsRng;
32use tokio::sync::Mutex;
33use tokio::sync::mpsc::Sender;
34use tokio_rustls::rustls;
35use tracing::warn;
36
37use crate::config::{ConfigGenParams, ConfigGenSettings, PeerSetupCode};
38use crate::net::api::HasApiContext;
39use crate::net::p2p_connector::gen_cert_and_key;
40
41#[derive(Debug, Clone, Default)]
43pub struct SetupState {
44 local_params: Option<LocalParams>,
46 setup_codes: BTreeSet<PeerSetupCode>,
48}
49
50#[derive(Clone, Debug)]
51pub struct LocalParams {
53 auth: ApiAuth,
55 tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
57 iroh_api_sk: Option<iroh::SecretKey>,
59 iroh_p2p_sk: Option<iroh::SecretKey>,
61 endpoints: PeerEndpoints,
63 name: String,
65 federation_name: Option<String>,
67 disable_base_fees: Option<bool>,
69}
70
71impl LocalParams {
72 pub fn setup_code(&self) -> PeerSetupCode {
73 PeerSetupCode {
74 name: self.name.clone(),
75 endpoints: self.endpoints.clone(),
76 federation_name: self.federation_name.clone(),
77 disable_base_fees: self.disable_base_fees,
78 }
79 }
80}
81
82#[derive(Clone)]
84pub struct SetupApi {
85 settings: ConfigGenSettings,
87 state: Arc<Mutex<SetupState>>,
89 db: Database,
91 sender: Sender<ConfigGenParams>,
93}
94
95impl SetupApi {
96 pub fn new(settings: ConfigGenSettings, db: Database, sender: Sender<ConfigGenParams>) -> Self {
97 Self {
98 settings,
99 state: Arc::new(Mutex::new(SetupState::default())),
100 db,
101 sender,
102 }
103 }
104
105 pub async fn setup_status(&self) -> SetupStatus {
106 match self.state.lock().await.local_params {
107 Some(..) => SetupStatus::SharingConnectionCodes,
108 None => SetupStatus::AwaitingLocalParams,
109 }
110 }
111}
112
113#[async_trait]
114impl ISetupApi for SetupApi {
115 async fn setup_code(&self) -> Option<String> {
116 self.state
117 .lock()
118 .await
119 .local_params
120 .as_ref()
121 .map(|lp| base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
122 }
123
124 async fn auth(&self) -> Option<ApiAuth> {
125 self.state
126 .lock()
127 .await
128 .local_params
129 .as_ref()
130 .map(|lp| lp.auth.clone())
131 }
132
133 async fn connected_peers(&self) -> Vec<String> {
134 self.state
135 .lock()
136 .await
137 .setup_codes
138 .clone()
139 .into_iter()
140 .map(|info| info.name)
141 .collect()
142 }
143
144 async fn reset_setup_codes(&self) {
145 self.state.lock().await.setup_codes.clear();
146 }
147
148 async fn set_local_parameters(
149 &self,
150 auth: ApiAuth,
151 name: String,
152 federation_name: Option<String>,
153 disable_base_fees: Option<bool>,
154 ) -> anyhow::Result<String> {
155 if let Some(existing_local_parameters) = self.state.lock().await.local_params.clone()
156 && existing_local_parameters.auth == auth
157 && existing_local_parameters.name == name
158 && existing_local_parameters.federation_name == federation_name
159 && existing_local_parameters.disable_base_fees == disable_base_fees
160 {
161 return Ok(base32::encode_prefixed(
162 FEDIMINT_PREFIX,
163 &existing_local_parameters.setup_code(),
164 ));
165 }
166
167 ensure!(!name.is_empty(), "The guardian name is empty");
168
169 ensure!(!auth.0.is_empty(), "The password is empty");
170
171 ensure!(
172 auth.0.trim() == auth.0,
173 "The password contains leading/trailing whitespace",
174 );
175
176 if let Some(federation_name) = federation_name.as_ref() {
177 ensure!(!federation_name.is_empty(), "The federation name is empty");
178 }
179
180 let mut state = self.state.lock().await;
181
182 ensure!(
183 state.local_params.is_none(),
184 "Local parameters have already been set"
185 );
186
187 let lp = if self.settings.enable_iroh {
188 warn!(target: LOG_SERVER, "Iroh support is experimental");
189
190 let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
191 SecretKey::from_str(&var)
192 .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
193 } else {
194 SecretKey::generate(&mut OsRng)
195 };
196
197 let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
198 SecretKey::from_str(&var)
199 .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
200 } else {
201 SecretKey::generate(&mut OsRng)
202 };
203
204 LocalParams {
205 auth,
206 tls_key: None,
207 iroh_api_sk: Some(iroh_api_sk.clone()),
208 iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
209 endpoints: PeerEndpoints::Iroh {
210 api_pk: iroh_api_sk.public(),
211 p2p_pk: iroh_p2p_sk.public(),
212 },
213 name,
214 federation_name,
215 disable_base_fees,
216 }
217 } else {
218 let (tls_cert, tls_key) =
219 gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
220
221 LocalParams {
222 auth,
223 tls_key: Some(tls_key),
224 iroh_api_sk: None,
225 iroh_p2p_sk: None,
226 endpoints: PeerEndpoints::Tcp {
227 api_url: self
228 .settings
229 .api_url
230 .clone()
231 .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
232 p2p_url: self
233 .settings
234 .p2p_url
235 .clone()
236 .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
237
238 cert: tls_cert.as_ref().to_vec(),
239 },
240 name,
241 federation_name,
242 disable_base_fees,
243 }
244 };
245
246 state.local_params = Some(lp.clone());
247
248 Ok(base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
249 }
250
251 async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
252 let info = base32::decode_prefixed(FEDIMINT_PREFIX, &info)?;
253
254 let mut state = self.state.lock().await;
255
256 if state.setup_codes.contains(&info) {
257 return Ok(info.name.clone());
258 }
259
260 let local_params = state
261 .local_params
262 .clone()
263 .expect("The endpoint is authenticated.");
264
265 ensure!(
266 info != local_params.setup_code(),
267 "You cannot add you own connection info"
268 );
269
270 ensure!(
271 discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
272 "Guardian has different endpoint variant (TCP/Iroh) than us.",
273 );
274
275 if let Some(federation_name) = state
276 .setup_codes
277 .iter()
278 .chain(once(&local_params.setup_code()))
279 .find_map(|info| info.federation_name.clone())
280 {
281 ensure!(
282 info.federation_name.is_none(),
283 "Federation name has already been set to {federation_name}"
284 );
285 }
286
287 if let Some(disable_base_fees) = state
288 .setup_codes
289 .iter()
290 .chain(once(&local_params.setup_code()))
291 .find_map(|info| info.disable_base_fees)
292 {
293 ensure!(
294 info.disable_base_fees.is_none(),
295 "Base fees setting has already been configured to disabled={disable_base_fees}"
296 );
297 }
298
299 state.setup_codes.insert(info.clone());
300
301 Ok(info.name)
302 }
303
304 async fn start_dkg(&self) -> anyhow::Result<()> {
305 let mut state = self.state.lock().await.clone();
306
307 let local_params = state
308 .local_params
309 .clone()
310 .expect("The endpoint is authenticated.");
311
312 let our_setup_code = local_params.setup_code();
313
314 state.setup_codes.insert(our_setup_code.clone());
315
316 ensure!(
317 state.setup_codes.len() == 1 || state.setup_codes.len() >= 4,
318 "The number of guardians is invalid"
319 );
320
321 let federation_name = state
322 .setup_codes
323 .iter()
324 .find_map(|info| info.federation_name.clone())
325 .context("We need one guardian to configure the federations name")?;
326
327 let disable_base_fees = state
328 .setup_codes
329 .iter()
330 .find_map(|info| info.disable_base_fees)
331 .unwrap_or(is_env_var_set(FM_DISABLE_BASE_FEES_ENV));
332
333 let our_id = state
334 .setup_codes
335 .iter()
336 .position(|info| info == &our_setup_code)
337 .expect("We inserted the key above.");
338
339 let params = ConfigGenParams {
340 identity: PeerId::from(our_id as u16),
341 tls_key: local_params.tls_key,
342 iroh_api_sk: local_params.iroh_api_sk,
343 iroh_p2p_sk: local_params.iroh_p2p_sk,
344 api_auth: local_params.auth,
345 peers: (0..)
346 .map(|i| PeerId::from(i as u16))
347 .zip(state.setup_codes.clone().into_iter())
348 .collect(),
349 meta: BTreeMap::from_iter(vec![(
350 META_FEDERATION_NAME_KEY.to_string(),
351 federation_name,
352 )]),
353 disable_base_fees,
354 };
355
356 self.sender
357 .send(params)
358 .await
359 .context("Failed to send config gen params")?;
360
361 Ok(())
362 }
363}
364
365#[async_trait]
366impl HasApiContext<SetupApi> for SetupApi {
367 async fn context(
368 &self,
369 request: &ApiRequestErased,
370 id: Option<ModuleInstanceId>,
371 ) -> (&SetupApi, ApiEndpointContext<'_>) {
372 assert!(id.is_none());
373
374 let db = self.db.clone();
375 let dbtx = self.db.begin_transaction().await;
376
377 let is_authenticated = match self.state.lock().await.local_params {
378 None => false,
379 Some(ref params) => match request.auth.as_ref() {
380 Some(auth) => *auth == params.auth,
381 None => false,
382 },
383 };
384
385 let context = ApiEndpointContext::new(db, dbtx, is_authenticated, request.auth.clone());
386
387 (self, context)
388 }
389}
390
391pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
392 vec![
393 api_endpoint! {
394 SETUP_STATUS_ENDPOINT,
395 ApiVersion::new(0, 0),
396 async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
397 Ok(config.setup_status().await)
398 }
399 },
400 api_endpoint! {
401 SET_LOCAL_PARAMS_ENDPOINT,
402 ApiVersion::new(0, 0),
403 async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
404 let auth = context
405 .request_auth()
406 .ok_or(ApiError::bad_request("Missing password".to_string()))?;
407
408 config.set_local_parameters(auth, request.name, request.federation_name, request.disable_base_fees)
409 .await
410 .map_err(|e| ApiError::bad_request(e.to_string()))
411 }
412 },
413 api_endpoint! {
414 ADD_PEER_SETUP_CODE_ENDPOINT,
415 ApiVersion::new(0, 0),
416 async |config: &SetupApi, context, info: String| -> String {
417 check_auth(context)?;
418
419 config.add_peer_setup_code(info.clone())
420 .await
421 .map_err(|e|ApiError::bad_request(e.to_string()))
422 }
423 },
424 api_endpoint! {
425 RESET_PEER_SETUP_CODES_ENDPOINT,
426 ApiVersion::new(0, 0),
427 async |config: &SetupApi, context, _v: ()| -> () {
428 check_auth(context)?;
429
430 config.reset_setup_codes().await;
431
432 Ok(())
433 }
434 },
435 api_endpoint! {
436 GET_SETUP_CODE_ENDPOINT,
437 ApiVersion::new(0, 0),
438 async |config: &SetupApi, context, _request: ()| -> Option<String> {
439 check_auth(context)?;
440
441 Ok(config.setup_code().await)
442 }
443 },
444 api_endpoint! {
445 START_DKG_ENDPOINT,
446 ApiVersion::new(0, 0),
447 async |config: &SetupApi, context, _v: ()| -> () {
448 check_auth(context)?;
449
450 config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
451 }
452 },
453 ]
454}