ln.rs
  1  use std::sync::{Arc, Mutex};
  2  use std::time::Duration;
  3  
  4  use async_stream::stream;
  5  use async_trait::async_trait;
  6  use bitcoin::hashes::{sha256, Hash};
  7  use bitcoin::key::KeyPair;
  8  use bitcoin::secp256k1::{PublicKey, SecretKey};
  9  use fedimint_core::task::TaskGroup;
 10  use fedimint_core::util::BoxStream;
 11  use fedimint_core::{secp256k1, Amount};
 12  use fedimint_logging::LOG_TEST;
 13  use lightning_invoice::{
 14      Bolt11Invoice, Bolt11InvoiceDescription, Currency, Description, InvoiceBuilder, PaymentSecret,
 15      SignedRawBolt11Invoice, DEFAULT_EXPIRY_TIME,
 16  };
 17  use ln_gateway::gateway_lnrpc::{
 18      self, CreateInvoiceRequest, CreateInvoiceResponse, EmptyResponse, GetFundingAddressResponse,
 19      GetNodeInfoResponse, GetRouteHintsResponse, InterceptHtlcResponse, PayInvoiceRequest,
 20      PayInvoiceResponse,
 21  };
 22  use ln_gateway::lightning::cln::{HtlcResult, RouteHtlcStream};
 23  use ln_gateway::lightning::{ChannelInfo, ILnRpcClient, LightningRpcError};
 24  use rand::rngs::OsRng;
 25  use tokio::sync::mpsc;
 26  use tracing::info;
 27  
 28  pub const INVALID_INVOICE_DESCRIPTION: &str = "INVALID";
 29  
 30  #[derive(Debug)]
 31  pub struct FakeLightningTest {
 32      pub gateway_node_pub_key: secp256k1::PublicKey,
 33      gateway_node_sec_key: secp256k1::SecretKey,
 34      amount_sent: Arc<Mutex<u64>>,
 35      receiver: mpsc::Receiver<HtlcResult>,
 36  }
 37  
 38  impl FakeLightningTest {
 39      pub fn new() -> Self {
 40          info!(target: LOG_TEST, "Setting up fake lightning test fixture");
 41          let ctx = bitcoin::secp256k1::Secp256k1::new();
 42          let kp = KeyPair::new(&ctx, &mut OsRng);
 43          let amount_sent = Arc::new(Mutex::new(0));
 44          let (_, receiver) = mpsc::channel::<HtlcResult>(10);
 45  
 46          FakeLightningTest {
 47              gateway_node_sec_key: SecretKey::from_keypair(&kp),
 48              gateway_node_pub_key: PublicKey::from_keypair(&kp),
 49              amount_sent,
 50              receiver,
 51          }
 52      }
 53  }
 54  
 55  impl Default for FakeLightningTest {
 56      fn default() -> Self {
 57          Self::new()
 58      }
 59  }
 60  
 61  impl FakeLightningTest {
 62      pub async fn invoice(
 63          &self,
 64          amount: Amount,
 65          expiry_time: Option<u64>,
 66      ) -> ln_gateway::Result<Bolt11Invoice> {
 67          let ctx = bitcoin::secp256k1::Secp256k1::new();
 68  
 69          Ok(InvoiceBuilder::new(Currency::Regtest)
 70              .description("".to_string())
 71              .payment_hash(sha256::Hash::hash(&[0; 32]))
 72              .current_timestamp()
 73              .min_final_cltv_expiry_delta(0)
 74              .payment_secret(PaymentSecret([0; 32]))
 75              .amount_milli_satoshis(amount.msats)
 76              .expiry_time(Duration::from_secs(
 77                  expiry_time.unwrap_or(DEFAULT_EXPIRY_TIME),
 78              ))
 79              .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &self.gateway_node_sec_key))
 80              .unwrap())
 81      }
 82  
 83      /// Creates an invoice that is not payable
 84      ///
 85      /// * Mocks use hard-coded invoice description to fail the payment
 86      /// * Real fixtures won't be able to route to randomly generated node pubkey
 87      pub fn unpayable_invoice(&self, amount: Amount, expiry_time: Option<u64>) -> Bolt11Invoice {
 88          let ctx = bitcoin::secp256k1::Secp256k1::new();
 89          // Generate fake node keypair
 90          let kp = KeyPair::new(&ctx, &mut OsRng);
 91  
 92          // `FakeLightningTest` will fail to pay any invoice with
 93          // `INVALID_INVOICE_DESCRIPTION` in the description of the invoice.
 94          InvoiceBuilder::new(Currency::Regtest)
 95              .payee_pub_key(kp.public_key())
 96              .description(INVALID_INVOICE_DESCRIPTION.to_string())
 97              .payment_hash(sha256::Hash::hash(&[0; 32]))
 98              .current_timestamp()
 99              .min_final_cltv_expiry_delta(0)
100              .payment_secret(PaymentSecret([0; 32]))
101              .amount_milli_satoshis(amount.msats)
102              .expiry_time(Duration::from_secs(
103                  expiry_time.unwrap_or(DEFAULT_EXPIRY_TIME),
104              ))
105              .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &SecretKey::from_keypair(&kp)))
106              .expect("Invoice creation failed")
107      }
108  
109      pub fn listening_address(&self) -> String {
110          "FakeListeningAddress".to_string()
111      }
112  }
113  
114  #[async_trait]
115  impl ILnRpcClient for FakeLightningTest {
116      async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
117          Ok(GetNodeInfoResponse {
118              pub_key: self.gateway_node_pub_key.serialize().to_vec(),
119              alias: "FakeLightningNode".to_string(),
120              network: "regtest".to_string(),
121              block_height: 0,
122              synced_to_chain: false,
123          })
124      }
125  
126      async fn routehints(
127          &self,
128          _num_route_hints: usize,
129      ) -> Result<GetRouteHintsResponse, LightningRpcError> {
130          Ok(GetRouteHintsResponse {
131              route_hints: vec![gateway_lnrpc::get_route_hints_response::RouteHint { hops: vec![] }],
132          })
133      }
134  
135      async fn pay(
136          &self,
137          invoice: PayInvoiceRequest,
138      ) -> Result<PayInvoiceResponse, LightningRpcError> {
139          let signed = invoice.invoice.parse::<SignedRawBolt11Invoice>().unwrap();
140          let invoice = Bolt11Invoice::from_signed(signed).unwrap();
141          *self.amount_sent.lock().unwrap() += invoice.amount_milli_satoshis().unwrap();
142  
143          if invoice.description()
144              == Bolt11InvoiceDescription::Direct(
145                  &Description::new(INVALID_INVOICE_DESCRIPTION.into()).unwrap(),
146              )
147          {
148              return Err(LightningRpcError::FailedPayment {
149                  failure_reason: "Description was invalid".to_string(),
150              });
151          }
152  
153          Ok(PayInvoiceResponse {
154              preimage: [0; 32].to_vec(),
155          })
156      }
157  
158      async fn route_htlcs<'a>(
159          mut self: Box<Self>,
160          task_group: &mut TaskGroup,
161      ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
162          let handle = task_group.make_handle();
163          let shutdown_receiver = handle.make_shutdown_rx().await;
164  
165          // `FakeLightningTest` will never intercept any HTLCs because there is no
166          // lightning connection, so instead we just create a stream that blocks
167          // until the task group is shutdown.
168          let stream: BoxStream<'a, HtlcResult> = Box::pin(stream! {
169              shutdown_receiver.await;
170              if let Some(htlc_result) = self.receiver.recv().await {
171                  yield htlc_result;
172              }
173          });
174          Ok((stream, Arc::new(Self::new())))
175      }
176  
177      async fn complete_htlc(
178          &self,
179          _htlc: InterceptHtlcResponse,
180      ) -> Result<EmptyResponse, LightningRpcError> {
181          Ok(EmptyResponse {})
182      }
183  
184      async fn create_invoice(
185          &self,
186          create_invoice_request: CreateInvoiceRequest,
187      ) -> Result<CreateInvoiceResponse, LightningRpcError> {
188          let ctx = bitcoin::secp256k1::Secp256k1::new();
189  
190          let payment_hash = sha256::Hash::from_slice(&create_invoice_request.payment_hash)
191              .expect("Failed to lookup FederationId");
192          let invoice = InvoiceBuilder::new(Currency::Regtest)
193              .description("".to_string())
194              .payment_hash(payment_hash)
195              .current_timestamp()
196              .min_final_cltv_expiry_delta(0)
197              .payment_secret(PaymentSecret([0; 32]))
198              .amount_milli_satoshis(create_invoice_request.amount_msat)
199              .expiry_time(Duration::from_secs(create_invoice_request.expiry as u64))
200              .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &self.gateway_node_sec_key))
201              .unwrap();
202  
203          Ok(CreateInvoiceResponse {
204              invoice: invoice.to_string(),
205          })
206      }
207  
208      async fn connect_to_peer(
209          &self,
210          _pubkey: bitcoin::secp256k1::PublicKey,
211          _host: String,
212      ) -> Result<EmptyResponse, LightningRpcError> {
213          unimplemented!("FakeLightningTest does not support connecting to peers")
214      }
215  
216      async fn get_funding_address(&self) -> Result<GetFundingAddressResponse, LightningRpcError> {
217          unimplemented!("FakeLightningTest does not support getting a funding address")
218      }
219  
220      async fn open_channel(
221          &self,
222          _pubkey: bitcoin::secp256k1::PublicKey,
223          _channel_size_sats: u64,
224          _push_amount_sats: u64,
225      ) -> Result<EmptyResponse, LightningRpcError> {
226          unimplemented!("FakeLightningTest does not support opening channels")
227      }
228  
229      async fn list_active_channels(&self) -> Result<Vec<ChannelInfo>, LightningRpcError> {
230          unimplemented!("FakeLightningTest does not support listing active channels")
231      }
232  }