fedimint_testing/
ln.rs

1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::Duration;
6
7use async_stream::stream;
8use async_trait::async_trait;
9use bitcoin::hashes::{Hash, sha256};
10use bitcoin::key::Keypair;
11use bitcoin::secp256k1::{self, PublicKey, SecretKey};
12use fedimint_core::Amount;
13use fedimint_core::task::TaskGroup;
14use fedimint_core::util::BoxStream;
15use fedimint_gateway_common::{
16    CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, GetInvoiceRequest,
17    GetInvoiceResponse, OpenChannelRequest, SendOnchainRequest,
18};
19use fedimint_lightning::{
20    CreateInvoiceRequest, CreateInvoiceResponse, GetBalancesResponse, GetLnOnchainAddressResponse,
21    GetNodeInfoResponse, GetRouteHintsResponse, ILnRpcClient, InterceptPaymentRequest,
22    InterceptPaymentResponse, LightningRpcError, ListActiveChannelsResponse, OpenChannelResponse,
23    PayInvoiceResponse, RouteHtlcStream, SendOnchainResponse,
24};
25use fedimint_ln_common::PrunedInvoice;
26use fedimint_ln_common::contracts::Preimage;
27use fedimint_ln_common::route_hints::RouteHint;
28use fedimint_logging::LOG_TEST;
29use lightning_invoice::{
30    Bolt11Invoice, Currency, DEFAULT_EXPIRY_TIME, InvoiceBuilder, PaymentSecret,
31};
32use rand::rngs::OsRng;
33use tokio::sync::mpsc;
34use tracing::info;
35
36pub const INVALID_INVOICE_PAYMENT_SECRET: [u8; 32] = [212; 32];
37
38pub const MOCK_INVOICE_PREIMAGE: [u8; 32] = [1; 32];
39
40#[derive(Debug)]
41pub struct FakeLightningTest {
42    pub gateway_node_pub_key: secp256k1::PublicKey,
43    gateway_node_sec_key: secp256k1::SecretKey,
44    amount_sent: AtomicU64,
45}
46
47impl FakeLightningTest {
48    pub fn new() -> Self {
49        info!(target: LOG_TEST, "Setting up fake lightning test fixture");
50        let ctx = bitcoin::secp256k1::Secp256k1::new();
51        let kp = Keypair::new(&ctx, &mut OsRng);
52        let amount_sent = AtomicU64::new(0);
53
54        FakeLightningTest {
55            gateway_node_sec_key: SecretKey::from_keypair(&kp),
56            gateway_node_pub_key: PublicKey::from_keypair(&kp),
57            amount_sent,
58        }
59    }
60}
61
62impl Default for FakeLightningTest {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl FakeLightningTest {
69    pub fn invoice(
70        &self,
71        amount: Amount,
72        expiry_time: Option<u64>,
73    ) -> fedimint_gateway_server::Result<Bolt11Invoice> {
74        let ctx = bitcoin::secp256k1::Secp256k1::new();
75        let payment_hash = sha256::Hash::hash(&MOCK_INVOICE_PREIMAGE);
76
77        Ok(InvoiceBuilder::new(Currency::Regtest)
78            .description(String::new())
79            .payment_hash(payment_hash)
80            .current_timestamp()
81            .min_final_cltv_expiry_delta(0)
82            .payment_secret(PaymentSecret([0; 32]))
83            .amount_milli_satoshis(amount.msats)
84            .expiry_time(Duration::from_secs(
85                expiry_time.unwrap_or(DEFAULT_EXPIRY_TIME),
86            ))
87            .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &self.gateway_node_sec_key))
88            .unwrap())
89    }
90
91    /// Creates an invoice that is not payable
92    ///
93    /// * Mocks use hard-coded invoice description to fail the payment
94    /// * Real fixtures won't be able to route to randomly generated node pubkey
95    pub fn unpayable_invoice(&self, amount: Amount, expiry_time: Option<u64>) -> Bolt11Invoice {
96        let ctx = secp256k1::Secp256k1::new();
97        // Generate fake node keypair
98        let kp = Keypair::new(&ctx, &mut OsRng);
99        let payment_hash = sha256::Hash::hash(&MOCK_INVOICE_PREIMAGE);
100
101        // `FakeLightningTest` will fail to pay any invoice with
102        // `INVALID_INVOICE_DESCRIPTION` in the description of the invoice.
103        InvoiceBuilder::new(Currency::Regtest)
104            .payee_pub_key(kp.public_key())
105            .description("INVALID INVOICE DESCRIPTION".to_string())
106            .payment_hash(payment_hash)
107            .current_timestamp()
108            .min_final_cltv_expiry_delta(0)
109            .payment_secret(PaymentSecret(INVALID_INVOICE_PAYMENT_SECRET))
110            .amount_milli_satoshis(amount.msats)
111            .expiry_time(Duration::from_secs(
112                expiry_time.unwrap_or(DEFAULT_EXPIRY_TIME),
113            ))
114            .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &SecretKey::from_keypair(&kp)))
115            .expect("Invoice creation failed")
116    }
117
118    pub fn listening_address(&self) -> String {
119        "FakeListeningAddress".to_string()
120    }
121}
122
123#[async_trait]
124impl ILnRpcClient for FakeLightningTest {
125    async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
126        Ok(GetNodeInfoResponse {
127            pub_key: self.gateway_node_pub_key,
128            alias: "FakeLightningNode".to_string(),
129            network: "regtest".to_string(),
130            block_height: 0,
131            synced_to_chain: false,
132        })
133    }
134
135    async fn routehints(
136        &self,
137        _num_route_hints: usize,
138    ) -> Result<GetRouteHintsResponse, LightningRpcError> {
139        Ok(GetRouteHintsResponse {
140            route_hints: vec![RouteHint(vec![])],
141        })
142    }
143
144    async fn pay(
145        &self,
146        invoice: Bolt11Invoice,
147        _max_delay: u64,
148        _max_fee: Amount,
149    ) -> Result<PayInvoiceResponse, LightningRpcError> {
150        self.amount_sent.fetch_add(
151            invoice
152                .amount_milli_satoshis()
153                .expect("Invoice missing amount"),
154            Ordering::Relaxed,
155        );
156
157        if *invoice.payment_secret() == PaymentSecret(INVALID_INVOICE_PAYMENT_SECRET) {
158            return Err(LightningRpcError::FailedPayment {
159                failure_reason: "Invoice was invalid".to_string(),
160            });
161        }
162
163        Ok(PayInvoiceResponse {
164            preimage: Preimage(MOCK_INVOICE_PREIMAGE),
165        })
166    }
167
168    fn supports_private_payments(&self) -> bool {
169        true
170    }
171
172    async fn pay_private(
173        &self,
174        invoice: PrunedInvoice,
175        _max_delay: u64,
176        _max_fee: Amount,
177    ) -> Result<PayInvoiceResponse, LightningRpcError> {
178        self.amount_sent
179            .fetch_add(invoice.amount.msats, Ordering::Relaxed);
180
181        if invoice.payment_secret == INVALID_INVOICE_PAYMENT_SECRET {
182            return Err(LightningRpcError::FailedPayment {
183                failure_reason: "Invoice was invalid".to_string(),
184            });
185        }
186
187        Ok(PayInvoiceResponse {
188            preimage: Preimage(MOCK_INVOICE_PREIMAGE),
189        })
190    }
191
192    async fn route_htlcs<'a>(
193        self: Box<Self>,
194        task_group: &TaskGroup,
195    ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
196        let handle = task_group.make_handle();
197        let shutdown_receiver = handle.make_shutdown_rx();
198
199        // `FakeLightningTest` will never intercept any HTLCs because there is no
200        // lightning connection, so instead we just create a stream that blocks
201        // until the task group is shutdown.
202        let (_, mut receiver) = mpsc::channel::<InterceptPaymentRequest>(0);
203        let stream: BoxStream<'a, InterceptPaymentRequest> = Box::pin(stream! {
204            shutdown_receiver.await;
205            // This block, and `receiver`, exist solely to satisfy the type checker.
206            if let Some(htlc_result) = receiver.recv().await {
207                yield htlc_result;
208            }
209        });
210        Ok((stream, Arc::new(Self::new())))
211    }
212
213    async fn complete_htlc(
214        &self,
215        _htlc: InterceptPaymentResponse,
216    ) -> Result<(), LightningRpcError> {
217        Ok(())
218    }
219
220    async fn create_invoice(
221        &self,
222        create_invoice_request: CreateInvoiceRequest,
223    ) -> Result<CreateInvoiceResponse, LightningRpcError> {
224        let ctx = secp256k1::Secp256k1::new();
225
226        let invoice = match create_invoice_request.payment_hash {
227            Some(payment_hash) => InvoiceBuilder::new(Currency::Regtest)
228                .description(String::new())
229                .payment_hash(payment_hash)
230                .current_timestamp()
231                .min_final_cltv_expiry_delta(0)
232                .payment_secret(PaymentSecret([0; 32]))
233                .amount_milli_satoshis(create_invoice_request.amount_msat)
234                .expiry_time(Duration::from_secs(u64::from(
235                    create_invoice_request.expiry_secs,
236                )))
237                .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &self.gateway_node_sec_key))
238                .unwrap(),
239            None => {
240                return Err(LightningRpcError::FailedToGetInvoice {
241                    failure_reason: "FakeLightningTest does not support creating invoices without a payment hash".to_string(),
242                });
243            }
244        };
245
246        Ok(CreateInvoiceResponse {
247            invoice: invoice.to_string(),
248        })
249    }
250
251    async fn get_ln_onchain_address(
252        &self,
253    ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
254        Err(LightningRpcError::FailedToGetLnOnchainAddress {
255            failure_reason: "FakeLightningTest does not support getting a funding address"
256                .to_string(),
257        })
258    }
259
260    async fn send_onchain(
261        &self,
262        _payload: SendOnchainRequest,
263    ) -> Result<SendOnchainResponse, LightningRpcError> {
264        Err(LightningRpcError::FailedToWithdrawOnchain {
265            failure_reason: "FakeLightningTest does not support withdrawing funds on-chain"
266                .to_string(),
267        })
268    }
269
270    async fn open_channel(
271        &self,
272        _payload: OpenChannelRequest,
273    ) -> Result<OpenChannelResponse, LightningRpcError> {
274        Err(LightningRpcError::FailedToOpenChannel {
275            failure_reason: "FakeLightningTest does not support opening channels".to_string(),
276        })
277    }
278
279    async fn close_channels_with_peer(
280        &self,
281        _payload: CloseChannelsWithPeerRequest,
282    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
283        Err(LightningRpcError::FailedToCloseChannelsWithPeer {
284            failure_reason: "FakeLightningTest does not support closing channels by peer"
285                .to_string(),
286        })
287    }
288
289    async fn list_active_channels(&self) -> Result<ListActiveChannelsResponse, LightningRpcError> {
290        Err(LightningRpcError::FailedToListActiveChannels {
291            failure_reason: "FakeLightningTest does not support listing active channels"
292                .to_string(),
293        })
294    }
295
296    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
297        Ok(GetBalancesResponse {
298            onchain_balance_sats: 0,
299            lightning_balance_msats: 0,
300            inbound_lightning_liquidity_msats: 0,
301        })
302    }
303
304    async fn get_invoice(
305        &self,
306        _get_invoice_request: GetInvoiceRequest,
307    ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
308        Err(LightningRpcError::FailedToGetInvoice {
309            failure_reason: "FakeLightningTest does not support getting invoices".to_string(),
310        })
311    }
312}
313
314#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
315pub enum LightningNodeType {
316    Lnd,
317    Ldk,
318}
319
320impl Display for LightningNodeType {
321    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
322        match self {
323            LightningNodeType::Lnd => write!(f, "lnd"),
324            LightningNodeType::Ldk => write!(f, "ldk"),
325        }
326    }
327}
328
329impl FromStr for LightningNodeType {
330    type Err = String;
331
332    fn from_str(s: &str) -> Result<Self, Self::Err> {
333        match s.to_lowercase().as_str() {
334            "lnd" => Ok(LightningNodeType::Lnd),
335            "ldk" => Ok(LightningNodeType::Ldk),
336            _ => Err(format!("Invalid value for LightningNodeType: {s}")),
337        }
338    }
339}