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 network: bitcoin::Network,
75}
76
77impl LocalParams {
78 pub fn setup_code(&self) -> PeerSetupCode {
79 PeerSetupCode {
80 name: self.name.clone(),
81 endpoints: self.endpoints.clone(),
82 federation_name: self.federation_name.clone(),
83 disable_base_fees: self.disable_base_fees,
84 enabled_modules: self.enabled_modules.clone(),
85 federation_size: self.federation_size,
86 network: self.network,
87 }
88 }
89}
90
91#[derive(Clone)]
93pub struct SetupApi {
94 settings: ConfigGenSettings,
96 state: Arc<Mutex<SetupState>>,
98 db: Database,
100 sender: Sender<ConfigGenParams>,
102}
103
104impl SetupApi {
105 pub fn new(settings: ConfigGenSettings, db: Database, sender: Sender<ConfigGenParams>) -> Self {
106 Self {
107 settings,
108 state: Arc::new(Mutex::new(SetupState::default())),
109 db,
110 sender,
111 }
112 }
113
114 pub async fn setup_status(&self) -> SetupStatus {
115 match self.state.lock().await.local_params {
116 Some(..) => SetupStatus::SharingConnectionCodes,
117 None => SetupStatus::AwaitingLocalParams,
118 }
119 }
120}
121
122#[async_trait]
123impl ISetupApi for SetupApi {
124 async fn setup_code(&self) -> Option<String> {
125 self.state
126 .lock()
127 .await
128 .local_params
129 .as_ref()
130 .map(|lp| base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
131 }
132
133 async fn guardian_name(&self) -> Option<String> {
134 self.state
135 .lock()
136 .await
137 .local_params
138 .as_ref()
139 .map(|lp| lp.name.clone())
140 }
141
142 async fn auth(&self) -> Option<ApiAuth> {
143 self.state
144 .lock()
145 .await
146 .local_params
147 .as_ref()
148 .map(|lp| lp.auth.clone())
149 }
150
151 async fn connected_peers(&self) -> Vec<String> {
152 self.state
153 .lock()
154 .await
155 .setup_codes
156 .clone()
157 .into_iter()
158 .map(|info| info.name)
159 .collect()
160 }
161
162 fn available_modules(&self) -> BTreeSet<ModuleKind> {
163 self.settings.available_modules.clone()
164 }
165
166 fn default_modules(&self) -> BTreeSet<ModuleKind> {
167 self.settings.default_modules.clone()
168 }
169
170 async fn reset_setup_codes(&self) {
171 self.state.lock().await.setup_codes.clear();
172 }
173
174 async fn set_local_parameters(
175 &self,
176 auth: ApiAuth,
177 name: String,
178 federation_name: Option<String>,
179 disable_base_fees: Option<bool>,
180 enabled_modules: Option<BTreeSet<ModuleKind>>,
181 federation_size: Option<u32>,
182 ) -> anyhow::Result<String> {
183 if let Some(existing_local_parameters) = self.state.lock().await.local_params.clone()
184 && existing_local_parameters.auth.as_str() == auth.as_str()
185 && existing_local_parameters.name == name
186 && existing_local_parameters.federation_name == federation_name
187 && existing_local_parameters.disable_base_fees == disable_base_fees
188 && existing_local_parameters.enabled_modules == enabled_modules
189 && existing_local_parameters.federation_size == federation_size
190 {
191 return Ok(base32::encode_prefixed(
192 FEDIMINT_PREFIX,
193 &existing_local_parameters.setup_code(),
194 ));
195 }
196
197 ensure!(!name.is_empty(), "The guardian name is empty");
198
199 ensure!(!auth.as_str().is_empty(), "The password is empty");
200
201 ensure!(
202 auth.as_str().trim() == auth.as_str(),
203 "The password contains leading/trailing whitespace",
204 );
205
206 if let Some(federation_name) = federation_name.as_ref() {
207 ensure!(!federation_name.is_empty(), "The federation name is empty");
208 }
209
210 if federation_name.is_some() {
211 ensure!(
212 federation_size.is_some(),
213 "The leader must set the federation size"
214 );
215 }
216
217 if let Some(size) = federation_size {
218 ensure!(
219 size == 1 || 4 <= size,
220 "Federation size must be 1 or at least 4"
221 );
222 }
223
224 let mut state = self.state.lock().await;
225
226 ensure!(
227 state.local_params.is_none(),
228 "Local parameters have already been set"
229 );
230
231 let lp = if self.settings.enable_iroh {
232 let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
233 SecretKey::from_str(&var)
234 .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
235 } else {
236 SecretKey::generate(&mut OsRng)
237 };
238
239 let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
240 SecretKey::from_str(&var)
241 .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
242 } else {
243 SecretKey::generate(&mut OsRng)
244 };
245
246 LocalParams {
247 auth,
248 tls_key: None,
249 iroh_api_sk: Some(iroh_api_sk.clone()),
250 iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
251 endpoints: PeerEndpoints::Iroh {
252 api_pk: iroh_api_sk.public(),
253 p2p_pk: iroh_p2p_sk.public(),
254 },
255 name,
256 federation_name,
257 disable_base_fees,
258 enabled_modules,
259 federation_size,
260 network: self.settings.network,
261 }
262 } else {
263 let (tls_cert, tls_key) =
264 gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
265
266 LocalParams {
267 auth,
268 tls_key: Some(tls_key),
269 iroh_api_sk: None,
270 iroh_p2p_sk: None,
271 endpoints: PeerEndpoints::Tcp {
272 api_url: self
273 .settings
274 .api_url
275 .clone()
276 .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
277 p2p_url: self
278 .settings
279 .p2p_url
280 .clone()
281 .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
282
283 cert: tls_cert.as_ref().to_vec(),
284 },
285 name,
286 federation_name,
287 disable_base_fees,
288 enabled_modules,
289 federation_size,
290 network: self.settings.network,
291 }
292 };
293
294 state.local_params = Some(lp.clone());
295
296 Ok(base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
297 }
298
299 async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
300 let info = base32::decode_prefixed(FEDIMINT_PREFIX, &info)?;
301
302 let mut state = self.state.lock().await;
303
304 if state.setup_codes.contains(&info) {
305 return Ok(info.name.clone());
306 }
307
308 let local_params = state
309 .local_params
310 .clone()
311 .expect("The endpoint is authenticated.");
312
313 ensure!(
314 info != local_params.setup_code(),
315 "You cannot add your own setup code"
316 );
317
318 ensure!(
319 discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
320 "Guardian has different endpoint variant (TCP/Iroh) than us.",
321 );
322
323 ensure!(
324 info.network == local_params.network,
325 "Guardian uses Bitcoin network {} but we use {}",
326 info.network,
327 local_params.network,
328 );
329
330 if let Some(federation_name) = state
331 .setup_codes
332 .iter()
333 .chain(once(&local_params.setup_code()))
334 .find_map(|info| info.federation_name.clone())
335 {
336 ensure!(
337 info.federation_name.is_none(),
338 "Federation name has already been set to {federation_name}"
339 );
340 }
341
342 if let Some(disable_base_fees) = state
343 .setup_codes
344 .iter()
345 .chain(once(&local_params.setup_code()))
346 .find_map(|info| info.disable_base_fees)
347 {
348 ensure!(
349 info.disable_base_fees.is_none(),
350 "Base fees setting has already been configured to disabled={disable_base_fees}"
351 );
352 }
353
354 if state
355 .setup_codes
356 .iter()
357 .chain(once(&local_params.setup_code()))
358 .any(|info| info.enabled_modules.is_some())
359 {
360 ensure!(
361 info.enabled_modules.is_none(),
362 "Enabled modules have already been configured by another guardian"
363 );
364 }
365
366 if let Some(federation_size) = state
367 .setup_codes
368 .iter()
369 .chain(once(&local_params.setup_code()))
370 .find_map(|info| info.federation_size)
371 {
372 ensure!(
373 info.federation_size.is_none(),
374 "Federation size has already been set to {federation_size}"
375 );
376 }
377
378 state.setup_codes.insert(info.clone());
379
380 Ok(info.name)
381 }
382
383 async fn start_dkg(&self) -> anyhow::Result<()> {
384 let mut state = self.state.lock().await.clone();
385
386 let local_params = state
387 .local_params
388 .clone()
389 .expect("The endpoint is authenticated.");
390
391 let our_setup_code = local_params.setup_code();
392
393 state.setup_codes.insert(our_setup_code.clone());
394
395 ensure!(
396 state.setup_codes.len() == 1 || 4 <= state.setup_codes.len(),
397 "The number of guardians is invalid"
398 );
399
400 if let Some(federation_size) = state
401 .setup_codes
402 .iter()
403 .find_map(|info| info.federation_size)
404 {
405 ensure!(
406 state.setup_codes.len() == federation_size as usize,
407 "Expected {federation_size} guardians but got {}",
408 state.setup_codes.len()
409 );
410 }
411
412 let federation_name = state
413 .setup_codes
414 .iter()
415 .find_map(|info| info.federation_name.clone())
416 .context("We need one guardian to configure the federations name")?;
417
418 let disable_base_fees = state
419 .setup_codes
420 .iter()
421 .find_map(|info| info.disable_base_fees)
422 .unwrap_or(is_env_var_set(FM_DISABLE_BASE_FEES_ENV));
423
424 let enabled_modules = state
425 .setup_codes
426 .iter()
427 .find_map(|info| info.enabled_modules.clone())
428 .unwrap_or_else(|| self.settings.default_modules.clone());
429
430 let our_id = state
431 .setup_codes
432 .iter()
433 .position(|info| info == &our_setup_code)
434 .expect("We inserted the key above.");
435
436 let params = ConfigGenParams {
437 identity: PeerId::from(our_id as u16),
438 tls_key: local_params.tls_key,
439 iroh_api_sk: local_params.iroh_api_sk,
440 iroh_p2p_sk: local_params.iroh_p2p_sk,
441 api_auth: local_params.auth,
442 peers: (0..)
443 .map(|i| PeerId::from(i as u16))
444 .zip(state.setup_codes.clone())
445 .collect(),
446 meta: BTreeMap::from_iter(vec![(
447 META_FEDERATION_NAME_KEY.to_string(),
448 federation_name,
449 )]),
450 disable_base_fees,
451 enabled_modules,
452 network: local_params.network,
453 };
454
455 self.sender
456 .send(params)
457 .await
458 .context("Failed to send config gen params")?;
459
460 Ok(())
461 }
462
463 async fn federation_size(&self) -> Option<u32> {
464 let state = self.state.lock().await;
465 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
466 state
467 .setup_codes
468 .iter()
469 .chain(local_setup_code.iter())
470 .find_map(|info| info.federation_size)
471 }
472
473 async fn cfg_federation_name(&self) -> Option<String> {
474 let state = self.state.lock().await;
475 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
476 state
477 .setup_codes
478 .iter()
479 .chain(local_setup_code.iter())
480 .find_map(|info| info.federation_name.clone())
481 }
482
483 async fn cfg_base_fees_disabled(&self) -> Option<bool> {
484 let state = self.state.lock().await;
485 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
486 state
487 .setup_codes
488 .iter()
489 .chain(local_setup_code.iter())
490 .find_map(|info| info.disable_base_fees)
491 }
492
493 async fn cfg_enabled_modules(&self) -> Option<BTreeSet<ModuleKind>> {
494 let state = self.state.lock().await;
495 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
496 state
497 .setup_codes
498 .iter()
499 .chain(local_setup_code.iter())
500 .find_map(|info| info.enabled_modules.clone())
501 }
502}
503
504#[async_trait]
505impl HasApiContext<SetupApi> for SetupApi {
506 async fn context(
507 &self,
508 request: &ApiRequestErased,
509 id: Option<ModuleInstanceId>,
510 ) -> (&SetupApi, ApiEndpointContext) {
511 assert!(id.is_none());
512
513 let db = self.db.clone();
514
515 let is_authenticated = match self.state.lock().await.local_params {
516 None => false,
517 Some(ref params) => match request.auth.as_ref() {
518 Some(auth) => params.auth.verify(auth.as_str()),
519 None => false,
520 },
521 };
522
523 let context = ApiEndpointContext::new(db, is_authenticated, request.auth.clone());
524
525 (self, context)
526 }
527}
528
529pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
530 vec![
531 api_endpoint! {
532 SETUP_STATUS_ENDPOINT,
533 ApiVersion::new(0, 0),
534 async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
535 Ok(config.setup_status().await)
536 }
537 },
538 api_endpoint! {
539 SET_LOCAL_PARAMS_ENDPOINT,
540 ApiVersion::new(0, 0),
541 async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
542 let auth = context
543 .request_auth()
544 .ok_or(ApiError::bad_request("Missing password".to_string()))?;
545
546 config.set_local_parameters(auth, request.name, request.federation_name, request.disable_base_fees, request.enabled_modules, request.federation_size)
547 .await
548 .map_err(|e| ApiError::bad_request(e.to_string()))
549 }
550 },
551 api_endpoint! {
552 ADD_PEER_SETUP_CODE_ENDPOINT,
553 ApiVersion::new(0, 0),
554 async |config: &SetupApi, context, info: String| -> String {
555 check_auth(context)?;
556
557 config.add_peer_setup_code(info.clone())
558 .await
559 .map_err(|e|ApiError::bad_request(e.to_string()))
560 }
561 },
562 api_endpoint! {
563 RESET_PEER_SETUP_CODES_ENDPOINT,
564 ApiVersion::new(0, 0),
565 async |config: &SetupApi, context, _v: ()| -> () {
566 check_auth(context)?;
567
568 config.reset_setup_codes().await;
569
570 Ok(())
571 }
572 },
573 api_endpoint! {
574 GET_SETUP_CODE_ENDPOINT,
575 ApiVersion::new(0, 0),
576 async |config: &SetupApi, context, _request: ()| -> Option<String> {
577 check_auth(context)?;
578
579 Ok(config.setup_code().await)
580 }
581 },
582 api_endpoint! {
583 START_DKG_ENDPOINT,
584 ApiVersion::new(0, 0),
585 async |config: &SetupApi, context, _v: ()| -> () {
586 check_auth(context)?;
587
588 config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
589 }
590 },
591 ]
592}
593
594#[cfg(test)]
595mod tests {
596 use std::collections::BTreeSet;
597 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
598
599 use bitcoin::Network;
600 use fedimint_core::db::IRawDatabaseExt;
601 use fedimint_core::db::mem_impl::MemDatabase;
602 use fedimint_core::module::ApiAuth;
603 use tokio::sync::mpsc;
604
605 use super::*;
606
607 fn setup_api(network: Network) -> SetupApi {
608 let (sender, _receiver) = mpsc::channel(1);
609 let bind = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
610
611 SetupApi::new(
612 ConfigGenSettings {
613 p2p_bind: bind,
614 api_bind: bind,
615 ui_bind: bind,
616 p2p_url: None,
617 api_url: None,
618 enable_iroh: true,
619 iroh_dns: None,
620 iroh_relays: Vec::new(),
621 network,
622 available_modules: BTreeSet::new(),
623 default_modules: BTreeSet::new(),
624 },
625 MemDatabase::new().into_database(),
626 sender,
627 )
628 }
629
630 async fn setup_code(api: &SetupApi, name: &str) -> String {
631 api.set_local_parameters(
632 ApiAuth::new(format!("{name}-password")),
633 name.to_string(),
634 None,
635 None,
636 None,
637 None,
638 )
639 .await
640 .expect("setting local parameters should succeed")
641 }
642
643 #[tokio::test]
644 async fn accepts_peer_setup_code_with_matching_network() {
645 let api = setup_api(Network::Regtest);
646 let peer_api = setup_api(Network::Regtest);
647
648 setup_code(&api, "local").await;
649 let peer_code = setup_code(&peer_api, "peer").await;
650
651 let added_peer = api
652 .add_peer_setup_code(peer_code)
653 .await
654 .expect("peer setup code with matching network should be accepted");
655
656 assert_eq!(added_peer, "peer");
657 }
658
659 #[tokio::test]
660 async fn rejects_peer_setup_code_with_different_network() {
661 let api = setup_api(Network::Regtest);
662 let peer_api = setup_api(Network::Signet);
663
664 setup_code(&api, "local").await;
665 let peer_code = setup_code(&peer_api, "peer").await;
666
667 let err = api
668 .add_peer_setup_code(peer_code)
669 .await
670 .expect_err("peer setup code with different network should be rejected");
671
672 assert!(
673 err.to_string()
674 .contains("Guardian uses Bitcoin network signet but we use regtest")
675 );
676 }
677}