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