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