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