fedimint_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::cast_precision_loss)]
5#![allow(clippy::cast_sign_loss)]
6#![allow(clippy::doc_markdown)]
7#![allow(clippy::missing_errors_doc)]
8#![allow(clippy::missing_panics_doc)]
9#![allow(clippy::module_name_repetitions)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::return_self_not_must_use)]
12#![allow(clippy::similar_names)]
13#![allow(clippy::too_many_lines)]
14
15extern crate fedimint_core;
16
17use std::fs;
18use std::path::{Path, PathBuf};
19
20use config::io::{read_server_config, PLAINTEXT_PASSWORD};
21use config::ServerConfig;
22use fedimint_aead::random_salt;
23use fedimint_core::config::ServerModuleInitRegistry;
24use fedimint_core::db::Database;
25use fedimint_core::epoch::ConsensusItem;
26use fedimint_core::task::TaskGroup;
27use fedimint_core::util::write_new;
28use fedimint_logging::{LOG_CONSENSUS, LOG_CORE};
29use net::api::ApiSecrets;
30use tracing::{info, warn};
31
32use crate::config::api::{ConfigGenApi, ConfigGenSettings};
33use crate::config::io::{write_server_config, SALT_FILE};
34use crate::metrics::initialize_gauge_metrics;
35use crate::net::api::announcement::start_api_announcement_service;
36use crate::net::api::RpcHandlerCtx;
37use crate::net::connect::TlsTcpConnector;
38
39pub mod envs;
40pub mod metrics;
41
42/// The actual implementation of consensus
43pub mod consensus;
44
45/// Networking for mint-to-mint and client-to-mint communiccation
46pub mod net;
47
48/// Fedimint toplevel config
49pub mod config;
50
51/// Implementation of multiplexed peer connections
52pub mod multiplexed;
53
54pub async fn run(
55    data_dir: PathBuf,
56    force_api_secrets: ApiSecrets,
57    settings: ConfigGenSettings,
58    db: Database,
59    code_version_str: String,
60    module_init_registry: &ServerModuleInitRegistry,
61    task_group: TaskGroup,
62) -> anyhow::Result<()> {
63    let cfg = match get_config(&data_dir)? {
64        Some(cfg) => cfg,
65        None => {
66            run_config_gen(
67                data_dir.clone(),
68                settings.clone(),
69                db.clone(),
70                code_version_str.clone(),
71                task_group.make_subgroup(),
72                force_api_secrets.clone(),
73            )
74            .await?
75        }
76    };
77
78    let decoders = module_init_registry.decoders_strict(
79        cfg.consensus
80            .modules
81            .iter()
82            .map(|(id, config)| (*id, &config.kind)),
83    )?;
84
85    let db = db.with_decoders(decoders);
86
87    initialize_gauge_metrics(&db).await;
88
89    start_api_announcement_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await;
90
91    consensus::run(
92        settings.p2p_bind,
93        settings.api_bind,
94        cfg,
95        db,
96        module_init_registry.clone(),
97        &task_group,
98        force_api_secrets,
99        data_dir,
100        code_version_str,
101    )
102    .await?;
103
104    info!(target: LOG_CONSENSUS, "Shutting down tasks");
105
106    task_group.shutdown();
107
108    Ok(())
109}
110
111pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
112    // Attempt get the config with local password, otherwise start config gen
113    let path = data_dir.join(PLAINTEXT_PASSWORD);
114    if let Ok(password_untrimmed) = fs::read_to_string(&path) {
115        // We definitely don't want leading/trailing newlines, and user
116        // editing the file manually will probably get a free newline added
117        // by the text editor.
118        let password = password_untrimmed.trim_matches('\n');
119        // In the future we also don't want to support any leading/trailing newlines
120        let password_fully_trimmed = password.trim();
121        if password_fully_trimmed != password {
122            warn!(
123                target: LOG_CORE,
124                path = %path.display(),
125                "Password in the password file contains leading/trailing whitespaces. This will an error in the future."
126            );
127        }
128        return Ok(Some(read_server_config(password, data_dir)?));
129    }
130
131    Ok(None)
132}
133
134pub async fn run_config_gen(
135    data_dir: PathBuf,
136    settings: ConfigGenSettings,
137    db: Database,
138    code_version_str: String,
139    task_group: TaskGroup,
140    force_api_secrets: ApiSecrets,
141) -> anyhow::Result<ServerConfig> {
142    info!(target: LOG_CONSENSUS, "Starting config gen");
143
144    initialize_gauge_metrics(&db).await;
145
146    let (cfg_sender, mut cfg_receiver) = tokio::sync::mpsc::channel(1);
147
148    let config_gen = ConfigGenApi::new(
149        settings.p2p_bind,
150        settings.clone(),
151        db.clone(),
152        cfg_sender,
153        &task_group,
154        code_version_str.clone(),
155        force_api_secrets.get_active(),
156    );
157
158    let mut rpc_module = RpcHandlerCtx::new_module(config_gen);
159
160    net::api::attach_endpoints(&mut rpc_module, config::api::server_endpoints(), None);
161
162    let api_handler = net::api::spawn(
163        "config-gen",
164        settings.api_bind,
165        rpc_module,
166        10,
167        force_api_secrets.clone(),
168    )
169    .await;
170
171    let cfg = cfg_receiver.recv().await.expect("should not close");
172
173    api_handler
174        .stop()
175        .expect("Config api should still be running");
176
177    api_handler.stopped().await;
178
179    // TODO: Make writing password optional
180    write_new(data_dir.join(PLAINTEXT_PASSWORD), &cfg.private.api_auth.0)?;
181    write_new(data_dir.join(SALT_FILE), random_salt())?;
182    write_server_config(
183        &cfg,
184        &data_dir,
185        &cfg.private.api_auth.0,
186        &settings.registry,
187        force_api_secrets.get_active(),
188    )?;
189
190    Ok(cfg)
191}