bark/lightning/
receive.rs

1use std::str::FromStr;
2
3use anyhow::Context;
4use bitcoin::{Amount, SignedAmount};
5use bitcoin::hex::DisplayHex;
6use futures::StreamExt;
7use lightning_invoice::Bolt11Invoice;
8use log::{trace, debug, info, warn};
9
10use ark::arkoor::ArkoorPackageBuilder;
11use ark::{ProtocolEncoding, Vtxo, VtxoPolicy, VtxoRequest, musig};
12use ark::challenges::{LightningReceiveChallenge};
13use ark::lightning::{PaymentHash, Preimage};
14use bitcoin_ext::{AmountExt, BlockDelta, BlockHeight};
15use server_rpc::protos;
16use server_rpc::protos::prepare_lightning_receive_claim_request::LightningReceiveAntiDos;
17
18use crate::subsystem::{LightningMovement, LightningReceiveMovement, Subsystem};
19use crate::{Wallet, error};
20use crate::movement::{MovementDestination, MovementStatus};
21use crate::movement::update::MovementUpdate;
22use crate::persist::models::LightningReceive;
23
24/// Leniency delta to allow claim when blocks were mined between htlc
25/// receive and claim preparation
26const LIGHTNING_PREPARE_CLAIM_DELTA: BlockDelta = 2;
27
28impl Wallet {
29	/// Create, store and return a [Bolt11Invoice] for offchain boarding
30	pub async fn bolt11_invoice(&self, amount: Amount) -> anyhow::Result<Bolt11Invoice> {
31		let mut srv = self.require_server()?;
32		let ark_info = srv.ark_info().await?;
33		let config = self.config();
34
35		// User needs to enfore the following delta:
36		// - vtxo exit delta + htlc expiry delta (to give him time to exit the vtxo before htlc expires)
37		// - vtxo exit margin (to give him time to exit the vtxo before htlc expires)
38		// - htlc recv claim delta (to give him time to claim the htlc before it expires)
39		let requested_min_cltv_delta = ark_info.vtxo_exit_delta +
40			ark_info.htlc_expiry_delta +
41			config.vtxo_exit_margin +
42			config.htlc_recv_claim_delta +
43			LIGHTNING_PREPARE_CLAIM_DELTA;
44
45		if requested_min_cltv_delta > ark_info.max_user_invoice_cltv_delta {
46			bail!("HTLC CLTV delta ({}) is greater than Server's max HTLC recv CLTV delta: {}",
47				requested_min_cltv_delta,
48				ark_info.max_user_invoice_cltv_delta,
49			);
50		}
51
52		let preimage = Preimage::random();
53		let payment_hash = preimage.compute_payment_hash();
54		info!("Start bolt11 board with preimage / payment hash: {} / {}",
55			preimage.as_hex(), payment_hash.as_hex());
56
57		let req = protos::StartLightningReceiveRequest {
58			payment_hash: payment_hash.to_vec(),
59			amount_sat: amount.to_sat(),
60			min_cltv_delta: requested_min_cltv_delta as u32,
61		};
62
63		let resp = srv.client.start_lightning_receive(req).await?.into_inner();
64		info!("Ark Server is ready to receive LN payment to invoice: {}.", resp.bolt11);
65
66		let invoice = Bolt11Invoice::from_str(&resp.bolt11)
67			.context("invalid bolt11 invoice returned by Ark server")?;
68
69		self.db.store_lightning_receive(
70			payment_hash,
71			preimage,
72			&invoice,
73			requested_min_cltv_delta,
74		).await?;
75
76		Ok(invoice)
77	}
78
79	/// Fetches the status of a lightning receive for the given [PaymentHash].
80	pub async fn lightning_receive_status(
81		&self,
82		payment: impl Into<PaymentHash>,
83	) -> anyhow::Result<Option<LightningReceive>> {
84		Ok(self.db.fetch_lightning_receive_by_payment_hash(payment.into()).await?)
85	}
86
87	/// Claim incoming lightning payment with the given [PaymentHash].
88	///
89	/// This function reveals the preimage of the lightning payment in
90	/// exchange of getting pubkey VTXOs from HTLC ones
91	///
92	/// # Arguments
93	///
94	/// * `payment_hash` - The [PaymentHash] of the lightning payment
95	/// to wait for.
96	/// * `vtxos` - The list of HTLC VTXOs that were previously granted
97	/// by the Server, with the hash lock clause matching payment hash.
98	///
99	/// # Returns
100	///
101	/// Returns an `anyhow::Result<()>`, which is:
102	/// * `Ok(())` if the process completes successfully.
103	/// * `Err` if an error occurs at any stage of the operation.
104	///
105	/// # Remarks
106	///
107	/// * The list of HTLC VTXOs must have the hash lock clause matching the given
108	///   [PaymentHash].
109	async fn claim_lightning_receive(
110		&self,
111		receive: &LightningReceive,
112	) -> anyhow::Result<LightningReceive> {
113		let movement_id = receive.movement_id
114			.context("No movement created for lightning receive")?;
115		let mut srv = self.require_server()?;
116
117		// order inputs by vtxoid before we generate nonces
118		let inputs = {
119			let htlc_vtxos = receive.htlc_vtxos.as_ref()
120				.context("no HTLC VTXOs set on record yet")?;
121			let mut ret = htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
122			ret.sort_by_key(|v| v.id());
123			ret
124		};
125
126		let mut keypairs = Vec::with_capacity(inputs.len());
127		let mut sec_nonces = Vec::with_capacity(inputs.len());
128		let mut pub_nonces = Vec::with_capacity(inputs.len());
129		for v in &inputs {
130			let keypair = self.get_vtxo_key(*v).await?;
131			let (sec_nonce, pub_nonce) = musig::nonce_pair(&keypair);
132			keypairs.push(keypair);
133			sec_nonces.push(sec_nonce);
134			pub_nonces.push(pub_nonce);
135		}
136
137		// Claiming arkoor against preimage
138		let (claim_keypair, _) = self.derive_store_next_keypair().await?;
139		let receive_policy = VtxoPolicy::new_pubkey(claim_keypair.public_key());
140
141		let pay_req = VtxoRequest {
142			policy: receive_policy.clone(),
143			amount: inputs.iter().map(|v| v.amount()).sum(),
144		};
145		trace!("ln arkoor builder params: inputs: {:?}; user_nonces: {:?}; req: {:?}",
146			inputs.iter().map(|v| v.id()).collect::<Vec<_>>(), pub_nonces, pay_req,
147		);
148		let builder = ArkoorPackageBuilder::new(
149			inputs.iter().copied(), &pub_nonces, pay_req, None,
150		)?;
151
152		info!("Claiming arkoor against payment preimage");
153		self.db.set_preimage_revealed(receive.payment_hash).await?;
154		let resp = srv.client.claim_lightning_receive(protos::ClaimLightningReceiveRequest {
155			payment_hash: receive.payment_hash.to_byte_array().to_vec(),
156			payment_preimage: receive.payment_preimage.to_vec(),
157			vtxo_policy: receive_policy.serialize(),
158			user_pub_nonces: pub_nonces.iter().map(|n| n.serialize().to_vec()).collect(),
159		}).await?.into_inner();
160		let cosign_resp: Vec<_> = resp.try_into().context("invalid cosign response")?;
161
162		ensure!(builder.verify_cosign_response(&cosign_resp),
163			"invalid arkoor cosignature received from server",
164		);
165
166		let (outputs, change) = builder.build_vtxos(&cosign_resp, &keypairs, sec_nonces)?;
167		if change.is_some() {
168			bail!("shouldn't have change VTXO, this is a bug");
169		}
170
171		let mut effective_balance = Amount::ZERO;
172		for vtxo in &outputs {
173			// TODO: bailing here results in vtxos not being registered despite preimage being revealed
174			// should we make `srv.client.claim_lightning_receive` idempotent, so that bark can at
175			// least retry some times before giving up and exiting?
176			trace!("Validating Lightning receive claim VTXO {}: {}",
177				vtxo.id(), vtxo.serialize_hex(),
178			);
179			self.validate_vtxo(vtxo).await
180				.context("invalid arkoor from lightning receive")?;
181			effective_balance += vtxo.amount();
182		}
183
184		self.store_spendable_vtxos(&outputs).await?;
185		self.mark_vtxos_as_spent(inputs).await?;
186		info!("Got arkoors from lightning: {}",
187			outputs.iter().map(|v| v.id().to_string()).collect::<Vec<_>>().join(", ")
188		);
189
190		self.movements.finish_movement_with_update(
191			movement_id,
192			MovementStatus::Successful,
193			MovementUpdate::new()
194				.effective_balance(effective_balance.to_signed()?)
195				.produced_vtxos(&outputs)
196		).await?;
197
198		self.db.finish_pending_lightning_receive(receive.payment_hash).await?;
199		let receive = self.db.fetch_lightning_receive_by_payment_hash(receive.payment_hash).await
200			.context("Database error")?
201			.context("Receive not found")?;
202
203		Ok(receive)
204	}
205
206	async fn compute_lightning_receive_anti_dos(
207		&self,
208		payment_hash: PaymentHash,
209		token: Option<&str>,
210	) -> anyhow::Result<LightningReceiveAntiDos> {
211		Ok(if let Some(token) = token {
212			LightningReceiveAntiDos::Token(token.to_string())
213		} else {
214			let challenge = LightningReceiveChallenge::new(payment_hash);
215			// We get an existing VTXO as an anti-dos measure.
216			let vtxo = self.select_vtxos_to_cover(Amount::ONE_SAT).await
217				.and_then(|vtxos| vtxos.into_iter().next().ok_or_else(|| anyhow!("have no spendable vtxo to prove ownership of")))?;
218			let vtxo_keypair = self.get_vtxo_key(&vtxo).await.expect("owned vtxo should be in database");
219			LightningReceiveAntiDos::InputVtxo(protos::InputVtxo {
220				vtxo_id: vtxo.id().to_bytes().to_vec(),
221				ownership_proof: {
222					let sig = challenge.sign_with(vtxo.id(), &vtxo_keypair);
223					sig.serialize().to_vec()
224				}
225			})
226		})
227	}
228
229	/// Check for incoming lightning payment with the given [PaymentHash].
230	///
231	/// This function checks for an incoming lightning payment with the
232	/// given [PaymentHash] and returns the HTLC VTXOs that are associated
233	/// with it.
234	///
235	/// # Arguments
236	///
237	/// * `payment_hash` - The [PaymentHash] of the lightning payment
238	/// to check for.
239	/// * `wait` - Whether to wait for the payment to be initiated by the sender.
240	/// * `token` - An optional lightning receive token used to authenticate a lightning
241	/// receive when no spendable VTXOs are owned by this wallet.
242	///
243	/// # Returns
244	///
245	/// Returns an `anyhow::Result<Option<LightningReceive>>`, which is:
246	/// * `Ok(Some(lightning_receive))` if the payment was initiated by
247	///   the sender and the HTLC VTXOs were successfully prepared.
248	/// * `Ok(None)` if the payment was not initiated by the sender or
249	///   the payment was canceled by server.
250	/// * `Err` if an error occurs at any stage of the operation.
251	///
252	/// # Remarks
253	///
254	/// * The invoice must contain an explicit amount specified in milli-satoshis.
255	/// * The HTLC expiry height is calculated by adding the servers' HTLC expiry delta to the
256	///   current chain tip.
257	/// * The payment hash must be from an invoice previously generated using
258	///   [Wallet::bolt11_invoice].
259	async fn check_lightning_receive(
260		&self,
261		payment_hash: PaymentHash,
262		wait: bool,
263		token: Option<&str>,
264	) -> anyhow::Result<Option<LightningReceive>> {
265		let mut srv = self.require_server()?;
266		let current_height = self.chain.tip().await?;
267
268		let mut receive = self.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
269			.context("no pending lightning receive found for payment hash, might already be claimed")?;
270
271		// If we have already HTLC VTXOs stored, we can return them without asking the server
272		if receive.htlc_vtxos.is_some() {
273			return Ok(Some(receive))
274		}
275
276		trace!("Requesting updates for ln-receive to server with for wait={} and hash={}", wait, payment_hash);
277		let sub = srv.client.check_lightning_receive(protos::CheckLightningReceiveRequest {
278			hash: payment_hash.to_byte_array().to_vec(), wait,
279		}).await?.into_inner();
280
281
282		let status = protos::LightningReceiveStatus::try_from(sub.status)
283			.with_context(|| format!("unknown payment status: {}", sub.status))?;
284
285		debug!("Received status {:?} for {}", status, payment_hash);
286		match status {
287			// this is the good case
288			protos::LightningReceiveStatus::Accepted |
289			protos::LightningReceiveStatus::HtlcsReady => {},
290			protos::LightningReceiveStatus::Created => {
291				warn!("sender didn't initiate payment yet");
292				return Ok(None);
293			},
294			protos::LightningReceiveStatus::Settled => bail!("payment already settled"),
295			protos::LightningReceiveStatus::Canceled => {
296				warn!("payment was canceled. removing pending lightning receive");
297				self.exit_or_cancel_lightning_receive(&receive).await?;
298				return Ok(None);
299			},
300		}
301
302		let lightning_receive_anti_dos = match self.compute_lightning_receive_anti_dos(
303			payment_hash, token,
304		).await {
305			Ok(anti_dos) => Some(anti_dos),
306			Err(e) => {
307				warn!("Could not compute anti-dos: {e}. Trying without");
308				None
309			},
310		};
311
312		let htlc_recv_expiry = current_height + receive.htlc_recv_cltv_delta as BlockHeight;
313
314		let (next_keypair, _) = self.derive_store_next_keypair().await?;
315		let req = protos::PrepareLightningReceiveClaimRequest {
316			payment_hash: receive.payment_hash.to_vec(),
317			user_pubkey: next_keypair.public_key().serialize().to_vec(),
318			htlc_recv_expiry,
319			lightning_receive_anti_dos,
320		};
321		let res = srv.client.prepare_lightning_receive_claim(req).await
322			.context("error preparing lightning receive claim")?.into_inner();
323		let vtxos = res.htlc_vtxos.into_iter()
324			.map(|b| Vtxo::deserialize(&b))
325			.collect::<Result<Vec<_>, _>>()
326			.context("invalid htlc vtxos from server")?;
327
328		// sanity check the vtxos
329		for vtxo in &vtxos {
330			trace!("Received HTLC VTXO {} from server: {}", vtxo.id(), vtxo.serialize_hex());
331			self.validate_vtxo(vtxo).await
332				.context("received invalid HTLC VTXO from server")?;
333
334			if let VtxoPolicy::ServerHtlcRecv(p) = vtxo.policy() {
335				if p.payment_hash != receive.payment_hash {
336					bail!("invalid payment hash on HTLC VTXOs received from server: {}",
337						p.payment_hash,
338					);
339				}
340				if p.user_pubkey != next_keypair.public_key() {
341					bail!("invalid pubkey on HTLC VTXOs received from server: {}", p.user_pubkey);
342				}
343				if p.htlc_expiry < htlc_recv_expiry {
344					bail!("HTLC VTXO expiry height is less than requested: Requested {}, received {}", htlc_recv_expiry, p.htlc_expiry);
345				}
346			} else {
347				bail!("invalid HTLC VTXO policy: {:?}", vtxo.policy());
348			}
349		}
350
351		// check sum match invoice amount
352		let invoice_amount = receive.invoice.amount_milli_satoshis().map(|a| Amount::from_msat_floor(a))
353			.expect("ln receive invoice should have amount");
354		let htlc_amount = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
355		ensure!(htlc_amount >= invoice_amount,
356			"Server didn't return enough VTXOs to cover invoice amount"
357		);
358
359		let movement_id = if let Some(movement_id) = receive.movement_id {
360			movement_id
361		} else {
362			self.movements.new_movement_with_update(
363				Subsystem::LIGHTNING_RECEIVE,
364				LightningReceiveMovement::Receive.to_string(),
365				MovementUpdate::new()
366					.intended_balance(invoice_amount.to_signed()?)
367					.effective_balance(htlc_amount.to_signed()?)
368					.metadata(LightningMovement::metadata(receive.payment_hash, &vtxos))
369					.received_on(
370						[MovementDestination::new(receive.invoice.clone().into(), htlc_amount)],
371					),
372			).await?
373		};
374		self.store_locked_vtxos(&vtxos, Some(movement_id)).await?;
375
376		let vtxo_ids = vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
377		self.db.update_lightning_receive(payment_hash, &vtxo_ids, movement_id).await?;
378
379		let mut wallet_vtxos = vec![];
380		for vtxo in vtxos {
381			let v =  self.db.get_wallet_vtxo(vtxo.id()).await?
382				.context("Failed to get wallet VTXO for lightning receive")?;
383			wallet_vtxos.push(v);
384		}
385
386		receive.htlc_vtxos = Some(wallet_vtxos);
387		receive.movement_id = Some(movement_id);
388
389		Ok(Some(receive))
390	}
391
392	async fn exit_or_cancel_lightning_receive(
393		&self,
394		lightning_receive: &LightningReceive,
395	) -> anyhow::Result<()> {
396		let vtxos = lightning_receive.htlc_vtxos.as_ref()
397			.map(|v| v.iter().map(|v| &v.vtxo).collect::<Vec<_>>());
398
399		let update_opt = match (vtxos, lightning_receive.preimage_revealed_at) {
400			(Some(vtxos), Some(_)) => {
401				warn!("LN receive is being canceled but preimage has been disclosed. Exiting");
402				self.exit.write().await.start_exit_for_vtxos(&vtxos).await?;
403				if let Some(movement_id) = lightning_receive.movement_id {
404					Some((
405						movement_id,
406						MovementUpdate::new().exited_vtxos(vtxos),
407						MovementStatus::Failed,
408					))
409				} else {
410					error!("movement id is missing but we disclosed preimage: {}", lightning_receive.payment_hash);
411					None
412				}
413			}
414			(Some(vtxos), None) => {
415				warn!("HTLC-recv VTXOs are about to expire, but preimage has not been disclosed yet. Canceling");
416				self.mark_vtxos_as_spent(vtxos).await?;
417				if let Some(movement_id) = lightning_receive.movement_id {
418					Some((
419						movement_id,
420						MovementUpdate::new()
421							.effective_balance(SignedAmount::ZERO),
422						MovementStatus::Canceled,
423					))
424				} else {
425					error!("movement id is missing but we got HTLC vtxos: {}", lightning_receive.payment_hash);
426					None
427				}
428			}
429			(None, Some(_)) => {
430				error!("No HTLC vtxos set on ln receive but preimage has been disclosed. Canceling");
431				lightning_receive.movement_id.map(|id| (id,
432					MovementUpdate::new()
433						.effective_balance(SignedAmount::ZERO),
434					MovementStatus::Canceled,
435				))
436			}
437			(None, None) => None,
438		};
439
440		if let Some((movement_id, update, status)) = update_opt {
441			self.movements.finish_movement_with_update(movement_id, status, update).await?;
442		}
443
444		self.db.finish_pending_lightning_receive(lightning_receive.payment_hash).await?;
445
446		Ok(())
447	}
448
449	/// Check and claim a Lightning receive
450	///
451	/// This function checks for an incoming lightning payment with the given [PaymentHash]
452	/// and then claims the payment using returned HTLC VTXOs.
453	///
454	/// # Arguments
455	///
456	/// * `payment_hash` - The [PaymentHash] of the lightning payment
457	/// to check for.
458	/// * `wait` - Whether to wait for the payment to be received.
459	/// * `token` - An optional lightning receive token used to authenticate a lightning
460	/// receive when no spendable VTXOs are owned by this wallet.
461	///
462	/// # Returns
463	///
464	/// Returns an `anyhow::Result<LightningReceive>`, which is:
465	/// * `Ok(LightningReceive)` if the claim was completed or is awaiting HTLC VTXOs
466	/// * `Err` if an error occurs at any stage of the operation.
467	///
468	/// # Remarks
469	///
470	/// * The payment hash must be from an invoice previously generated using
471	///   [Wallet::bolt11_invoice].
472	pub async fn try_claim_lightning_receive(
473		&self,
474		payment_hash: PaymentHash,
475		wait: bool,
476		token: Option<&str>,
477	) -> anyhow::Result<LightningReceive> {
478		let srv = self.require_server()?;
479		let ark_info = srv.ark_info().await?;
480
481		// check_lightning_receive returns None if there is no incoming payment (yet)
482		// In that case we just return and don't try to claim
483		let receive = match self.check_lightning_receive(payment_hash, wait, token).await? {
484			Some(receive) => receive,
485			None => {
486				return self.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
487					.context("No receive for payment_hash")
488			}
489		};
490
491		if receive.finished_at.is_some() {
492			return Ok(receive);
493		}
494
495		// No need to claim anything if there
496		// are no htlcs yet
497		let vtxos = match receive.htlc_vtxos.as_ref() {
498			None => return Ok(receive),
499			Some(vtxos) => vtxos
500		};
501
502		match self.claim_lightning_receive(&receive).await {
503			Ok(receive) => Ok(receive),
504			Err(e) => {
505				error!("Failed to claim htlcs for payment_hash: {}", receive.payment_hash);
506
507				let tip = self.chain.tip().await?;
508
509				let first_vtxo = &vtxos.first()
510					.context("HTLC VTXOs unexpectedly empty")?.vtxo;
511				debug_assert!(vtxos.iter().all(|v| {
512					v.vtxo.policy() == first_vtxo.policy() && v.vtxo.exit_delta() == first_vtxo.exit_delta()
513				}), "all htlc vtxos for the same payment hash should have the same policy and exit delta");
514
515				let vtxo_htlc_expiry = first_vtxo.policy().as_server_htlc_recv()
516					.expect("only server htlc recv vtxos can be pending lightning recv").htlc_expiry;
517
518				let safe_exit_margin = first_vtxo.exit_delta() +
519					ark_info.htlc_expiry_delta +
520					self.config.vtxo_exit_margin;
521
522				if tip > vtxo_htlc_expiry.saturating_sub(safe_exit_margin as BlockHeight) {
523					warn!("HTLC-recv VTXOs are about to expire, interupting lightning receive");
524					self.exit_or_cancel_lightning_receive(&receive).await?;
525				}
526
527				return Err(e)
528			}
529		}
530	}
531
532	/// Check and claim all opened Lightning receive
533	///
534	/// This function fetches all opened lightning receives and then
535	/// concurrently tries to check and claim them.
536	///
537	/// # Arguments
538	///
539	/// * `wait` - Whether to wait for each payment to be received.
540	///
541	/// # Returns
542	///
543	/// Returns an `anyhow::Result<()>`, which is:
544	/// * `Ok(())` if at least one claim succeeded or there were no pending receives.
545	/// * `Err` if all claim attempts failed.
546	pub async fn try_claim_all_lightning_receives(&self, wait: bool) -> anyhow::Result<()> {
547		let pending = self.pending_lightning_receives().await?;
548		let total = pending.len();
549
550		if total == 0 {
551			return Ok(());
552		}
553
554		let results: Vec<_> = tokio_stream::iter(pending)
555			.map(|rcv| async move {
556				self.try_claim_lightning_receive(rcv.invoice.into(), wait, None).await
557			})
558			.buffer_unordered(3)
559			.collect()
560			.await;
561
562		let succeeded = results.iter().filter(|r| r.is_ok()).count();
563		let failed = total - succeeded;
564
565		for result in &results {
566			if let Err(e) = result {
567				error!("Error claiming lightning receive: {:#}", e);
568			}
569		}
570
571		if failed > 0 {
572			info!(
573				"Lightning receive claims: {} succeeded, {} failed out of {} pending",
574				succeeded, failed, total
575			);
576		}
577
578		if succeeded == 0 {
579			anyhow::bail!("All {} lightning receive claim(s) failed", failed);
580		}
581
582		Ok(())
583	}
584}