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::attestations::{LightningReceiveAttestation};
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 mailbox_kp = self.seed.to_mailbox_keypair();
84 let mailbox_id = ark::mailbox::MailboxIdentifier::from_pubkey(mailbox_kp.public_key());
85
86 let req = protos::StartLightningReceiveRequest {
87 payment_hash: payment_hash.to_vec(),
88 amount_sat: amount.to_sat(),
89 min_cltv_delta: requested_min_cltv_delta as u32,
90 mailbox_id: Some(mailbox_id.to_vec()),
91 };
92
93 let resp = srv.client.start_lightning_receive(req).await?.into_inner();
94 info!("Ark Server is ready to receive LN payment to invoice: {}.", resp.bolt11);
95
96 let invoice = Bolt11Invoice::from_str(&resp.bolt11)
97 .context("invalid bolt11 invoice returned by Ark server")?;
98
99 self.db.store_lightning_receive(
100 payment_hash,
101 preimage,
102 &invoice,
103 requested_min_cltv_delta,
104 ).await?;
105
106 Ok(invoice)
107 }
108
109 pub async fn lightning_receive_status(
111 &self,
112 payment: impl Into<PaymentHash>,
113 ) -> anyhow::Result<Option<LightningReceive>> {
114 Ok(self.db.fetch_lightning_receive_by_payment_hash(payment.into()).await?)
115 }
116
117 async fn claim_lightning_receive(
138 &self,
139 receive: &mut LightningReceive,
140 ) -> anyhow::Result<()> {
141 let movement_id = receive.movement_id
142 .context("No movement created for lightning receive")?;
143 let (mut srv, _) = self.require_server().await?;
144
145 let inputs = {
147 ensure!(!receive.htlc_vtxos.is_empty(), "no HTLC VTXOs set on record yet");
148 let mut ret = receive.htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
149 ret.sort_by_key(|v| v.id());
150 ret
151 };
152
153 let mut keypairs = Vec::with_capacity(inputs.len());
154 for v in &inputs {
155 keypairs.push(self.get_vtxo_key(*v).await?);
156 }
157
158 let (claim_keypair, _) = self.derive_store_next_keypair().await?;
160 let receive_policy = VtxoPolicy::new_pubkey(claim_keypair.public_key());
161
162 trace!("ln arkoor builder params: inputs: {:?}; policy: {:?}",
163 inputs.iter().map(|v| v.id()).collect::<Vec<_>>(), receive_policy,
164 );
165 let builder = ArkoorPackageBuilder::new_claim_all_without_checkpoints(
166 inputs.iter().copied().cloned(),
167 receive_policy.clone(),
168 ).context("creating claim arkoor builder failed")?;
169 let builder = builder.generate_user_nonces(&keypairs)
170 .context("arkoor nonce generation for claim failed")?;
171
172 info!("Claiming arkoor against payment preimage");
173 self.db.set_preimage_revealed(receive.payment_hash).await?;
174 let package_cosign_request = protos::ArkoorPackageCosignRequest::from(
175 builder.cosign_request(),
176 );
177 let resp = srv.client.claim_lightning_receive(protos::ClaimLightningReceiveRequest {
178 payment_hash: receive.payment_hash.to_byte_array().to_vec(),
179 payment_preimage: receive.payment_preimage.to_vec(),
180 cosign_request: Some(package_cosign_request),
181 }).await?.into_inner();
182 let cosign_resp = resp.try_into().context("invalid cosign response")?;
183
184 let outputs = builder.user_cosign(&keypairs, cosign_resp)
185 .context("claim arkoor cosign failed with user response")?
186 .build_signed_vtxos();
187
188 let mut effective_balance = Amount::ZERO;
189 for vtxo in &outputs {
190 trace!("Validating Lightning receive claim VTXO {}: {}",
195 vtxo.id(), vtxo.serialize_hex(),
196 );
197 self.validate_vtxo(vtxo).await
198 .context("invalid arkoor from lightning receive")?;
199 effective_balance += vtxo.amount();
200 }
201
202 self.store_spendable_vtxos(&outputs).await?;
203 self.mark_vtxos_as_spent(inputs).await?;
204
205 info!("Got arkoors from lightning: {}",
206 outputs.iter().map(|v| v.id().to_string()).collect::<Vec<_>>().join(", ")
207 );
208
209 self.movements.finish_movement_with_update(
210 movement_id,
211 MovementStatus::Successful,
212 MovementUpdate::new()
213 .effective_balance(effective_balance.to_signed()?)
214 .produced_vtxos(&outputs)
215 ).await?;
216
217 self.db.finish_pending_lightning_receive(receive.payment_hash).await?;
218 *receive = self.db.fetch_lightning_receive_by_payment_hash(receive.payment_hash).await
219 .context("Database error")?
220 .context("Receive not found")?;
221
222 Ok(())
223 }
224
225 async fn compute_lightning_receive_anti_dos(
226 &self,
227 payment_hash: PaymentHash,
228 token: Option<&str>,
229 ) -> anyhow::Result<LightningReceiveAntiDos> {
230 Ok(if let Some(token) = token {
231 LightningReceiveAntiDos::Token(token.to_string())
232 } else {
233 let vtxo = self.select_vtxos_to_cover(Amount::ONE_SAT).await
235 .and_then(|vtxos| vtxos.into_iter().next()
236 .context("have no spendable vtxo to prove ownership of")
237 )?;
238 let vtxo_keypair = self.get_vtxo_key(&vtxo).await.expect("owned vtxo should be in database");
239 let attestation = LightningReceiveAttestation::new(payment_hash, vtxo.id(), &vtxo_keypair);
240 LightningReceiveAntiDos::InputVtxo(protos::InputVtxo {
241 vtxo_id: vtxo.id().to_bytes().to_vec(),
242 attestation: attestation.serialize(),
243 })
244 })
245 }
246
247 async fn check_lightning_receive(
278 &self,
279 payment_hash: PaymentHash,
280 wait: bool,
281 token: Option<&str>,
282 ) -> anyhow::Result<Option<LightningReceive>> {
283 let (mut srv, ark_info) = self.require_server().await?;
284 let current_height = self.chain.tip().await?;
285
286 let mut receive = self.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
287 .context("no pending lightning receive found for payment hash, might already be claimed")?;
288
289 if !receive.htlc_vtxos.is_empty() {
291 return Ok(Some(receive))
292 }
293
294 trace!("Requesting updates for ln-receive to server with for wait={} and hash={}", wait, payment_hash);
295 let sub = srv.client.check_lightning_receive(protos::CheckLightningReceiveRequest {
296 hash: payment_hash.to_byte_array().to_vec(), wait,
297 }).await?.into_inner();
298
299
300 let status = protos::LightningReceiveStatus::try_from(sub.status)
301 .with_context(|| format!("unknown payment status: {}", sub.status))?;
302
303 debug!("Received status {:?} for {}", status, payment_hash);
304 match status {
305 protos::LightningReceiveStatus::Accepted |
307 protos::LightningReceiveStatus::HtlcsReady => {},
308 protos::LightningReceiveStatus::Created => {
309 return Ok(None);
310 },
311 protos::LightningReceiveStatus::Settled => bail!("payment already settled"),
312 protos::LightningReceiveStatus::Canceled => {
313 warn!("payment was canceled. removing pending lightning receive");
314 self.exit_or_cancel_lightning_receive(&receive).await?;
315 return Ok(None);
316 },
317 }
318
319 let lightning_receive_anti_dos = match self.compute_lightning_receive_anti_dos(
320 payment_hash, token,
321 ).await {
322 Ok(anti_dos) => Some(anti_dos),
323 Err(e) => {
324 warn!("Could not compute anti-dos: {e:#}. Trying without");
325 None
326 },
327 };
328
329 let htlc_recv_expiry = current_height + receive.htlc_recv_cltv_delta as BlockHeight;
330
331 let (next_keypair, _) = self.derive_store_next_keypair().await?;
332 let req = protos::PrepareLightningReceiveClaimRequest {
333 payment_hash: receive.payment_hash.to_vec(),
334 user_pubkey: next_keypair.public_key().serialize().to_vec(),
335 htlc_recv_expiry,
336 lightning_receive_anti_dos,
337 };
338 let res = srv.client.prepare_lightning_receive_claim(req).await
339 .context("error preparing lightning receive claim")?.into_inner();
340 let vtxos = res.htlc_vtxos.into_iter()
341 .map(|b| Vtxo::deserialize(&b))
342 .collect::<Result<Vec<_>, _>>()
343 .context("invalid htlc vtxos from server")?;
344
345 let mut htlc_amount = Amount::ZERO;
347 for vtxo in &vtxos {
348 trace!("Received HTLC VTXO {} from server: {}", vtxo.id(), vtxo.serialize_hex());
349 self.validate_vtxo(vtxo).await
350 .context("received invalid HTLC VTXO from server")?;
351 htlc_amount += vtxo.amount();
352
353 if let VtxoPolicy::ServerHtlcRecv(p) = vtxo.policy() {
354 if p.payment_hash != receive.payment_hash {
355 bail!("invalid payment hash on HTLC VTXOs received from server: {}",
356 p.payment_hash,
357 );
358 }
359 if p.user_pubkey != next_keypair.public_key() {
360 bail!("invalid pubkey on HTLC VTXOs received from server: {}", p.user_pubkey);
361 }
362 if p.htlc_expiry < htlc_recv_expiry {
363 bail!("HTLC VTXO expiry height is less than requested: Requested {}, received {}", htlc_recv_expiry, p.htlc_expiry);
364 }
365 } else {
366 bail!("invalid HTLC VTXO policy: {:?}", vtxo.policy());
367 }
368 }
369
370 let invoice_amount = receive.invoice.get_final_amount(None)
374 .context("ln receive invoice should have amount")?;
375 let server_received_amount = res.receive.map(|r| Amount::from_sat(r.amount_sat));
376 let fee = {
377 let fee = server_received_amount
378 .and_then(|a| ark_info.fees.lightning_receive.calculate(a));
379 match (server_received_amount, fee) {
380 (Some(amount), Some(fee)) if htlc_amount + fee == amount => {
381 fee
383 },
384 _ => {
385 ark_info.fees.lightning_receive.calculate(invoice_amount)
390 .expect("we previously validated this")
391 }
392 }
393 };
394 let received = htlc_amount + fee;
395 ensure!(received >= invoice_amount,
396 "Server didn't return enough VTXOs to cover invoice amount"
397 );
398
399 let movement_id = if let Some(movement_id) = receive.movement_id {
400 movement_id
401 } else {
402 self.movements.new_movement_with_update(
403 Subsystem::LIGHTNING_RECEIVE,
404 LightningReceiveMovement::Receive.to_string(),
405 MovementUpdate::new()
406 .intended_balance(invoice_amount.to_signed()?)
407 .effective_balance(htlc_amount.to_signed()?)
408 .fee(fee)
409 .metadata(LightningMovement::metadata(
410 receive.payment_hash, &vtxos, Some(receive.payment_preimage),
411 ))
412 .received_on(
413 [MovementDestination::new(receive.invoice.clone().into(), received)],
414 ),
415 ).await?
416 };
417 self.store_locked_vtxos(&vtxos, Some(movement_id)).await?;
418
419 let vtxo_ids = vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
420 self.db.update_lightning_receive(payment_hash, &vtxo_ids, movement_id).await?;
421
422 let mut wallet_vtxos = vec![];
423 for vtxo in vtxos {
424 let v = self.db.get_wallet_vtxo(vtxo.id()).await?
425 .context("Failed to get wallet VTXO for lightning receive")?;
426 wallet_vtxos.push(v);
427 }
428
429 receive.htlc_vtxos = wallet_vtxos;
430 receive.movement_id = Some(movement_id);
431
432 Ok(Some(receive))
433 }
434
435 async fn exit_lightning_receive(
441 &self,
442 lightning_receive: &LightningReceive,
443 ) -> anyhow::Result<()> {
444 ensure!(!lightning_receive.htlc_vtxos.is_empty(), "no HTLC VTXOs to exit");
445 let vtxos = lightning_receive.htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
446
447 info!("Exiting HTLC VTXOs for lightning_receive with payment hash {}", lightning_receive.payment_hash);
448 self.exit.write().await.start_exit_for_vtxos(&vtxos).await?;
449
450 if let Some(movement_id) = lightning_receive.movement_id {
451 self.movements.finish_movement_with_update(
452 movement_id,
453 MovementStatus::Failed,
454 MovementUpdate::new().exited_vtxos(vtxos),
455 ).await?;
456 } else {
457 error!("movement id is missing but we disclosed preimage: {}", lightning_receive.payment_hash);
458 }
459
460 self.db.finish_pending_lightning_receive(lightning_receive.payment_hash).await?;
461 Ok(())
462 }
463
464 pub(crate) async fn exit_or_cancel_lightning_receive(
465 &self,
466 lightning_receive: &LightningReceive,
467 ) -> anyhow::Result<()> {
468 let vtxos = &lightning_receive.htlc_vtxos;
469
470 let update_opt = match (vtxos.is_empty(), lightning_receive.preimage_revealed_at) {
471 (false, Some(_)) => {
472 return self.exit_lightning_receive(lightning_receive).await;
473 }
474 (false, None) => {
475 warn!("HTLC-recv VTXOs are about to expire, but preimage has not been disclosed yet. Canceling");
476 self.mark_vtxos_as_spent(vtxos).await?;
477 if let Some(movement_id) = lightning_receive.movement_id {
478 Some((
479 movement_id,
480 MovementUpdate::new()
481 .effective_balance(SignedAmount::ZERO),
482 MovementStatus::Canceled,
483 ))
484 } else {
485 error!("movement id is missing but we got HTLC vtxos: {}", lightning_receive.payment_hash);
486 None
487 }
488 }
489 (true, Some(_)) => {
490 error!("No HTLC vtxos set on ln receive but preimage has been disclosed. Canceling");
491 lightning_receive.movement_id.map(|id| (id,
492 MovementUpdate::new()
493 .effective_balance(SignedAmount::ZERO),
494 MovementStatus::Canceled,
495 ))
496 }
497 (true, None) => None,
498 };
499
500 if let Some((movement_id, update, status)) = update_opt {
501 self.movements.finish_movement_with_update(movement_id, status, update).await?;
502 }
503
504 self.db.finish_pending_lightning_receive(lightning_receive.payment_hash).await?;
505
506 Ok(())
507 }
508
509 pub async fn try_claim_lightning_receive(
533 &self,
534 payment_hash: PaymentHash,
535 wait: bool,
536 token: Option<&str>,
537 ) -> anyhow::Result<LightningReceive> {
538 trace!("Claiming lightning receive for payment hash: {}", payment_hash);
539
540 {
544 let mut inflight = self.inflight_lightning_payments.lock().await;
545 if !inflight.insert(payment_hash) {
546 bail!("Receive operation already in progress for this payment");
547 }
548 }
549
550 let result = self.try_claim_lightning_receive_inner(payment_hash, wait, token).await;
551
552 {
554 let mut inflight = self.inflight_lightning_payments.lock().await;
555 inflight.remove(&payment_hash);
556 }
557
558 result
559 }
560
561 async fn try_claim_lightning_receive_inner(
563 &self,
564 payment_hash: PaymentHash,
565 wait: bool,
566 token: Option<&str>,
567 ) -> anyhow::Result<LightningReceive> {
568 let mut receive = match self.check_lightning_receive(payment_hash, wait, token).await? {
571 Some(receive) => receive,
572 None => {
573 return self.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
574 .context("No receive for payment_hash")
575 }
576 };
577
578 if receive.finished_at.is_some() {
579 return Ok(receive);
580 }
581
582 if receive.htlc_vtxos.is_empty() {
585 return Ok(receive);
586 }
587
588 match self.claim_lightning_receive(&mut receive).await {
589 Ok(()) => Ok(receive),
590 Err(e) => {
591 error!("Failed to claim htlcs for payment_hash: {}", receive.payment_hash);
592 warn!("Exiting lightning receive VTXOs");
596 self.exit_lightning_receive(&receive).await?;
597 return Err(e)
598 }
599 }
600 }
601
602 pub async fn try_claim_all_lightning_receives(&self, wait: bool) -> anyhow::Result<Vec<LightningReceive>> {
618 let pending = self.pending_lightning_receives().await?;
619 let total = pending.len();
620
621 if total == 0 {
622 return Ok(vec![]);
623 }
624
625 let results: Vec<_> = tokio_stream::iter(pending)
626 .map(|rcv| async move {
627 self.try_claim_lightning_receive(rcv.invoice.into(), wait, None).await
628 })
629 .buffer_unordered(3)
630 .collect()
631 .await;
632
633 let mut claimed = vec![];
634 let mut failed = 0;
635
636 for result in results {
637 match result {
638 Ok(receive) => claimed.push(receive),
639 Err(e) => {
640 error!("Error claiming lightning receive: {:#}", e);
641 failed += 1;
642 }
643 }
644 }
645
646 if failed > 0 {
647 info!(
648 "Lightning receive claims: {} succeeded, {} failed out of {} pending",
649 claimed.len(), failed, total
650 );
651 }
652
653 if claimed.is_empty() {
654 anyhow::bail!("All {} lightning receive claim(s) failed", failed);
655 }
656
657 Ok(claimed)
658 }
659}