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
37const MAX_MAILBOX_REQUEST_BURST: usize = 10;
47
48const MAILBOX_PROCESSING_LOCK_KEY: &str = "mailbox.processing";
66
67const MAILBOX_PROCESSING_LOCK_TIMEOUT: std::time::Duration =
77 std::time::Duration::from_secs(30);
78
79impl Wallet {
80 pub fn mailbox_keypair(&self) -> Keypair {
82 self.inner.seed.to_mailbox_keypair()
83 }
84
85 pub fn recovery_mailbox_keypair(&self) -> Keypair {
87 self.inner.seed.to_recovery_mailbox_keypair()
88 }
89
90 pub fn mailbox_identifier(&self) -> MailboxIdentifier {
92 let mailbox_kp = self.mailbox_keypair();
93 MailboxIdentifier::from_pubkey(mailbox_kp.public_key())
94 }
95
96 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 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 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 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 pub async fn subscribe_process_mailbox_messages(
160 &self,
161 since_checkpoint: Option<u64>,
162 shutdown: CancellationToken,
163 ) -> anyhow::Result<()> {
164 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 reconnect_count = 0;
184 if self.process_mailbox_message(message).await.is_break() {
185 trace!("Halting mailbox stream after unadvanced message; resubscribing");
194 break 'stream;
195 }
196 backoff.reset();
198 },
199 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 if reconnect_count >= MAX_RECONNECT_ATTEMPTS {
229 bail!("Mailbox stream dropped by server, giving up to retry later");
230 }
231
232 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 pub async fn sync_mailbox(&self) -> anyhow::Result<()> {
247 let (mut srv, _) = self.require_server().await?;
248
249 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 return Ok(());
272 }
273 }
274
275 if !mailbox_resp.have_more {
276 break;
277 }
278 }
279
280 Ok(())
281 }
282
283 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}