fedimint_gateway_server/
federation_status.rs1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::{Context as _, Result};
4use fedimint_connectors::PeerStatus;
5use fedimint_core::config::FederationId;
6use fedimint_core::core::ModuleInstanceId;
7use fedimint_core::module::{CommonModuleInit, ModuleConsensusVersion};
8use fedimint_core::util::Spanned;
9use fedimint_core::{NumPeers, PeerId};
10use fedimint_gateway_common::{
11 FederationConnectivity, FederationStatusResponse, LightningMode, LightningModuleStatus,
12 Lnv1RegistrationStatus, Lnv2RegistrationStatus,
13};
14use fedimint_gateway_server_db::GatewayDbtxNcExt as _;
15use fedimint_gw_client::GatewayClientModule;
16use fedimint_gwv2_client::GatewayClientModuleV2;
17use fedimint_ln_common::LightningCommonInit;
18use fedimint_lnv2_common::LightningCommonInit as LightningV2CommonInit;
19use futures::StreamExt as _;
20use lightning_invoice::RoutingFees;
21
22use crate::{GW_ANNOUNCEMENT_TTL, Gateway};
23
24fn federation_connectivity(
25 connection_status: &BTreeMap<PeerId, PeerStatus>,
26) -> FederationConnectivity {
27 let connected = connection_status
28 .values()
29 .filter(|status| matches!(status, PeerStatus::Connected(_)))
30 .count();
31 if connected == 0 {
32 FederationConnectivity::Disconnected
33 } else if connected < NumPeers::from(connection_status.len()).threshold() {
34 FederationConnectivity::Degraded
35 } else {
36 FederationConnectivity::Connected
37 }
38}
39
40struct LightningModuleSnapshot<R> {
41 configured: Vec<(ModuleInstanceId, ModuleConsensusVersion)>,
43 initialized: BTreeSet<ModuleInstanceId>,
45 operational: Option<ModuleInstanceId>,
47 registration: R,
49}
50
51impl<R> LightningModuleSnapshot<R> {
52 fn into_status(self) -> Result<LightningModuleStatus<R>> {
54 if let Some((uninitialized_id, _)) = self
55 .configured
56 .iter()
57 .find(|(id, _)| !self.initialized.contains(id))
58 {
59 anyhow::bail!("configured Lightning module {uninitialized_id} was not initialized");
60 }
61 let Some(operational_id) = self.operational else {
62 return if self.configured.is_empty() {
63 Ok(LightningModuleStatus::Absent {})
64 } else {
65 anyhow::bail!("configured Lightning module was not initialized")
66 };
67 };
68 let (_, consensus_version) = self
69 .configured
70 .iter()
71 .find(|(configured_id, _)| *configured_id == operational_id)
72 .context("initialized Lightning module is absent from the federation config")?;
73 Ok(LightningModuleStatus::Supported {
74 consensus_version: *consensus_version,
75 registration: self.registration,
76 })
77 }
78}
79
80struct FederationStatusSnapshot {
81 federation_id: FederationId,
83 connectivity: FederationConnectivity,
85 lnv1: LightningModuleSnapshot<Lnv1RegistrationStatus>,
87 lnv2: LightningModuleSnapshot<Lnv2RegistrationStatus>,
89}
90
91impl FederationStatusSnapshot {
92 fn into_response(self) -> Result<FederationStatusResponse> {
94 Ok(FederationStatusResponse::served(
95 self.federation_id,
96 self.connectivity,
97 self.lnv1
98 .into_status()
99 .context("LNv1 status invariant failed")?,
100 self.lnv2
101 .into_status()
102 .context("LNv2 status invariant failed")?,
103 ))
104 }
105}
106
107impl Gateway {
108 pub async fn handle_federation_status(
111 &self,
112 federation_id: FederationId,
113 ) -> Result<FederationStatusResponse> {
114 let federation_manager = self.federation_manager.read().await;
119 let Some(client) = federation_manager
120 .client(&federation_id)
121 .map(Spanned::value)
122 else {
123 return Ok(FederationStatusResponse::unserved(federation_id));
124 };
125
126 let connection_status = client
127 .connection_status_stream()
128 .next()
129 .await
130 .unwrap_or_default();
131 let connectivity = federation_connectivity(&connection_status);
132
133 let config = client.config().await;
134 let lnv1_operational_id = client
135 .get_first_module::<GatewayClientModule>()
136 .ok()
137 .map(|module| module.id);
138 let lnv2_operational_id = client
139 .get_first_module::<GatewayClientModuleV2>()
140 .ok()
141 .map(|module| module.id);
142 let lnv1_configured_modules = config
143 .modules
144 .iter()
145 .filter(|(_, module)| module.kind == LightningCommonInit::KIND)
146 .map(|(id, module)| (*id, module.version))
147 .collect();
148 let lnv2_configured_modules = config
149 .modules
150 .iter()
151 .filter(|(_, module)| module.kind == LightningV2CommonInit::KIND)
152 .map(|(id, module)| (*id, module.version))
153 .collect();
154 let initialized_modules = config
155 .modules
156 .keys()
157 .filter(|id| client.has_module(**id))
158 .copied()
159 .collect::<BTreeSet<_>>();
160
161 let lnv1_registration_configured = matches!(self.lightning_mode, LightningMode::Lnd { .. })
162 && !self.registrations.is_empty()
163 && self
164 .gateway_db
165 .begin_transaction_nc()
166 .await
167 .load_federation_config(federation_id)
168 .await
169 .is_some_and(|config| RoutingFees::try_from(config.lightning_fee).is_ok());
170 let lnv1_endpoints = self
171 .registration_health
172 .lnv1_status(
173 federation_id,
174 self.registrations.keys().cloned(),
175 GW_ANNOUNCEMENT_TTL,
176 fedimint_core::runtime::Instant::now(),
177 )
178 .await;
179 FederationStatusSnapshot {
180 federation_id,
181 connectivity,
182 lnv1: LightningModuleSnapshot {
183 configured: lnv1_configured_modules,
184 initialized: initialized_modules.clone(),
185 operational: lnv1_operational_id,
186 registration: Lnv1RegistrationStatus::GatewayManaged {
187 configured: lnv1_registration_configured,
188 endpoints: lnv1_endpoints,
189 },
190 },
191 lnv2: LightningModuleSnapshot {
192 configured: lnv2_configured_modules,
193 initialized: initialized_modules,
194 operational: lnv2_operational_id,
195 registration: Lnv2RegistrationStatus::FederationManaged,
196 },
197 }
198 .into_response()
199 }
200}
201
202#[cfg(test)]
203mod tests;