1
2pub extern crate ark;
3
4pub extern crate bip39;
5pub extern crate lightning_invoice;
6pub extern crate lnurl as lnurllib;
7
8use std::collections::HashMap;
9use std::ops::ControlFlow;
10use std::sync::{Arc, Weak};
11
12use anyhow::Context;
13use ark::tree::signed::UnlockHash;
14use bitcoin::hashes::Hash;
15use bitcoin::Amount;
16use bitcoin::hex::DisplayHex;
17use bitcoin::secp256k1::Keypair;
18use futures::{FutureExt, Stream, StreamExt};
19use log::{debug, error, info, trace, warn};
20use tokio_util::sync::CancellationToken;
21
22
23use ark::{ProtocolEncoding, Vtxo, VtxoId};
24use ark::lightning::{PaymentHash, Preimage};
25use ark::mailbox::{MailboxAuthorization, MailboxIdentifier};
26use ark::vtxo::Full;
27use ark::vtxo::policy::signing::VtxoSigner;
28use server_rpc::{protos, MAX_NB_MAILBOX_RECOVERY_IDS};
29use server_rpc::protos::mailbox_server::MailboxMessage;
30
31use crate::{Wallet, WalletInner, SUBSCRIBE_REQUEST_TIMEOUT};
32use crate::actions::DriveMode;
33use crate::actions::lightning::pay::Progress;
34use crate::movement::{MovementDestination, MovementStatus};
35use crate::movement::update::MovementUpdate;
36use crate::subsystem::{ArkoorMovement, Subsystem};
37use crate::utils::ReconnectBackoff;
38
39
40const MAX_MAILBOX_REQUEST_BURST: usize = 10;
50
51const MAILBOX_PROCESSING_LOCK_KEY: &str = "mailbox.processing";
69
70const MAILBOX_PROCESSING_LOCK_TIMEOUT: std::time::Duration =
80 std::time::Duration::from_secs(30);
81
82fn check_arkoor_receive_policy(
102 vtxo: &Vtxo<Full>,
103 server_pubkey: bitcoin::secp256k1::PublicKey,
104) -> anyhow::Result<()> {
105 let kind = vtxo.policy().policy_type();
106 if kind != ark::vtxo::policy::VtxoPolicyKind::Pubkey {
107 bail!("not a final payment VTXO (policy: {})", kind);
108 }
109 if vtxo.server_pubkey() != server_pubkey {
110 bail!("VTXO commits to a foreign server pubkey {}", vtxo.server_pubkey());
111 }
112 Ok(())
113}
114
115impl Wallet {
116 pub fn mailbox_keypair(&self) -> Keypair {
118 self.inner.seed.to_mailbox_keypair()
119 }
120
121 pub fn recovery_mailbox_keypair(&self) -> Keypair {
123 self.inner.seed.to_recovery_mailbox_keypair()
124 }
125
126 pub fn mailbox_identifier(&self) -> MailboxIdentifier {
128 let mailbox_kp = self.mailbox_keypair();
129 MailboxIdentifier::from_pubkey(mailbox_kp.public_key())
130 }
131
132 pub fn recovery_mailbox_identifier(&self) -> MailboxIdentifier {
134 let mailbox_kp = self.recovery_mailbox_keypair();
135 MailboxIdentifier::from_pubkey(mailbox_kp.public_key())
136 }
137
138 pub fn mailbox_authorization(
143 &self,
144 authorization_expiry: chrono::DateTime<chrono::Local>,
145 ) -> MailboxAuthorization {
146 MailboxAuthorization::new(&self.mailbox_keypair(), authorization_expiry)
147 }
148
149 pub async fn subscribe_mailbox_messages(
155 &self,
156 since_checkpoint: Option<u64>,
157 ) -> anyhow::Result<impl Stream<Item = anyhow::Result<MailboxMessage>> + Unpin + use<>> {
158 let (mut srv, _) = self.require_server().await?;
159
160 let checkpoint = if let Some(since) = since_checkpoint {
161 since
162 } else {
163 self.get_mailbox_checkpoint().await?
164 };
165
166 let expiry = chrono::Local::now() + std::time::Duration::from_secs(10);
168 let auth = self.mailbox_authorization(expiry);
169 let mailbox_id = auth.mailbox();
170
171 let mut req = tonic::IntoRequest::into_request(protos::mailbox_server::MailboxRequest {
172 mailbox_id: mailbox_id.serialize(),
173 authorization: Some(auth.serialize()),
174 checkpoint: checkpoint,
175 });
176 req.set_timeout(SUBSCRIBE_REQUEST_TIMEOUT);
177 trace!("Requesting mailbox stream from checkpoint {}", checkpoint);
178
179 let stream = srv.mailbox_client.subscribe_mailbox(req).await?.into_inner().map(|m| {
180 let m = m.context("received error on mailbox message stream")?;
181 Ok::<_, anyhow::Error>(m)
182 });
183
184 Ok(stream)
185 }
186
187 pub async fn subscribe_process_mailbox_messages(
196 &self,
197 since_checkpoint: Option<u64>,
198 shutdown: CancellationToken,
199 ) -> anyhow::Result<()> {
200 Wallet::subscribe_process_mailbox_messages_weak(
201 Arc::downgrade(&self.inner), since_checkpoint, shutdown,
202 ).await
203 }
204
205 pub(crate) async fn subscribe_process_mailbox_messages_weak(
217 wallet: Weak<WalletInner>,
218 since_checkpoint: Option<u64>,
219 shutdown: CancellationToken,
220 ) -> anyhow::Result<()> {
221 let upgrade = || wallet.upgrade().map(|inner| Wallet { inner });
222
223 let mut reconnect_count = 0;
228 const MAX_RECONNECT_ATTEMPTS: usize = 5;
229 let mut backoff = ReconnectBackoff::new();
230
231 loop {
232 let mut stream = {
233 let Some(wallet) = upgrade() else { return Ok(()) };
234 wallet.subscribe_mailbox_messages(since_checkpoint).await?
235 };
236 trace!("Connected to mailbox stream with server");
237
238 'stream: loop {
239 futures::select! {
240 message = stream.next().fuse() => {
241 match message {
242 Some(Ok(message)) => {
243 reconnect_count = 0;
246 let Some(wallet) = upgrade() else { return Ok(()) };
247 if wallet.process_mailbox_message(message).await.is_break() {
248 trace!("Halting mailbox stream after unadvanced message; resubscribing");
257 break 'stream;
258 }
259 backoff.reset();
261 },
262 Some(Err(e)) if crate::utils::is_h2_stream_error(&e) => {
269 reconnect_count += 1;
270 trace!("Mailbox stream reset by server, reconnecting: {e:#}");
271 break 'stream;
272 },
273 Some(Err(e)) => {
274 return Err(e).context("error on mailbox message stream");
275 },
276 None => {
277 reconnect_count += 1;
278 warn!("Mailbox stream dropped by server, reconnecting");
279 break 'stream;
280 },
281 }
282 },
283 _ = shutdown.cancelled().fuse() => {
284 info!("Shutdown signal received! Shutting mailbox messages process...");
285 return Ok(());
286 },
287 }
288 }
289
290 if reconnect_count >= MAX_RECONNECT_ATTEMPTS {
292 bail!("Mailbox stream dropped by server, giving up to retry later");
293 }
294
295 futures::select! {
299 _ = backoff.wait().fuse() => {},
300 _ = shutdown.cancelled().fuse() => {
301 info!("Shutdown signal received! Shutting mailbox messages process...");
302 return Ok(());
303 },
304 }
305 }
306 }
307
308 pub async fn sync_mailbox(&self) -> anyhow::Result<()> {
310 let (mut srv, _) = self.require_server().await?;
311
312 let expiry = chrono::Local::now() + std::time::Duration::from_secs(10 * 60);
314 let auth = self.mailbox_authorization(expiry);
315 let mailbox_id = auth.mailbox();
316
317 for _ in 0..MAX_MAILBOX_REQUEST_BURST {
318 let checkpoint = self.get_mailbox_checkpoint().await?;
319 let mailbox_req = protos::mailbox_server::MailboxRequest {
320 mailbox_id: mailbox_id.serialize(),
321 authorization: Some(auth.serialize()),
322 checkpoint,
323 };
324
325 let mailbox_resp = srv.mailbox_client.read_mailbox(mailbox_req).await
326 .context("error fetching mailbox")?.into_inner();
327 debug!("Ark server has {} mailbox messages for us", mailbox_resp.messages.len());
328
329 for mailbox_msg in mailbox_resp.messages {
330 if self.process_mailbox_message(mailbox_msg).await.is_break() {
331 return Ok(());
335 }
336 }
337
338 if !mailbox_resp.have_more {
339 break;
340 }
341 }
342
343 Ok(())
344 }
345
346 async fn process_raw_vtxos(
352 &self,
353 raw_vtxos: Vec<Vec<u8>>,
354 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
355 let ark_info = self.require_server().await
356 .context("refuse_htlc_send_vtxo_posted_to_arkoor_mailbox")?.1;
357 let tip = self.inner.chain.tip().await
358 .context("cannot vet received arkoor VTXOs, no chain tip")?;
359
360 let mut invalid_vtxos = Vec::with_capacity(raw_vtxos.len());
361 let mut valid_vtxos = Vec::with_capacity(raw_vtxos.len());
362
363 for bytes in &raw_vtxos {
364 let vtxo = match Vtxo::<Full>::deserialize(&bytes) {
365 Ok(vtxo) => vtxo,
366 Err(e) => {
367 error!("Failed to deserialize arkoor VTXO: {}: {}", bytes.as_hex(), e);
368 invalid_vtxos.push(bytes);
369 continue;
370 }
371 };
372
373 if let Err(e) = self.validate_vtxo(&vtxo).await {
374 error!("Received invalid arkoor VTXO {} from server: {}", vtxo.id(), e);
375 invalid_vtxos.push(bytes);
376 continue;
377 }
378
379 if let Err(e) = check_arkoor_receive_policy(&vtxo, ark_info.server_pubkey) {
380 error!("Refusing received arkoor VTXO {}: {}", vtxo.id(), e);
381 invalid_vtxos.push(bytes);
382 continue;
383 }
384 if self.find_signable_clause(&vtxo).await.is_none() {
385 error!("Refusing received arkoor VTXO {}: not owned by this wallet", vtxo.id());
386 invalid_vtxos.push(bytes);
387 continue;
388 }
389
390 if vtxo.expiry_height() <= tip {
391 warn!("Received arkoor VTXO {} that expired at height {} (tip {}); \
392 keeping it, it needs a refresh", vtxo.id(), vtxo.expiry_height(), tip,
393 );
394 }
395
396 valid_vtxos.push(vtxo);
397 }
398
399 if !invalid_vtxos.is_empty() {
401 error!("Received {} invalid arkoor VTXOs out of {} from server",
402 invalid_vtxos.len(), raw_vtxos.len(),
403 );
404 }
405
406 Ok(valid_vtxos)
407 }
408
409 pub(crate) async fn process_mailbox_message(
418 &self,
419 mailbox_msg: MailboxMessage,
420 ) -> ControlFlow<()> {
421 use protos::mailbox_server::mailbox_message::Message;
422
423 let advance = match mailbox_msg.message {
429 Some(Message::Arkoor(msg)) => {
430 match self.process_received_arkoor_package(msg.vtxos).await {
431 Ok(()) => true,
432 Err(e) => {
433 error!("Error processing received arkoor package: {:#}", e);
434 false
435 }
436 }
437 }
438 Some(Message::RoundParticipationCompleted(m)) => {
439 info!("Server informed that round participation is ready, unlock_hash:{:?}",
440 UnlockHash::from_slice(&m.unlock_hash).ok(),
441 );
442 if let Err(e) = self.sync_pending_rounds().await {
443 error!("Error syncing pending rounds: {:#}", e);
444 }
445 true
446 },
447 Some(Message::IncomingLightningPayment(msg)) => {
448 if let Err(e) = self.handle_lightning_receive_notification(msg).await {
449 error!("Error handling lightning receive notification: {:#}", e);
450 }
451 true
452 },
453 Some(Message::RecoveryVtxoIds(_)) => {
454 trace!("Received recovery VTXO IDs, ignoring");
455 true
456 }
457 Some(Message::LightningSendFinished(msg)) => {
458 if let Err(e) = self.handle_lightning_send_finished(msg, mailbox_msg.checkpoint).await {
459 error!("Error handling lightning send finished notification: {:#}", e);
460 }
461 true
462 }
463 None => {
464 warn!("Received unknown mailbox message kind at checkpoint {}; bark may need to be upgraded",
465 mailbox_msg.checkpoint);
466 true
467 }
468 };
469
470 if advance {
471 if let Err(e) = self.store_mailbox_checkpoint(mailbox_msg.checkpoint).await {
472 error!("Error storing mailbox checkpoint: {:#}", e);
473 }
474 ControlFlow::Continue(())
475 } else {
476 ControlFlow::Break(())
480 }
481 }
482
483 async fn process_received_arkoor_package(
484 &self,
485 raw_vtxos: Vec<Vec<u8>>,
486 ) -> anyhow::Result<()> {
487 let vtxos = self.process_raw_vtxos(raw_vtxos).await?;
488
489 let _guard = self.inner.lock_manager.lock(
495 MAILBOX_PROCESSING_LOCK_KEY, MAILBOX_PROCESSING_LOCK_TIMEOUT,
496 ).await.context("failed to acquire mailbox processing lock")?;
497
498 let mut new_vtxos = Vec::with_capacity(vtxos.len());
499 for vtxo in &vtxos {
500 if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
502 debug!("Ignoring duplicate arkoor VTXO {}", vtxo.id());
503 continue;
504 }
505
506 trace!("Received arkoor VTXO {} for {}", vtxo.id(), vtxo.amount());
507 new_vtxos.push(vtxo);
508 }
509
510 if new_vtxos.is_empty() {
511 return Ok(());
512 }
513
514 if let Err(e) = self.register_vtxo_transactions_with_server(&new_vtxos).await {
522 warn!("Failed to register received arkoor vtxo transactions with server: {:#}", e);
523 }
524
525 let balance = vtxos
526 .iter()
527 .map(|vtxo| vtxo.amount()).sum::<Amount>()
528 .to_signed()?;
529 self.store_spendable_vtxos(&vtxos).await?;
530
531 let mut received_by_address = HashMap::<ark::Address, Amount>::new();
533 for vtxo in &vtxos {
534 if let Ok(Some((index, _))) = self.pubkey_keypair(&vtxo.user_pubkey()).await {
535 if let Ok(address) = self.peek_address(index).await {
536 *received_by_address.entry(address).or_default() += vtxo.amount();
537 }
538 }
539 }
540 let received_on: Vec<_> = received_by_address
541 .iter()
542 .map(|(addr, amount)| MovementDestination::ark(addr.clone(), *amount))
543 .collect();
544
545 let movement_id = self.inner.movements.new_finished_movement(
546 Subsystem::ARKOOR,
547 ArkoorMovement::Receive.to_string(),
548 MovementStatus::Successful,
549 MovementUpdate::new()
550 .produced_vtxos(&vtxos)
551 .intended_and_effective_balance(balance)
552 .received_on(received_on),
553 ).await?;
554
555 info!("Received arkoor (movement {}) for {}", movement_id, balance);
556
557 Ok(())
558 }
559
560 async fn handle_lightning_receive_notification(
565 &self,
566 notif: protos::mailbox_server::IncomingLightningPaymentMessage,
567 ) -> anyhow::Result<()> {
568 let payment_hash = PaymentHash::try_from(notif.payment_hash)
569 .context("invalid payment hash in lightning receive notification")?;
570
571 debug!("Lightning receive notification: payment_hash={}", payment_hash);
572
573 match self.try_claim_lightning_receive(payment_hash, false).await {
574 Ok(_) => info!("Lightning receive claimed via mailbox notification for {}", payment_hash),
575 Err(e) => error!("Failed to claim lightning receive for {}: {:#}", payment_hash, e),
576 }
577
578 Ok(())
579 }
580
581 async fn handle_lightning_send_finished(
586 &self,
587 notif: protos::mailbox_server::LightningSendFinishedMessage,
588 checkpoint: u64,
589 ) -> anyhow::Result<()> {
590 let payment_hash = PaymentHash::try_from(notif.payment_hash)
591 .context("invalid payment hash in lightning send finished notification")?;
592
593 let known_preimage = notif.preimage
594 .and_then(|bytes| Preimage::try_from(bytes).ok());
595
596 if known_preimage.is_some() {
597 debug!("Lightning send finished notification (success): payment_hash={}", payment_hash);
598 } else {
599 debug!("Lightning send finished notification (failed): payment_hash={}", payment_hash);
600 }
601
602 match self.is_invoice_paid(payment_hash).await {
606 Ok(true) => {
607 debug!("Lightning send {} already settled; ignoring notification", payment_hash);
608 },
609 Ok(false) => {
610 let lookup = self.lightning_send_checkpoint(payment_hash).await;
611 match lookup {
612 Ok(Some(send)) => {
613 let result = match (&send.progress, known_preimage) {
614 (Progress::PaymentInitiated(htlcs), Some(preimage)) => {
615 let htlcs = htlcs.clone();
616 self.settle_lightning_send_with_preimage(send, htlcs, preimage).await
617 },
618 (Progress::PaymentInitiated(_), None) => {
619 self.drive_action(send, DriveMode::UntilParkOrDone).await
620 },
621 _ => {
622 debug!("Lightning send finished notification for {} but checkpoint is not PaymentInitiated; ignoring", payment_hash);
623 Ok(())
624 },
625 };
626 match result {
627 Ok(()) => info!("Processed lightning send finished for {}", payment_hash),
628 Err(e) => error!("Failed to process lightning send finished for {}: {:#}", payment_hash, e),
629 }
630 },
631 Ok(None) => {
632 warn!("Lightning send finished notification for unknown payment hash {}", payment_hash);
633 },
634 Err(e) => {
635 error!("Failed to look up lightning send checkpoint for {}: {:#}", payment_hash, e);
636 },
637 }
638 },
639 Err(e) => {
640 error!("Failed to look up paid_invoice for {}: {:#}", payment_hash, e);
641 },
642 }
643
644 self.store_mailbox_checkpoint(checkpoint).await?;
645 Ok(())
646 }
647
648 pub async fn post_recovery_vtxo_ids(
650 &self,
651 vtxo_ids: impl IntoIterator<Item = VtxoId>,
652 ) -> anyhow::Result<()> {
653 let vtxo_ids = vtxo_ids.into_iter().map(|id| id.to_bytes().to_vec()).collect::<Vec<_>>();
654 if vtxo_ids.is_empty() {
655 return Ok(());
656 }
657 let nb_vtxos = vtxo_ids.len();
658
659 let expiry = chrono::Local::now() + std::time::Duration::from_secs(60);
662 let auth = MailboxAuthorization::new(&self.recovery_mailbox_keypair(), expiry);
663 let mailbox_id = self.recovery_mailbox_identifier().serialize();
664
665 let (mut srv, _) = self.require_server().await?;
666 for chunk in vtxo_ids.chunks(MAX_NB_MAILBOX_RECOVERY_IDS) {
667 let req = protos::mailbox_server::PostRecoveryVtxoIdsRequest {
668 mailbox_id: mailbox_id.clone(),
669 vtxo_ids: chunk.to_vec(),
670 authorization: Some(auth.serialize()),
671 };
672
673 srv.mailbox_client.post_recovery_vtxo_ids(req).await
674 .context("error posting recovery vtxo IDs to server")?;
675 }
676
677 debug!("Posted {} recovery vtxo IDs to server", nb_vtxos);
678 Ok(())
679 }
680
681 pub async fn get_mailbox_checkpoint(&self) -> anyhow::Result<u64> {
685 Ok(self.inner.db.get_mailbox_checkpoint().await?)
686 }
687
688 async fn store_mailbox_checkpoint(&self, checkpoint: u64) -> anyhow::Result<()> {
689 Ok(self.inner.db.store_mailbox_checkpoint(checkpoint).await?)
690 }
691}
692
693#[cfg(test)]
694mod test {
695 use super::*;
696
697 use ark::SECP;
698 use ark::test_util::VTXO_VECTORS;
699 use bitcoin::secp256k1::Keypair;
700
701 #[test]
704 fn arkoor_receive_policy_gate() {
705 let vectors = &*VTXO_VECTORS;
706 let server_pubkey = vectors.server_key.public_key();
707
708 let good = &vectors.board_vtxo;
710 check_arkoor_receive_policy(good, server_pubkey)
711 .expect("a Pubkey vtxo must be accepted");
712
713 let htlc = &vectors.arkoor_htlc_out_vtxo;
715 check_arkoor_receive_policy(htlc, server_pubkey)
716 .expect_err("an HTLC-send vtxo must be refused as a payment");
717
718 let foreign_server = Keypair::from_seckey_slice(&SECP, &[0x11; 32])
720 .unwrap().public_key();
721 check_arkoor_receive_policy(good, foreign_server)
722 .expect_err("a vtxo for a foreign server must be refused");
723 }
724}