Skip to main content

fedimint_server/consensus/
mod.rs

1pub mod aleph_bft;
2pub mod api;
3pub mod db;
4pub mod debug;
5pub mod engine;
6mod iroh_api;
7pub mod transaction;
8
9use std::collections::BTreeMap;
10use std::net::SocketAddr;
11use std::path::PathBuf;
12use std::sync::Arc;
13use std::time::Duration;
14
15use anyhow::{Context as _, bail};
16use async_channel::Sender;
17use db::{ServerDbMigrationContext, get_global_database_migrations};
18use fedimint_api_client::api::DynGlobalApi;
19use fedimint_connectors::ConnectorRegistry;
20use fedimint_core::NumPeers;
21use fedimint_core::config::P2PMessage;
22use fedimint_core::core::{ModuleInstanceId, ModuleKind};
23use fedimint_core::db::{Database, apply_migrations_dbtx, verify_module_db_integrity_dbtx};
24use fedimint_core::envs::is_running_in_test_env;
25use fedimint_core::epoch::ConsensusItem;
26use fedimint_core::module::registry::ModuleRegistry;
27use fedimint_core::module::{ApiAuth, FEDIMINT_API_ALPN};
28use fedimint_core::net::iroh::build_iroh_endpoint;
29use fedimint_core::net::peers::DynP2PConnections;
30use fedimint_core::task::{TaskGroup, sleep};
31use fedimint_core::util::SafeUrl;
32use fedimint_logging::{LOG_CONSENSUS, LOG_CORE};
33use fedimint_server_core::bitcoin_rpc::{DynServerBitcoinRpc, ServerBitcoinRpcMonitor};
34use fedimint_server_core::dashboard_ui::IDashboardApi;
35use fedimint_server_core::migration::apply_migrations_server_dbtx;
36use fedimint_server_core::{DynServerModule, ServerModuleInitRegistry};
37use jsonrpsee::RpcModule;
38use jsonrpsee::server::ServerHandle;
39use tokio::net::TcpListener;
40use tokio::sync::watch;
41use tracing::{debug, info, warn};
42
43use crate::config::{ServerConfig, ServerConfigLocal};
44use crate::connection_limits::ConnectionLimits;
45use crate::consensus::api::ConsensusApi;
46use crate::consensus::engine::ConsensusEngine;
47use crate::consensus::iroh_api::{IrohApiState, run_iroh_api, run_iroh_api_next};
48use crate::db::verify_server_db_integrity_dbtx;
49use crate::net::api::ApiSecrets;
50use crate::net::api::announcement::get_api_urls;
51use crate::net::api::guardian_metadata::{
52    prepare_guardian_metadata_service, reconcile_guardian_metadata, start_guardian_metadata_service,
53};
54use crate::net::iroh::{build_iroh_v1_endpoint, derive_iroh_v1_api_secret_key};
55use crate::net::p2p::P2PStatusReceivers;
56use crate::{DashboardUiRouter, IrohNextApiSettings, net, update_server_info_version_dbtx};
57
58/// How many txs can be stored in memory before blocking the API
59const TRANSACTION_BUFFER: usize = 1000;
60
61struct IrohApiEndpoints {
62    legacy: Option<iroh::Endpoint>,
63    next: Option<iroh_next::Endpoint>,
64}
65
66fn eligible_iroh_next_api_settings(
67    has_legacy_iroh_api: bool,
68    iroh_next_api_settings: Option<&IrohNextApiSettings>,
69) -> Option<&IrohNextApiSettings> {
70    if iroh_next_api_settings.is_some() && !has_legacy_iroh_api {
71        warn!(
72            target: LOG_CONSENSUS,
73            "Not starting the transitional Iroh 1.0 API because this federation was configured \
74             without the legacy Iroh API"
75        );
76        None
77    } else {
78        iroh_next_api_settings
79    }
80}
81
82fn resolve_iroh_next_api_bind(
83    api_bind: SocketAddr,
84    iroh_next_api_settings: Option<&IrohNextApiSettings>,
85) -> anyhow::Result<Option<SocketAddr>> {
86    iroh_next_api_settings
87        .map(|settings| {
88            settings.bind_override().map_or_else(
89                || {
90                    let mut bind = api_bind;
91                    bind.set_port(
92                        bind.port()
93                            .checked_add(10)
94                            .context("Default Iroh 1.0 API bind port would overflow")?,
95                    );
96                    anyhow::Ok(bind)
97                },
98                anyhow::Ok,
99            )
100        })
101        .transpose()
102}
103
104#[cfg(test)]
105#[test]
106fn ineligible_iroh_next_api_does_not_validate_unused_default_bind() {
107    let api_bind = "127.0.0.1:65535".parse().expect("valid socket address");
108    let settings = IrohNextApiSettings::new(None);
109    let settings = eligible_iroh_next_api_settings(false, Some(&settings));
110
111    assert!(
112        resolve_iroh_next_api_bind(api_bind, settings)
113            .expect("ineligible listener should be skipped")
114            .is_none()
115    );
116}
117
118async fn prepare_iroh_api_endpoints(
119    cfg: &ServerConfig,
120    api_bind: SocketAddr,
121    iroh_dns: Option<SafeUrl>,
122    iroh_relays: Vec<SafeUrl>,
123    iroh_next_api_settings: Option<&IrohNextApiSettings>,
124) -> anyhow::Result<IrohApiEndpoints> {
125    let legacy = if let Some(iroh_api_sk) = cfg.private.iroh_api_sk.clone() {
126        Some(
127            build_iroh_endpoint(
128                iroh_api_sk,
129                api_bind,
130                iroh_dns.clone(),
131                iroh_relays,
132                FEDIMINT_API_ALPN,
133            )
134            .await?,
135        )
136    } else {
137        None
138    };
139
140    let next_bind = resolve_iroh_next_api_bind(api_bind, iroh_next_api_settings)?;
141    let next = if let Some(bind) = next_bind {
142        let next_api_sk = derive_iroh_v1_api_secret_key(&cfg.private.broadcast_secret_key);
143        Some(build_iroh_v1_endpoint(next_api_sk, bind, iroh_dns, FEDIMINT_API_ALPN).await?)
144    } else {
145        None
146    };
147
148    Ok(IrohApiEndpoints { legacy, next })
149}
150
151fn spawn_iroh_api_tasks(
152    consensus_api: ConsensusApi,
153    iroh_api_limits: ConnectionLimits,
154    endpoints: IrohApiEndpoints,
155    task_group: &TaskGroup,
156) {
157    let iroh_api = IrohApiState::new(consensus_api, iroh_api_limits);
158
159    if let Some(endpoint) = endpoints.legacy {
160        task_group.spawn_cancellable(
161            "iroh-api",
162            run_iroh_api(iroh_api.clone(), endpoint, task_group.clone()),
163        );
164    }
165
166    if let Some(endpoint) = endpoints.next {
167        task_group.spawn_cancellable(
168            "iroh-next-api",
169            run_iroh_api_next(iroh_api, endpoint, task_group.clone()),
170        );
171    }
172}
173
174#[allow(clippy::too_many_arguments)]
175pub async fn run(
176    connectors: ConnectorRegistry,
177    auth_ui: Option<ApiAuth>,
178    auth_api: Option<ApiAuth>,
179    connections: DynP2PConnections<P2PMessage>,
180    p2p_status_receivers: P2PStatusReceivers,
181    api_bind: SocketAddr,
182    iroh_dns: Option<SafeUrl>,
183    iroh_relays: Vec<SafeUrl>,
184    cfg: ServerConfig,
185    db: Database,
186    module_init_registry: ServerModuleInitRegistry,
187    task_group: &TaskGroup,
188    force_api_secrets: ApiSecrets,
189    data_dir: PathBuf,
190    code_version_str: String,
191    code_version_hash: String,
192    dyn_server_bitcoin_rpc: DynServerBitcoinRpc,
193    ui_bind: SocketAddr,
194    dashboard_ui_router: DashboardUiRouter,
195    db_checkpoint_retention: u64,
196    session_timeout: Duration,
197    iroh_api_limits: ConnectionLimits,
198    iroh_next_api_settings: Option<&IrohNextApiSettings>,
199) -> anyhow::Result<()> {
200    cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
201
202    let iroh_next_api_settings =
203        eligible_iroh_next_api_settings(cfg.private.iroh_api_sk.is_some(), iroh_next_api_settings);
204
205    let mut global_dbtx = db.begin_transaction().await;
206    apply_migrations_server_dbtx(
207        &mut global_dbtx.to_ref_nc(),
208        Arc::new(ServerDbMigrationContext),
209        "fedimint-server".to_string(),
210        get_global_database_migrations(),
211    )
212    .await?;
213
214    update_server_info_version_dbtx(&mut global_dbtx.to_ref_nc(), &code_version_str).await;
215
216    if is_running_in_test_env() {
217        verify_server_db_integrity_dbtx(&mut global_dbtx.to_ref_nc()).await;
218    }
219    global_dbtx.commit_tx_result().await?;
220
221    let mut modules = BTreeMap::new();
222
223    // TODO: make it work with all transports and federation secrets
224    let global_api = DynGlobalApi::new(
225        connectors.clone(),
226        cfg.consensus
227            .api_endpoints()
228            .iter()
229            .map(|(&peer_id, url)| (peer_id, url.url.clone()))
230            .collect(),
231        None,
232    )?;
233
234    let bitcoin_rpc_connection = ServerBitcoinRpcMonitor::new(
235        dyn_server_bitcoin_rpc,
236        if is_running_in_test_env() {
237            Duration::from_millis(100)
238        } else {
239            Duration::from_mins(1)
240        },
241        task_group,
242    );
243
244    for (module_id, module_cfg) in &cfg.consensus.modules {
245        match module_init_registry.get(&module_cfg.kind) {
246            Some(module_init) => {
247                info!(target: LOG_CORE, "Initialise module {module_id}...");
248
249                let mut dbtx = db.begin_transaction().await;
250                apply_migrations_dbtx(
251                    &mut dbtx.to_ref_nc(),
252                    Arc::new(ServerDbMigrationContext) as Arc<_>,
253                    module_init.module_kind().to_string(),
254                    module_init.get_database_migrations(),
255                    Some(*module_id),
256                    None,
257                )
258                .await?;
259
260                if let Some(used_db_prefixes) = module_init.used_db_prefixes()
261                    && is_running_in_test_env()
262                {
263                    verify_module_db_integrity_dbtx(
264                        &mut dbtx.to_ref_nc(),
265                        *module_id,
266                        module_init.module_kind(),
267                        &used_db_prefixes,
268                    )
269                    .await;
270                }
271                dbtx.commit_tx_result().await?;
272
273                let module = module_init
274                    .init(
275                        NumPeers::from(cfg.consensus.api_endpoints().len()),
276                        cfg.get_module_config(*module_id)?,
277                        db.with_prefix_module_id(*module_id).0,
278                        task_group,
279                        cfg.local.identity,
280                        global_api.with_module(*module_id),
281                        bitcoin_rpc_connection.clone(),
282                    )
283                    .await?;
284
285                modules.insert(*module_id, (module_cfg.kind.clone(), module));
286            }
287            None => bail!("Detected configuration for unsupported module id: {module_id}"),
288        }
289    }
290
291    let module_registry = ModuleRegistry::from(modules);
292
293    let client_cfg = cfg.consensus.to_client_config(&module_init_registry)?;
294
295    let (submission_sender, submission_receiver) = async_channel::bounded(TRANSACTION_BUFFER);
296    let (shutdown_sender, shutdown_receiver) = watch::channel(None);
297    let (ord_latency_sender, ord_latency_receiver) = watch::channel(None);
298
299    let mut ci_status_senders = BTreeMap::new();
300    let mut ci_status_receivers = BTreeMap::new();
301
302    for peer in cfg.consensus.broadcast_public_keys.keys().copied() {
303        let (ci_sender, ci_receiver) = watch::channel(None);
304
305        ci_status_senders.insert(peer, ci_sender);
306        ci_status_receivers.insert(peer, ci_receiver);
307    }
308
309    let supported_api_versions =
310        ServerConfig::supported_api_versions_summary(&cfg.consensus.modules, &module_registry);
311    debug!(
312        target: LOG_CONSENSUS,
313        ?supported_api_versions,
314        "Supported API versions",
315    );
316
317    let consensus_api = ConsensusApi {
318        cfg: cfg.clone(),
319        db: db.clone(),
320        modules: module_registry.clone(),
321        client_cfg: client_cfg.clone(),
322        submission_sender: submission_sender.clone(),
323        shutdown_sender,
324        shutdown_receiver: shutdown_receiver.clone(),
325        supported_api_versions,
326        auth_ui,
327        auth_api,
328        p2p_status_receivers,
329        ci_status_receivers,
330        ord_latency_receiver,
331        bitcoin_rpc_connection: bitcoin_rpc_connection.clone(),
332        force_api_secret: force_api_secrets.get_active(),
333        code_version_str,
334        code_version_hash,
335        task_group: task_group.clone(),
336    };
337
338    let guardian_metadata_api =
339        prepare_guardian_metadata_service(&db, &cfg, force_api_secrets.get_active()).await?;
340
341    let iroh_api_endpoints = prepare_iroh_api_endpoints(
342        &cfg,
343        api_bind,
344        iroh_dns,
345        iroh_relays,
346        iroh_next_api_settings,
347    )
348    .await?;
349
350    let guardian_metadata_updated =
351        reconcile_guardian_metadata(&db, &cfg, iroh_next_api_settings).await?;
352
353    info!(target: LOG_CONSENSUS, "Starting Consensus Api...");
354
355    let api_handler = start_consensus_api(
356        &cfg.local,
357        consensus_api.clone(),
358        force_api_secrets.clone(),
359        api_bind,
360    )
361    .await;
362
363    spawn_iroh_api_tasks(
364        consensus_api.clone(),
365        iroh_api_limits,
366        iroh_api_endpoints,
367        task_group,
368    );
369
370    start_guardian_metadata_service(
371        &db,
372        task_group,
373        &cfg,
374        guardian_metadata_api,
375        guardian_metadata_updated,
376    );
377
378    info!(target: LOG_CONSENSUS, "Starting Submission of Module CI proposals...");
379
380    for (module_id, kind, module) in module_registry.iter_modules() {
381        submit_module_ci_proposals(
382            task_group,
383            db.clone(),
384            module_id,
385            kind.clone(),
386            module.clone(),
387            submission_sender.clone(),
388        );
389    }
390
391    let ui_service = dashboard_ui_router(consensus_api.clone().into_dyn()).into_make_service();
392
393    let ui_listener = TcpListener::bind(ui_bind)
394        .await
395        .expect("Failed to bind dashboard UI");
396
397    task_group.spawn("dashboard-ui", move |handle| async move {
398        axum::serve(ui_listener, ui_service)
399            .with_graceful_shutdown(handle.make_shutdown_rx())
400            .await
401            .expect("Failed to serve dashboard UI");
402    });
403
404    info!(target: LOG_CONSENSUS, "Dashboard UI running at http://{ui_bind} 🚀");
405
406    loop {
407        match bitcoin_rpc_connection.status() {
408            Some(status) => {
409                if let Some(progress) = status.sync_progress {
410                    if progress >= 0.999 {
411                        break;
412                    }
413
414                    info!(target: LOG_CONSENSUS, "Waiting for bitcoin backend to sync... {progress:.1}%");
415                } else {
416                    break;
417                }
418            }
419            None => {
420                info!(target: LOG_CONSENSUS, "Waiting to connect to bitcoin backend...");
421            }
422        }
423
424        sleep(Duration::from_secs(1)).await;
425    }
426
427    info!(target: LOG_CONSENSUS, "Starting Consensus Engine...");
428
429    let api_urls = get_api_urls(&db, &cfg.consensus).await;
430
431    // FIXME: (@leonardo) How should this be handled ?
432    // Using the `Connector::default()` for now!
433    ConsensusEngine {
434        db,
435        federation_api: DynGlobalApi::new(
436            connectors,
437            api_urls,
438            force_api_secrets.get_active().as_deref(),
439        )?,
440        cfg: cfg.clone(),
441        connections,
442        ord_latency_sender,
443        ci_status_senders,
444        submission_receiver,
445        shutdown_receiver,
446        modules: module_registry,
447        task_group: task_group.clone(),
448        data_dir,
449        db_checkpoint_retention,
450        session_timeout,
451    }
452    .run()
453    .await?;
454
455    api_handler
456        .stop()
457        .expect("Consensus api should still be running");
458
459    api_handler.stopped().await;
460
461    Ok(())
462}
463
464async fn start_consensus_api(
465    cfg: &ServerConfigLocal,
466    api: ConsensusApi,
467    force_api_secrets: ApiSecrets,
468    api_bind: SocketAddr,
469) -> ServerHandle {
470    let mut rpc_module = RpcModule::new(api.clone());
471
472    net::api::attach_endpoints(&mut rpc_module, api::server_endpoints(), None);
473
474    for (id, _, module) in api.modules.iter_modules() {
475        net::api::attach_endpoints(&mut rpc_module, module.api_endpoints(), Some(id));
476    }
477
478    net::api::spawn(
479        "consensus",
480        api_bind,
481        rpc_module,
482        cfg.max_connections,
483        force_api_secrets,
484    )
485    .await
486}
487
488const CONSENSUS_PROPOSAL_TIMEOUT: Duration = Duration::from_secs(30);
489
490fn submit_module_ci_proposals(
491    task_group: &TaskGroup,
492    db: Database,
493    module_id: ModuleInstanceId,
494    kind: ModuleKind,
495    module: DynServerModule,
496    submission_sender: Sender<ConsensusItem>,
497) {
498    let mut interval = tokio::time::interval(if is_running_in_test_env() {
499        Duration::from_millis(100)
500    } else {
501        Duration::from_secs(1)
502    });
503
504    task_group.spawn(
505        format!("citem_proposals_{module_id}"),
506        move |task_handle| async move {
507            while !task_handle.is_shutting_down() {
508                let module_consensus_items = tokio::time::timeout(
509                    CONSENSUS_PROPOSAL_TIMEOUT,
510                    module.consensus_proposal(
511                        &mut db
512                            .begin_transaction_nc()
513                            .await
514                            .to_ref_with_prefix_module_id(module_id)
515                            .0
516                            .into_nc(),
517                        module_id,
518                    ),
519                )
520                .await;
521
522                match module_consensus_items {
523                    Ok(items) => {
524                        for item in items {
525                            if submission_sender
526                                .send(ConsensusItem::Module(item))
527                                .await
528                                .is_err()
529                            {
530                                warn!(
531                                    target: LOG_CONSENSUS,
532                                    module_id,
533                                    "Unable to submit module consensus item proposal via channel"
534                                );
535                            }
536                        }
537                    }
538                    Err(..) => {
539                        warn!(
540                            target: LOG_CONSENSUS,
541                            module_id,
542                            %kind,
543                            "Module failed to propose consensus items on time"
544                        );
545                    }
546                }
547
548                interval.tick().await;
549            }
550        },
551    );
552}