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