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