fedimint_ln_client/
api.rs

1use std::collections::{BTreeMap, HashMap};
2use std::convert::identity;
3use std::time::Duration;
4
5use anyhow::anyhow;
6use bitcoin::hashes::sha256::{self, Hash as Sha256Hash};
7use fedimint_api_client::api::{
8    FederationApiExt, FederationResult, IModuleFederationApi, PeerError,
9};
10use fedimint_api_client::query::FilterMapThreshold;
11use fedimint_core::module::ApiRequestErased;
12use fedimint_core::secp256k1::PublicKey;
13use fedimint_core::task::{MaybeSend, MaybeSync, timeout};
14use fedimint_core::{NumPeersExt, PeerId, apply, async_trait_maybe_send};
15use fedimint_ln_common::contracts::incoming::{IncomingContractAccount, IncomingContractOffer};
16use fedimint_ln_common::contracts::{ContractId, DecryptedPreimageStatus, Preimage};
17use fedimint_ln_common::federation_endpoint_constants::{
18    ACCOUNT_ENDPOINT, AWAIT_ACCOUNT_ENDPOINT, AWAIT_BLOCK_HEIGHT_ENDPOINT, AWAIT_OFFER_ENDPOINT,
19    AWAIT_OUTGOING_CONTRACT_CANCELLED_ENDPOINT, AWAIT_PREIMAGE_DECRYPTION, BLOCK_COUNT_ENDPOINT,
20    GET_DECRYPTED_PREIMAGE_STATUS, LIST_GATEWAYS_ENDPOINT, OFFER_ENDPOINT,
21    REGISTER_GATEWAY_ENDPOINT, REMOVE_GATEWAY_CHALLENGE_ENDPOINT, REMOVE_GATEWAY_ENDPOINT,
22};
23use fedimint_ln_common::{
24    ContractAccount, LightningGateway, LightningGatewayAnnouncement, RemoveGatewayRequest,
25};
26use itertools::Itertools;
27use tracing::{info, warn};
28
29#[apply(async_trait_maybe_send!)]
30pub trait LnFederationApi {
31    async fn fetch_consensus_block_count(&self) -> FederationResult<Option<u64>>;
32
33    async fn fetch_contract(
34        &self,
35        contract: ContractId,
36    ) -> FederationResult<Option<ContractAccount>>;
37
38    async fn await_contract(&self, contract: ContractId) -> ContractAccount;
39
40    async fn wait_block_height(&self, block_height: u64);
41
42    async fn wait_outgoing_contract_cancelled(
43        &self,
44        contract: ContractId,
45    ) -> FederationResult<ContractAccount>;
46
47    async fn get_decrypted_preimage_status(
48        &self,
49        contract: ContractId,
50    ) -> FederationResult<(IncomingContractAccount, DecryptedPreimageStatus)>;
51
52    async fn wait_preimage_decrypted(
53        &self,
54        contract: ContractId,
55    ) -> FederationResult<(IncomingContractAccount, Option<Preimage>)>;
56
57    async fn fetch_offer(
58        &self,
59        payment_hash: Sha256Hash,
60    ) -> FederationResult<IncomingContractOffer>;
61
62    async fn fetch_gateways(&self) -> FederationResult<Vec<LightningGatewayAnnouncement>>;
63
64    async fn register_gateway(
65        &self,
66        gateway: &LightningGatewayAnnouncement,
67    ) -> FederationResult<()>;
68
69    /// Retrieves the map of gateway remove challenges from the server. Each
70    /// challenge needs to be signed by the gateway's private key in order
71    /// for the registration record to be removed.
72    async fn get_remove_gateway_challenge(
73        &self,
74        gateway_id: PublicKey,
75    ) -> BTreeMap<PeerId, Option<sha256::Hash>>;
76
77    /// Removes the gateway's registration record. First checks the provided
78    /// signature to verify the gateway authorized the removal of the
79    /// registration.
80    async fn remove_gateway(&self, remove_gateway_request: RemoveGatewayRequest);
81
82    async fn offer_exists(&self, payment_hash: Sha256Hash) -> FederationResult<bool>;
83}
84
85#[apply(async_trait_maybe_send!)]
86impl<T: ?Sized> LnFederationApi for T
87where
88    T: IModuleFederationApi + MaybeSend + MaybeSync + 'static,
89{
90    async fn fetch_consensus_block_count(&self) -> FederationResult<Option<u64>> {
91        self.request_current_consensus(
92            BLOCK_COUNT_ENDPOINT.to_string(),
93            ApiRequestErased::default(),
94        )
95        .await
96    }
97
98    async fn fetch_contract(
99        &self,
100        contract: ContractId,
101    ) -> FederationResult<Option<ContractAccount>> {
102        self.request_current_consensus(
103            ACCOUNT_ENDPOINT.to_string(),
104            ApiRequestErased::new(contract),
105        )
106        .await
107    }
108
109    async fn await_contract(&self, contract: ContractId) -> ContractAccount {
110        self.request_current_consensus_retry(
111            AWAIT_ACCOUNT_ENDPOINT.to_string(),
112            ApiRequestErased::new(contract),
113        )
114        .await
115    }
116
117    async fn wait_block_height(&self, block_height: u64) {
118        self.request_current_consensus_retry::<()>(
119            AWAIT_BLOCK_HEIGHT_ENDPOINT.to_string(),
120            ApiRequestErased::new(block_height),
121        )
122        .await;
123    }
124
125    async fn wait_outgoing_contract_cancelled(
126        &self,
127        contract: ContractId,
128    ) -> FederationResult<ContractAccount> {
129        self.request_current_consensus(
130            AWAIT_OUTGOING_CONTRACT_CANCELLED_ENDPOINT.to_string(),
131            ApiRequestErased::new(contract),
132        )
133        .await
134    }
135
136    async fn get_decrypted_preimage_status(
137        &self,
138        contract: ContractId,
139    ) -> FederationResult<(IncomingContractAccount, DecryptedPreimageStatus)> {
140        self.request_current_consensus(
141            GET_DECRYPTED_PREIMAGE_STATUS.to_string(),
142            ApiRequestErased::new(contract),
143        )
144        .await
145    }
146
147    async fn wait_preimage_decrypted(
148        &self,
149        contract: ContractId,
150    ) -> FederationResult<(IncomingContractAccount, Option<Preimage>)> {
151        self.request_current_consensus(
152            AWAIT_PREIMAGE_DECRYPTION.to_string(),
153            ApiRequestErased::new(contract),
154        )
155        .await
156    }
157
158    async fn fetch_offer(
159        &self,
160        payment_hash: Sha256Hash,
161    ) -> FederationResult<IncomingContractOffer> {
162        self.request_current_consensus(
163            AWAIT_OFFER_ENDPOINT.to_string(),
164            ApiRequestErased::new(payment_hash),
165        )
166        .await
167    }
168
169    /// There is no consensus within Fedimint on the gateways, each guardian
170    /// might be aware of different ones, so we just return the union of all
171    /// responses and allow client selection.
172    async fn fetch_gateways(&self) -> FederationResult<Vec<LightningGatewayAnnouncement>> {
173        let gateway_announcements = self
174            .request_with_strategy(
175                FilterMapThreshold::new(
176                    |_, gateways| Ok(gateways),
177                    self.all_peers().to_num_peers(),
178                ),
179                LIST_GATEWAYS_ENDPOINT.to_string(),
180                ApiRequestErased::default(),
181            )
182            .await?;
183
184        // Filter out duplicate gateways so that we don't have to deal with
185        // multiple guardians having different TTLs for the same gateway.
186        Ok(filter_duplicate_gateways(&gateway_announcements))
187    }
188
189    async fn register_gateway(
190        &self,
191        gateway: &LightningGatewayAnnouncement,
192    ) -> FederationResult<()> {
193        self.request_current_consensus(
194            REGISTER_GATEWAY_ENDPOINT.to_string(),
195            ApiRequestErased::new(gateway),
196        )
197        .await
198    }
199
200    async fn get_remove_gateway_challenge(
201        &self,
202        gateway_id: PublicKey,
203    ) -> BTreeMap<PeerId, Option<sha256::Hash>> {
204        let mut responses = BTreeMap::new();
205
206        for peer in self.all_peers() {
207            // Only wait a second since removing a gateway is "best effort"
208            if let Ok(response) = timeout(
209                Duration::from_secs(1),
210                self.request_single_peer::<Option<sha256::Hash>>(
211                    REMOVE_GATEWAY_CHALLENGE_ENDPOINT.to_string(),
212                    ApiRequestErased::new(gateway_id),
213                    *peer,
214                ),
215            )
216            .await
217            .map_err(|e| PeerError::Transport(anyhow!("Request timed out: {e}")))
218            .and_then(identity)
219            {
220                responses.insert(*peer, response);
221            }
222        }
223
224        responses
225    }
226
227    async fn remove_gateway(&self, remove_gateway_request: RemoveGatewayRequest) {
228        let gateway_id = remove_gateway_request.gateway_id;
229
230        for peer in self.all_peers() {
231            // Only wait a second since removing a gateway is "best effort"
232            if let Ok(response) = timeout(
233                Duration::from_secs(1),
234                self.request_single_peer::<bool>(
235                    REMOVE_GATEWAY_ENDPOINT.to_string(),
236                    ApiRequestErased::new(remove_gateway_request.clone()),
237                    *peer,
238                ),
239            )
240            .await
241            .map_err(|e| PeerError::Transport(anyhow!("Request timed out: {e}")))
242            .and_then(identity)
243            {
244                if response {
245                    info!("Successfully removed {gateway_id} gateway from peer: {peer}",);
246                } else {
247                    warn!("Unable to remove gateway {gateway_id} registration from peer: {peer}");
248                }
249            }
250        }
251    }
252
253    async fn offer_exists(&self, payment_hash: Sha256Hash) -> FederationResult<bool> {
254        Ok(self
255            .request_current_consensus::<Option<IncomingContractOffer>>(
256                OFFER_ENDPOINT.to_string(),
257                ApiRequestErased::new(payment_hash),
258            )
259            .await?
260            .is_some())
261    }
262}
263
264/// Filter out duplicate gateways. This is necessary because different guardians
265/// may have different TTLs for the same gateway, so two
266/// `LightningGatewayAnnouncement`s representing the same gateway registration
267/// may not be equal.
268fn filter_duplicate_gateways(
269    gateways: &BTreeMap<PeerId, Vec<LightningGatewayAnnouncement>>,
270) -> Vec<LightningGatewayAnnouncement> {
271    let gateways_by_gateway_id = gateways
272        .values()
273        .flatten()
274        .cloned()
275        .map(|announcement| (announcement.info.gateway_id, announcement))
276        .into_group_map();
277
278    // For each gateway, we may have multiple announcements with different settings
279    // and/or TTLs. We want to filter out duplicates in a way that doesn't allow a
280    // malicious guardian to override the caller's view of the gateways by
281    // returning a gateway with a shorter TTL. Instead, if we receive multiple
282    // announcements for the same gateway ID, we only filter out announcements
283    // that have the same settings, keeping the one with the longest TTL.
284    gateways_by_gateway_id
285        .into_values()
286        .flat_map(|announcements| {
287            let mut gateways: HashMap<LightningGateway, Duration> = HashMap::new();
288            for announcement in announcements {
289                let ttl = announcement.ttl;
290                let gateway = announcement.info.clone();
291                // Only insert if the TTL is longer than the one we already have
292                gateways
293                    .entry(gateway)
294                    .and_modify(|t| {
295                        if ttl > *t {
296                            *t = ttl;
297                        }
298                    })
299                    .or_insert(ttl);
300            }
301
302            gateways
303                .into_iter()
304                .map(|(gateway, ttl)| LightningGatewayAnnouncement {
305                    info: gateway,
306                    ttl,
307                    vetted: false,
308                })
309        })
310        .collect()
311}