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
24const LIGHTNING_PREPARE_CLAIM_DELTA: BlockDelta = 2;
27
28impl Wallet {
29 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}