bark/lightning/
pay.rs

1use std::fmt;
2
3use anyhow::Context;
4use bitcoin::Amount;
5use bitcoin::hex::DisplayHex;
6use lightning::util::ser::Writeable;
7use lightning_invoice::Bolt11Invoice;
8use lnurllib::lightning_address::LightningAddress;
9use log::{debug, error, info, trace, warn};
10use server_rpc::protos;
11
12use ark::arkoor::ArkoorPackageBuilder;
13use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, Preimage};
14use ark::{ProtocolEncoding, VtxoPolicy, VtxoRequest, musig};
15use bitcoin_ext::P2TR_DUST;
16
17use crate::Wallet;
18use crate::lightning::lnaddr_invoice;
19use crate::movement::{MovementDestination, MovementStatus};
20use crate::movement::update::MovementUpdate;
21use crate::persist::models::LightningSend;
22use crate::subsystem::{BarkSubsystem, LightningMovement, LightningSendMovement};
23
24
25impl Wallet {
26	async fn process_lightning_revocation(&self, payment: &LightningSend) -> anyhow::Result<()> {
27		let mut srv = self.require_server()?;
28		let htlc_vtxos = payment.htlc_vtxos.clone().into_iter()
29			.map(|v| v.vtxo).collect::<Vec<_>>();
30
31		info!("Processing {} HTLC VTXOs for revocation", htlc_vtxos.len());
32
33		let mut secs = Vec::with_capacity(htlc_vtxos.len());
34		let mut pubs = Vec::with_capacity(htlc_vtxos.len());
35		let mut keypairs = Vec::with_capacity(htlc_vtxos.len());
36		for input in htlc_vtxos.iter() {
37			let keypair = self.get_vtxo_key(&input)?;
38			let (s, p) = musig::nonce_pair(&keypair);
39			secs.push(s);
40			pubs.push(p);
41			keypairs.push(keypair);
42		}
43
44		let revocation = ArkoorPackageBuilder::new_htlc_revocation(&htlc_vtxos, &pubs)?;
45
46		let req = protos::RevokeLightningPayHtlcRequest {
47			htlc_vtxo_ids: revocation.arkoors.iter()
48				.map(|i| i.input.id().to_bytes().to_vec())
49				.collect(),
50			user_nonces: revocation.arkoors.iter()
51				.map(|i| i.user_nonce.serialize().to_vec())
52				.collect(),
53		};
54		let cosign_resp: Vec<_> = srv.client.request_lightning_pay_htlc_revocation(req).await?
55			.into_inner().try_into().context("invalid server cosign response")?;
56		ensure!(revocation.verify_cosign_response(&cosign_resp),
57			"invalid arkoor cosignature received from server",
58		);
59
60		let (vtxos, _) = revocation.build_vtxos(&cosign_resp, &keypairs, secs)?;
61		let mut revoked = Amount::ZERO;
62		for vtxo in &vtxos {
63			info!("Got revocation VTXO: {}: {}", vtxo.id(), vtxo.amount());
64			revoked += vtxo.amount();
65		}
66
67		let count = vtxos.len();
68		self.movements.update_movement(
69			payment.movement_id,
70			MovementUpdate::new()
71				.effective_balance(-payment.amount.to_signed()? + revoked.to_signed()?)
72				.produced_vtxos(&vtxos)
73		).await?;
74		self.store_spendable_vtxos(&vtxos)?;
75		self.mark_vtxos_as_spent(&htlc_vtxos)?;
76		self.movements.finish_movement(payment.movement_id, MovementStatus::Failed).await?;
77
78		self.db.remove_lightning_send(payment.invoice.payment_hash())?;
79
80		info!("Revoked {} HTLC VTXOs", count);
81
82		Ok(())
83	}
84
85	/// Processes the result of a lightning payment by checking the preimage sent by the server and
86	/// completing the payment if successful.
87	///
88	/// Note:
89	/// - That function cannot return an Error if the server provides a valid preimage, meaning
90	/// that if some occur, it is useless to ask for revocation as server wouldn't accept it.
91	/// In that case, it is better to keep the payment pending and try again later
92	///
93	/// # Returns
94	///
95	/// Returns `Ok(Some(Preimage))` if the payment is successfully completed and a preimage is
96	/// received.
97	/// Returns `Ok(None)` if preimage is missing, invalid or does not match the payment hash.
98	/// Returns an `Err` if an error occurs during the payment completion.
99	async fn process_lightning_send_server_preimage(
100		&self,
101		preimage: Option<Vec<u8>>,
102		payment: &LightningSend,
103	) -> anyhow::Result<Option<Preimage>> {
104		let payment_hash = payment.invoice.payment_hash();
105		let preimage_res = preimage
106			.context("preimage is missing")
107			.map(|p| Ok(Preimage::try_from(p)?))
108			.flatten();
109
110		match preimage_res {
111			Ok(preimage) if preimage.compute_payment_hash() == payment_hash => {
112				info!("Lightning payment succeeded! Preimage: {}. Payment hash: {}",
113					preimage.as_hex(), payment.invoice.payment_hash().as_hex());
114
115				// Complete the payment
116				self.db.finish_lightning_send(payment_hash, Some(preimage))?;
117				self.mark_vtxos_as_spent(&payment.htlc_vtxos)?;
118				self.movements.finish_movement(payment.movement_id,
119					MovementStatus::Finished).await?;
120
121				Ok(Some(preimage))
122			},
123			_ => {
124				error!("Server failed to provide a valid preimage. \
125					Payment hash: {}. Preimage result: {:#?}", payment_hash, preimage_res
126				);
127				Ok(None)
128			}
129		}
130	}
131
132	/// Checks the status of a lightning payment associated with a set of VTXOs, processes the
133	/// payment result and optionally takes appropriate actions based on the payment outcome.
134	///
135	/// # Arguments
136	///
137	/// * `htlc_vtxos` - Slice of [WalletVtxo] objects that represent HTLC outputs involved in the
138	///                  payment.
139	///
140	/// # Returns
141	///
142	/// Returns `Ok(Some(Preimage))` if the payment is successfully completed and a preimage is
143	/// received.
144	/// Returns `Ok(None)` for payments still pending, failed payments or if necessary revocation
145	/// or exit processing occurs.
146	/// Returns an `Err` if an error occurs during the process.
147	///
148	/// # Behavior
149	///
150	/// - Validates that all HTLC VTXOs share the same invoice, amount and policy.
151	/// - Sends a request to the lightning payment server to check the payment status.
152	/// - Depending on the payment status:
153	///   - **Failed**: Revokes the associated VTXOs.
154	///   - **Pending**: Checks if the HTLC has expired based on the tip height. If expired,
155	///     revokes the VTXOs.
156	///   - **Complete**: Extracts the payment preimage, logs the payment, registers movement
157	///     in the database and returns
158	pub async fn check_lightning_payment(&self, payment: &LightningSend)
159		-> anyhow::Result<Option<Preimage>>
160	{
161		let mut srv = self.require_server()?;
162		let tip = self.chain.tip().await?;
163
164		let payment_hash = payment.invoice.payment_hash();
165
166		let policy = payment.htlc_vtxos.first().context("no vtxo provided")?.vtxo.policy();
167		debug_assert!(payment.htlc_vtxos.iter().all(|v| v.vtxo.policy() == policy),
168			"All lightning htlc should have the same policy",
169		);
170		let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
171		if policy.payment_hash != payment_hash {
172			bail!("Payment hash mismatch");
173		}
174
175		let req = protos::CheckLightningPaymentRequest {
176			hash: policy.payment_hash.to_vec(),
177			wait: false,
178		};
179		let res = srv.client.check_lightning_payment(req).await?.into_inner();
180
181		let payment_status = protos::PaymentStatus::try_from(res.status)?;
182
183		let should_revoke = match payment_status {
184			protos::PaymentStatus::Failed => {
185				info!("Payment failed ({}): revoking VTXO", res.progress_message);
186				true
187			},
188			protos::PaymentStatus::Pending => {
189				if tip > policy.htlc_expiry {
190					trace!("Payment is still pending, but HTLC is expired (tip: {}, \
191						expiry: {}): revoking VTXO", tip, policy.htlc_expiry);
192					true
193				} else {
194					trace!("Payment is still pending and HTLC is not expired (tip: {}, \
195						expiry: {}): doing nothing for now", tip, policy.htlc_expiry);
196					false
197				}
198			},
199			protos::PaymentStatus::Complete => {
200				let preimage_opt = self.process_lightning_send_server_preimage(
201					res.payment_preimage, &payment,
202				).await?;
203
204				if let Some(preimage) = preimage_opt {
205					return Ok(Some(preimage));
206				} else {
207					if tip > policy.htlc_expiry {
208						trace!("Completed payment has no valid preimage and HTLC is \
209							expired (tip: {}, expiry: {}): revoking VTXO", tip, policy.htlc_expiry);
210						true
211					} else {
212						trace!("Completed payment has no valid preimage, but HTLC is \
213							not expired (tip: {}, expiry: {}): doing nothing for now", tip, policy.htlc_expiry);
214						false
215					}
216				}
217			},
218		};
219
220		if should_revoke {
221			if let Err(e) = self.process_lightning_revocation(payment).await {
222				warn!("Failed to revoke VTXO: {}", e);
223
224				// if one of the htlc is about to expire, we exit all of them.
225				// Maybe we want a different behavior here, but we have to decide whether
226				// htlc vtxos revocation is a all or nothing process.
227				let min_expiry = payment.htlc_vtxos.iter()
228					.map(|v| v.vtxo.spec().expiry_height).min().unwrap();
229
230				if tip > min_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
231					warn!("Some VTXO is about to expire soon, marking to exit");
232					let vtxos = payment.htlc_vtxos
233						.iter()
234						.map(|v| v.vtxo.clone())
235						.collect::<Vec<_>>();
236					self.exit.write().await.mark_vtxos_for_exit(&vtxos).await?;
237
238					let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
239					self.movements.update_movement(
240						payment.movement_id,
241						MovementUpdate::new()
242							.effective_balance(-payment.amount.to_signed()? + exited.to_signed()?)
243							.exited_vtxos(&vtxos)
244					).await?;
245					self.movements.finish_movement(
246						payment.movement_id, MovementStatus::Failed,
247					).await?;
248					self.db.finish_lightning_send(payment.invoice.payment_hash(), None)?;
249				}
250			}
251		}
252
253		Ok(None)
254	}
255
256	/// Pays a Lightning [Invoice] using Ark VTXOs. This is also an out-of-round payment
257	/// so the same [Wallet::send_arkoor_payment] rules apply.
258	pub async fn pay_lightning_invoice<T>(
259		&self,
260		invoice: T,
261		user_amount: Option<Amount>,
262	) -> anyhow::Result<Preimage>
263	where
264		T: TryInto<Invoice>,
265		T::Error: std::error::Error + fmt::Display + Send + Sync + 'static,
266	{
267		let mut srv = self.require_server()?;
268
269		let properties = self.db.read_properties()?.context("Missing config")?;
270
271		let invoice = invoice.try_into().context("failed to parse invoice")?;
272		if invoice.network() != properties.network {
273			bail!("Invoice is for wrong network: {}", invoice.network());
274		}
275
276		if self.db.get_lightning_send(invoice.payment_hash())?.is_some() {
277			bail!("Invoice has already been paid");
278		}
279
280		invoice.check_signature()?;
281
282		let amount = invoice.get_final_amount(user_amount)?;
283		if amount < P2TR_DUST {
284			bail!("Sent amount must be at least {}", P2TR_DUST);
285		}
286
287		let (change_keypair, _) = self.derive_store_next_keypair()?;
288
289		let inputs = self.select_vtxos_to_cover(amount, None)
290			.context("Could not find enough suitable VTXOs to cover lightning payment")?;
291
292		let mut secs = Vec::with_capacity(inputs.len());
293		let mut pubs = Vec::with_capacity(inputs.len());
294		let mut keypairs = Vec::with_capacity(inputs.len());
295		let mut input_ids = Vec::with_capacity(inputs.len());
296		for input in inputs.iter() {
297			let keypair = self.get_vtxo_key(&input)?;
298			let (s, p) = musig::nonce_pair(&keypair);
299			secs.push(s);
300			pubs.push(p);
301			keypairs.push(keypair);
302			input_ids.push(input.id());
303		}
304
305		let req = protos::LightningPayHtlcCosignRequest {
306			invoice: invoice.to_string(),
307			user_amount_sat: user_amount.map(|a| a.to_sat()),
308			input_vtxo_ids: input_ids.iter().map(|v| v.to_bytes().to_vec()).collect(),
309			user_nonces: pubs.iter().map(|p| p.serialize().to_vec()).collect(),
310			user_pubkey: change_keypair.public_key().serialize().to_vec(),
311		};
312
313		let resp = srv.client.request_lightning_pay_htlc_cosign(req).await
314			.context("htlc request failed")?.into_inner();
315
316		let cosign_resp = resp.sigs.into_iter().map(|i| i.try_into())
317			.collect::<Result<Vec<_>, _>>()?;
318		let policy = VtxoPolicy::deserialize(&resp.policy)?;
319
320		let pay_req = match policy {
321			VtxoPolicy::ServerHtlcSend(policy) => {
322				ensure!(policy.user_pubkey == change_keypair.public_key(), "user pubkey mismatch");
323				ensure!(policy.payment_hash == invoice.payment_hash(), "payment hash mismatch");
324				// TODO: ensure expiry is not too high? add new bark config to check against?
325				VtxoRequest { amount: amount, policy: policy.into() }
326			},
327			_ => bail!("invalid policy returned from server"),
328		};
329
330		let builder = ArkoorPackageBuilder::new(
331			&inputs, &pubs, pay_req, Some(change_keypair.public_key()),
332		)?;
333
334		ensure!(builder.verify_cosign_response(&cosign_resp),
335			"invalid arkoor cosignature received from server",
336		);
337
338		let (htlc_vtxos, change_vtxo) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
339
340		// Validate the new vtxos. They have the same chain anchor.
341		let mut effective_balance = Amount::ZERO;
342		for vtxo in &htlc_vtxos {
343			self.validate_vtxo(vtxo).await?;
344			effective_balance += vtxo.amount();
345		}
346
347		let movement_id = self.movements.new_movement(
348			self.subsystem_ids[&BarkSubsystem::LightningSend],
349			LightningSendMovement::Send.to_string(),
350		).await?;
351		self.movements.update_movement(
352			movement_id,
353			MovementUpdate::new()
354				.intended_balance(-amount.to_signed()?)
355				.effective_balance(-effective_balance.to_signed()?)
356				.consumed_vtxos(&inputs)
357				.sent_to([MovementDestination::new(invoice.to_string(), amount)])
358		).await?;
359		self.store_locked_vtxos(&htlc_vtxos, Some(movement_id))?;
360		self.mark_vtxos_as_spent(&input_ids)?;
361
362		// Validate the change vtxo. It has the same chain anchor as the last input.
363		if let Some(ref change) = change_vtxo {
364			let last_input = inputs.last().context("no inputs provided")?;
365			let tx = self.chain.get_tx(&last_input.chain_anchor().txid).await?;
366			let tx = tx.with_context(|| {
367				format!("input vtxo chain anchor not found for lightning change vtxo: {}", last_input.chain_anchor().txid)
368			})?;
369			change.validate(&tx).context("invalid lightning change vtxo")?;
370			self.store_spendable_vtxos([change])?;
371		}
372
373		self.movements.update_movement(
374			movement_id,
375			MovementUpdate::new()
376				.produced_vtxo_if_some(change_vtxo)
377				.metadata(LightningMovement::htlc_metadata(&htlc_vtxos)?)
378		).await?;
379
380		let payment = self.db.store_new_pending_lightning_send(
381			&invoice, &amount, &htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>(), movement_id,
382		)?;
383
384		let req = protos::InitiateLightningPaymentRequest {
385			invoice: invoice.to_string(),
386			htlc_vtxo_ids: htlc_vtxos.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
387			wait: true,
388		};
389
390		let res = srv.client.initiate_lightning_payment(req).await?.into_inner();
391		debug!("Progress update: {}", res.progress_message);
392
393		let preimage_opt = self.process_lightning_send_server_preimage(
394			res.payment_preimage, &payment,
395		).await?;
396
397		if let Some(preimage) = preimage_opt {
398			return Ok(preimage);
399		} else {
400			self.process_lightning_revocation(&payment).await?;
401			bail!("Payment failed, but got revocation vtxos: {}", res.progress_message);
402		}
403	}
404
405	/// Same as [Wallet::pay_lightning_invoice] but instead it pays a [LightningAddress].
406	pub async fn pay_lightning_address(
407		&self,
408		addr: &LightningAddress,
409		amount: Amount,
410		comment: Option<&str>,
411	) -> anyhow::Result<(Bolt11Invoice, Preimage)> {
412		let invoice = lnaddr_invoice(addr, amount, comment).await
413			.context("lightning address error")?;
414		info!("Attempting to pay invoice {}", invoice);
415		let preimage = self.pay_lightning_invoice(invoice.clone(), None).await
416			.context("bolt11 payment error")?;
417		Ok((invoice, preimage))
418	}
419
420	/// Attempts to pay the given BOLT12 [Offer] using offchain funds.
421	pub async fn pay_lightning_offer(
422		&self,
423		offer: Offer,
424		amount: Option<Amount>,
425	) -> anyhow::Result<(Bolt12Invoice, Preimage)> {
426		let mut srv = self.require_server()?;
427
428		let offer_bytes = {
429			let mut bytes = Vec::new();
430			offer.write(&mut bytes).unwrap();
431			bytes
432		};
433
434		let req = protos::FetchBolt12InvoiceRequest {
435			offer: offer_bytes,
436			amount_sat: amount.map(|a| a.to_sat()),
437		};
438
439		let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
440
441		let invoice = Bolt12Invoice::try_from(resp.invoice)
442			.map_err(|_| anyhow::anyhow!("invalid invoice"))?;
443
444		invoice.validate_issuance(offer)?;
445
446		let preimage = self.pay_lightning_invoice(invoice.clone(), None).await
447			.context("bolt11 payment error")?;
448		Ok((invoice, preimage))
449	}
450
451}