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