1use std::str::FromStr;
2
3use anyhow::Context;
4use ark::arkoor::package::ArkoorPackageBuilder;
5use bitcoin::{Amount, SignedAmount};
6use bitcoin::hex::DisplayHex;
7use futures::StreamExt;
8use lightning_invoice::Bolt11Invoice;
9use log::{trace, debug, info, warn};
10
11use ark::{ProtocolEncoding, Vtxo, VtxoPolicy};
12use ark::challenges::{LightningReceiveChallenge};
13use ark::fees::validate_and_subtract_fee;
14use ark::lightning::{Bolt11InvoiceExt, PaymentHash, Preimage};
15use bitcoin_ext::{BlockDelta, BlockHeight};
16use server_rpc::protos;
17use server_rpc::protos::prepare_lightning_receive_claim_request::LightningReceiveAntiDos;
18
19use crate::subsystem::{LightningMovement, LightningReceiveMovement, Subsystem};
20use crate::{Wallet, error};
21use crate::movement::{MovementDestination, MovementStatus};
22use crate::movement::update::MovementUpdate;
23use crate::persist::models::LightningReceive;
24
25const LIGHTNING_PREPARE_CLAIM_DELTA: BlockDelta = 2;
28
29impl Wallet {
30 pub async fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
32 Ok(self.db.get_all_pending_lightning_receives().await?)
33 }
34
35 pub async fn claimable_lightning_receive_balance(&self) -> anyhow::Result<Amount> {
38 let receives = self.pending_lightning_receives().await?;
39
40 let mut total = Amount::ZERO;
41 for receive in receives {
42 total += receive.htlc_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
43 }
44
45 Ok(total)
46 }
47
48 pub async fn bolt11_invoice(&self, amount: Amount) -> anyhow::Result<Bolt11Invoice> {
50 if amount == Amount::ZERO {
51 bail!("Cannot create invoice for 0 sats (this would create an explicit 0 sat invoice, not an any-amount invoice)");
52 }
53
54 let (mut srv, ark_info) = self.require_server().await?;
55 let config = self.config();
56
57 let fee = ark_info.fees.lightning_receive.calculate(amount).context("fee overflowed")?;
59 validate_and_subtract_fee(amount, fee)?;
60
61 let requested_min_cltv_delta = ark_info.vtxo_exit_delta +
66 ark_info.htlc_expiry_delta +
67 config.vtxo_exit_margin +
68 config.htlc_recv_claim_delta +
69 LIGHTNING_PREPARE_CLAIM_DELTA;
70
71 if requested_min_cltv_delta > ark_info.max_user_invoice_cltv_delta {
72 bail!("HTLC CLTV delta ({}) is greater than Server's max HTLC recv CLTV delta: {}",
73 requested_min_cltv_delta,
74 ark_info.max_user_invoice_cltv_delta,
75 );
76 }
77
78 let preimage = Preimage::random();
79 let payment_hash = preimage.compute_payment_hash();
80 info!("Start bolt11 board with preimage / payment hash: {} / {}",
81 preimage.as_hex(), payment_hash.as_hex());
82
83 let req = protos::StartLightningReceiveRequest {
84 payment_hash: payment_hash.to_vec(),
85 amount_sat: amount.to_sat(),
86 min_cltv_delta: requested_min_cltv_delta as u32,
87 };
88
89 let resp = srv.client.start_lightning_receive(req).await?.into_inner();
90 info!("Ark Server is ready to receive LN payment to invoice: {}.", resp.bolt11);
91
92 let invoice = Bolt11Invoice::from_str(&resp.bolt11)
93 .context("invalid bolt11 invoice returned by Ark server")?;
94
95 self.db.store_lightning_receive(
96 payment_hash,
97 preimage,
98 &invoice,
99 requested_min_cltv_delta,
100 ).await?;
101
102 Ok(invoice)
103 }
104
105 pub async fn lightning_receive_status(
107 &self,
108 payment: impl Into<PaymentHash>,
109 ) -> anyhow::Result<Option<LightningReceive>> {
110 Ok(self.db.fetch_lightning_receive_by_payment_hash(payment.into()).await?)
111 }
112
113 async fn claim_lightning_receive(
134 &self,
135 receive: &mut LightningReceive,
136 ) -> anyhow::Result<()> {
137 let movement_id = receive.movement_id
138 .context("No movement created for lightning receive")?;
139 let (mut srv, _) = self.require_server().await?;
140
141 let inputs = {
143 ensure!(!receive.htlc_vtxos.is_empty(), "no HTLC VTXOs set on record yet");
144 let mut ret = receive.htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
145 ret.sort_by_key(|v| v.id());
146 ret
147 };
148
149 let mut keypairs = Vec::with_capacity(inputs.len());
150 for v in &inputs {
151 keypairs.push(self.get_vtxo_key(*v).await?);
152 }
153
154 let (claim_keypair, _) = self.derive_store_next_keypair().await?;
156 let receive_policy = VtxoPolicy::new_pubkey(claim_keypair.public_key());
157
158 trace!("ln arkoor builder params: inputs: {:?}; policy: {:?}",
159 inputs.iter().map(|v| v.id()).collect::<Vec<_>>(), receive_policy,
160 );
161 let builder = ArkoorPackageBuilder::new_claim_all_without_checkpoints(
162 inputs.iter().copied().cloned(),
163 receive_policy.clone(),
164 ).context("creating claim arkoor builder failed")?;
165 let builder = builder.generate_user_nonces(&keypairs)
166 .context("arkoor nonce generation for claim failed")?;
167
168 info!("Claiming arkoor against payment preimage");
169 self.db.set_preimage_revealed(receive.payment_hash).await?;
170 let cosign_request = builder.cosign_request();
171 let resp = srv.client.claim_lightning_receive(protos::ClaimLightningReceiveRequest {
172 payment_hash: receive.payment_hash.to_byte_array().to_vec(),
173 payment_preimage: receive.payment_preimage.to_vec(),
174 cosign_request: Some(cosign_request.into()),
175 }).await?.into_inner();
176 let cosign_resp = resp.try_into().context("invalid cosign response")?;
177
178 let outputs = builder.user_cosign(&keypairs, cosign_resp)
179 .context("claim arkoor cosign failed with user response")?
180 .build_signed_vtxos();
181
182 let mut effective_balance = Amount::ZERO;
183 for vtxo in &outputs {
184 trace!("Validating Lightning receive claim VTXO {}: {}",
189 vtxo.id(), vtxo.serialize_hex(),
190 );
191 self.validate_vtxo(vtxo).await
192 .context("invalid arkoor from lightning receive")?;
193 effective_balance += vtxo.amount();
194 }
195
196 self.store_spendable_vtxos(&outputs).await?;
197 self.mark_vtxos_as_spent(inputs).await?;
198 info!("Got arkoors from lightning: {}",
199 outputs.iter().map(|v| v.id().to_string()).collect::<Vec<_>>().join(", ")
200 );
201
202 self.movements.finish_movement_with_update(
203 movement_id,
204 MovementStatus::Successful,
205 MovementUpdate::new()
206 .effective_balance(effective_balance.to_signed()?)
207 .produced_vtxos(&outputs)
208 ).await?;
209
210 self.db.finish_pending_lightning_receive(receive.payment_hash).await?;
211 *receive = self.db.fetch_lightning_receive_by_payment_hash(receive.payment_hash).await
212 .context("Database error")?
213 .context("Receive not found")?;
214
215 Ok(())
216 }
217
218 async fn compute_lightning_receive_anti_dos(
219 &self,
220 payment_hash: PaymentHash,
221 token: Option<&str>,
222 ) -> anyhow::Result<LightningReceiveAntiDos> {
223 Ok(if let Some(token) = token {
224 LightningReceiveAntiDos::Token(token.to_string())
225 } else {
226 let challenge = LightningReceiveChallenge::new(payment_hash);
227 let vtxo = self.select_vtxos_to_cover(Amount::ONE_SAT).await
229 .and_then(|vtxos| vtxos.into_iter().next().ok_or_else(|| anyhow!("have no spendable vtxo to prove ownership of")))?;
230 let vtxo_keypair = self.get_vtxo_key(&vtxo).await.expect("owned vtxo should be in database");
231 LightningReceiveAntiDos::InputVtxo(protos::InputVtxo {
232 vtxo_id: vtxo.id().to_bytes().to_vec(),
233 ownership_proof: {
234 let sig = challenge.sign_with(vtxo.id(), &vtxo_keypair);
235 sig.serialize().to_vec()
236 }
237 })
238 })
239 }
240
241 async fn check_lightning_receive(
272 &self,
273 payment_hash: PaymentHash,
274 wait: bool,
275 token: Option<&str>,
276 ) -> anyhow::Result<Option<LightningReceive>> {
277 let (mut srv, ark_info) = self.require_server().await?;
278 let current_height = self.chain.tip().await?;
279
280 let mut receive = self.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
281 .context("no pending lightning receive found for payment hash, might already be claimed")?;
282
283 if !receive.htlc_vtxos.is_empty() {
285 return Ok(Some(receive))
286 }
287
288 trace!("Requesting updates for ln-receive to server with for wait={} and hash={}", wait, payment_hash);
289 let sub = srv.client.check_lightning_receive(protos::CheckLightningReceiveRequest {
290 hash: payment_hash.to_byte_array().to_vec(), wait,
291 }).await?.into_inner();
292
293
294 let status = protos::LightningReceiveStatus::try_from(sub.status)
295 .with_context(|| format!("unknown payment status: {}", sub.status))?;
296
297 debug!("Received status {:?} for {}", status, payment_hash);
298 match status {
299 protos::LightningReceiveStatus::Accepted |
301 protos::LightningReceiveStatus::HtlcsReady => {},
302 protos::LightningReceiveStatus::Created => {
303 warn!("sender didn't initiate payment yet");
304 return Ok(None);
305 },
306 protos::LightningReceiveStatus::Settled => bail!("payment already settled"),
307 protos::LightningReceiveStatus::Canceled => {
308 warn!("payment was canceled. removing pending lightning receive");
309 self.exit_or_cancel_lightning_receive(&receive).await?;
310 return Ok(None);
311 },
312 }
313
314 let lightning_receive_anti_dos = match self.compute_lightning_receive_anti_dos(
315 payment_hash, token,
316 ).await {
317 Ok(anti_dos) => Some(anti_dos),
318 Err(e) => {
319 warn!("Could not compute anti-dos: {e:#}. Trying without");
320 None
321 },
322 };
323
324 let htlc_recv_expiry = current_height + receive.htlc_recv_cltv_delta as BlockHeight;
325
326 let (next_keypair, _) = self.derive_store_next_keypair().await?;
327 let req = protos::PrepareLightningReceiveClaimRequest {
328 payment_hash: receive.payment_hash.to_vec(),
329 user_pubkey: next_keypair.public_key().serialize().to_vec(),
330 htlc_recv_expiry,
331 lightning_receive_anti_dos,
332 };
333 let res = srv.client.prepare_lightning_receive_claim(req).await
334 .context("error preparing lightning receive claim")?.into_inner();
335 let vtxos = res.htlc_vtxos.into_iter()
336 .map(|b| Vtxo::deserialize(&b))
337 .collect::<Result<Vec<_>, _>>()
338 .context("invalid htlc vtxos from server")?;
339
340 let mut htlc_amount = Amount::ZERO;
342 for vtxo in &vtxos {
343 trace!("Received HTLC VTXO {} from server: {}", vtxo.id(), vtxo.serialize_hex());
344 self.validate_vtxo(vtxo).await
345 .context("received invalid HTLC VTXO from server")?;
346 htlc_amount += vtxo.amount();
347
348 if let VtxoPolicy::ServerHtlcRecv(p) = vtxo.policy() {
349 if p.payment_hash != receive.payment_hash {
350 bail!("invalid payment hash on HTLC VTXOs received from server: {}",
351 p.payment_hash,
352 );
353 }
354 if p.user_pubkey != next_keypair.public_key() {
355 bail!("invalid pubkey on HTLC VTXOs received from server: {}", p.user_pubkey);
356 }
357 if p.htlc_expiry < htlc_recv_expiry {
358 bail!("HTLC VTXO expiry height is less than requested: Requested {}, received {}", htlc_recv_expiry, p.htlc_expiry);
359 }
360 } else {
361 bail!("invalid HTLC VTXO policy: {:?}", vtxo.policy());
362 }
363 }
364
365 let invoice_amount = receive.invoice.get_final_amount(None)
369 .context("ln receive invoice should have amount")?;
370 let server_received_amount = res.receive.map(|r| Amount::from_sat(r.amount_sat));
371 let fee = {
372 let fee = server_received_amount
373 .and_then(|a| ark_info.fees.lightning_receive.calculate(a));
374 match (server_received_amount, fee) {
375 (Some(amount), Some(fee)) if htlc_amount + fee == amount => {
376 fee
378 },
379 _ => {
380 ark_info.fees.lightning_receive.calculate(invoice_amount)
385 .expect("we previously validated this")
386 }
387 }
388 };
389 let received = htlc_amount + fee;
390 ensure!(received >= invoice_amount,
391 "Server didn't return enough VTXOs to cover invoice amount"
392 );
393
394 let movement_id = if let Some(movement_id) = receive.movement_id {
395 movement_id
396 } else {
397 self.movements.new_movement_with_update(
398 Subsystem::LIGHTNING_RECEIVE,
399 LightningReceiveMovement::Receive.to_string(),
400 MovementUpdate::new()
401 .intended_balance(invoice_amount.to_signed()?)
402 .effective_balance(htlc_amount.to_signed()?)
403 .fee(fee)
404 .metadata(LightningMovement::metadata(
405 receive.payment_hash, &vtxos, Some(receive.payment_preimage),
406 ))
407 .received_on(
408 [MovementDestination::new(receive.invoice.clone().into(), received)],
409 ),
410 ).await?
411 };
412 self.store_locked_vtxos(&vtxos, Some(movement_id)).await?;
413
414 let vtxo_ids = vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
415 self.db.update_lightning_receive(payment_hash, &vtxo_ids, movement_id).await?;
416
417 let mut wallet_vtxos = vec![];
418 for vtxo in vtxos {
419 let v = self.db.get_wallet_vtxo(vtxo.id()).await?
420 .context("Failed to get wallet VTXO for lightning receive")?;
421 wallet_vtxos.push(v);
422 }
423
424 receive.htlc_vtxos = wallet_vtxos;
425 receive.movement_id = Some(movement_id);
426
427 Ok(Some(receive))
428 }
429
430 async fn exit_lightning_receive(
436 &self,
437 lightning_receive: &LightningReceive,
438 ) -> anyhow::Result<()> {
439 ensure!(!lightning_receive.htlc_vtxos.is_empty(), "no HTLC VTXOs to exit");
440 let vtxos = lightning_receive.htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
441
442 info!("Exiting HTLC VTXOs for lightning_receive with payment hash {}", lightning_receive.payment_hash);
443 self.exit.write().await.start_exit_for_vtxos(&vtxos).await?;
444
445 if let Some(movement_id) = lightning_receive.movement_id {
446 self.movements.finish_movement_with_update(
447 movement_id,
448 MovementStatus::Failed,
449 MovementUpdate::new().exited_vtxos(vtxos),
450 ).await?;
451 } else {
452 error!("movement id is missing but we disclosed preimage: {}", lightning_receive.payment_hash);
453 }
454
455 self.db.finish_pending_lightning_receive(lightning_receive.payment_hash).await?;
456 Ok(())
457 }
458
459 async fn exit_or_cancel_lightning_receive(
460 &self,
461 lightning_receive: &LightningReceive,
462 ) -> anyhow::Result<()> {
463 let vtxos = &lightning_receive.htlc_vtxos;
464
465 let update_opt = match (vtxos.is_empty(), lightning_receive.preimage_revealed_at) {
466 (false, Some(_)) => {
467 return self.exit_lightning_receive(lightning_receive).await;
468 }
469 (false, None) => {
470 warn!("HTLC-recv VTXOs are about to expire, but preimage has not been disclosed yet. Canceling");
471 self.mark_vtxos_as_spent(vtxos).await?;
472 if let Some(movement_id) = lightning_receive.movement_id {
473 Some((
474 movement_id,
475 MovementUpdate::new()
476 .effective_balance(SignedAmount::ZERO),
477 MovementStatus::Canceled,
478 ))
479 } else {
480 error!("movement id is missing but we got HTLC vtxos: {}", lightning_receive.payment_hash);
481 None
482 }
483 }
484 (true, Some(_)) => {
485 error!("No HTLC vtxos set on ln receive but preimage has been disclosed. Canceling");
486 lightning_receive.movement_id.map(|id| (id,
487 MovementUpdate::new()
488 .effective_balance(SignedAmount::ZERO),
489 MovementStatus::Canceled,
490 ))
491 }
492 (true, None) => None,
493 };
494
495 if let Some((movement_id, update, status)) = update_opt {
496 self.movements.finish_movement_with_update(movement_id, status, update).await?;
497 }
498
499 self.db.finish_pending_lightning_receive(lightning_receive.payment_hash).await?;
500
501 Ok(())
502 }
503
504 pub async fn try_claim_lightning_receive(
528 &self,
529 payment_hash: PaymentHash,
530 wait: bool,
531 token: Option<&str>,
532 ) -> anyhow::Result<LightningReceive> {
533 trace!("Claiming lightning receive for payment hash: {}", payment_hash);
534
535 {
539 let mut inflight = self.inflight_lightning_payments.lock().await;
540 if !inflight.insert(payment_hash) {
541 bail!("Receive operation already in progress for this payment");
542 }
543 }
544
545 let result = self.try_claim_lightning_receive_inner(payment_hash, wait, token).await;
546
547 {
549 let mut inflight = self.inflight_lightning_payments.lock().await;
550 inflight.remove(&payment_hash);
551 }
552
553 result
554 }
555
556 async fn try_claim_lightning_receive_inner(
558 &self,
559 payment_hash: PaymentHash,
560 wait: bool,
561 token: Option<&str>,
562 ) -> anyhow::Result<LightningReceive> {
563 let mut receive = match self.check_lightning_receive(payment_hash, wait, token).await? {
566 Some(receive) => receive,
567 None => {
568 return self.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
569 .context("No receive for payment_hash")
570 }
571 };
572
573 if receive.finished_at.is_some() {
574 return Ok(receive);
575 }
576
577 if receive.htlc_vtxos.is_empty() {
580 return Ok(receive);
581 }
582
583 match self.claim_lightning_receive(&mut receive).await {
584 Ok(()) => Ok(receive),
585 Err(e) => {
586 error!("Failed to claim htlcs for payment_hash: {}", receive.payment_hash);
587 warn!("Exiting lightning receive VTXOs");
591 self.exit_lightning_receive(&receive).await?;
592 return Err(e)
593 }
594 }
595 }
596
597 pub async fn try_claim_all_lightning_receives(&self, wait: bool) -> anyhow::Result<()> {
612 let pending = self.pending_lightning_receives().await?;
613 let total = pending.len();
614
615 if total == 0 {
616 return Ok(());
617 }
618
619 let results: Vec<_> = tokio_stream::iter(pending)
620 .map(|rcv| async move {
621 self.try_claim_lightning_receive(rcv.invoice.into(), wait, None).await
622 })
623 .buffer_unordered(3)
624 .collect()
625 .await;
626
627 let succeeded = results.iter().filter(|r| r.is_ok()).count();
628 let failed = total - succeeded;
629
630 for result in &results {
631 if let Err(e) = result {
632 error!("Error claiming lightning receive: {:#}", e);
633 }
634 }
635
636 if failed > 0 {
637 info!(
638 "Lightning receive claims: {} succeeded, {} failed out of {} pending",
639 succeeded, failed, total
640 );
641 }
642
643 if succeeded == 0 {
644 anyhow::bail!("All {} lightning receive claim(s) failed", failed);
645 }
646
647 Ok(())
648 }
649}