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::<Vtxo<Full>>::with_capacity(vtxos.len());
502 for vtxo in vtxos {
503 let vtxo_id = vtxo.id();
505 if new_vtxos.iter().any(|v| v.id() == vtxo_id) {
506 debug!("Ignoring duplicate VTXO {} in arkoor package", vtxo_id);
507 } else if self.inner.db.get_wallet_vtxo(vtxo_id).await?.is_some() {
508 debug!("Ignoring already-known VTXO {} in arkoor package", vtxo_id);
509 } else {
510 trace!("Received VTXO {} for {} in arkoor package", vtxo_id, vtxo.amount());
511 new_vtxos.push(vtxo);
512 }
513 }
514 if new_vtxos.is_empty() {
515 return Ok(());
516 }
517
518 if let Err(e) = self.register_vtxo_transactions_with_server(&new_vtxos).await {
526 warn!("Failed to register received arkoor vtxo transactions with server: {:#}", e);
527 }
528
529 let balance = new_vtxos
532 .iter()
533 .map(|vtxo| vtxo.amount()).sum::<Amount>()
534 .to_signed()?;
535 self.store_spendable_vtxos(&new_vtxos).await?;
536
537 let mut received_by_address = HashMap::<ark::Address, Amount>::new();
539 for vtxo in &new_vtxos {
540 if let Ok(Some((index, _))) = self.pubkey_keypair(&vtxo.user_pubkey()).await {
541 if let Ok(address) = self.peek_address(index).await {
542 *received_by_address.entry(address).or_default() += vtxo.amount();
543 }
544 }
545 }
546 let received_on: Vec<_> = received_by_address
547 .iter()
548 .map(|(addr, amount)| MovementDestination::ark(addr.clone(), *amount))
549 .collect();
550
551 let movement_id = self.inner.movements.new_finished_movement(
552 Subsystem::ARKOOR,
553 ArkoorMovement::Receive.to_string(),
554 MovementStatus::Successful,
555 MovementUpdate::new()
556 .produced_vtxos(&new_vtxos)
557 .intended_and_effective_balance(balance)
558 .received_on(received_on),
559 ).await?;
560
561 info!("Received arkoor (movement {}) for {}", movement_id, balance);
562
563 Ok(())
564 }
565
566 async fn handle_lightning_receive_notification(
571 &self,
572 notif: protos::mailbox_server::IncomingLightningPaymentMessage,
573 ) -> anyhow::Result<()> {
574 let payment_hash = PaymentHash::try_from(notif.payment_hash)
575 .context("invalid payment hash in lightning receive notification")?;
576
577 debug!("Lightning receive notification: payment_hash={}", payment_hash);
578
579 match self.try_claim_lightning_receive(payment_hash, false).await {
580 Ok(_) => info!("Lightning receive claimed via mailbox notification for {}", payment_hash),
581 Err(e) => error!("Failed to claim lightning receive for {}: {:#}", payment_hash, e),
582 }
583
584 Ok(())
585 }
586
587 async fn handle_lightning_send_finished(
592 &self,
593 notif: protos::mailbox_server::LightningSendFinishedMessage,
594 checkpoint: u64,
595 ) -> anyhow::Result<()> {
596 let payment_hash = PaymentHash::try_from(notif.payment_hash)
597 .context("invalid payment hash in lightning send finished notification")?;
598
599 let known_preimage = notif.preimage
600 .and_then(|bytes| Preimage::try_from(bytes).ok());
601
602 if known_preimage.is_some() {
603 debug!("Lightning send finished notification (success): payment_hash={}", payment_hash);
604 } else {
605 debug!("Lightning send finished notification (failed): payment_hash={}", payment_hash);
606 }
607
608 match self.is_invoice_paid(payment_hash).await {
612 Ok(true) => {
613 debug!("Lightning send {} already settled; ignoring notification", payment_hash);
614 },
615 Ok(false) => {
616 let lookup = self.lightning_send_checkpoint(payment_hash).await;
617 match lookup {
618 Ok(Some(send)) => {
619 let result = match (&send.progress, known_preimage) {
620 (Progress::PaymentInitiated(htlcs), Some(preimage)) => {
621 let htlcs = htlcs.clone();
622 self.settle_lightning_send_with_preimage(send, htlcs, preimage).await
623 },
624 (Progress::PaymentInitiated(_), None) => {
625 self.drive_action(send, DriveMode::UntilParkOrDone).await
626 },
627 _ => {
628 debug!("Lightning send finished notification for {} but checkpoint is not PaymentInitiated; ignoring", payment_hash);
629 Ok(())
630 },
631 };
632 match result {
633 Ok(()) => info!("Processed lightning send finished for {}", payment_hash),
634 Err(e) => error!("Failed to process lightning send finished for {}: {:#}", payment_hash, e),
635 }
636 },
637 Ok(None) => {
638 warn!("Lightning send finished notification for unknown payment hash {}", payment_hash);
639 },
640 Err(e) => {
641 error!("Failed to look up lightning send checkpoint for {}: {:#}", payment_hash, e);
642 },
643 }
644 },
645 Err(e) => {
646 error!("Failed to look up paid_invoice for {}: {:#}", payment_hash, e);
647 },
648 }
649
650 self.store_mailbox_checkpoint(checkpoint).await?;
651 Ok(())
652 }
653
654 pub async fn post_recovery_vtxo_ids(
656 &self,
657 vtxo_ids: impl IntoIterator<Item = VtxoId>,
658 ) -> anyhow::Result<()> {
659 let vtxo_ids = vtxo_ids.into_iter().map(|id| id.to_bytes().to_vec()).collect::<Vec<_>>();
660 if vtxo_ids.is_empty() {
661 return Ok(());
662 }
663 let nb_vtxos = vtxo_ids.len();
664
665 let expiry = chrono::Local::now() + std::time::Duration::from_secs(60);
668 let auth = MailboxAuthorization::new(&self.recovery_mailbox_keypair(), expiry);
669 let mailbox_id = self.recovery_mailbox_identifier().serialize();
670
671 let (mut srv, _) = self.require_server().await?;
672 for chunk in vtxo_ids.chunks(MAX_NB_MAILBOX_RECOVERY_IDS) {
673 let req = protos::mailbox_server::PostRecoveryVtxoIdsRequest {
674 mailbox_id: mailbox_id.clone(),
675 vtxo_ids: chunk.to_vec(),
676 authorization: Some(auth.serialize()),
677 };
678
679 srv.mailbox_client.post_recovery_vtxo_ids(req).await
680 .context("error posting recovery vtxo IDs to server")?;
681 }
682
683 debug!("Posted {} recovery vtxo IDs to server", nb_vtxos);
684 Ok(())
685 }
686
687 pub async fn get_mailbox_checkpoint(&self) -> anyhow::Result<u64> {
691 Ok(self.inner.db.get_mailbox_checkpoint().await?)
692 }
693
694 async fn store_mailbox_checkpoint(&self, checkpoint: u64) -> anyhow::Result<()> {
695 Ok(self.inner.db.store_mailbox_checkpoint(checkpoint).await?)
696 }
697}
698
699#[cfg(test)]
700mod test {
701 use super::*;
702
703 use ark::SECP;
704 use ark::test_util::VTXO_VECTORS;
705 use bitcoin::secp256k1::Keypair;
706
707 #[test]
710 fn arkoor_receive_policy_gate() {
711 let vectors = &*VTXO_VECTORS;
712 let server_pubkey = vectors.server_key.public_key();
713
714 let good = &vectors.board_vtxo;
716 check_arkoor_receive_policy(good, server_pubkey)
717 .expect("a Pubkey vtxo must be accepted");
718
719 let htlc = &vectors.arkoor_htlc_out_vtxo;
721 check_arkoor_receive_policy(htlc, server_pubkey)
722 .expect_err("an HTLC-send vtxo must be refused as a payment");
723
724 let foreign_server = Keypair::from_seckey_slice(&SECP, &[0x11; 32])
726 .unwrap().public_key();
727 check_arkoor_receive_policy(good, foreign_server)
728 .expect_err("a vtxo for a foreign server must be refused");
729 }
730}