Skip to main content

fedimint_gateway_server/
federation_status.rs

1use 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 instances of this module generation.
42    configured: Vec<(ModuleInstanceId, ModuleConsensusVersion)>,
43    /// Configured instances that the client initialized.
44    initialized: BTreeSet<ModuleInstanceId>,
45    /// Exact operational instance.
46    operational: Option<ModuleInstanceId>,
47    /// Discovery ownership and retained registration facts.
48    registration: R,
49}
50
51impl<R> LightningModuleSnapshot<R> {
52    /// Converts a coherent module snapshot into its public status.
53    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    /// Requested federation.
82    federation_id: FederationId,
83    /// Aggregated live connection state.
84    connectivity: FederationConnectivity,
85    /// LNv1 module snapshot.
86    lnv1: LightningModuleSnapshot<Lnv1RegistrationStatus>,
87    /// LNv2 module snapshot.
88    lnv2: LightningModuleSnapshot<Lnv2RegistrationStatus>,
89}
90
91impl FederationStatusSnapshot {
92    /// Assembles the complete served response from one bounded snapshot.
93    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    /// Returns public capability and health data scoped to one requested
109    /// federation.
110    pub async fn handle_federation_status(
111        &self,
112        federation_id: FederationId,
113    ) -> Result<FederationStatusResponse> {
114        // Keep the manager read guard until the snapshot is complete. This avoids
115        // cloning the client Arc, which would make concurrent admin leave fail its
116        // exclusive-ownership check; Tokio's writer-preferring lock prevents a
117        // stream of public readers from starving leave.
118        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;