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, ModuleKind};
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::net::auth::check_auth;
26use fedimint_core::setup_code::PeerEndpoints;
27use fedimint_core::{PeerId, base32};
28use fedimint_server_core::setup_ui::ISetupApi;
29use iroh::SecretKey;
30use rand::rngs::OsRng;
31use tokio::sync::Mutex;
32use tokio::sync::mpsc::Sender;
33use tokio_rustls::rustls;
34
35use crate::config::{ConfigGenParams, ConfigGenSettings, PeerSetupCode};
36use crate::net::api::HasApiContext;
37use crate::net::p2p_connector::gen_cert_and_key;
38
39#[derive(Debug, Clone, Default)]
41pub struct SetupState {
42 local_params: Option<LocalParams>,
44 setup_codes: BTreeSet<PeerSetupCode>,
46}
47
48#[derive(Clone, Debug)]
49pub struct LocalParams {
51 auth: ApiAuth,
53 tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
55 iroh_api_sk: Option<iroh::SecretKey>,
57 iroh_p2p_sk: Option<iroh::SecretKey>,
59 endpoints: PeerEndpoints,
61 name: String,
63 federation_name: Option<String>,
65 disable_base_fees: Option<bool>,
67 enabled_modules: Option<BTreeSet<ModuleKind>>,
70 federation_size: Option<u32>,
73}
74
75impl LocalParams {
76 pub fn setup_code(&self) -> PeerSetupCode {
77 PeerSetupCode {
78 name: self.name.clone(),
79 endpoints: self.endpoints.clone(),
80 federation_name: self.federation_name.clone(),
81 disable_base_fees: self.disable_base_fees,
82 enabled_modules: self.enabled_modules.clone(),
83 federation_size: self.federation_size,
84 }
85 }
86}
87
88#[derive(Clone)]
90pub struct SetupApi {
91 settings: ConfigGenSettings,
93 state: Arc<Mutex<SetupState>>,
95 db: Database,
97 sender: Sender<ConfigGenParams>,
99}
100
101impl SetupApi {
102 pub fn new(settings: ConfigGenSettings, db: Database, sender: Sender<ConfigGenParams>) -> Self {
103 Self {
104 settings,
105 state: Arc::new(Mutex::new(SetupState::default())),
106 db,
107 sender,
108 }
109 }
110
111 pub async fn setup_status(&self) -> SetupStatus {
112 match self.state.lock().await.local_params {
113 Some(..) => SetupStatus::SharingConnectionCodes,
114 None => SetupStatus::AwaitingLocalParams,
115 }
116 }
117}
118
119#[async_trait]
120impl ISetupApi for SetupApi {
121 async fn setup_code(&self) -> Option<String> {
122 self.state
123 .lock()
124 .await
125 .local_params
126 .as_ref()
127 .map(|lp| base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
128 }
129
130 async fn guardian_name(&self) -> Option<String> {
131 self.state
132 .lock()
133 .await
134 .local_params
135 .as_ref()
136 .map(|lp| lp.name.clone())
137 }
138
139 async fn auth(&self) -> Option<ApiAuth> {
140 self.state
141 .lock()
142 .await
143 .local_params
144 .as_ref()
145 .map(|lp| lp.auth.clone())
146 }
147
148 async fn connected_peers(&self) -> Vec<String> {
149 self.state
150 .lock()
151 .await
152 .setup_codes
153 .clone()
154 .into_iter()
155 .map(|info| info.name)
156 .collect()
157 }
158
159 fn available_modules(&self) -> BTreeSet<ModuleKind> {
160 self.settings.available_modules.clone()
161 }
162
163 fn default_modules(&self) -> BTreeSet<ModuleKind> {
164 self.settings.default_modules.clone()
165 }
166
167 async fn reset_setup_codes(&self) {
168 self.state.lock().await.setup_codes.clear();
169 }
170
171 async fn set_local_parameters(
172 &self,
173 auth: ApiAuth,
174 name: String,
175 federation_name: Option<String>,
176 disable_base_fees: Option<bool>,
177 enabled_modules: Option<BTreeSet<ModuleKind>>,
178 federation_size: Option<u32>,
179 ) -> anyhow::Result<String> {
180 if let Some(existing_local_parameters) = self.state.lock().await.local_params.clone()
181 && existing_local_parameters.auth == auth
182 && existing_local_parameters.name == name
183 && existing_local_parameters.federation_name == federation_name
184 && existing_local_parameters.disable_base_fees == disable_base_fees
185 && existing_local_parameters.enabled_modules == enabled_modules
186 && existing_local_parameters.federation_size == federation_size
187 {
188 return Ok(base32::encode_prefixed(
189 FEDIMINT_PREFIX,
190 &existing_local_parameters.setup_code(),
191 ));
192 }
193
194 ensure!(!name.is_empty(), "The guardian name is empty");
195
196 ensure!(!auth.0.is_empty(), "The password is empty");
197
198 ensure!(
199 auth.0.trim() == auth.0,
200 "The password contains leading/trailing whitespace",
201 );
202
203 if let Some(federation_name) = federation_name.as_ref() {
204 ensure!(!federation_name.is_empty(), "The federation name is empty");
205 }
206
207 if federation_name.is_some() {
208 ensure!(
209 federation_size.is_some(),
210 "The leader must set the federation size"
211 );
212 }
213
214 if let Some(size) = federation_size {
215 ensure!(
216 size == 1 || 4 <= size,
217 "Federation size must be 1 or at least 4"
218 );
219 }
220
221 let mut state = self.state.lock().await;
222
223 ensure!(
224 state.local_params.is_none(),
225 "Local parameters have already been set"
226 );
227
228 let lp = if self.settings.enable_iroh {
229 let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
230 SecretKey::from_str(&var)
231 .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
232 } else {
233 SecretKey::generate(&mut OsRng)
234 };
235
236 let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
237 SecretKey::from_str(&var)
238 .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
239 } else {
240 SecretKey::generate(&mut OsRng)
241 };
242
243 LocalParams {
244 auth,
245 tls_key: None,
246 iroh_api_sk: Some(iroh_api_sk.clone()),
247 iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
248 endpoints: PeerEndpoints::Iroh {
249 api_pk: iroh_api_sk.public(),
250 p2p_pk: iroh_p2p_sk.public(),
251 },
252 name,
253 federation_name,
254 disable_base_fees,
255 enabled_modules,
256 federation_size,
257 }
258 } else {
259 let (tls_cert, tls_key) =
260 gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
261
262 LocalParams {
263 auth,
264 tls_key: Some(tls_key),
265 iroh_api_sk: None,
266 iroh_p2p_sk: None,
267 endpoints: PeerEndpoints::Tcp {
268 api_url: self
269 .settings
270 .api_url
271 .clone()
272 .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
273 p2p_url: self
274 .settings
275 .p2p_url
276 .clone()
277 .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
278
279 cert: tls_cert.as_ref().to_vec(),
280 },
281 name,
282 federation_name,
283 disable_base_fees,
284 enabled_modules,
285 federation_size,
286 }
287 };
288
289 state.local_params = Some(lp.clone());
290
291 Ok(base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
292 }
293
294 async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
295 let info = base32::decode_prefixed(FEDIMINT_PREFIX, &info)?;
296
297 let mut state = self.state.lock().await;
298
299 if state.setup_codes.contains(&info) {
300 return Ok(info.name.clone());
301 }
302
303 let local_params = state
304 .local_params
305 .clone()
306 .expect("The endpoint is authenticated.");
307
308 ensure!(
309 info != local_params.setup_code(),
310 "You cannot add you own connection info"
311 );
312
313 ensure!(
314 discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
315 "Guardian has different endpoint variant (TCP/Iroh) than us.",
316 );
317
318 if let Some(federation_name) = state
319 .setup_codes
320 .iter()
321 .chain(once(&local_params.setup_code()))
322 .find_map(|info| info.federation_name.clone())
323 {
324 ensure!(
325 info.federation_name.is_none(),
326 "Federation name has already been set to {federation_name}"
327 );
328 }
329
330 if let Some(disable_base_fees) = state
331 .setup_codes
332 .iter()
333 .chain(once(&local_params.setup_code()))
334 .find_map(|info| info.disable_base_fees)
335 {
336 ensure!(
337 info.disable_base_fees.is_none(),
338 "Base fees setting has already been configured to disabled={disable_base_fees}"
339 );
340 }
341
342 if state
343 .setup_codes
344 .iter()
345 .chain(once(&local_params.setup_code()))
346 .any(|info| info.enabled_modules.is_some())
347 {
348 ensure!(
349 info.enabled_modules.is_none(),
350 "Enabled modules have already been configured by another guardian"
351 );
352 }
353
354 if let Some(federation_size) = state
355 .setup_codes
356 .iter()
357 .chain(once(&local_params.setup_code()))
358 .find_map(|info| info.federation_size)
359 {
360 ensure!(
361 info.federation_size.is_none(),
362 "Federation size has already been set to {federation_size}"
363 );
364 }
365
366 state.setup_codes.insert(info.clone());
367
368 Ok(info.name)
369 }
370
371 async fn start_dkg(&self) -> anyhow::Result<()> {
372 let mut state = self.state.lock().await.clone();
373
374 let local_params = state
375 .local_params
376 .clone()
377 .expect("The endpoint is authenticated.");
378
379 let our_setup_code = local_params.setup_code();
380
381 state.setup_codes.insert(our_setup_code.clone());
382
383 ensure!(
384 state.setup_codes.len() == 1 || 4 <= state.setup_codes.len(),
385 "The number of guardians is invalid"
386 );
387
388 if let Some(federation_size) = state
389 .setup_codes
390 .iter()
391 .find_map(|info| info.federation_size)
392 {
393 ensure!(
394 state.setup_codes.len() == federation_size as usize,
395 "Expected {federation_size} guardians but got {}",
396 state.setup_codes.len()
397 );
398 }
399
400 let federation_name = state
401 .setup_codes
402 .iter()
403 .find_map(|info| info.federation_name.clone())
404 .context("We need one guardian to configure the federations name")?;
405
406 let disable_base_fees = state
407 .setup_codes
408 .iter()
409 .find_map(|info| info.disable_base_fees)
410 .unwrap_or(is_env_var_set(FM_DISABLE_BASE_FEES_ENV));
411
412 let enabled_modules = state
413 .setup_codes
414 .iter()
415 .find_map(|info| info.enabled_modules.clone())
416 .unwrap_or_else(|| self.settings.default_modules.clone());
417
418 let our_id = state
419 .setup_codes
420 .iter()
421 .position(|info| info == &our_setup_code)
422 .expect("We inserted the key above.");
423
424 let params = ConfigGenParams {
425 identity: PeerId::from(our_id as u16),
426 tls_key: local_params.tls_key,
427 iroh_api_sk: local_params.iroh_api_sk,
428 iroh_p2p_sk: local_params.iroh_p2p_sk,
429 api_auth: local_params.auth,
430 peers: (0..)
431 .map(|i| PeerId::from(i as u16))
432 .zip(state.setup_codes.clone().into_iter())
433 .collect(),
434 meta: BTreeMap::from_iter(vec![(
435 META_FEDERATION_NAME_KEY.to_string(),
436 federation_name,
437 )]),
438 disable_base_fees,
439 enabled_modules,
440 network: self.settings.network,
441 };
442
443 self.sender
444 .send(params)
445 .await
446 .context("Failed to send config gen params")?;
447
448 Ok(())
449 }
450
451 async fn federation_size(&self) -> Option<u32> {
452 let state = self.state.lock().await;
453 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
454 state
455 .setup_codes
456 .iter()
457 .chain(local_setup_code.iter())
458 .find_map(|info| info.federation_size)
459 }
460
461 async fn cfg_federation_name(&self) -> Option<String> {
462 let state = self.state.lock().await;
463 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
464 state
465 .setup_codes
466 .iter()
467 .chain(local_setup_code.iter())
468 .find_map(|info| info.federation_name.clone())
469 }
470
471 async fn cfg_base_fees_disabled(&self) -> Option<bool> {
472 let state = self.state.lock().await;
473 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
474 state
475 .setup_codes
476 .iter()
477 .chain(local_setup_code.iter())
478 .find_map(|info| info.disable_base_fees)
479 }
480
481 async fn cfg_enabled_modules(&self) -> Option<BTreeSet<ModuleKind>> {
482 let state = self.state.lock().await;
483 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
484 state
485 .setup_codes
486 .iter()
487 .chain(local_setup_code.iter())
488 .find_map(|info| info.enabled_modules.clone())
489 }
490}
491
492#[async_trait]
493impl HasApiContext<SetupApi> for SetupApi {
494 async fn context(
495 &self,
496 request: &ApiRequestErased,
497 id: Option<ModuleInstanceId>,
498 ) -> (&SetupApi, ApiEndpointContext) {
499 assert!(id.is_none());
500
501 let db = self.db.clone();
502
503 let is_authenticated = match self.state.lock().await.local_params {
504 None => false,
505 Some(ref params) => match request.auth.as_ref() {
506 Some(auth) => *auth == params.auth,
507 None => false,
508 },
509 };
510
511 let context = ApiEndpointContext::new(db, is_authenticated, request.auth.clone());
512
513 (self, context)
514 }
515}
516
517pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
518 vec![
519 api_endpoint! {
520 SETUP_STATUS_ENDPOINT,
521 ApiVersion::new(0, 0),
522 async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
523 Ok(config.setup_status().await)
524 }
525 },
526 api_endpoint! {
527 SET_LOCAL_PARAMS_ENDPOINT,
528 ApiVersion::new(0, 0),
529 async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
530 let auth = context
531 .request_auth()
532 .ok_or(ApiError::bad_request("Missing password".to_string()))?;
533
534 config.set_local_parameters(auth, request.name, request.federation_name, request.disable_base_fees, request.enabled_modules, request.federation_size)
535 .await
536 .map_err(|e| ApiError::bad_request(e.to_string()))
537 }
538 },
539 api_endpoint! {
540 ADD_PEER_SETUP_CODE_ENDPOINT,
541 ApiVersion::new(0, 0),
542 async |config: &SetupApi, context, info: String| -> String {
543 check_auth(context)?;
544
545 config.add_peer_setup_code(info.clone())
546 .await
547 .map_err(|e|ApiError::bad_request(e.to_string()))
548 }
549 },
550 api_endpoint! {
551 RESET_PEER_SETUP_CODES_ENDPOINT,
552 ApiVersion::new(0, 0),
553 async |config: &SetupApi, context, _v: ()| -> () {
554 check_auth(context)?;
555
556 config.reset_setup_codes().await;
557
558 Ok(())
559 }
560 },
561 api_endpoint! {
562 GET_SETUP_CODE_ENDPOINT,
563 ApiVersion::new(0, 0),
564 async |config: &SetupApi, context, _request: ()| -> Option<String> {
565 check_auth(context)?;
566
567 Ok(config.setup_code().await)
568 }
569 },
570 api_endpoint! {
571 START_DKG_ENDPOINT,
572 ApiVersion::new(0, 0),
573 async |config: &SetupApi, context, _v: ()| -> () {
574 check_auth(context)?;
575
576 config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
577 }
578 },
579 ]
580}