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