bark/lightning/
pay.rs

1use std::fmt;
2
3use anyhow::Context;
4use bitcoin::Amount;
5use bitcoin::hex::DisplayHex;
6use lightning::util::ser::Writeable;
7use lnurllib::lightning_address::LightningAddress;
8use log::{error, info, trace, warn};
9use server_rpc::protos::{self, lightning_payment_status::PaymentStatus};
10
11use ark::arkoor::ArkoorPackageBuilder;
12use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, PaymentHash, Preimage};
13use ark::{ProtocolEncoding, VtxoPolicy, VtxoRequest, musig};
14use bitcoin_ext::P2TR_DUST;
15
16use crate::Wallet;
17use crate::lightning::lnaddr_invoice;
18use crate::movement::{MovementDestination, MovementStatus, PaymentMethod};
19use crate::movement::update::MovementUpdate;
20use crate::persist::models::LightningSend;
21use crate::subsystem::{LightningMovement, LightningSendMovement, Subsystem};
22
23
24impl Wallet {
25	/// Performs the revocation of HTLC VTXOs associated with a failed Lightning payment.
26	///
27	/// Builds a revocation package, requests server cosign,
28	/// then constructs new spendable VTXOs from server response.
29	///
30	/// Updates wallet database and movement logs to reflect the failed
31	/// payment and new produced VTXOs; removes the pending send record.
32	///
33	/// # Arguments
34	///
35	/// * `payment` - A reference to the [`LightningSend`] representing the failed payment whose
36	///     associated HTLC VTXOs should be revoked.
37	///
38	/// # Errors
39	///
40	/// Returns an error if revocation fails at any step.
41	///
42	/// # Returns
43	///
44	/// Returns `Ok(())` if revocation succeeds and the wallet state is properly updated.
45	async fn process_lightning_revocation(&self, payment: &LightningSend) -> anyhow::Result<()> {
46		let mut srv = self.require_server()?;
47		let htlc_vtxos = payment.htlc_vtxos.clone().into_iter()
48			.map(|v| v.vtxo).collect::<Vec<_>>();
49
50		info!("Processing {} HTLC VTXOs for revocation", htlc_vtxos.len());
51
52		let mut secs = Vec::with_capacity(htlc_vtxos.len());
53		let mut pubs = Vec::with_capacity(htlc_vtxos.len());
54		let mut keypairs = Vec::with_capacity(htlc_vtxos.len());
55		for input in htlc_vtxos.iter() {
56			let keypair = self.get_vtxo_key(input).await?;
57			let (s, p) = musig::nonce_pair(&keypair);
58			secs.push(s);
59			pubs.push(p);
60			keypairs.push(keypair);
61		}
62
63		let revocation = ArkoorPackageBuilder::new_htlc_revocation(&htlc_vtxos, &pubs)?;
64
65		let req = protos::RevokeLightningPayHtlcRequest {
66			htlc_vtxo_ids: revocation.arkoors.iter()
67				.map(|i| i.input.id().to_bytes().to_vec())
68				.collect(),
69			user_nonces: revocation.arkoors.iter()
70				.map(|i| i.user_nonce.serialize().to_vec())
71				.collect(),
72		};
73		let cosign_resp: Vec<_> = srv.client.request_lightning_pay_htlc_revocation(req).await?
74			.into_inner().try_into().context("invalid server cosign response")?;
75		ensure!(revocation.verify_cosign_response(&cosign_resp),
76			"invalid arkoor cosignature received from server",
77		);
78
79		let (vtxos, _) = revocation.build_vtxos(&cosign_resp, &keypairs, secs)?;
80		let mut revoked = Amount::ZERO;
81		for vtxo in &vtxos {
82			info!("Got revocation VTXO: {}: {}", vtxo.id(), vtxo.amount());
83			revoked += vtxo.amount();
84		}
85
86		let count = vtxos.len();
87		self.movements.finish_movement_with_update(
88			payment.movement_id,
89			MovementStatus::Failed,
90			MovementUpdate::new()
91				.effective_balance(-payment.amount.to_signed()? + revoked.to_signed()?)
92				.produced_vtxos(&vtxos)
93		).await?;
94		self.store_spendable_vtxos(&vtxos).await?;
95		self.mark_vtxos_as_spent(&htlc_vtxos).await?;
96
97		self.db.remove_lightning_send(payment.invoice.payment_hash()).await?;
98
99		info!("Revoked {} HTLC VTXOs", count);
100
101		Ok(())
102	}
103
104	/// Processes the result of a lightning payment by checking the preimage sent by the server and
105	/// completing the payment if successful.
106	///
107	/// Note:
108	/// - That function cannot return an Error if the server provides a valid preimage, meaning
109	/// that if some occur, it is useless to ask for revocation as server wouldn't accept it.
110	/// In that case, it is better to keep the payment pending and try again later
111	///
112	/// # Returns
113	///
114	/// Returns `Ok(Some(Preimage))` if the payment is successfully completed and a preimage is
115	/// received.
116	/// Returns `Ok(None)` if preimage is missing, invalid or does not match the payment hash.
117	/// Returns an `Err` if an error occurs during the payment completion.
118	async fn process_lightning_send_server_preimage(
119		&self,
120		preimage: Option<Vec<u8>>,
121		payment: &LightningSend,
122	) -> anyhow::Result<Option<Preimage>> {
123		let payment_hash = payment.invoice.payment_hash();
124		let preimage_res = preimage
125			.context("preimage is missing")
126			.map(|p| Ok(Preimage::try_from(p)?))
127			.flatten();
128
129		match preimage_res {
130			Ok(preimage) if preimage.compute_payment_hash() == payment_hash => {
131				info!("Lightning payment succeeded! Preimage: {}. Payment hash: {}",
132					preimage.as_hex(), payment.invoice.payment_hash().as_hex());
133
134				// Complete the payment
135				self.db.finish_lightning_send(payment_hash, Some(preimage)).await?;
136				self.mark_vtxos_as_spent(&payment.htlc_vtxos).await?;
137				self.movements.finish_movement(
138					payment.movement_id, MovementStatus::Successful,
139				).await?;
140
141				Ok(Some(preimage))
142			},
143			_ => {
144				error!("Server failed to provide a valid preimage. \
145					Payment hash: {}. Preimage result: {:#?}", payment_hash, preimage_res
146				);
147				Ok(None)
148			}
149		}
150	}
151
152	/// Checks the status of a lightning payment associated with a set of VTXOs, processes the
153	/// payment result and optionally takes appropriate actions based on the payment outcome.
154	///
155	/// # Arguments
156	///
157	/// * `payment_hash` - The [PaymentHash] identifying the lightning payment.
158	/// * `wait`         - If true, asks the server to wait for payment completion (may block longer).
159	///
160	/// # Returns
161	///
162	/// Returns `Ok(Some(Preimage))` if the payment is successfully completed and a preimage is
163	/// received.
164	/// Returns `Ok(None)` for payments still pending, failed payments or if necessary revocation
165	/// or exit processing occurs.
166	/// Returns an `Err` if an error occurs during the process.
167	///
168	/// # Behavior
169	///
170	/// - Validates that all HTLC VTXOs share the same invoice, amount and policy.
171	/// - Sends a request to the Ark server to check the payment status.
172	/// - Depending on the payment status:
173	///   - **Failed**: Revokes the associated VTXOs.
174	///   - **Pending**: Checks if the HTLC has expired based on the tip height. If expired,
175	///     revokes the VTXOs.
176	///   - **Complete**: Extracts the payment preimage, logs the payment, registers movement
177	///     in the database and returns the preimage.
178	pub async fn check_lightning_payment(&self, payment_hash: PaymentHash, wait: bool)
179		-> anyhow::Result<Option<Preimage>>
180	{
181		trace!("Checking lightning payment status for payment hash: {}", payment_hash);
182
183		let mut srv = self.require_server()?;
184
185		let payment = self.db.get_lightning_send(payment_hash).await?
186			.context("no lightning send found for payment hash")?;
187
188		// If the payment already has a preimage, it was already completed successfully
189		if let Some(preimage) = payment.preimage {
190			trace!("Payment already completed with preimage: {}", preimage.as_hex());
191			return Ok(Some(preimage));
192		}
193
194		let policy = payment.htlc_vtxos.first().context("no vtxo provided")?.vtxo.policy();
195		debug_assert!(payment.htlc_vtxos.iter().all(|v| v.vtxo.policy() == policy),
196			"All lightning htlc should have the same policy",
197		);
198		let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
199		if policy.payment_hash != payment_hash {
200			bail!("Payment hash mismatch");
201		}
202
203		let req = protos::CheckLightningPaymentRequest {
204			hash: payment_hash.to_vec(),
205			wait,
206		};
207		// NB: we don't early return on server error or bad response because we
208		// don't want it to prevent us from revoking or exiting HTLCs if necessary.
209		let response = srv.client.check_lightning_payment(req).await
210			.map(|r| r.into_inner().payment_status);
211
212		let tip = self.chain.tip().await?;
213		let expired = tip > policy.htlc_expiry;
214
215		let should_revoke = match response {
216			Ok(Some(PaymentStatus::Success(status))) => {
217				let preimage_opt = self.process_lightning_send_server_preimage(
218					Some(status.preimage), &payment,
219				).await?;
220
221				if let Some(preimage) = preimage_opt {
222					return Ok(Some(preimage));
223				} else {
224					trace!("Server said payment is complete, but has no valid preimage: {:?}", preimage_opt);
225					expired
226				}
227			},
228			Ok(Some(PaymentStatus::Failed(_))) => {
229				info!("Payment failed, revoking VTXO");
230				true
231			},
232			Ok(Some(PaymentStatus::Pending(_))) => {
233				trace!("Payment is still pending");
234				expired
235			},
236			// bad server response or request error
237			Ok(None) | Err(_) => expired,
238		};
239
240		if should_revoke {
241			info!("Revoking HTLC VTXOs for payment {} (tip: {}, expiry: {})",
242				payment_hash, tip, policy.htlc_expiry);
243
244			if let Err(e) = self.process_lightning_revocation(&payment).await {
245				warn!("Failed to revoke VTXO: {}", e);
246
247				// if one of the htlc is about to expire, we exit all of them.
248				// Maybe we want a different behavior here, but we have to decide whether
249				// htlc vtxos revocation is a all or nothing process.
250				let min_expiry = payment.htlc_vtxos.iter()
251					.map(|v| v.vtxo.spec().expiry_height)
252					.min().context("no HTLC VTXOs for expiry check")?;
253
254				if tip > min_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
255					warn!("Some HTLC VTXOs for payment {} are about to expire soon, marking to exit", payment_hash);
256
257					let vtxos = payment.htlc_vtxos
258						.iter()
259						.map(|v| v.vtxo.clone())
260						.collect::<Vec<_>>();
261					self.exit.write().await.start_exit_for_vtxos(&vtxos).await?;
262
263					let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
264					self.movements.finish_movement_with_update(
265						payment.movement_id,
266						MovementStatus::Failed,
267						MovementUpdate::new()
268							.effective_balance(-payment.amount.to_signed()? + exited.to_signed()?)
269							.exited_vtxos(&vtxos)
270					).await?;
271					self.db.finish_lightning_send(payment.invoice.payment_hash(), None).await?;
272				}
273
274				return Err(e)
275			}
276		}
277
278		Ok(None)
279	}
280
281	/// Pays a Lightning [Invoice] using Ark VTXOs. This is also an out-of-round payment
282	/// so the same [Wallet::send_arkoor_payment] rules apply.
283	///
284	/// # Returns
285	///
286	/// Returns the [Invoice] for which payment was initiated.
287	pub async fn pay_lightning_invoice<T>(
288		&self,
289		invoice: T,
290		user_amount: Option<Amount>,
291	) -> anyhow::Result<LightningSend>
292	where
293		T: TryInto<Invoice>,
294		T::Error: std::error::Error + fmt::Display + Send + Sync + 'static,
295	{
296		let invoice = invoice.try_into().context("failed to parse invoice")?;
297		let amount = invoice.get_final_amount(user_amount)?;
298		info!("Sending bolt11 payment of {} to invoice {}", amount, invoice);
299		self.make_lightning_payment(&invoice, invoice.clone().into(), user_amount).await
300	}
301
302	/// Same as [Wallet::pay_lightning_invoice] but instead it pays a [LightningAddress].
303	pub async fn pay_lightning_address(
304		&self,
305		addr: &LightningAddress,
306		amount: Amount,
307		comment: Option<impl AsRef<str>>,
308	) -> anyhow::Result<LightningSend> {
309		let comment = comment.as_ref();
310		let invoice = lnaddr_invoice(addr, amount, comment).await
311			.context("lightning address error")?;
312		info!("Sending {} to lightning address {}", amount, addr);
313		let ret = self.make_lightning_payment(&invoice.into(), addr.clone().into(), None).await
314			.context("bolt11 payment error")?;
315		info!("Paid invoice {}", ret.invoice);
316		Ok(ret)
317	}
318
319	/// Attempts to pay the given BOLT12 [Offer] using offchain funds.
320	pub async fn pay_lightning_offer(
321		&self,
322		offer: Offer,
323		user_amount: Option<Amount>,
324	) -> anyhow::Result<LightningSend> {
325		let mut srv = self.require_server()?;
326
327		let offer_bytes = {
328			let mut bytes = Vec::new();
329			offer.write(&mut bytes).context("failed to serialize BOLT12 offer")?;
330			bytes
331		};
332
333		let req = protos::FetchBolt12InvoiceRequest {
334			offer: offer_bytes,
335			amount_sat: user_amount.map(|a| a.to_sat()),
336		};
337
338		if let Some(amt) = user_amount {
339			info!("Sending bolt12 payment of {} (user amount) to offer {}", amt, offer);
340		} else if let Some(amt) = offer.amount() {
341			info!("Sending bolt12 payment of {:?} (invoice amount) to offer {}", amt, offer);
342		} else {
343			warn!("Paying offer without amount nor user amount provided: {}", offer);
344		}
345
346		let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
347		let invoice = Bolt12Invoice::try_from(resp.invoice)
348			.map_err(|e| anyhow!("invalid invoice: {:?}", e))?;
349
350		invoice.validate_issuance(&offer)
351			.context("invalid BOLT12 invoice received from offer")?;
352
353		let ret = self.make_lightning_payment(&invoice.into(), offer.into(), None).await
354			.context("bolt12 payment error")?;
355		info!("Paid invoice: {:?}", ret.invoice);
356
357		Ok(ret)
358	}
359
360	/// Makes a payment using the Lightning Network. This is a low-level primitive to allow for
361	/// more fine-grained control over the payment process. The primary purpose of using this method
362	/// is to support [PaymentMethod::Custom] for other payment use cases such as LNURL-Pay.
363	///
364	/// It's recommended to use the following higher-level functions where suitable:
365	/// - BOLT11: [Wallet::pay_lightning_invoice]
366	/// - BOLT12: [Wallet::pay_lightning_offer]
367	/// - Lightning Address: [Wallet::pay_lightning_address]
368	///
369	/// # Parameters
370	/// - `invoice`: A reference to the BOLT11/BOLT12 invoice to be paid.
371	/// - `original_payment_method`: The payment method that the given invoice was originally
372	///   derived from (e.g., BOLT11, an offer, lightning address). This will appear in the stored
373	///   [Movement](crate::movement::Movement).
374	/// - `user_amount`: An optional custom amount to override the amount specified in the invoice.
375	///   If not provided, the invoice's amount is used.
376	///
377	/// # Returns
378	/// Returns a `LightningSend` representing the successful payment.
379	/// If an error occurs during the process, an `anyhow::Error` is returned.
380	///
381	/// # Errors
382	/// This function can return an error for the following reasons:
383	/// - If the given payment method is not either an officially supported lightning payment method
384	///   or [PaymentMethod::Custom].
385	/// - The `invoice` belongs to a different network than the one configured in the server's
386	///   properties.
387	/// - The `invoice` has already been paid (the payment hash exists in the database).
388	/// - The `invoice` contains an invalid or tampered signature.
389	/// - The amount to be sent is smaller than the dust limit (`P2TR_DUST`).
390	/// - The wallet doesn't have enough funds to cover the payment.
391	/// - Validation, signing, server or network issues occur.
392	///
393	/// # Notes
394	/// - A movement won't be recorded until we receive an intermediary HTLC VTXO.
395	/// - This is effectively an arkoor payment with an additional HTLC conversion step, so the
396	///   same [Wallet::send_arkoor_payment] rules apply.
397	pub async fn make_lightning_payment(
398		&self,
399		invoice: &Invoice,
400		original_payment_method: PaymentMethod,
401		user_amount: Option<Amount>,
402	) -> anyhow::Result<LightningSend> {
403		if !original_payment_method.is_lightning() && !original_payment_method.is_custom() {
404			bail!("Invalid original payment method for lightning payment");
405		}
406		let mut srv = self.require_server()?;
407
408		let properties = self.db.read_properties().await?.context("Missing config")?;
409		if invoice.network() != properties.network {
410			bail!("Invoice is for wrong network: {}", invoice.network());
411		}
412
413		let lightning_send = self.db.get_lightning_send(invoice.payment_hash()).await?;
414		if lightning_send.is_some() {
415			bail!("Invoice has already been paid");
416		}
417
418		invoice.check_signature()?;
419
420		let amount = invoice.get_final_amount(user_amount)?;
421		if amount < P2TR_DUST {
422			bail!("Sent amount must be at least {}", P2TR_DUST);
423		}
424
425		let (change_keypair, _) = self.derive_store_next_keypair().await?;
426
427		let inputs = self.select_vtxos_to_cover(amount).await
428			.context("Could not find enough suitable VTXOs to cover lightning payment")?;
429
430		let mut secs = Vec::with_capacity(inputs.len());
431		let mut pubs = Vec::with_capacity(inputs.len());
432		let mut keypairs = Vec::with_capacity(inputs.len());
433		let mut input_ids = Vec::with_capacity(inputs.len());
434		for input in inputs.iter() {
435			let keypair = self.get_vtxo_key(input).await?;
436			let (s, p) = musig::nonce_pair(&keypair);
437			secs.push(s);
438			pubs.push(p);
439			keypairs.push(keypair);
440			input_ids.push(input.id());
441		}
442
443		let req = protos::LightningPayHtlcCosignRequest {
444			invoice: invoice.to_string(),
445			user_amount_sat: user_amount.map(|a| a.to_sat()),
446			input_vtxo_ids: input_ids.iter().map(|v| v.to_bytes().to_vec()).collect(),
447			user_nonces: pubs.iter().map(|p| p.serialize().to_vec()).collect(),
448			user_pubkey: change_keypair.public_key().serialize().to_vec(),
449		};
450
451		let resp = srv.client.request_lightning_pay_htlc_cosign(req).await
452			.context("htlc request failed")?.into_inner();
453
454		let cosign_resp = resp.sigs.into_iter().map(|i| i.try_into())
455			.collect::<Result<Vec<_>, _>>()?;
456		let policy = VtxoPolicy::deserialize(&resp.policy)?;
457
458		let pay_req = match &policy {
459			VtxoPolicy::ServerHtlcSend(policy) => {
460				ensure!(policy.user_pubkey == change_keypair.public_key(), "user pubkey mismatch");
461				ensure!(policy.payment_hash == invoice.payment_hash(), "payment hash mismatch");
462				// TODO: ensure expiry is not too high? add new bark config to check against?
463				VtxoRequest { amount: amount, policy: policy.clone().into() }
464			},
465			_ => bail!("invalid policy returned from server"),
466		};
467
468		let builder = ArkoorPackageBuilder::new(
469			&inputs, &pubs, pay_req, Some(change_keypair.public_key()),
470		)?;
471
472		ensure!(builder.verify_cosign_response(&cosign_resp),
473			"invalid arkoor cosignature received from server",
474		);
475
476		let (htlc_vtxos, change_vtxo) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
477
478		// Validate the new vtxos. They have the same chain anchor.
479		let mut effective_balance = Amount::ZERO;
480		for vtxo in &htlc_vtxos {
481			self.validate_vtxo(vtxo).await?;
482			effective_balance += vtxo.amount();
483		}
484
485		let movement_id = self.movements.new_movement_with_update(
486			Subsystem::LIGHTNING_SEND,
487			LightningSendMovement::Send.to_string(),
488			MovementUpdate::new()
489				.intended_balance(-amount.to_signed()?)
490				.effective_balance(-effective_balance.to_signed()?)
491				.consumed_vtxos(&inputs)
492				.sent_to([MovementDestination::new(original_payment_method, amount)])
493		).await?;
494		self.store_locked_vtxos(&htlc_vtxos, Some(movement_id)).await?;
495		self.mark_vtxos_as_spent(&input_ids).await?;
496
497		// Validate the change vtxo. It has the same chain anchor as the last input.
498		if let Some(ref change) = change_vtxo {
499			let last_input = inputs.last().context("no inputs provided")?;
500			let tx = self.chain.get_tx(&last_input.chain_anchor().txid).await?;
501			let tx = tx.with_context(|| {
502				format!("input vtxo chain anchor not found for lightning change vtxo: {}", last_input.chain_anchor().txid)
503			})?;
504			change.validate(&tx).context("invalid lightning change vtxo")?;
505			self.store_spendable_vtxos([change]).await?;
506		}
507
508		self.movements.update_movement(
509			movement_id,
510			MovementUpdate::new()
511				.produced_vtxo_if_some(change_vtxo)
512				.metadata(LightningMovement::metadata(invoice.payment_hash(), &htlc_vtxos))
513		).await?;
514
515		let lightning_send = self.db.store_new_pending_lightning_send(
516			&invoice, &amount,
517			&htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>(),
518			movement_id,
519		).await?;
520
521		let req = protos::InitiateLightningPaymentRequest {
522			invoice: invoice.to_string(),
523			htlc_vtxo_ids: htlc_vtxos.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
524		};
525
526		srv.client.initiate_lightning_payment(req).await?;
527
528		Ok(lightning_send)
529	}
530}