Skip to main content

bark/lightning/
receive.rs

1use std::str::FromStr;
2use std::time::Duration;
3
4use anyhow::Context;
5use ark::arkoor::package::ArkoorPackageBuilder;
6use bitcoin::{Amount, SignedAmount};
7use bitcoin::hex::DisplayHex;
8use futures::StreamExt;
9use lightning_invoice::Bolt11Invoice;
10use log::{trace, debug, info, warn};
11
12use ark::{ProtocolEncoding, Vtxo, VtxoPolicy};
13use ark::attestations::{LightningReceiveAttestation};
14use ark::fees::validate_and_subtract_fee;
15use ark::lightning::{Bolt11InvoiceExt, PaymentHash, Preimage};
16use bitcoin_ext::{BlockDelta, BlockHeight};
17use server_rpc::protos;
18use server_rpc::protos::prepare_lightning_receive_claim_request::LightningReceiveAntiDos;
19
20use crate::subsystem::{LightningMovement, LightningReceiveMovement, Subsystem};
21use crate::{Wallet, error};
22use crate::movement::{MovementDestination, MovementStatus};
23use crate::movement::update::MovementUpdate;
24use crate::persist::models::LightningReceive;
25
26const LIGHTNING_RECEIVE_LOCK_PREFIX: &str = "lightning_receive";
27
28/// Leniency delta to allow claim when blocks were mined between htlc
29/// receive and claim preparation
30const LIGHTNING_PREPARE_CLAIM_DELTA: BlockDelta = 2;
31
32/// Initial backoff between retries of a Lightning receive claim. Each
33/// successive retry doubles the wait, capped by [CLAIM_RETRY_BACKOFF_MAX].
34/// With the default 5 retries this gives ~60s of grace before we give up
35/// and exit on-chain, which is enough for a brief server restart.
36const CLAIM_RETRY_BACKOFF_INITIAL: Duration = Duration::from_secs(2);
37const CLAIM_RETRY_BACKOFF_MAX: Duration = Duration::from_secs(30);
38
39fn validate_bolt11_payment_hash(
40	invoice: &Bolt11Invoice,
41	expected_payment_hash: PaymentHash,
42) -> anyhow::Result<()> {
43	let invoice_payment_hash = PaymentHash::from(invoice);
44	ensure!(
45		invoice_payment_hash == expected_payment_hash,
46		"Ark server returned invoice with payment hash {}, expected {}",
47		invoice_payment_hash,
48		expected_payment_hash,
49	);
50
51	Ok(())
52}
53
54impl Wallet {
55	/// Fetches all pending lightning receives ordered from newest to oldest.
56	pub async fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
57		Ok(self.inner.db.get_all_pending_lightning_receives().await?)
58	}
59
60	/// Calculates how much balance can currently be claimed via inbound lightning payments.
61	/// Invoices which have yet to be paid are not including in this.
62	pub async fn claimable_lightning_receive_balance(&self) -> anyhow::Result<Amount> {
63		let receives = self.pending_lightning_receives().await?;
64
65		let mut total = Amount::ZERO;
66		for receive in receives {
67			total += receive.htlc_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
68		}
69
70		Ok(total)
71	}
72
73	/// Create, store and return a [Bolt11Invoice] for offchain boarding.
74	///
75	/// An optional `description` is embedded in the invoice as its memo. Note
76	/// that on retry for the same payment hash, the description of the
77	/// originally-generated invoice is preserved (the server returns the
78	/// previously-issued invoice).
79	pub async fn bolt11_invoice(
80		&self,
81		amount: Amount,
82		description: Option<String>,
83	) -> anyhow::Result<Bolt11Invoice> {
84		if amount == Amount::ZERO {
85			bail!("Cannot create invoice for 0 sats (this would create an explicit 0 sat invoice, not an any-amount invoice)");
86		}
87
88		let (mut srv, ark_info) = self.require_server().await?;
89		let config = self.config();
90
91		// Calculate and validate lightning receive fees
92		let fee = ark_info.fees.lightning_receive.calculate(amount).context("fee overflowed")?;
93		validate_and_subtract_fee(amount, fee)?;
94
95		// User needs to enfore the following delta:
96		// - vtxo exit delta + htlc expiry delta (to give him time to exit the vtxo before htlc expires)
97		// - vtxo exit margin (to give him time to exit the vtxo before htlc expires)
98		// - htlc recv claim delta (to give him time to claim the htlc before it expires)
99		let requested_min_cltv_delta = ark_info.vtxo_exit_delta +
100			ark_info.htlc_expiry_delta +
101			config.vtxo_exit_margin +
102			config.htlc_recv_claim_delta +
103			LIGHTNING_PREPARE_CLAIM_DELTA;
104
105		if requested_min_cltv_delta > ark_info.max_user_invoice_cltv_delta {
106			bail!("HTLC CLTV delta ({}) is greater than Server's max HTLC recv CLTV delta: {}",
107				requested_min_cltv_delta,
108				ark_info.max_user_invoice_cltv_delta,
109			);
110		}
111
112		let preimage = Preimage::random();
113		let payment_hash = preimage.compute_payment_hash();
114		info!("Start bolt11 board with preimage / payment hash: {} / {}",
115			preimage.as_hex(), payment_hash.as_hex());
116
117		let mailbox_kp = self.inner.seed.to_mailbox_keypair();
118		let mailbox_id = ark::mailbox::MailboxIdentifier::from_pubkey(mailbox_kp.public_key());
119
120		let req = protos::StartLightningReceiveRequest {
121			payment_hash: payment_hash.to_vec(),
122			amount_sat: amount.to_sat(),
123			min_cltv_delta: requested_min_cltv_delta as u32,
124			mailbox_id: Some(mailbox_id.serialize()),
125			description,
126		};
127
128		let resp = srv.client.start_lightning_receive(req).await?.into_inner();
129		info!("Ark Server is ready to receive LN payment to invoice: {}.", resp.bolt11);
130
131		let invoice = Bolt11Invoice::from_str(&resp.bolt11)
132			.context("invalid bolt11 invoice returned by Ark server")?;
133		validate_bolt11_payment_hash(&invoice, payment_hash)?;
134
135		self.inner.db.store_lightning_receive(
136			payment_hash,
137			preimage,
138			&invoice,
139			requested_min_cltv_delta,
140		).await?;
141
142		Ok(invoice)
143	}
144
145	/// Fetches the status of a lightning receive for the given [PaymentHash].
146	pub async fn lightning_receive_status(
147		&self,
148		payment: impl Into<PaymentHash>,
149	) -> anyhow::Result<Option<LightningReceive>> {
150		Ok(self.inner.db.fetch_lightning_receive_by_payment_hash(payment.into()).await?)
151	}
152
153	/// Attempts to exit a [LightningReceive] for the given [PaymentHash]. This method is provided
154	/// for the rare situation where HTLCs were received, the preimage was revealed, but the HTLCs
155	/// can't be revoked.
156	///
157	/// Certain preconditions must be met for this method to succeed:
158	/// - The preimage must have been previously revealed
159	/// - The HTLC VTXOs must all be locked
160	pub async fn attempt_lightning_receive_exit(
161		&self,
162		payment: impl Into<PaymentHash>,
163	) -> anyhow::Result<()> {
164		let receive = self.inner.db.fetch_lightning_receive_by_payment_hash(payment.into()).await?
165			.context("no pending lightning receive found for payment hash")?;
166		if receive.preimage_revealed_at.is_none() {
167			bail!("preimage must be revealed before attempting to exit");
168		}
169		if receive.htlc_vtxos.is_empty() {
170			bail!("Nothing to exit, no htlcs have been created yet!");
171		}
172		self.exit_lightning_receive(&receive).await
173	}
174
175	/// Claim given incoming lightning payment.
176	///
177	/// This function reveals the preimage of the lightning payment in
178	/// exchange of getting pubkey VTXOs from HTLC ones
179	///
180	/// # Returns
181	///
182	/// Returns an `anyhow::Result<()>`, which is:
183	/// * `Ok(())` if the process completes successfully.
184	///   the receive object is also updated correctly
185	/// * `Err` if an error occurs at any stage of the operation.
186	///
187	/// # Remarks
188	///
189	/// * The list of HTLC VTXOs must have the hash lock clause matching the given
190	///   [PaymentHash].
191	/// * The preimage is revealed to the server before the cosign response is
192	///   received. If the call fails after that point, the server's
193	///   `claim_lightning_receive` is idempotent so this method can be retried
194	///   to obtain fresh cosign signatures.
195	async fn claim_lightning_receive(
196		&self,
197		receive: &mut LightningReceive,
198	) -> anyhow::Result<()> {
199		let movement_id = receive.movement_id
200			.context("No movement created for lightning receive")?;
201		let (mut srv, _) = self.require_server().await?;
202
203		// order inputs by vtxoid before we generate nonces, then hydrate
204		// to full so the arkoor builder has the genesis chain.
205		ensure!(!receive.htlc_vtxos.is_empty(), "no HTLC VTXOs set on record yet");
206		let mut input_ids = receive.htlc_vtxos.iter().map(|v| v.vtxo.id()).collect::<Vec<_>>();
207		input_ids.sort();
208		let inputs = self.inner.db.get_full_vtxos(&input_ids).await
209			.context("failed to hydrate htlc input vtxos")?;
210
211		let mut keypairs = Vec::with_capacity(inputs.len());
212		for v in &inputs {
213			keypairs.push(self.get_vtxo_key(v).await?);
214		}
215
216		// Claiming arkoor against preimage
217		let (claim_keypair, _) = self.derive_store_next_keypair().await?;
218		let receive_policy = VtxoPolicy::new_pubkey(claim_keypair.public_key());
219
220		trace!("ln arkoor builder params: inputs: {:?}; policy: {:?}", input_ids, receive_policy);
221		let builder = ArkoorPackageBuilder::new_claim_all_with_checkpoints(
222			inputs,
223			receive_policy.clone(),
224		).context("creating claim arkoor builder failed")?;
225		let builder = builder.generate_user_nonces(&keypairs)
226			.context("arkoor nonce generation for claim failed")?;
227
228		info!("Claiming arkoor against payment preimage");
229		self.inner.db.set_preimage_revealed(receive.payment_hash).await?;
230		// NB: also refresh the in-memory receive: if the claim fails from here
231		// on, the failure handling must see that the preimage has been
232		// revealed, or it would wrongly cancel the receive and mark the HTLC
233		// VTXOs as spent.
234		*receive = self.inner.db.fetch_lightning_receive_by_payment_hash(receive.payment_hash).await
235			.context("Database error")?
236			.context("Receive not found")?;
237		let package_cosign_request = protos::ArkoorPackageCosignRequest::from(
238			builder.cosign_request(),
239		);
240		let resp = srv.client.claim_lightning_receive(protos::ClaimLightningReceiveRequest {
241			payment_hash: receive.payment_hash.to_byte_array().to_vec(),
242			payment_preimage: receive.payment_preimage.to_vec(),
243			cosign_request: Some(package_cosign_request),
244		}).await?.into_inner();
245		let cosign_resp = resp.try_into().context("invalid cosign response")?;
246
247		let outputs = builder.user_cosign(&keypairs, cosign_resp)
248			.context("claim arkoor cosign failed with user response")?
249			.build_signed_vtxos();
250
251		// Register the claim output so it is spendable for any later flow.
252		self.register_vtxo_transactions_with_server(&outputs).await?;
253
254		let mut effective_balance = Amount::ZERO;
255		for vtxo in &outputs {
256			// NB: bailing here results in vtxos not being registered despite the
257			// preimage being revealed.  The server's claim_lightning_receive is
258			// idempotent, so bark can retry and obtain fresh cosign signatures,
259			// but if all retries fail the user will be forced to exit on-chain.
260			trace!("Validating Lightning receive claim VTXO {}: {}",
261				vtxo.id(), vtxo.serialize_hex(),
262			);
263			self.validate_vtxo(vtxo).await
264				.context("invalid arkoor from lightning receive")?;
265			effective_balance += vtxo.amount();
266		}
267
268		self.store_spendable_vtxos(&outputs).await?;
269		self.mark_vtxos_as_spent(&receive.htlc_vtxos).await?;
270
271		info!("Got arkoors from lightning: {}",
272			outputs.iter().map(|v| v.id().to_string()).collect::<Vec<_>>().join(", ")
273		);
274
275		self.inner.movements.finish_movement_with_update(
276			movement_id,
277			MovementStatus::Successful,
278			MovementUpdate::new()
279				.effective_balance(effective_balance.to_signed()?)
280				.produced_vtxos(&outputs)
281		).await?;
282
283		self.inner.db.finish_pending_lightning_receive(receive.payment_hash).await?;
284		*receive = self.inner.db.fetch_lightning_receive_by_payment_hash(receive.payment_hash).await
285			.context("Database error")?
286			.context("Receive not found")?;
287
288		Ok(())
289	}
290
291	async fn compute_lightning_receive_anti_dos(
292		&self,
293		payment_hash: PaymentHash,
294		token: Option<&str>,
295	) -> anyhow::Result<LightningReceiveAntiDos> {
296		Ok(if let Some(token) = token {
297			LightningReceiveAntiDos::Token(token.to_string())
298		} else {
299			// We get an existing VTXO as an anti-dos measure.
300			let vtxo = self.select_vtxos_to_cover(Amount::ONE_SAT).await
301				.and_then(|vtxos| vtxos.into_iter().next()
302					.context("have no spendable vtxo to prove ownership of")
303				)?;
304			let vtxo_keypair = self.get_vtxo_key(&vtxo).await.expect("owned vtxo should be in database");
305			let attestation = LightningReceiveAttestation::new(payment_hash, vtxo.id(), &vtxo_keypair);
306			LightningReceiveAntiDos::InputVtxo(protos::InputVtxo {
307				vtxo_id: vtxo.id().to_bytes().to_vec(),
308				attestation: attestation.serialize(),
309			})
310		})
311	}
312
313	/// Check for incoming lightning payment with the given [PaymentHash].
314	///
315	/// This function checks for an incoming lightning payment with the
316	/// given [PaymentHash] and returns the HTLC VTXOs that are associated
317	/// with it.
318	///
319	/// # Arguments
320	///
321	/// * `payment_hash` - The [PaymentHash] of the lightning payment
322	/// to check for.
323	/// * `wait` - Whether to wait for the payment to be initiated by the sender.
324	/// * `token` - An optional lightning receive token used to authenticate a lightning
325	/// receive when no spendable VTXOs are owned by this wallet.
326	///
327	/// # Returns
328	///
329	/// Returns an `anyhow::Result<Option<LightningReceive>>`, which is:
330	/// * `Ok(Some(lightning_receive))` if the payment was initiated by
331	///   the sender and the HTLC VTXOs were successfully prepared.
332	/// * `Ok(None)` if the payment was not initiated by the sender or
333	///   the payment was canceled by server.
334	/// * `Err` if an error occurs at any stage of the operation.
335	///
336	/// # Remarks
337	///
338	/// * The invoice must contain an explicit amount specified in milli-satoshis.
339	/// * The HTLC expiry height is calculated by adding the servers' HTLC expiry delta to the
340	///   current chain tip.
341	/// * The payment hash must be from an invoice previously generated using
342	///   [Wallet::bolt11_invoice].
343	async fn check_lightning_receive(
344		&self,
345		payment_hash: PaymentHash,
346		wait: bool,
347		token: Option<&str>,
348	) -> anyhow::Result<Option<LightningReceive>> {
349		let (mut srv, ark_info) = self.require_server().await?;
350		let current_height = self.inner.chain.tip().await?;
351
352		let mut receive = self.inner.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
353			.context("no pending lightning receive found for payment hash, might already be claimed")?;
354
355		// If we have already HTLC VTXOs stored, we can return them without asking the server
356		if !receive.htlc_vtxos.is_empty() {
357			return Ok(Some(receive))
358		}
359
360		trace!("Requesting updates for ln-receive to server with for wait={} and hash={}", wait, payment_hash);
361		let sub = srv.client.check_lightning_receive(protos::CheckLightningReceiveRequest {
362			hash: payment_hash.to_byte_array().to_vec(), wait,
363		}).await?.into_inner();
364
365
366		let status = protos::LightningReceiveStatus::try_from(sub.status)
367			.with_context(|| format!("unknown payment status: {}", sub.status))?;
368
369		debug!("Received status {:?} for {}", status, payment_hash);
370		match status {
371			// this is the good case
372			protos::LightningReceiveStatus::Accepted |
373			protos::LightningReceiveStatus::HtlcsReady => {},
374			protos::LightningReceiveStatus::Created => {
375				return Ok(None);
376			},
377			protos::LightningReceiveStatus::Settled => bail!("payment already settled"),
378			protos::LightningReceiveStatus::Canceled => {
379				warn!("payment was canceled. removing pending lightning receive");
380				self.handle_failed_lightning_receive(&receive).await?;
381				return Ok(None);
382			},
383		}
384
385		let lightning_receive_anti_dos = match self.compute_lightning_receive_anti_dos(
386			payment_hash, token,
387		).await {
388			Ok(anti_dos) => Some(anti_dos),
389			Err(e) => {
390				info!("Could not compute anti-dos: {e:#}. Trying without");
391				None
392			},
393		};
394
395		let htlc_recv_expiry = current_height + receive.htlc_recv_cltv_delta as BlockHeight;
396
397		let (next_keypair, _) = self.derive_store_next_keypair().await?;
398		let req = protos::PrepareLightningReceiveClaimRequest {
399			payment_hash: receive.payment_hash.to_vec(),
400			user_pubkey: next_keypair.public_key().serialize().to_vec(),
401			htlc_recv_expiry,
402			lightning_receive_anti_dos,
403		};
404		let res = srv.client.prepare_lightning_receive_claim(req).await
405			.context("error preparing lightning receive claim")?.into_inner();
406		let vtxos = res.htlc_vtxos.into_iter()
407			.map(|b| Vtxo::deserialize(&b))
408			.collect::<Result<Vec<_>, _>>()
409			.context("invalid htlc vtxos from server")?;
410
411		// sanity check the vtxos
412		let mut htlc_amount = Amount::ZERO;
413		for vtxo in &vtxos {
414			trace!("Received HTLC VTXO {} from server: {}", vtxo.id(), vtxo.serialize_hex());
415			self.validate_vtxo(vtxo).await
416				.context("received invalid HTLC VTXO from server")?;
417			htlc_amount += vtxo.amount();
418
419			if let VtxoPolicy::ServerHtlcRecv(p) = vtxo.policy() {
420				if p.payment_hash != receive.payment_hash {
421					bail!("invalid payment hash on HTLC VTXOs received from server: {}",
422						p.payment_hash,
423					);
424				}
425				if p.user_pubkey != next_keypair.public_key() {
426					bail!("invalid pubkey on HTLC VTXOs received from server: {}", p.user_pubkey);
427				}
428				if p.htlc_expiry < htlc_recv_expiry {
429					bail!("HTLC VTXO expiry height is less than requested: Requested {}, received {}", htlc_recv_expiry, p.htlc_expiry);
430				}
431			} else {
432				bail!("invalid HTLC VTXO policy: {:?}", vtxo.policy());
433			}
434		}
435
436		// Check that the sum exceeds the invoice amount; we can't entirely trust the
437		// server-reported payment amount, so if there is a discrepancy, we should fall back to
438		// checking the invoice amount.
439		let invoice_amount = receive.invoice.get_payment_amount(None)
440			.context("ln receive invoice should have amount")?;
441		let server_received_amount = res.receive.map(|r| Amount::from_sat(r.amount_sat));
442		let fee = {
443			let fee = server_received_amount
444				.and_then(|a| ark_info.fees.lightning_receive.calculate(a));
445			match (server_received_amount, fee) {
446				(Some(amount), Some(fee)) if htlc_amount + fee == amount => {
447					// If this is true then the server is telling the truth.
448					fee
449				},
450				_ => {
451					// We should verify against the invoice amount instead. Unfortunately, that
452					// means the fee value in the movement won't be entirely accurate, however, it's
453					// better to avoid rejecting payments when we have received enough to cover an
454					// invoice.
455					ark_info.fees.lightning_receive.calculate(invoice_amount)
456						.expect("we previously validated this")
457				}
458			}
459		};
460		let received = htlc_amount + fee;
461		ensure!(received >= invoice_amount,
462			"Server didn't return enough VTXOs to cover invoice amount"
463		);
464
465		let movement_id = if let Some(movement_id) = receive.movement_id {
466			movement_id
467		} else {
468			self.inner.movements.new_movement_with_update(
469				Subsystem::LIGHTNING_RECEIVE,
470				LightningReceiveMovement::Receive.to_string(),
471				MovementUpdate::new()
472					.intended_balance(invoice_amount.to_signed()?)
473					.effective_balance(htlc_amount.to_signed()?)
474					.fee(fee)
475					.metadata(LightningMovement::metadata(
476						receive.payment_hash, &vtxos, Some(receive.payment_preimage),
477					))
478					.received_on(
479						[MovementDestination::new(receive.invoice.clone().into(), received)],
480					),
481			).await?
482		};
483		self.store_locked_vtxos(
484			&vtxos,
485			Some(crate::vtxo::VtxoLockHolder::Movement { id: movement_id }),
486		).await?;
487
488		let vtxo_ids = vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
489		self.inner.db.update_lightning_receive(payment_hash, &vtxo_ids, movement_id).await?;
490
491		let mut wallet_vtxos = vec![];
492		for vtxo in vtxos {
493			let v =  self.inner.db.get_wallet_vtxo(vtxo.id()).await?
494				.context("Failed to get wallet VTXO for lightning receive")?;
495			wallet_vtxos.push(v);
496		}
497
498		receive.htlc_vtxos = wallet_vtxos;
499		receive.movement_id = Some(movement_id);
500
501		Ok(Some(receive))
502	}
503
504	/// Exit HTLC-recv VTXOs when preimage has been disclosed but the claim failed.
505	///
506	/// NOTE: Calling this function will always result in the HTLC VTXO being exited
507	/// regardless of the presence of the `preimage_revealed_at` field of
508	/// the `lightning_receive` struct.
509	async fn exit_lightning_receive(
510		&self,
511		lightning_receive: &LightningReceive,
512	) -> anyhow::Result<()> {
513		ensure!(!lightning_receive.htlc_vtxos.is_empty(), "no HTLC VTXOs to exit");
514		let vtxos = lightning_receive.htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
515
516		info!("Exiting HTLC VTXOs for lightning_receive with payment hash {}", lightning_receive.payment_hash);
517		self.inner.exit.start_exit_for_vtxos(&vtxos).await?;
518
519		if let Some(movement_id) = lightning_receive.movement_id {
520			self.inner.movements.finish_movement_with_update(
521				movement_id,
522				MovementStatus::Failed,
523				MovementUpdate::new().exited_vtxos(vtxos),
524			).await?;
525		} else {
526			error!("movement id is missing but we disclosed preimage: {}", lightning_receive.payment_hash);
527		}
528
529		self.inner.db.finish_pending_lightning_receive(lightning_receive.payment_hash).await?;
530		Ok(())
531	}
532
533	pub(crate) async fn handle_failed_lightning_receive(
534		&self,
535		lightning_receive: &LightningReceive,
536	) -> anyhow::Result<()> {
537		let vtxos = &lightning_receive.htlc_vtxos;
538
539		let update_opt = match (vtxos.is_empty(), lightning_receive.preimage_revealed_at) {
540			(false, Some(_)) => {
541				// Preimage was disclosed but the claim failed: the HTLC VTXOs are
542				// still locked and need to be exited on-chain. We don't auto-exit
543				// anymore — leave the receive pending and let the caller decide
544				// when to call `attempt_lightning_receive_exit`.
545				return Ok(());
546			}
547			(false, None) => {
548				warn!("HTLC-recv VTXOs are about to expire, but preimage has not been disclosed yet. Canceling");
549				self.mark_vtxos_as_spent(vtxos).await?;
550				if let Some(movement_id) = lightning_receive.movement_id {
551					Some((
552						movement_id,
553						MovementUpdate::new()
554							.effective_balance(SignedAmount::ZERO),
555						MovementStatus::Canceled,
556					))
557				} else {
558					error!("movement id is missing but we got HTLC vtxos: {}", lightning_receive.payment_hash);
559					None
560				}
561			}
562			(true, Some(_)) => {
563				error!("No HTLC vtxos set on ln receive but preimage has been disclosed. Canceling");
564				lightning_receive.movement_id.map(|id| (id,
565					MovementUpdate::new()
566						.effective_balance(SignedAmount::ZERO),
567					MovementStatus::Canceled,
568				))
569			}
570			(true, None) => None,
571		};
572
573		if let Some((movement_id, update, status)) = update_opt {
574			self.inner.movements.finish_movement_with_update(movement_id, status, update).await?;
575		}
576
577		self.inner.db.finish_pending_lightning_receive(lightning_receive.payment_hash).await?;
578
579		Ok(())
580	}
581
582	/// Cancel a pending lightning receive.
583	///
584	/// This asks the server to cancel the hold invoice. The server will
585	/// refuse if HTLC-recv vtxos have already been granted.
586	///
587	/// Bark additionally prevents cancellation when the preimage has
588	/// already been revealed, since revealing the preimage means the
589	/// sender's payment is in flight and cancelling would lose funds.
590	pub async fn cancel_lightning_receive(
591		&self,
592		payment_hash: PaymentHash,
593	) -> anyhow::Result<()> {
594		let receive = self.inner.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
595			.context("no pending lightning receive found for this payment hash")?;
596
597		if receive.preimage_revealed_at.is_some() {
598			bail!("cannot cancel: preimage has already been revealed");
599		}
600
601		if receive.finished_at.is_some() {
602			bail!("lightning receive is already finished");
603		}
604
605		let (mut srv, _) = self.require_server().await?;
606		srv.client.cancel_lightning_receive(protos::CancelLightningReceiveRequest {
607			payment_hash: payment_hash.to_vec(),
608		}).await.context("server refused cancellation")?;
609
610		// Clean up local state: mark htlc vtxos as spent and finish the receive
611		self.handle_failed_lightning_receive(&receive).await?;
612
613		Ok(())
614	}
615
616	/// Check and claim a Lightning receive
617	///
618	/// This function checks for an incoming lightning payment with the given [PaymentHash]
619	/// and then claims the payment using returned HTLC VTXOs. If another task is
620	/// already claiming the same payment hash, returns the current receive state.
621	///
622	/// # Arguments
623	///
624	/// * `payment_hash` - The [PaymentHash] of the lightning payment
625	/// to check for.
626	/// * `wait` - Whether to wait for the payment to be received.
627	/// * `token` - An optional lightning receive token used to authenticate a lightning
628	/// receive when no spendable VTXOs are owned by this wallet.
629	///
630	/// # Returns
631	///
632	/// Returns an `anyhow::Result<LightningReceive>`, which is:
633	/// * `Ok(LightningReceive)` if the claim was completed, is awaiting HTLC
634	///   VTXOs, or another claim for the same payment hash is already in flight.
635	/// * `Err` if an error occurs at any stage of the operation.
636	///
637	/// # Remarks
638	///
639	/// * The payment hash must be from an invoice previously generated using
640	///   [Wallet::bolt11_invoice].
641	pub async fn try_claim_lightning_receive(
642		&self,
643		payment_hash: PaymentHash,
644		wait: bool,
645		token: Option<&str>,
646	) -> anyhow::Result<LightningReceive> {
647		trace!("Claiming lightning receive for payment hash: {}", payment_hash);
648
649		// Mark this payment as in-flight. If another task is already claiming
650		// the same payment hash, return the current receive state instead of
651		// starting a duplicate claim.
652		let key = format!("{}.{}", LIGHTNING_RECEIVE_LOCK_PREFIX, payment_hash);
653		let _guard = match self.inner.lock_manager.try_lock(&key).await {
654			Some(guard) => guard,
655			None => {
656				debug!("Receive operation already in progress for this payment");
657				return self.inner.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
658					.context("no receive for payment hash");
659			},
660		};
661
662		self.try_claim_lightning_receive_inner(payment_hash, wait, token).await
663	}
664
665	/// Internal implementation of lightning receive claim after concurrency check.
666	async fn try_claim_lightning_receive_inner(
667		&self,
668		payment_hash: PaymentHash,
669		wait: bool,
670		token: Option<&str>,
671	) -> anyhow::Result<LightningReceive> {
672		// check_lightning_receive returns None if there is no incoming payment (yet)
673		// In that case we just return and don't try to claim
674		let mut receive = match self.check_lightning_receive(payment_hash, wait, token).await? {
675			Some(receive) => receive,
676			None => {
677				return self.inner.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
678					.context("No receive for payment_hash")
679			}
680		};
681
682		if receive.finished_at.is_some() {
683			return Ok(receive);
684		}
685
686		// No need to claim anything if there
687		// are no htlcs yet
688		if receive.htlc_vtxos.is_empty() {
689			return Ok(receive);
690		}
691
692		let mut retries_left = self.inner.config.lightning_receive_claim_retries;
693		let mut backoff = CLAIM_RETRY_BACKOFF_INITIAL;
694		let claim_result = loop {
695			match self.claim_lightning_receive(&mut receive).await {
696				Ok(()) => break Ok(()),
697				Err(e) if retries_left == 0 => break Err(e),
698				Err(e) => {
699					warn!(
700						"Error claiming lightning receive {} ({} retries left, retrying in {:?}): {:#}",
701						receive.payment_hash, retries_left, backoff, e,
702					);
703					retries_left -= 1;
704					tokio::time::sleep(backoff).await;
705					backoff = (backoff * 2).min(CLAIM_RETRY_BACKOFF_MAX);
706				}
707			}
708		};
709
710		match claim_result {
711			Ok(()) => Ok(receive),
712			Err(e) => {
713				error!("Failed to claim htlcs for payment_hash: {}", receive.payment_hash);
714				// We could be having temporary problems with the server. Instead of auto-exiting we
715				// should fall through to our normal error handling flow.
716				self.handle_failed_lightning_receive(&receive).await?;
717				Err(e)
718			}
719		}
720	}
721
722	/// Check and claim all opened Lightning receive
723	///
724	/// This function fetches all opened lightning receives and then
725	/// concurrently tries to check and claim them.
726	///
727	/// # Arguments
728	///
729	/// * `wait` - Whether to wait for each payment to be received.
730	///
731	/// # Returns
732	///
733	/// Returns an `anyhow::Result<()>`, which is:
734	/// * `Ok(Vec<LightningReceive>)` contains the successfully claimed receives, if at least one
735	/// claim succeeded, or an empty vector if no claim succeeded.
736	/// * `Err` if all claim attempts failed.
737	pub async fn try_claim_all_lightning_receives(&self, wait: bool) -> anyhow::Result<Vec<LightningReceive>> {
738		let pending = self.pending_lightning_receives().await?;
739		let total = pending.len();
740
741		if total == 0 {
742			return Ok(vec![]);
743		}
744
745		let results: Vec<_> = tokio_stream::iter(pending)
746			.map(|rcv| async move {
747				self.try_claim_lightning_receive(rcv.invoice.into(), wait, None).await
748			})
749			.buffer_unordered(3)
750			.collect()
751			.await;
752
753		let mut claimed = vec![];
754		let mut failed = 0;
755
756		for result in results {
757			match result {
758				Ok(receive) => claimed.push(receive),
759				Err(e) => {
760					error!("Error claiming lightning receive: {:#}", e);
761					failed += 1;
762				}
763			}
764		}
765
766		if failed > 0 {
767			info!(
768				"Lightning receive claims: {} succeeded, {} failed out of {} pending",
769				claimed.len(), failed, total
770			);
771		}
772
773		if claimed.is_empty() {
774			anyhow::bail!("All {} lightning receive claim(s) failed", failed);
775		}
776
777		Ok(claimed)
778	}
779}
780
781#[cfg(test)]
782mod tests {
783	use super::*;
784
785	const TEST_INVOICE_STR: &str = "lntbs100u1p5j0x82sp5d0rwfh7tgrrlwsegy9rx3tzpt36cqwjqza5x4wvcjxjzscfaf6jspp5d8q7354dg3p8h0kywhqq5dq984r8f5en98hf9ln85ug0w8fx6hhsdqqcqzpc9qyysgqyk54v7tpzprxll7e0jyvtxcpgwttzk84wqsfjsqvcdtq47zt2wssxsmtjhz8dka62mdnf9jafhu3l4cpyfnsx449v4wstrwzzql2w5qqs8uh7p";
786
787	fn test_bolt11() -> Bolt11Invoice {
788		Bolt11Invoice::from_str(TEST_INVOICE_STR).expect("valid test invoice")
789	}
790
791	#[test]
792	fn validate_bolt11_payment_hash_accepts_matching_hash() {
793		let invoice = test_bolt11();
794		let payment_hash = PaymentHash::from(&invoice);
795
796		validate_bolt11_payment_hash(&invoice, payment_hash).unwrap();
797	}
798
799	#[test]
800	fn validate_bolt11_payment_hash_rejects_mismatched_hash() {
801		let invoice = test_bolt11();
802		let mismatched_payment_hash = PaymentHash::from_slice(&[0xabu8; 32]).unwrap();
803
804		let err = validate_bolt11_payment_hash(&invoice, mismatched_payment_hash)
805			.expect_err("mismatched payment hash should fail");
806
807		assert!(
808			err.to_string().contains("returned invoice with payment hash"),
809			"{err:?}",
810		);
811	}
812}