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::{info, trace, 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::{BarkSubsystem, LightningMovement, LightningReceiveMovement};
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		)?;
75
76		Ok(invoice)
77	}
78
79	/// Fetches the status of a lightning receive for the given [PaymentHash].
80	pub 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())?)
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<Vec<Vtxo>> {
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 (keypairs, sec_nonces, pub_nonces) = inputs.iter().map(|v| {
127			let keypair = self.get_vtxo_key(v)?;
128			let (sec_nonce, pub_nonce) = musig::nonce_pair(&keypair);
129			Ok((keypair, sec_nonce, pub_nonce))
130		}).collect::<anyhow::Result<(Vec<_>, Vec<_>, Vec<_>)>>()?;
131
132		// Claiming arkoor against preimage
133		let (claim_keypair, _) = self.derive_store_next_keypair()?;
134		let receive_policy = VtxoPolicy::new_pubkey(claim_keypair.public_key());
135
136		let pay_req = VtxoRequest {
137			policy: receive_policy.clone(),
138			amount: inputs.iter().map(|v| v.amount()).sum(),
139		};
140		trace!("ln arkoor builder params: inputs: {:?}; user_nonces: {:?}; req: {:?}",
141			inputs.iter().map(|v| v.id()).collect::<Vec<_>>(), pub_nonces, pay_req,
142		);
143		let builder = ArkoorPackageBuilder::new(
144			inputs.iter().copied(), &pub_nonces, pay_req, None,
145		)?;
146
147		info!("Claiming arkoor against payment preimage");
148		self.db.set_preimage_revealed(receive.payment_hash)?;
149		let resp = srv.client.claim_lightning_receive(protos::ClaimLightningReceiveRequest {
150			payment_hash: receive.payment_hash.to_byte_array().to_vec(),
151			payment_preimage: receive.payment_preimage.to_vec(),
152			vtxo_policy: receive_policy.serialize(),
153			user_pub_nonces: pub_nonces.iter().map(|n| n.serialize().to_vec()).collect(),
154		}).await?.into_inner();
155		let cosign_resp: Vec<_> = resp.try_into().context("invalid cosign response")?;
156
157		ensure!(builder.verify_cosign_response(&cosign_resp),
158			"invalid arkoor cosignature received from server",
159		);
160
161		let (outputs, change) = builder.build_vtxos(&cosign_resp, &keypairs, sec_nonces)?;
162		if change.is_some() {
163			bail!("shouldn't have change VTXO, this is a bug");
164		}
165
166		let mut effective_balance = Amount::ZERO;
167		for vtxo in &outputs {
168			// TODO: bailing here results in vtxos not being registered despite preimage being revealed
169			// should we make `srv.client.claim_lightning_receive` idempotent, so that bark can at
170			// least retry some times before giving up and exiting?
171			trace!("Validating Lightning receive claim VTXO {}: {}",
172				vtxo.id(), vtxo.serialize_hex(),
173			);
174			self.validate_vtxo(vtxo).await
175				.context("invalid arkoor from lightning receive")?;
176			effective_balance += vtxo.amount();
177		}
178
179		self.store_spendable_vtxos(&outputs)?;
180		self.mark_vtxos_as_spent(inputs)?;
181		info!("Got arkoors from lightning: {}",
182			outputs.iter().map(|v| v.id().to_string()).collect::<Vec<_>>().join(", ")
183		);
184
185		self.movements.update_movement(
186			movement_id,
187			MovementUpdate::new()
188				.effective_balance(effective_balance.to_signed()?)
189				.produced_vtxos(&outputs)
190		).await?;
191		self.movements.finish_movement(
192			movement_id, MovementStatus::Finished,
193		).await?;
194
195		self.db.finish_pending_lightning_receive(receive.payment_hash)?;
196
197		Ok(outputs)
198	}
199
200	async fn compute_lightning_receive_anti_dos(
201		&self,
202		payment_hash: PaymentHash,
203		token: Option<&str>,
204	) -> anyhow::Result<LightningReceiveAntiDos> {
205		Ok(if let Some(token) = token {
206			LightningReceiveAntiDos::Token(token.to_string())
207		} else {
208			let challenge = LightningReceiveChallenge::new(payment_hash);
209			// We get an existing VTXO as an anti-dos measure.
210			let vtxo = self.select_vtxos_to_cover(Amount::ONE_SAT, None)
211				.and_then(|vtxos| vtxos.into_iter().next().ok_or_else(|| anyhow!("have no spendable vtxo to prove ownership of")))?;
212			let vtxo_keypair = self.get_vtxo_key(&vtxo).expect("owned vtxo should be in database");
213			LightningReceiveAntiDos::InputVtxo(protos::InputVtxo {
214				vtxo_id: vtxo.id().to_bytes().to_vec(),
215				ownership_proof: {
216					let sig = challenge.sign_with(vtxo.id(), vtxo_keypair);
217					sig.serialize().to_vec()
218				}
219			})
220		})
221	}
222
223	/// Check for incoming lightning payment with the given [PaymentHash].
224	///
225	/// This function checks for an incoming lightning payment with the
226	/// given [PaymentHash] and returns the HTLC VTXOs that are associated
227	/// with it.
228	///
229	/// # Arguments
230	///
231	/// * `payment_hash` - The [PaymentHash] of the lightning payment
232	/// to check for.
233	/// * `wait` - Whether to wait for the payment to be initiated by the sender.
234	/// * `token` - An optional lightning receive token used to authenticate a lightning
235	/// receive when no spendable VTXOs are owned by this wallet.
236	///
237	/// # Returns
238	///
239	/// Returns an `anyhow::Result<Option<LightningReceive>>`, which is:
240	/// * `Ok(Some(lightning_receive))` if the payment was initiated by
241	///   the sender and the HTLC VTXOs were successfully prepared.
242	/// * `Ok(None)` if the payment was not initiated by the sender or
243	///   the payment was cancelled by server.
244	/// * `Err` if an error occurs at any stage of the operation.
245	///
246	/// # Remarks
247	///
248	/// * The invoice must contain an explicit amount specified in milli-satoshis.
249	/// * The HTLC expiry height is calculated by adding the servers' HTLC expiry delta to the
250	///   current chain tip.
251	/// * The payment hash must be from an invoice previously generated using
252	///   [Wallet::bolt11_invoice].
253	async fn check_lightning_receive(
254		&self,
255		payment_hash: PaymentHash,
256		wait: bool,
257		token: Option<&str>,
258	) -> anyhow::Result<Option<LightningReceive>> {
259		let mut srv = self.require_server()?;
260		let current_height = self.chain.tip().await?;
261
262		let mut receive = self.db.fetch_lightning_receive_by_payment_hash(payment_hash)?
263			.context("no pending lightning receive found for payment hash, might already be claimed")?;
264
265		// If we have already HTLC VTXOs stored, we can return them without asking the server
266		if receive.htlc_vtxos.is_some() {
267			return Ok(Some(receive))
268		}
269
270		info!("Waiting for payment...");
271		let sub = srv.client.check_lightning_receive(protos::CheckLightningReceiveRequest {
272			hash: payment_hash.to_byte_array().to_vec(), wait,
273		}).await?.into_inner();
274
275		let status = protos::LightningReceiveStatus::try_from(sub.status)
276			.with_context(|| format!("unknown payment status: {}", sub.status))?;
277		match status {
278			// this is the good case
279			protos::LightningReceiveStatus::Accepted |
280			protos::LightningReceiveStatus::HtlcsReady => {},
281
282			protos::LightningReceiveStatus::Created => {
283				warn!("sender didn't initiate payment yet");
284				return Ok(None);
285			},
286			protos::LightningReceiveStatus::Settled => bail!("payment already settled"),
287			protos::LightningReceiveStatus::Cancelled => {
288				warn!("payment was cancelled. removing pending lightning receive");
289				self.exit_or_cancel_lightning_receive(&receive).await?;
290				return Ok(None);
291			},
292		}
293
294		let lightning_receive_anti_dos = match self.compute_lightning_receive_anti_dos(
295			payment_hash, token,
296		).await {
297			Ok(anti_dos) => Some(anti_dos),
298			Err(e) => {
299				warn!("Could not compute anti-dos: {e}. Trying without");
300				None
301			},
302		};
303
304		let htlc_recv_expiry = current_height + receive.htlc_recv_cltv_delta as BlockHeight;
305
306		let (next_keypair, _) = self.derive_store_next_keypair()?;
307		let req = protos::PrepareLightningReceiveClaimRequest {
308			payment_hash: receive.payment_hash.to_vec(),
309			user_pubkey: next_keypair.public_key().serialize().to_vec(),
310			htlc_recv_expiry,
311			lightning_receive_anti_dos,
312		};
313		let res = srv.client.prepare_lightning_receive_claim(req).await
314			.context("error preparing lightning receive claim")?.into_inner();
315		let vtxos = res.htlc_vtxos.into_iter()
316			.map(|b| Vtxo::deserialize(&b))
317			.collect::<Result<Vec<_>, _>>()
318			.context("invalid htlc vtxos from server")?;
319
320		// sanity check the vtxos
321		for vtxo in &vtxos {
322			trace!("Received HTLC VTXO {} from server: {}", vtxo.id(), vtxo.serialize_hex());
323			self.validate_vtxo(vtxo).await
324				.context("received invalid HTLC VTXO from server")?;
325
326			if let VtxoPolicy::ServerHtlcRecv(p) = vtxo.policy() {
327				if p.payment_hash != receive.payment_hash {
328					bail!("invalid payment hash on HTLC VTXOs received from server: {}",
329						p.payment_hash,
330					);
331				}
332				if p.user_pubkey != next_keypair.public_key() {
333					bail!("invalid pubkey on HTLC VTXOs received from server: {}", p.user_pubkey);
334				}
335				if p.htlc_expiry < htlc_recv_expiry {
336					bail!("HTLC VTXO expiry height is less than requested: Requested {}, received {}", htlc_recv_expiry, p.htlc_expiry);
337				}
338			} else {
339				bail!("invalid HTLC VTXO policy: {:?}", vtxo.policy());
340			}
341		}
342
343		// check sum match invoice amount
344		let invoice_amount = receive.invoice.amount_milli_satoshis().map(|a| Amount::from_msat_floor(a))
345			.expect("ln receive invoice should have amount");
346		let htlc_amount = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
347		ensure!(vtxos.iter().map(|v| v.amount()).sum::<Amount>() >= invoice_amount,
348			"Server didn't return enough VTXOs to cover invoice amount"
349		);
350
351		let movement_id = if let Some(movement_id) = receive.movement_id {
352			movement_id
353		} else {
354			self.movements.new_movement(
355				self.subsystem_ids[&BarkSubsystem::LightningReceive],
356				LightningReceiveMovement::Receive.to_string(),
357			).await?
358		};
359		self.store_locked_vtxos(&vtxos, Some(movement_id))?;
360
361		let vtxo_ids = vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
362		self.db.update_lightning_receive(payment_hash, &vtxo_ids, movement_id)?;
363
364		self.movements.update_movement(
365			movement_id,
366			MovementUpdate::new()
367				.intended_balance(invoice_amount.to_signed()?)
368				.effective_balance(htlc_amount.to_signed()?)
369				.metadata(LightningMovement::htlc_metadata(&vtxos)?)
370				.received_on(
371					[MovementDestination::new(receive.invoice.to_string(), htlc_amount)],
372				),
373		).await?;
374
375		let vtxos = vtxos
376			.into_iter()
377			.map(|v| self.db
378				.get_wallet_vtxo(v.id())
379				.and_then(|op| op.context("Failed to get wallet VTXO for lightning receive"))
380			).collect::<Result<Vec<_>, _>>()?;
381
382		receive.htlc_vtxos = Some(vtxos);
383		receive.movement_id = Some(movement_id);
384
385		Ok(Some(receive))
386	}
387
388	async fn exit_or_cancel_lightning_receive(
389		&self,
390		lightning_receive: &LightningReceive,
391	) -> anyhow::Result<()> {
392		let vtxos = lightning_receive.htlc_vtxos.as_ref()
393			.map(|v| v.iter().map(|v| &v.vtxo).collect::<Vec<_>>());
394
395		let update_opt = match (vtxos, lightning_receive.preimage_revealed_at) {
396			(Some(vtxos), Some(_)) => {
397				warn!("LN receive is being cancelled but preimage has been disclosed. Exiting");
398				self.exit.write().await.mark_vtxos_for_exit(&vtxos).await?;
399				if let Some(movement_id) = lightning_receive.movement_id {
400					Some((
401						movement_id,
402						MovementUpdate::new().exited_vtxos(vtxos),
403						MovementStatus::Failed,
404					))
405				} else {
406					error!("movement id is missing but we disclosed preimage: {}", lightning_receive.payment_hash);
407					None
408				}
409			}
410			(Some(vtxos), None) => {
411				warn!("HTLC-recv VTXOs are about to expire, but preimage has not been disclosed yet. Cancelling");
412				self.mark_vtxos_as_spent(vtxos)?;
413				if let Some(movement_id) = lightning_receive.movement_id {
414					Some((
415						movement_id,
416						MovementUpdate::new()
417							.effective_balance(SignedAmount::ZERO),
418						MovementStatus::Cancelled,
419					))
420				} else {
421					error!("movement id is missing but we got HTLC vtxos: {}", lightning_receive.payment_hash);
422					None
423				}
424			}
425			(None, Some(_)) => {
426				error!("No HTLC vtxos set on ln receive but preimage has been disclosed. Cancelling");
427				lightning_receive.movement_id.map(|id| (id,
428					MovementUpdate::new()
429						.effective_balance(SignedAmount::ZERO),
430					MovementStatus::Cancelled,
431				))
432			}
433			(None, None) => None,
434		};
435
436		if let Some((movement_id, update, status)) = update_opt {
437			self.movements.update_movement(movement_id, update).await?;
438			self.movements.finish_movement(movement_id, status).await?;
439		}
440
441		self.db.finish_pending_lightning_receive(lightning_receive.payment_hash)?;
442
443		Ok(())
444	}
445
446	/// Check and claim a Lightning receive
447	///
448	/// This function checks for an incoming lightning payment with the given [PaymentHash]
449	/// and then claims the payment using returned HTLC VTXOs.
450	///
451	/// # Arguments
452	///
453	/// * `payment_hash` - The [PaymentHash] of the lightning payment
454	/// to check for.
455	/// * `wait` - Whether to wait for the payment to be received.
456	/// * `token` - An optional lightning receive token used to authenticate a lightning
457	/// receive when no spendable VTXOs are owned by this wallet.
458	///
459	/// # Returns
460	///
461	/// Returns an `anyhow::Result<Option<Vec<Vtxo>>>`, which is:
462	/// * `Ok(Some(outputs))` if the process completes successfully
463	///   and VTXOs were received.
464	/// * `Ok(None)` if the payment could not be settled yet.
465	/// * `Err` if an error occurs at any stage of the operation.
466	///
467	/// # Remarks
468	///
469	/// * The payment hash must be from an invoice previously generated using
470	///   [Wallet::bolt11_invoice].
471	pub async fn try_claim_lightning_receive(
472		&self,
473		payment_hash: PaymentHash,
474		wait: bool,
475		token: Option<&str>,
476	) -> anyhow::Result<()> {
477		let srv = self.require_server()?;
478		let ark_info = srv.ark_info().await?;
479
480		let receive = match self.check_lightning_receive(payment_hash, wait, token).await? {
481			Some(receive) => receive,
482			None => return Ok(()),
483		};
484
485		if receive.finished_at.is_some() {
486			return Ok(());
487		}
488
489		let vtxos = match receive.htlc_vtxos {
490			// payment still not available
491			None => return Ok(()),
492			Some(ref vtxos) => vtxos,
493		};
494
495		if let Err(e) = self.claim_lightning_receive(&receive).await {
496			error!("Failed to claim pubkey vtxo from htlc vtxo: {}", e);
497			let tip = self.chain.tip().await?;
498
499			let first_vtxo = &vtxos.first().unwrap().vtxo;
500			debug_assert!(vtxos.iter().all(|v| {
501				v.vtxo.policy() == first_vtxo.policy() && v.vtxo.exit_delta() == first_vtxo.exit_delta()
502			}), "all htlc vtxos for the same payment hash should have the same policy and exit delta");
503
504			let vtxo_htlc_expiry = first_vtxo.policy().as_server_htlc_recv()
505				.expect("only server htlc recv vtxos can be pending lightning recv").htlc_expiry;
506
507			let safe_exit_margin = first_vtxo.exit_delta() +
508				ark_info.htlc_expiry_delta +
509				self.config.vtxo_exit_margin;
510
511			if tip > vtxo_htlc_expiry.saturating_sub(safe_exit_margin as BlockHeight) {
512				warn!("HTLC-recv VTXOs are about to expire, interupting lightning receive");
513				self.exit_or_cancel_lightning_receive(&receive).await?;
514			}
515
516			return Err(e)
517		}
518
519		Ok(())
520	}
521
522	/// Check and claim all opened Lightning receive
523	///
524	/// This function fetches all opened lightning receives and then
525	/// concurrently tries to check and claim them
526	///
527	/// # Arguments
528	///
529	/// * `wait` - Whether to wait for each payment to be received.
530	///
531	/// # Returns
532	///
533	/// Returns an `anyhow::Result<()>`, which is:
534	/// * `Ok(())` if the process completes successfully.
535	/// * `Err` if an error occurs at any stage of the operation.
536	pub async fn try_claim_all_lightning_receives(&self, wait: bool) -> anyhow::Result<()> {
537		// Asynchronously attempts to claim all pending receive by converting the list into a stream
538		tokio_stream::iter(self.pending_lightning_receives()?)
539			.for_each_concurrent(3, |rcv| async move {
540				if let Err(e) = self.try_claim_lightning_receive(rcv.invoice.into(), wait, None).await {
541					error!("Error claiming lightning receive: {:#}", e);
542				}
543			}).await;
544
545		Ok(())
546	}
547}