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<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}
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 && 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 ensure!(!name.is_empty(), "The guardian name is empty");
158
159 ensure!(!auth.0.is_empty(), "The password is empty");
160
161 ensure!(
162 auth.0.trim() == auth.0,
163 "The password contains leading/trailing whitespace",
164 );
165
166 if let Some(federation_name) = federation_name.as_ref() {
167 ensure!(!federation_name.is_empty(), "The federation name is empty");
168 }
169
170 let mut state = self.state.lock().await;
171
172 ensure!(
173 state.local_params.is_none(),
174 "Local parameters have already been set"
175 );
176
177 let lp = if self.settings.enable_iroh {
178 warn!(target: LOG_SERVER, "Iroh support is experimental");
179
180 let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
181 SecretKey::from_str(&var)
182 .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
183 } else {
184 SecretKey::generate(&mut OsRng)
185 };
186
187 let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
188 SecretKey::from_str(&var)
189 .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
190 } else {
191 SecretKey::generate(&mut OsRng)
192 };
193
194 LocalParams {
195 auth,
196 tls_key: None,
197 iroh_api_sk: Some(iroh_api_sk.clone()),
198 iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
199 endpoints: PeerEndpoints::Iroh {
200 api_pk: iroh_api_sk.public(),
201 p2p_pk: iroh_p2p_sk.public(),
202 },
203 name,
204 federation_name,
205 }
206 } else {
207 let (tls_cert, tls_key) =
208 gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
209
210 LocalParams {
211 auth,
212 tls_key: Some(tls_key),
213 iroh_api_sk: None,
214 iroh_p2p_sk: None,
215 endpoints: PeerEndpoints::Tcp {
216 api_url: self
217 .settings
218 .api_url
219 .clone()
220 .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
221 p2p_url: self
222 .settings
223 .p2p_url
224 .clone()
225 .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
226
227 cert: tls_cert.as_ref().to_vec(),
228 },
229 name,
230 federation_name,
231 }
232 };
233
234 state.local_params = Some(lp.clone());
235
236 Ok(lp.setup_code().encode_base32())
237 }
238
239 async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
240 let info = PeerSetupCode::decode_base32(&info)?;
241
242 let mut state = self.state.lock().await;
243
244 if state.setup_codes.contains(&info) {
245 return Ok(info.name.clone());
246 }
247
248 let local_params = state
249 .local_params
250 .clone()
251 .expect("The endpoint is authenticated.");
252
253 ensure!(
254 info != local_params.setup_code(),
255 "You cannot add you own connection info"
256 );
257
258 ensure!(
259 discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
260 "Guardian has different endpoint variant (TCP/Iroh) than us.",
261 );
262
263 if let Some(federation_name) = state
264 .setup_codes
265 .iter()
266 .chain(once(&local_params.setup_code()))
267 .find_map(|info| info.federation_name.clone())
268 {
269 ensure!(
270 info.federation_name.is_none(),
271 "Federation name has already been set to {federation_name}"
272 );
273 }
274
275 state.setup_codes.insert(info.clone());
276
277 Ok(info.name)
278 }
279
280 async fn start_dkg(&self) -> anyhow::Result<()> {
281 let mut state = self.state.lock().await.clone();
282
283 let local_params = state
284 .local_params
285 .clone()
286 .expect("The endpoint is authenticated.");
287
288 let our_setup_code = local_params.setup_code();
289
290 state.setup_codes.insert(our_setup_code.clone());
291
292 ensure!(
293 state.setup_codes.len() == 1 || state.setup_codes.len() >= 4,
294 "The number of guardians is invalid"
295 );
296
297 let federation_name = state
298 .setup_codes
299 .iter()
300 .find_map(|info| info.federation_name.clone())
301 .context("We need one guardian to configure the federations name")?;
302
303 let our_id = state
304 .setup_codes
305 .iter()
306 .position(|info| info == &our_setup_code)
307 .expect("We inserted the key above.");
308
309 let params = ConfigGenParams {
310 identity: PeerId::from(our_id as u16),
311 tls_key: local_params.tls_key,
312 iroh_api_sk: local_params.iroh_api_sk,
313 iroh_p2p_sk: local_params.iroh_p2p_sk,
314 api_auth: local_params.auth,
315 peers: (0..)
316 .map(|i| PeerId::from(i as u16))
317 .zip(state.setup_codes.clone().into_iter())
318 .collect(),
319 meta: BTreeMap::from_iter(vec![(
320 META_FEDERATION_NAME_KEY.to_string(),
321 federation_name,
322 )]),
323 };
324
325 self.sender
326 .send(params)
327 .await
328 .context("Failed to send config gen params")?;
329
330 Ok(())
331 }
332}
333
334#[async_trait]
335impl HasApiContext<SetupApi> for SetupApi {
336 async fn context(
337 &self,
338 request: &ApiRequestErased,
339 id: Option<ModuleInstanceId>,
340 ) -> (&SetupApi, ApiEndpointContext<'_>) {
341 assert!(id.is_none());
342
343 let db = self.db.clone();
344 let dbtx = self.db.begin_transaction().await;
345
346 let is_authenticated = match self.state.lock().await.local_params {
347 None => false,
348 Some(ref params) => match request.auth.as_ref() {
349 Some(auth) => *auth == params.auth,
350 None => false,
351 },
352 };
353
354 let context = ApiEndpointContext::new(db, dbtx, is_authenticated, request.auth.clone());
355
356 (self, context)
357 }
358}
359
360pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
361 vec![
362 api_endpoint! {
363 SETUP_STATUS_ENDPOINT,
364 ApiVersion::new(0, 0),
365 async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
366 Ok(config.setup_status().await)
367 }
368 },
369 api_endpoint! {
370 SET_LOCAL_PARAMS_ENDPOINT,
371 ApiVersion::new(0, 0),
372 async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
373 let auth = context
374 .request_auth()
375 .ok_or(ApiError::bad_request("Missing password".to_string()))?;
376
377 config.set_local_parameters(auth, request.name, request.federation_name)
378 .await
379 .map_err(|e| ApiError::bad_request(e.to_string()))
380 }
381 },
382 api_endpoint! {
383 ADD_PEER_SETUP_CODE_ENDPOINT,
384 ApiVersion::new(0, 0),
385 async |config: &SetupApi, context, info: String| -> String {
386 check_auth(context)?;
387
388 config.add_peer_setup_code(info.clone())
389 .await
390 .map_err(|e|ApiError::bad_request(e.to_string()))
391 }
392 },
393 api_endpoint! {
394 RESET_PEER_SETUP_CODES_ENDPOINT,
395 ApiVersion::new(0, 0),
396 async |config: &SetupApi, context, _v: ()| -> () {
397 check_auth(context)?;
398
399 config.reset_setup_codes().await;
400
401 Ok(())
402 }
403 },
404 api_endpoint! {
405 GET_SETUP_CODE_ENDPOINT,
406 ApiVersion::new(0, 0),
407 async |config: &SetupApi, context, _request: ()| -> Option<String> {
408 check_auth(context)?;
409
410 Ok(config.setup_code().await)
411 }
412 },
413 api_endpoint! {
414 START_DKG_ENDPOINT,
415 ApiVersion::new(0, 0),
416 async |config: &SetupApi, context, _v: ()| -> () {
417 check_auth(context)?;
418
419 config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
420 }
421 },
422 ]
423}