Skip to main content

bark/
mailbox.rs

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;
10
11use anyhow::Context;
12use ark::tree::signed::UnlockHash;
13use bitcoin::hashes::Hash;
14use bitcoin::Amount;
15use bitcoin::hex::DisplayHex;
16use bitcoin::secp256k1::Keypair;
17use futures::{FutureExt, Stream, StreamExt};
18use log::{debug, error, info, trace, warn};
19use tokio_util::sync::CancellationToken;
20
21use ark::{ProtocolEncoding, Vtxo, VtxoId};
22use ark::lightning::{PaymentHash, Preimage};
23use ark::mailbox::{MailboxAuthorization, MailboxIdentifier};
24use ark::vtxo::Full;
25use server_rpc::{protos, MAX_NB_MAILBOX_RECOVERY_IDS};
26use server_rpc::protos::mailbox_server::MailboxMessage;
27
28use crate::{Wallet, SUBSCRIBE_REQUEST_TIMEOUT};
29use crate::actions::DriveMode;
30use crate::actions::lightning::pay::Progress;
31use crate::movement::{MovementDestination, MovementStatus};
32use crate::movement::update::MovementUpdate;
33use crate::subsystem::{ArkoorMovement, Subsystem};
34use crate::utils::ReconnectBackoff;
35
36
37/// The maximum number of times we will call the fetch mailbox endpoint in one go
38///
39/// We can't trust the server to honestly tell us to keep trying more forever.
40/// A malicious server could send us empty messages or invalid messages and
41/// lock up our resources forever. So we limit the number of times we will fetch.
42/// If a user actually has more messages left, he will have to call sync again.
43///
44/// (Note that currently the server sends 100 messages per fetch, so this would
45/// only happen for users with more than 1000 pending items.)
46const MAX_MAILBOX_REQUEST_BURST: usize = 10;
47
48/// Key for the lock that serializes the arkoor receive dedup within a wallet.
49///
50/// Several consumers of one wallet process the same mailbox messages: the
51/// daemon's always-on stream ([`Wallet::subscribe_process_mailbox_messages`])
52/// runs alongside the startup / periodic `sync()` (which calls
53/// [`Wallet::sync_mailbox`]), and a REST deployment can fire concurrent
54/// `/sync` and `/sync/mailbox` requests. The server hands each consumer the
55/// same messages from its checkpoint, so without serialization the
56/// peek-then-store dedup in arkoor processing races: every consumer that wins
57/// the [`crate::persist::BarkPersister::get_wallet_vtxo`] check before the
58/// others store records its own receive movement, double-counting the receive.
59///
60/// Holding this lock across that check-then-store (in
61/// [`Wallet::process_received_arkoor_package`]) makes it atomic across
62/// consumers. The VTXO row is `INSERT OR IGNORE`, so once one consumer has
63/// stored the package the rest see the VTXO already present and skip the
64/// movement.
65const MAILBOX_PROCESSING_LOCK_KEY: &str = "mailbox.processing";
66
67/// How long a consumer waits for the arkoor dedup lock before giving up.
68///
69/// The critical section is a couple of server round trips plus local DB
70/// writes, normally well under a second. On timeout we error rather than
71/// block, handled like any other arkoor processing error: this message's
72/// checkpoint isn't advanced and processing halts (see
73/// [`Wallet::process_mailbox_message`]) so a later message can't bury it. A
74/// timeout only happens if a holder is stuck for the full duration; normally
75/// waiters acquire the lock and dedup as usual.
76const MAILBOX_PROCESSING_LOCK_TIMEOUT: std::time::Duration =
77	std::time::Duration::from_secs(30);
78
79impl Wallet {
80	/// Get the keypair used for the server mailbox
81	pub fn mailbox_keypair(&self) -> Keypair {
82		self.inner.seed.to_mailbox_keypair()
83	}
84
85	/// Get the keypair used for the server recovery mailbox
86	pub fn recovery_mailbox_keypair(&self) -> Keypair {
87		self.inner.seed.to_recovery_mailbox_keypair()
88	}
89
90	/// Get this wallet's server mailbox ID
91	pub fn mailbox_identifier(&self) -> MailboxIdentifier {
92		let mailbox_kp = self.mailbox_keypair();
93		MailboxIdentifier::from_pubkey(mailbox_kp.public_key())
94	}
95
96	/// Get this wallet's server recovery mailbox ID
97	pub fn recovery_mailbox_identifier(&self) -> MailboxIdentifier {
98		let mailbox_kp = self.recovery_mailbox_keypair();
99		MailboxIdentifier::from_pubkey(mailbox_kp.public_key())
100	}
101
102	/// Create a mailbox authorization that is valid until the given expiry time
103	///
104	/// This authorization can be used by third parties to lookup your mailbox
105	/// with the Ark server.
106	pub fn mailbox_authorization(
107		&self,
108		authorization_expiry: chrono::DateTime<chrono::Local>,
109	) -> MailboxAuthorization {
110		MailboxAuthorization::new(&self.mailbox_keypair(), authorization_expiry)
111	}
112
113	/// Subscribe to mailbox message stream.
114	///
115	/// If `since` is `None`, the stream will start from the last checkpoint stored in the database.
116	///
117	/// Returns a stream of mailbox messages.
118	pub async fn subscribe_mailbox_messages(
119		&self,
120		since_checkpoint: Option<u64>,
121	) -> anyhow::Result<impl Stream<Item = anyhow::Result<MailboxMessage>> + Unpin> {
122		let (mut srv, _) = self.require_server().await?;
123
124		let checkpoint = if let Some(since) = since_checkpoint {
125			since
126		} else {
127			self.get_mailbox_checkpoint().await?
128		};
129
130		// we just need a short authorization for the stream initialization
131		let expiry = chrono::Local::now() + std::time::Duration::from_secs(10);
132		let auth = self.mailbox_authorization(expiry);
133		let mailbox_id = auth.mailbox();
134
135		let mut req = tonic::IntoRequest::into_request(protos::mailbox_server::MailboxRequest {
136			mailbox_id: mailbox_id.serialize(),
137			authorization: Some(auth.serialize()),
138			checkpoint: checkpoint,
139		});
140		req.set_timeout(SUBSCRIBE_REQUEST_TIMEOUT);
141		trace!("Requesting mailbox stream from checkpoint {}", checkpoint);
142
143		let stream = srv.mailbox_client.subscribe_mailbox(req).await?.into_inner().map(|m| {
144			let m = m.context("received error on mailbox message stream")?;
145			Ok::<_, anyhow::Error>(m)
146		});
147
148		Ok(stream)
149	}
150
151	/// Similar to [Wallet::subscribe_mailbox_messages] but it will also process each mailbox
152	/// message indefinitely. This method won't stop until the given `shutdown` `CancellationToken`
153	/// is triggered.
154	///
155	/// If `since_checkpoint` is `None`, the stream will start from the last checkpoint stored in
156	/// the database.
157	///
158	/// Returns only once the stream is closed.
159	pub async fn subscribe_process_mailbox_messages(
160		&self,
161		since_checkpoint: Option<u64>,
162		shutdown: CancellationToken,
163	) -> anyhow::Result<()> {
164		// Count consecutive reconnects that made no progress. A delivered
165		// message resets it (the connection is healthy); enough failures in a
166		// row means the server is unreachable, so we bail and let the daemon
167		// mark itself disconnected and back off a full sync interval.
168		let mut reconnect_count = 0;
169		const MAX_RECONNECT_ATTEMPTS: usize = 5;
170		let mut backoff = ReconnectBackoff::new();
171
172		loop {
173			let mut stream = self.subscribe_mailbox_messages(since_checkpoint).await?;
174			trace!("Connected to mailbox stream with server");
175
176			'stream: loop {
177				futures::select! {
178					message = stream.next().fuse() => {
179						match message {
180							Some(Ok(message)) => {
181								// A delivered message proves the connection is
182								// healthy, so stop counting reconnects as failures.
183								reconnect_count = 0;
184								if self.process_mailbox_message(message).await.is_break() {
185									// A message failed without advancing its
186									// checkpoint. Stop consuming this stream and
187									// resubscribe from the unadvanced checkpoint
188									// so it's redelivered before a later message
189									// can bury it. We deliberately do NOT reset
190									// the backoff here: if the same message keeps
191									// failing, the growing delay keeps this from
192									// becoming a tight redeliver loop.
193									trace!("Halting mailbox stream after unadvanced message; resubscribing");
194									break 'stream;
195								}
196								// Progress made: reconnect promptly next time.
197								backoff.reset();
198							},
199							// A tonic h2 stream reset is almost always a
200							// proxy- or server-side idle timeout rather than
201							// a real failure; resubscribe quietly. Unlike before
202							// we count it toward the reconnect cap, so a server
203							// that keeps resetting us is treated as unreachable
204							// rather than triggering an unbounded resubscribe.
205							Some(Err(e)) if crate::utils::is_h2_stream_error(&e) => {
206								reconnect_count += 1;
207								trace!("Mailbox stream reset by server, reconnecting: {e:#}");
208								break 'stream;
209							},
210							Some(Err(e)) => {
211								return Err(e).context("error on mailbox message stream");
212							},
213							None => {
214								reconnect_count += 1;
215								warn!("Mailbox stream dropped by server, reconnecting");
216								break 'stream;
217							},
218						}
219					},
220					_ = shutdown.cancelled().fuse() => {
221						info!("Shutdown signal received! Shutting mailbox messages process...");
222						return Ok(());
223					},
224				}
225			}
226
227			// Reached only by breaking out of 'stream to resubscribe.
228			if reconnect_count >= MAX_RECONNECT_ATTEMPTS {
229				bail!("Mailbox stream dropped by server, giving up to retry later");
230			}
231
232			// Back off before resubscribing so a fast-closing or rate-limited
233			// stream can't become a tight reconnect loop that floods the server
234			// with opened-then-reset streams.
235			futures::select! {
236				_ = backoff.wait().fuse() => {},
237				_ = shutdown.cancelled().fuse() => {
238					info!("Shutdown signal received! Shutting mailbox messages process...");
239					return Ok(());
240				},
241			}
242		}
243	}
244
245	/// Sync with the mailbox on the Ark server and look for out-of-round received VTXOs.
246	pub async fn sync_mailbox(&self) -> anyhow::Result<()> {
247		let (mut srv, _) = self.require_server().await?;
248
249		// we should be able to do all our syncing in 10 minutes
250		let expiry = chrono::Local::now() + std::time::Duration::from_secs(10 * 60);
251		let auth = self.mailbox_authorization(expiry);
252		let mailbox_id = auth.mailbox();
253
254		for _ in 0..MAX_MAILBOX_REQUEST_BURST {
255			let checkpoint = self.get_mailbox_checkpoint().await?;
256			let mailbox_req = protos::mailbox_server::MailboxRequest {
257				mailbox_id: mailbox_id.serialize(),
258				authorization: Some(auth.serialize()),
259				checkpoint,
260			};
261
262			let mailbox_resp = srv.mailbox_client.read_mailbox(mailbox_req).await
263				.context("error fetching mailbox")?.into_inner();
264			debug!("Ark server has {} mailbox messages for us", mailbox_resp.messages.len());
265
266			for mailbox_msg in mailbox_resp.messages {
267				if self.process_mailbox_message(mailbox_msg).await.is_break() {
268					// A message failed without advancing its checkpoint. Stop
269					// so we don't advance past it; the next sync retries from
270					// the same checkpoint.
271					return Ok(());
272				}
273			}
274
275			if !mailbox_resp.have_more {
276				break;
277			}
278		}
279
280		Ok(())
281	}
282
283	/// Turn raw byte arrays into VTXOs, then validate them.
284	///
285	/// This function doesn't return a result on purpose,
286	/// because we want to make sure we don't early return on
287	/// the first error. This ensure we process all VTXOs, even
288	/// if some are invalid, and print everything we received.
289	async fn process_raw_vtxos(
290		&self,
291		raw_vtxos: Vec<Vec<u8>>,
292	) -> Vec<Vtxo<Full>> {
293		let mut invalid_vtxos = Vec::with_capacity(raw_vtxos.len());
294		let mut valid_vtxos = Vec::with_capacity(raw_vtxos.len());
295
296		for bytes in &raw_vtxos {
297			let vtxo = match Vtxo::<Full>::deserialize(&bytes) {
298				Ok(vtxo) => vtxo,
299				Err(e) => {
300					error!("Failed to deserialize arkoor VTXO: {}: {}", bytes.as_hex(), e);
301					invalid_vtxos.push(bytes);
302					continue;
303				}
304			};
305
306			if let Err(e) = self.validate_vtxo(&vtxo).await {
307				error!("Received invalid arkoor VTXO {} from server: {}", vtxo.id(), e);
308				invalid_vtxos.push(bytes);
309				continue;
310			}
311
312			valid_vtxos.push(vtxo);
313		}
314
315		// We log all invalid VTXOs to keep track
316		if !invalid_vtxos.is_empty() {
317			error!("Received {} invalid arkoor VTXOs out of {} from server", invalid_vtxos.len(), raw_vtxos.len());
318		}
319
320		valid_vtxos
321	}
322
323	/// Process a single mailbox message and report whether the caller should
324	/// keep consuming the mailbox.
325	///
326	/// Returns [`ControlFlow::Break`] when an arkoor package failed to process
327	/// and its checkpoint was therefore not advanced. Because checkpoints are
328	/// monotonic, the caller must stop before a later message stores a higher
329	/// checkpoint and buries the unprocessed one; the next sync/resubscribe
330	/// re-fetches from the unadvanced checkpoint and retries.
331	pub(crate) async fn process_mailbox_message(
332		&self,
333		mailbox_msg: MailboxMessage,
334	) -> ControlFlow<()> {
335		use protos::mailbox_server::mailbox_message::Message;
336
337		// Each arm returns whether the checkpoint should advance. Only
338		// arkoor returns false on processing error so the server
339		// redelivers and we retry. Every other arm advances regardless,
340		// either because the work is idempotent and re-done on every
341		// wallet sync, or because the message is informational/ignored.
342		let advance = match mailbox_msg.message {
343			Some(Message::Arkoor(msg)) => {
344				match self.process_received_arkoor_package(msg.vtxos).await {
345					Ok(()) => true,
346					Err(e) => {
347						error!("Error processing received arkoor package: {:#}", e);
348						false
349					}
350				}
351			}
352			Some(Message::RoundParticipationCompleted(m)) => {
353				info!("Server informed that round participation is ready, unlock_hash:{:?}",
354					UnlockHash::from_slice(&m.unlock_hash).ok(),
355				);
356				if let Err(e) = self.sync_pending_rounds().await {
357					error!("Error syncing pending rounds: {:#}", e);
358				}
359				true
360			},
361			Some(Message::IncomingLightningPayment(msg)) => {
362				if let Err(e) = self.handle_lightning_receive_notification(msg).await {
363					error!("Error handling lightning receive notification: {:#}", e);
364				}
365				true
366			},
367			Some(Message::RecoveryVtxoIds(_)) => {
368				trace!("Received recovery VTXO IDs, ignoring");
369				true
370			}
371			Some(Message::LightningSendFinished(msg)) => {
372				if let Err(e) = self.handle_lightning_send_finished(msg, mailbox_msg.checkpoint).await {
373					error!("Error handling lightning send finished notification: {:#}", e);
374				}
375				true
376			}
377			None => {
378				warn!("Received unknown mailbox message kind at checkpoint {}; bark may need to be upgraded",
379					mailbox_msg.checkpoint);
380				true
381			}
382		};
383
384		if advance {
385			if let Err(e) = self.store_mailbox_checkpoint(mailbox_msg.checkpoint).await {
386				error!("Error storing mailbox checkpoint: {:#}", e);
387			}
388			ControlFlow::Continue(())
389		} else {
390			// An arkoor package didn't process and its checkpoint wasn't
391			// advanced. Stop here so a later message can't store a higher
392			// checkpoint and bury it.
393			ControlFlow::Break(())
394		}
395	}
396
397	async fn process_received_arkoor_package(
398		&self,
399		raw_vtxos: Vec<Vec<u8>>,
400	) -> anyhow::Result<()> {
401		let vtxos = self.process_raw_vtxos(raw_vtxos).await;
402
403		// Serialize the receive dedup across all consumers of this wallet's
404		// mailbox so two of them can't both record a movement for the same
405		// package. See MAILBOX_PROCESSING_LOCK_KEY. On lock failure we
406		// return like any other arkoor processing error, leaving this
407		// message's checkpoint unadvanced.
408		let _guard = self.inner.lock_manager.lock(
409			MAILBOX_PROCESSING_LOCK_KEY, MAILBOX_PROCESSING_LOCK_TIMEOUT,
410		).await.context("failed to acquire mailbox processing lock")?;
411
412		let mut new_vtxos = Vec::with_capacity(vtxos.len());
413		for vtxo in &vtxos {
414			// Skip if already in wallet
415			if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
416				debug!("Ignoring duplicate arkoor VTXO {}", vtxo.id());
417				continue;
418			}
419
420			trace!("Received arkoor VTXO {} for {}", vtxo.id(), vtxo.amount());
421			new_vtxos.push(vtxo);
422		}
423
424		if new_vtxos.is_empty() {
425			return Ok(());
426		}
427
428		// Redundantly re-register the received vtxos with the server. An
429		// up-to-date sender already does this after cosign, but older
430		// senders may not, so we do it on receive too to make sure the
431		// server has signed_tx rows for our spendable vtxos. Any failure
432		// is logged and swallowed: the receive must still proceed so we
433		// don't lose track of the vtxos locally, and later spends will
434		// retry registration if still needed.
435		if let Err(e) = self.register_vtxo_transactions_with_server(&new_vtxos).await {
436			warn!("Failed to register received arkoor vtxo transactions with server: {:#}", e);
437		}
438
439		let balance = vtxos
440			.iter()
441			.map(|vtxo| vtxo.amount()).sum::<Amount>()
442			.to_signed()?;
443		self.store_spendable_vtxos(&vtxos).await?;
444
445		// Build received_on destinations from received VTXOs, aggregated by address
446		let mut received_by_address = HashMap::<ark::Address, Amount>::new();
447		for vtxo in &vtxos {
448			if let Ok(Some((index, _))) = self.pubkey_keypair(&vtxo.user_pubkey()).await {
449				if let Ok(address) = self.peek_address(index).await {
450					*received_by_address.entry(address).or_default() += vtxo.amount();
451				}
452			}
453		}
454		let received_on: Vec<_> = received_by_address
455			.iter()
456			.map(|(addr, amount)| MovementDestination::ark(addr.clone(), *amount))
457			.collect();
458
459		let movement_id = self.inner.movements.new_finished_movement(
460			Subsystem::ARKOOR,
461			ArkoorMovement::Receive.to_string(),
462			MovementStatus::Successful,
463			MovementUpdate::new()
464				.produced_vtxos(&vtxos)
465				.intended_and_effective_balance(balance)
466				.received_on(received_on),
467		).await?;
468
469		info!("Received arkoor (movement {}) for {}", movement_id, balance);
470
471		Ok(())
472	}
473
474	/// Handle a lightning receive notification from the mailbox.
475	///
476	/// This is a signal that the server has received a lightning payment for us
477	/// and we should come online to claim it.
478	async fn handle_lightning_receive_notification(
479		&self,
480		notif: protos::mailbox_server::IncomingLightningPaymentMessage,
481	) -> anyhow::Result<()> {
482		let payment_hash = PaymentHash::try_from(notif.payment_hash)
483			.context("invalid payment hash in lightning receive notification")?;
484
485		debug!("Lightning receive notification: payment_hash={}", payment_hash);
486
487		match self.try_claim_lightning_receive(payment_hash, false).await {
488			Ok(_) => info!("Lightning receive claimed via mailbox notification for {}", payment_hash),
489			Err(e) => error!("Failed to claim lightning receive for {}: {:#}", payment_hash, e),
490		}
491
492		Ok(())
493	}
494
495	/// Handle a lightning send finished notification from the mailbox.
496	///
497	/// This notification indicates that the server has completed processing
498	/// a lightning payment we initiated, either successfully or with failure.
499	async fn handle_lightning_send_finished(
500		&self,
501		notif: protos::mailbox_server::LightningSendFinishedMessage,
502		checkpoint: u64,
503	) -> anyhow::Result<()> {
504		let payment_hash = PaymentHash::try_from(notif.payment_hash)
505			.context("invalid payment hash in lightning send finished notification")?;
506
507		let known_preimage = notif.preimage
508			.and_then(|bytes| Preimage::try_from(bytes).ok());
509
510		if known_preimage.is_some() {
511			debug!("Lightning send finished notification (success): payment_hash={}", payment_hash);
512		} else {
513			debug!("Lightning send finished notification (failed): payment_hash={}", payment_hash);
514		}
515
516		// Errors are logged but not propagated: we always advance the
517		// mailbox checkpoint to avoid re-processing the same
518		// notification on the next poll.
519		match self.is_invoice_paid(payment_hash).await {
520			Ok(true) => {
521				debug!("Lightning send {} already settled; ignoring notification", payment_hash);
522			},
523			Ok(false) => {
524				let lookup = self.lightning_send_checkpoint(payment_hash).await;
525				match lookup {
526					Ok(Some(send)) => {
527						let result = match (&send.progress, known_preimage) {
528							(Progress::PaymentInitiated(htlcs), Some(preimage)) => {
529								let htlcs = htlcs.clone();
530								self.settle_lightning_send_with_preimage(send, htlcs, preimage).await
531							},
532							(Progress::PaymentInitiated(_), None) => {
533								self.drive_action(send, DriveMode::UntilParkOrDone).await
534							},
535							_ => {
536								debug!("Lightning send finished notification for {} but checkpoint is not PaymentInitiated; ignoring", payment_hash);
537								Ok(())
538							},
539						};
540						match result {
541							Ok(()) => info!("Processed lightning send finished for {}", payment_hash),
542							Err(e) => error!("Failed to process lightning send finished for {}: {:#}", payment_hash, e),
543						}
544					},
545					Ok(None) => {
546						warn!("Lightning send finished notification for unknown payment hash {}", payment_hash);
547					},
548					Err(e) => {
549						error!("Failed to look up lightning send checkpoint for {}: {:#}", payment_hash, e);
550					},
551				}
552			},
553			Err(e) => {
554				error!("Failed to look up paid_invoice for {}: {:#}", payment_hash, e);
555			},
556		}
557
558		self.store_mailbox_checkpoint(checkpoint).await?;
559		Ok(())
560	}
561
562	/// Post vtxo IDs to the server's recovery mailbox
563	pub async fn post_recovery_vtxo_ids(
564		&self,
565		vtxo_ids: impl IntoIterator<Item = VtxoId>,
566	) -> anyhow::Result<()> {
567		let vtxo_ids = vtxo_ids.into_iter().map(|id| id.to_bytes().to_vec()).collect::<Vec<_>>();
568		if vtxo_ids.is_empty() {
569			return Ok(());
570		}
571		let nb_vtxos = vtxo_ids.len();
572
573		// Prove ownership of the recovery mailbox; short validity is enough as
574		// it's consumed by this single request.
575		let expiry = chrono::Local::now() + std::time::Duration::from_secs(60);
576		let auth = MailboxAuthorization::new(&self.recovery_mailbox_keypair(), expiry);
577		let mailbox_id = self.recovery_mailbox_identifier().serialize();
578
579		let (mut srv, _) = self.require_server().await?;
580		for chunk in vtxo_ids.chunks(MAX_NB_MAILBOX_RECOVERY_IDS) {
581			let req = protos::mailbox_server::PostRecoveryVtxoIdsRequest {
582				mailbox_id: mailbox_id.clone(),
583				vtxo_ids: chunk.to_vec(),
584				authorization: Some(auth.serialize()),
585			};
586
587			srv.mailbox_client.post_recovery_vtxo_ids(req).await
588				.context("error posting recovery vtxo IDs to server")?;
589		}
590
591		debug!("Posted {} recovery vtxo IDs to server", nb_vtxos);
592		Ok(())
593	}
594
595	/// Return the stored mailbox checkpoint — the tip position the wallet
596	/// has consumed up to. After a successful [`Self::sync_mailbox`], this value
597	/// reflects the server's latest advertised tip.
598	pub async fn get_mailbox_checkpoint(&self) -> anyhow::Result<u64> {
599		Ok(self.inner.db.get_mailbox_checkpoint().await?)
600	}
601
602	async fn store_mailbox_checkpoint(&self, checkpoint: u64) -> anyhow::Result<()> {
603		Ok(self.inner.db.store_mailbox_checkpoint(checkpoint).await?)
604	}
605}