1use std::collections::HashMap;
6use std::iter;
7use std::borrow::Cow;
8use std::convert::Infallible;
9use std::sync::Arc;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12use anyhow::Context;
13use ark::vtxo::VtxoValidationError;
14use bdk_esplora::esplora_client::Amount;
15use bip39::rand;
16use bitcoin::{OutPoint, SignedAmount, Transaction, Txid};
17use bitcoin::consensus::encode::{deserialize, serialize_hex};
18use bitcoin::hashes::Hash;
19use bitcoin::hex::DisplayHex;
20use bitcoin::key::Keypair;
21use bitcoin::secp256k1::schnorr;
22use futures::future::join_all;
23use futures::{Stream, StreamExt};
24use log::{debug, error, info, trace, warn};
25
26use ark::{ProtocolEncoding, SignedVtxoRequest, Vtxo, VtxoRequest};
27use ark::vtxo::Full;
28use ark::attestations::{DelegatedRoundParticipationAttestation, RoundAttemptAttestation};
29use ark::forfeit::HashLockedForfeitBundle;
30use ark::musig::{self, PublicNonce, SecretNonce};
31use ark::rounds::{RoundAttempt, RoundEvent, RoundFinished, RoundSeq, ROUND_TX_VTXO_TREE_VOUT};
32use ark::tree::signed::{LeafVtxoCosignContext, UnlockHash, VtxoTreeSpec};
33use bitcoin_ext::{BlockHeight, TxStatus};
34use server_rpc::{protos, ServerConnection, TryFromBytes, MAX_NB_FORFEIT_NONCE_IDS};
35
36use crate::movement::manager::OnDropStatus;
37use crate::{Wallet, WalletVtxo, SECP, SUBSCRIBE_REQUEST_TIMEOUT};
38use crate::movement::{MovementId, MovementStatus};
39use crate::movement::update::MovementUpdate;
40use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
41
42const ROUND_LOCK_TIMEOUT: Duration = Duration::from_secs(10);
45use crate::subsystem::{RoundMovement, Subsystem};
46
47
48const HARK_TRANSITION_KIND: &str = "hash-locked-cosigned";
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct RoundParticipation {
54 #[serde(with = "ark::encode::serde::vec")]
55 pub inputs: Vec<Vtxo<Full>>,
56 pub outputs: Vec<VtxoRequest>,
59 #[serde(default, skip_serializing_if = "Option::is_none", with = "ark::encode::serde::opt")]
61 pub unblinded_mailbox_id: Option<ark::mailbox::MailboxIdentifier>,
62}
63
64impl RoundParticipation {
65 pub fn to_movement_update(&self) -> anyhow::Result<MovementUpdate> {
66 let input_amount = self.inputs.iter().map(|i| i.amount()).sum::<Amount>();
67 let output_amount = self.outputs.iter().map(|r| r.amount).sum::<Amount>();
68 let fee = input_amount - output_amount;
69 Ok(MovementUpdate::new()
70 .consumed_vtxos(&self.inputs)
71 .intended_balance(SignedAmount::ZERO)
72 .effective_balance( - fee.to_signed()?)
73 .fee(fee)
74 )
75 }
76}
77
78#[derive(Debug, Clone)]
79pub enum RoundStatus {
80 Confirmed {
82 funding_txid: Txid,
83 },
84 Unconfirmed {
86 funding_txid: Txid,
87 },
88 Pending,
90 Failed {
92 error: String,
93 },
94 Canceled,
96}
97
98impl RoundStatus {
99 pub fn is_final(&self) -> bool {
101 match self {
102 Self::Confirmed { .. } => true,
103 Self::Unconfirmed { .. } => false,
104 Self::Pending => false,
105 Self::Failed { .. } => true,
106 Self::Canceled => true,
107 }
108 }
109
110 pub fn is_success(&self) -> bool {
112 match self {
113 Self::Confirmed { .. } => true,
114 Self::Unconfirmed { .. } => true,
115 Self::Pending => false,
116 Self::Failed { .. } => false,
117 Self::Canceled => false,
118 }
119 }
120}
121
122pub struct RoundState {
135 pub(crate) done: bool,
137
138 pub(crate) participation: RoundParticipation,
140
141 pub(crate) flow: RoundFlowState,
143
144 pub(crate) new_vtxos: Vec<Vtxo<Full>>,
149
150 pub(crate) sent_forfeit_sigs: bool,
157
158 pub(crate) movement_id: Option<MovementId>,
160}
161
162impl RoundState {
163 fn new_interactive(
164 participation: RoundParticipation,
165 movement_id: Option<MovementId>,
166 ) -> Self {
167 Self {
168 participation,
169 movement_id,
170 flow: RoundFlowState::InteractivePending,
171 new_vtxos: Vec::new(),
172 sent_forfeit_sigs: false,
173 done: false,
174 }
175 }
176
177 fn new_delegated(
178 participation: RoundParticipation,
179 unlock_hash: UnlockHash,
180 movement_id: Option<MovementId>,
181 ) -> Self {
182 Self {
183 participation,
184 movement_id,
185 flow: RoundFlowState::NonInteractivePending { unlock_hash },
186 new_vtxos: Vec::new(),
187 sent_forfeit_sigs: false,
188 done: false,
189 }
190 }
191
192 pub fn participation(&self) -> &RoundParticipation {
194 &self.participation
195 }
196
197 pub fn unlock_hash(&self) -> Option<UnlockHash> {
199 match self.flow {
200 RoundFlowState::NonInteractivePending { unlock_hash } => Some(unlock_hash),
201 RoundFlowState::InteractivePending => None,
202 RoundFlowState::InteractiveOngoing { .. } => None,
203 RoundFlowState::Failed { .. } => None,
204 RoundFlowState::Canceled => None,
205 RoundFlowState::Finished { unlock_hash, .. } => Some(unlock_hash),
206 }
207 }
208
209 pub fn funding_tx(&self) -> Option<&Transaction> {
210 match self.flow {
211 RoundFlowState::NonInteractivePending { .. } => None,
212 RoundFlowState::InteractivePending => None,
213 RoundFlowState::InteractiveOngoing { .. } => None,
214 RoundFlowState::Failed { .. } => None,
215 RoundFlowState::Canceled => None,
216 RoundFlowState::Finished { ref funding_tx, .. } => Some(funding_tx),
217 }
218 }
219
220 pub fn ongoing_participation(&self) -> bool {
222 match self.flow {
223 RoundFlowState::NonInteractivePending { .. } => false,
224 RoundFlowState::InteractivePending => true,
225 RoundFlowState::InteractiveOngoing { .. } => true,
226 RoundFlowState::Failed { .. } => false,
227 RoundFlowState::Canceled => false,
228 RoundFlowState::Finished { .. } => false,
229 }
230 }
231
232 pub async fn try_cancel(&mut self, wallet: &Wallet) -> anyhow::Result<bool> {
235 let ret = match self.flow {
236 RoundFlowState::NonInteractivePending { .. } => {
237 bail!("it is currently not yet possible to cancel pending delegated rounds");
239 },
240 RoundFlowState::Canceled => true,
241 RoundFlowState::Failed { .. } => true,
242 RoundFlowState::InteractivePending | RoundFlowState::InteractiveOngoing { .. } => {
243 self.flow = RoundFlowState::Canceled;
244 true
245 },
246 RoundFlowState::Finished { .. } => false,
247 };
248 if ret {
249 persist_round_failure(wallet, &self.participation, self.movement_id).await
250 .context("failed to persist round failure for cancelation")?;
251 }
252 Ok(ret)
253 }
254
255 async fn try_start_attempt(
256 &mut self,
257 wallet: &Wallet,
258 attempt: &RoundAttempt,
259 ) {
260 if let RoundFlowState::InteractiveOngoing {
263 state: AttemptState::AwaitingUnsignedVtxoTree { ref cosign_keys, .. },
264 ..
265 } = self.flow {
266 if let Some(k) = cosign_keys.first() {
267 wallet.inner.round_secret_nonces.forget(&k.public_key());
268 }
269 }
270
271 match start_attempt(wallet, &self.participation, attempt).await {
272 Ok(state) => {
273 self.flow = RoundFlowState::InteractiveOngoing {
274 round_seq: attempt.round_seq,
275 attempt_seq: attempt.attempt_seq,
276 state: state,
277 };
278 },
279 Err(e) => {
280 self.flow = RoundFlowState::Failed {
281 error: format!("{:#}", e),
282 };
283 },
284 }
285 }
286
287 pub async fn process_event(
289 &mut self,
290 wallet: &Wallet,
291 event: &RoundEvent,
292 ) -> bool {
293 let _: Infallible = match self.flow {
294 RoundFlowState::InteractivePending => {
295 if let RoundEvent::Attempt(e) = event && e.attempt_seq == 0 {
296 trace!("Joining round attempt {}:{}", e.round_seq, e.attempt_seq);
297 self.try_start_attempt(wallet, e).await;
298 return true;
299 } else {
300 trace!("Ignoring {} event (seq {}:{}), waiting for round to start",
301 event.kind(), event.round_seq(), event.attempt_seq(),
302 );
303 return false;
304 }
305 },
306 RoundFlowState::InteractiveOngoing { round_seq, attempt_seq, ref mut state } => {
307 if let RoundEvent::Failed(e) = event && e.round_seq == round_seq {
310 warn!("Round {} failed by server", round_seq);
311 self.flow = RoundFlowState::Failed {
312 error: format!("round {} failed by server", round_seq),
313 };
314 return true;
315 }
316
317 if event.round_seq() > round_seq {
318 self.flow = RoundFlowState::Failed {
321 error: format!("round {} started while we were on {}",
322 event.round_seq(), round_seq,
323 ),
324 };
325 return true;
326 }
327
328 if event.attempt_seq() < attempt_seq {
329 trace!("ignoring replayed message from old attempt");
330 return false;
331 }
332
333 if let RoundEvent::Attempt(e) = event && e.attempt_seq > attempt_seq {
334 trace!("Joining new round attempt {}:{}", e.round_seq, e.attempt_seq);
335 self.try_start_attempt(wallet, e).await;
336 return true;
337 }
338 trace!("Processing event {} for round attempt {}:{} in state {}",
339 event.kind(), round_seq, attempt_seq, state.kind(),
340 );
341
342 return match progress_attempt(state, wallet, &self.participation, event).await {
343 AttemptProgressResult::NotUpdated => false,
344 AttemptProgressResult::Updated { new_state } => {
345 *state = new_state;
346 true
347 },
348 AttemptProgressResult::Failed(e) => {
349 warn!("Round failed with error: {:#}", e);
350 self.flow = RoundFlowState::Failed {
351 error: format!("{:#}", e),
352 };
353 true
354 },
355 AttemptProgressResult::Finished { funding_tx, vtxos, unlock_hash } => {
356 self.new_vtxos = vtxos;
357 let funding_txid = funding_tx.compute_txid();
358 self.flow = RoundFlowState::Finished { funding_tx, unlock_hash };
359 if let Some(mid) = self.movement_id {
360 if let Err(e) = update_funding_txid(wallet, mid, funding_txid).await {
361 warn!("Error updating the round funding txid: {:#}", e);
362 }
363 }
364 true
365 },
366 };
367 },
368 RoundFlowState::NonInteractivePending { .. }
369 | RoundFlowState::Finished { .. }
370 | RoundFlowState::Failed { .. }
371 | RoundFlowState::Canceled => return false,
372 };
373 }
374
375 pub async fn sync(&mut self, wallet: &Wallet) -> anyhow::Result<RoundStatus> {
380 match self.flow {
381 RoundFlowState::Finished { ref funding_tx, .. } if self.done => {
382 Ok(RoundStatus::Confirmed {
383 funding_txid: funding_tx.compute_txid(),
384 })
385 },
386
387 RoundFlowState::InteractivePending | RoundFlowState::InteractiveOngoing { .. } => {
388 Ok(RoundStatus::Pending)
389 },
390 RoundFlowState::Failed { ref error } => {
391 persist_round_failure(wallet, &self.participation, self.movement_id).await
392 .context("failed to persist round failure")?;
393 Ok(RoundStatus::Failed { error: error.clone() })
394 },
395 RoundFlowState::Canceled => {
396 persist_round_failure(wallet, &self.participation, self.movement_id).await
397 .context("failed to persist round failure")?;
398 Ok(RoundStatus::Canceled)
399 },
400
401 RoundFlowState::NonInteractivePending { unlock_hash } => {
402 match progress_delegated(wallet, &self.participation, unlock_hash).await {
403 Ok(HarkProgressResult::RoundPending) => Ok(RoundStatus::Pending),
404 Ok(HarkProgressResult::RoundNotFound) => {
407 info!("Server reports round participation not found (no forfeits sent)");
408 self.flow = RoundFlowState::Failed {
409 error: "server reports round participation not found".into(),
410 };
411 if let Some(movement_id) = self.movement_id {
412 wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
413 .context("failed to mark refresh movement as failed")?;
414 }
415 Ok(RoundStatus::Failed {
416 error: "server reports round participation not found".into(),
417 })
418 },
419 Ok(HarkProgressResult::Ok { funding_tx, new_vtxos }) => {
420 let funding_txid = funding_tx.compute_txid();
421 self.new_vtxos = new_vtxos;
422 self.flow = RoundFlowState::Finished {
423 funding_tx: funding_tx.clone(),
424 unlock_hash: unlock_hash,
425 };
426
427 persist_round_success(
428 wallet,
429 &self.participation,
430 self.movement_id,
431 &self.new_vtxos,
432 &funding_tx,
433 ).await.context("failed to store successful round in DB!")?;
434
435 self.done = true;
436
437 Ok(RoundStatus::Confirmed { funding_txid })
438 },
439 Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid }) => {
440 if let Some(mid) = self.movement_id {
441 update_funding_txid(wallet, mid, funding_txid).await
442 .context("failed to update funding txid in DB")?;
443 }
444 Ok(RoundStatus::Unconfirmed { funding_txid })
445 },
446
447 Err(HarkForfeitError::Err(e)) => {
450 Err(e.context("error progressing delegated round"))
454 },
455 Err(HarkForfeitError::SentForfeits(e)) => {
456 self.sent_forfeit_sigs = true;
457 Err(e.context("error progressing delegated round \
458 after sending forfeit tx signatures"))
459 },
460 }
461 },
462 RoundFlowState::Finished { ref funding_tx, unlock_hash } => {
464 let funding_txid = funding_tx.compute_txid();
465 let confirmed = check_funding_tx_confirmations(
466 wallet, funding_txid, &funding_tx,
467 ).await.context("error checking funding tx confirmations")?;
468 if !confirmed {
469 trace!("Funding tx {} not yet deeply enough confirmed", funding_txid);
470 return Ok(RoundStatus::Unconfirmed { funding_txid });
471 }
472
473 match hark_vtxo_swap(
474 wallet, &self.participation, &mut self.new_vtxos, &funding_tx, unlock_hash,
475 ).await {
476 Ok(()) => {
477 persist_round_success(
478 wallet,
479 &self.participation,
480 self.movement_id,
481 &self.new_vtxos,
482 &funding_tx,
483 ).await.context("failed to store successful round in DB!")?;
484
485 self.done = true;
486
487 Ok(RoundStatus::Confirmed { funding_txid })
488 },
489 Err(HarkForfeitError::Err(e)) => {
490 Err(e.context("error forfeiting VTXOs after round"))
491 },
492 Err(HarkForfeitError::SentForfeits(e)) => {
493 self.sent_forfeit_sigs = true;
494 Err(e.context("error after having signed and sent \
495 forfeit signatures to server"))
496 },
497 }
498 },
499 }
500 }
501
502 pub fn output_vtxos(&self) -> Option<&[Vtxo<Full>]> {
505 if self.new_vtxos.is_empty() {
506 None
507 } else {
508 Some(&self.new_vtxos)
509 }
510 }
511
512 pub fn locked_pending_inputs(&self) -> &[Vtxo<Full>] {
515 match self.flow {
517 RoundFlowState::NonInteractivePending { .. }
518 | RoundFlowState::InteractivePending
519 | RoundFlowState::InteractiveOngoing { .. }
520 => {
521 &self.participation.inputs
522 },
523 RoundFlowState::Finished { .. } => if self.done {
524 &[]
526 } else {
527 &self.participation.inputs
528 },
529 RoundFlowState::Failed { .. }
530 | RoundFlowState::Canceled
531 => {
532 &[]
534 },
535 }
536 }
537
538 pub fn pending_balance(&self) -> Amount {
542 if self.done {
543 return Amount::ZERO;
544 }
545
546 match self.flow {
547 RoundFlowState::NonInteractivePending { .. }
548 | RoundFlowState::InteractivePending
549 | RoundFlowState::InteractiveOngoing { .. }
550 | RoundFlowState::Finished { .. }
551 => {
552 self.participation.outputs.iter().map(|o| o.amount).sum()
553 },
554 RoundFlowState::Failed { .. } | RoundFlowState::Canceled => {
555 Amount::ZERO
556 },
557 }
558 }
559
560}
561
562pub enum RoundFlowState {
567 NonInteractivePending {
569 unlock_hash: UnlockHash,
570 },
571
572 InteractivePending,
574 InteractiveOngoing {
576 round_seq: RoundSeq,
577 attempt_seq: usize,
578 state: AttemptState,
579 },
580
581 Finished {
583 funding_tx: Transaction,
584 unlock_hash: UnlockHash,
585 },
586
587 Failed {
589 error: String,
590 },
591
592 Canceled,
594}
595
596pub enum AttemptState {
601 AwaitingAttempt,
602 AwaitingUnsignedVtxoTree {
603 cosign_keys: Vec<Keypair>,
604 unlock_hash: UnlockHash,
605 },
606 AwaitingFinishedRound {
607 unsigned_round_tx: Transaction,
608 vtxos_spec: VtxoTreeSpec,
609 unlock_hash: UnlockHash,
610 },
611}
612
613impl AttemptState {
614 fn kind(&self) -> &'static str {
616 match self {
617 Self::AwaitingAttempt => "AwaitingAttempt",
618 Self::AwaitingUnsignedVtxoTree { .. } => "AwaitingUnsignedVtxoTree",
619 Self::AwaitingFinishedRound { .. } => "AwaitingFinishedRound",
620 }
621 }
622}
623
624enum AttemptProgressResult {
626 Finished {
627 funding_tx: Transaction,
628 vtxos: Vec<Vtxo<Full>>,
629 unlock_hash: UnlockHash,
630 },
631 Failed(anyhow::Error),
632 Updated {
638 new_state: AttemptState,
639 },
640 NotUpdated,
641}
642
643async fn start_attempt(
645 wallet: &Wallet,
646 participation: &RoundParticipation,
647 event: &RoundAttempt,
648) -> anyhow::Result<AttemptState> {
649 let (mut srv, ark_info) = wallet.require_server().await.context("server not available")?;
650
651 let cosign_keys = iter::repeat_with(|| Keypair::new(&SECP, &mut rand::thread_rng()))
653 .take(participation.outputs.len())
654 .collect::<Vec<_>>();
655
656 let cosign_nonces = cosign_keys.iter()
659 .map(|key| {
660 let mut secs = Vec::with_capacity(ark_info.nb_round_nonces);
661 let mut pubs = Vec::with_capacity(ark_info.nb_round_nonces);
662 for _ in 0..ark_info.nb_round_nonces {
663 let (s, p) = musig::nonce_pair(key);
664 secs.push(s);
665 pubs.push(p);
666 }
667 (secs, pubs)
668 })
669 .take(participation.outputs.len())
670 .collect::<Vec<(Vec<SecretNonce>, Vec<PublicNonce>)>>();
671
672
673 debug!("Submitting payment request with {} inputs and {} vtxo outputs",
675 participation.inputs.len(), participation.outputs.len(),
676 );
677
678 let unblinded_mailbox_id = wallet.mailbox_identifier();
680 let signed_reqs = participation.outputs.iter()
681 .zip(cosign_keys.iter())
682 .zip(cosign_nonces.iter())
683 .map(|((req, cosign_key), (_sec, pub_nonces))| {
684 SignedVtxoRequest {
685 vtxo: req.clone(),
686 cosign_pubkey: cosign_key.public_key(),
687 nonces: pub_nonces.clone(),
688 }
689 })
690 .collect::<Vec<_>>();
691
692 let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
693 for vtxo in participation.inputs.iter() {
694 let keypair = wallet.get_vtxo_key(vtxo).await
695 .map_err(HarkForfeitError::Err)?;
696 input_vtxos.push(protos::InputVtxo {
697 vtxo_id: vtxo.id().to_bytes().to_vec(),
698 attestation: {
699 let attestation = RoundAttemptAttestation::new(
700 event.challenge, vtxo.id(), &signed_reqs, &keypair,
701 );
702 attestation.serialize()
703 },
704 });
705 }
706
707 wallet.register_vtxo_transactions_with_server(&participation.inputs).await
709 .map_err(HarkForfeitError::Err)?;
710
711 let resp = srv.client.submit_payment(protos::SubmitPaymentRequest {
712 input_vtxos: input_vtxos,
713 vtxo_requests: signed_reqs.into_iter().map(Into::into).collect(),
714 #[allow(deprecated)]
715 offboard_requests: vec![],
716 unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
717 }).await.context("Ark server refused our payment submission")?;
718 let unlock_hash = UnlockHash::from_bytes(&resp.into_inner().unlock_hash)?;
719
720 if let Some(k) = cosign_keys.first() {
723 wallet.inner.round_secret_nonces.stash(
724 k.public_key(),
725 cosign_nonces.into_iter().map(|(sec, _pub)| sec).collect(),
726 );
727 }
728
729 Ok(AttemptState::AwaitingUnsignedVtxoTree { unlock_hash, cosign_keys })
730}
731
732#[derive(Debug, thiserror::Error)]
734enum HarkForfeitError {
735 #[error("error after forfeits were sent")]
737 SentForfeits(#[source] anyhow::Error),
738 #[error("error before forfeits were sent")]
740 Err(#[source] anyhow::Error),
741}
742
743async fn hark_cosign_leaf(
744 wallet: &Wallet,
745 srv: &mut ServerConnection,
746 funding_tx: &Transaction,
747 vtxo: &mut Vtxo<Full>,
748) -> anyhow::Result<()> {
749 let key = wallet.pubkey_keypair(&vtxo.user_pubkey()).await
750 .context("error fetching keypair").map_err(HarkForfeitError::Err)?
751 .with_context(|| format!(
752 "keypair {} not found for VTXO {}", vtxo.user_pubkey(), vtxo.id(),
753 ))?.1;
754 let (ctx, cosign_req) = LeafVtxoCosignContext::new(vtxo, funding_tx, &key);
755 let cosign_resp = srv.client.request_leaf_vtxo_cosign(
756 protos::LeafVtxoCosignRequest::from(cosign_req),
757 ).await
758 .with_context(|| format!("error requesting leaf cosign for vtxo {}", vtxo.id()))?
759 .into_inner().try_into()
760 .context("bad leaf vtxo cosign response")?;
761 ensure!(ctx.finalize(vtxo, cosign_resp),
762 "failed to finalize VTXO leaf signature for VTXO {}", vtxo.id(),
763 );
764
765 Ok(())
766}
767
768async fn hark_vtxo_swap(
778 wallet: &Wallet,
779 participation: &RoundParticipation,
780 output_vtxos: &mut [Vtxo<Full>],
781 funding_tx: &Transaction,
782 unlock_hash: UnlockHash,
783) -> Result<(), HarkForfeitError> {
784 let (mut srv, _) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
785
786 wallet.register_vtxo_transactions_with_server(&participation.inputs).await
788 .context("couldn't send our input vtxo transactions to server")
789 .map_err(HarkForfeitError::Err)?;
790
791 for vtxo in output_vtxos.iter_mut() {
793 hark_cosign_leaf(wallet, &mut srv, funding_tx, vtxo).await
794 .map_err(HarkForfeitError::Err)?;
795 }
796
797 let mut server_nonces = Vec::with_capacity(participation.inputs.len());
802 for inputs in participation.inputs.chunks(MAX_NB_FORFEIT_NONCE_IDS) {
803 let nonces = srv.client.request_forfeit_nonces(protos::ForfeitNoncesRequest {
804 unlock_hash: unlock_hash.to_byte_array().to_vec(),
805 vtxo_ids: inputs.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
806 }).await
807 .context("request forfeits nonces call failed")
808 .map_err(HarkForfeitError::Err)?
809 .into_inner().public_nonces.into_iter()
810 .map(|b| musig::PublicNonce::from_bytes(b))
811 .collect::<Result<Vec<_>, _>>()
812 .context("invalid forfeit nonces")
813 .map_err(HarkForfeitError::Err)?;
814
815 if nonces.len() != inputs.len() {
816 return Err(HarkForfeitError::Err(anyhow!(
817 "server sent {} nonce pairs, expected {}",
818 nonces.len(), inputs.len(),
819 )));
820 }
821 server_nonces.extend(nonces);
822 }
823
824 let mut forfeit_bundles = Vec::with_capacity(participation.inputs.len());
825 for (input, nonces) in participation.inputs.iter().zip(server_nonces.into_iter()) {
826 let user_key = wallet.pubkey_keypair(&input.user_pubkey()).await
827 .ok().flatten().with_context(|| format!(
828 "failed to fetch keypair for vtxo user pubkey {}", input.user_pubkey(),
829 )).map_err(HarkForfeitError::Err)?.1;
830 forfeit_bundles.push(HashLockedForfeitBundle::new(
831 input, unlock_hash, &user_key, &nonces,
832 ))
833 }
834
835 let preimage = srv.client.forfeit_vtxos(protos::ForfeitVtxosRequest {
836 forfeit_bundles: forfeit_bundles.iter().map(|b| b.serialize()).collect(),
837 }).await
838 .context("forfeit vtxos call failed")
839 .map_err(HarkForfeitError::SentForfeits)?
840 .into_inner().unlock_preimage.as_slice().try_into()
841 .context("invalid preimage length")
842 .map_err(HarkForfeitError::SentForfeits)?;
843
844 for vtxo in output_vtxos.iter_mut() {
845 if !vtxo.provide_unlock_preimage(preimage) {
846 return Err(HarkForfeitError::SentForfeits(anyhow!(
847 "invalid preimage {} for vtxo {} with supposed unlock hash {}",
848 preimage.as_hex(), vtxo.id(), unlock_hash,
849 )));
850 }
851
852 vtxo.validate(&funding_tx).with_context(|| format!(
854 "new VTXO {} does not pass validation after hArk forfeit protocol", vtxo.id(),
855 )).map_err(HarkForfeitError::SentForfeits)?;
856 }
857
858 wallet.register_vtxo_transactions_with_server(output_vtxos).await
860 .context("couldn't register output vtxo transactions with server")
861 .map_err(HarkForfeitError::SentForfeits)?;
862
863 Ok(())
864}
865
866fn check_vtxo_fails_hash_lock(funding_tx: &Transaction, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
867 match vtxo.validate(funding_tx) {
868 Err(VtxoValidationError::GenesisTransition {
869 genesis_idx, genesis_len, transition_kind, ..
870 }) if genesis_idx + 1 == genesis_len && transition_kind == HARK_TRANSITION_KIND => Ok(()),
871 Ok(()) => Err(anyhow!("new un-unlocked VTXO should fail validation but doesn't: {}",
872 vtxo.serialize_hex(),
873 )),
874 Err(e) => Err(anyhow!("new VTXO {} failed validation: {:#}", vtxo.id(), e)),
875 }
876}
877
878fn check_round_matches_participation(
879 part: &RoundParticipation,
880 new_vtxos: &[Vtxo<Full>],
881 funding_tx: &Transaction,
882) -> anyhow::Result<()> {
883 ensure!(new_vtxos.len() == part.outputs.len(),
884 "unexpected number of VTXOs: got {}, expected {}", new_vtxos.len(), part.outputs.len(),
885 );
886
887 for (vtxo, req) in new_vtxos.iter().zip(&part.outputs) {
888 ensure!(vtxo.amount() == req.amount,
889 "unexpected VTXO amount: got {}, expected {}", vtxo.amount(), req.amount,
890 );
891 ensure!(*vtxo.policy() == req.policy,
892 "unexpected VTXO policy: got {:?}, expected {:?}", vtxo.policy(), req.policy,
893 );
894
895 check_vtxo_fails_hash_lock(funding_tx, vtxo)?;
897 }
898
899 Ok(())
900}
901
902async fn check_funding_tx_confirmations(
912 wallet: &Wallet,
913 funding_txid: Txid,
914 funding_tx: &Transaction,
915) -> anyhow::Result<bool> {
916 let tip = wallet.inner.chain.tip().await.context("chain source error")?;
917 let conf_height = tip - wallet.inner.config.round_tx_required_confirmations + 1;
918 let tx_status = wallet.inner.chain.tx_status(funding_txid).await.context("chain source error")?;
919 trace!("Round funding tx {} confirmation status: {:?} (tip={})",
920 funding_txid, tx_status, tip,
921 );
922 match tx_status {
923 TxStatus::Confirmed(b) if b.height <= conf_height => Ok(true),
924 TxStatus::Mempool | TxStatus::Confirmed(_) => {
925 if wallet.inner.config.round_tx_required_confirmations == 0 {
926 debug!("Accepting round funding tx without confirmations because of configuration");
927 Ok(true)
928 } else {
929 trace!("Hark round funding tx not confirmed (deep enough) yet: {:?}", tx_status);
930 Ok(false)
931 }
932 },
933 TxStatus::NotFound => {
934 if let Err(e) = wallet.inner.chain.broadcast_tx(&funding_tx).await {
939 Err(anyhow!("hark funding tx {} server sent us is rejected by mempool (hex={}): {:#}",
940 funding_txid, serialize_hex(funding_tx), e,
941 ))
942 } else {
943 trace!("hark funding tx {} was not in mempool but we broadcast it", funding_txid);
944 Ok(false)
945 }
946 },
947 }
948}
949
950enum HarkProgressResult {
951 RoundPending,
952 RoundNotFound,
953 FundingTxUnconfirmed {
954 funding_txid: Txid,
955 },
956 Ok {
957 funding_tx: Transaction,
958 new_vtxos: Vec<Vtxo<Full>>,
959 },
960}
961
962async fn progress_delegated(
963 wallet: &Wallet,
964 participation: &RoundParticipation,
965 unlock_hash: UnlockHash,
966) -> Result<HarkProgressResult, HarkForfeitError> {
967 let (mut srv, _) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
968
969 let resp = match srv.client.round_participation_status(protos::RoundParticipationStatusRequest {
970 unlock_hash: unlock_hash.to_byte_array().to_vec(),
971 }).await {
972 Ok(resp) => resp.into_inner(),
973 Err(err) if err.code() == tonic::Code::NotFound => {
974 return Ok(HarkProgressResult::RoundNotFound);
975 },
976 Err(err) => {
977 return Err(HarkForfeitError::Err(
978 anyhow::Error::from(err).context("error checking round participation status"),
979 ));
980 },
981 };
982 let status = protos::RoundParticipationStatus::try_from(resp.status)
983 .context("unknown status from server")
984 .map_err(HarkForfeitError::Err) ?;
985
986 if status == protos::RoundParticipationStatus::RoundPartPending {
987 trace!("Hark round still pending");
988 return Ok(HarkProgressResult::RoundPending);
989 }
990
991 if status == protos::RoundParticipationStatus::RoundPartReleased {
996 let preimage = resp.unlock_preimage.as_ref().map(|p| p.as_hex());
997 warn!("Server says preimage was already released for hArk participation \
998 with unlock hash {}. Supposed preimage: {:?}", unlock_hash, preimage,
999 );
1000 }
1001
1002 let funding_tx_bytes = resp.round_funding_tx
1003 .context("funding txid should be provided when status is not pending")
1004 .map_err(HarkForfeitError::Err)?;
1005 let funding_tx = deserialize::<Transaction>(&funding_tx_bytes)
1006 .context("invalid funding txid")
1007 .map_err(HarkForfeitError::Err)?;
1008 let funding_txid = funding_tx.compute_txid();
1009 trace!("Funding tx for round participation with unlock hash {}: {} ({})",
1010 unlock_hash, funding_tx.compute_txid(), funding_tx_bytes.as_hex(),
1011 );
1012
1013 match check_funding_tx_confirmations(wallet, funding_txid, &funding_tx).await {
1015 Ok(true) => {},
1016 Ok(false) => return Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid }),
1017 Err(e) => return Err(HarkForfeitError::Err(e.context("checking funding tx confirmations"))),
1018 }
1019
1020 let mut new_vtxos = resp.output_vtxos.into_iter()
1021 .map(|v| <Vtxo<Full>>::deserialize(&v))
1022 .collect::<Result<Vec<_>, _>>()
1023 .context("invalid output VTXOs from server")
1024 .map_err(HarkForfeitError::Err)?;
1025
1026 check_round_matches_participation(participation, &new_vtxos, &funding_tx)
1028 .context("new VTXOs received from server don't match our participation")
1029 .map_err(HarkForfeitError::Err)?;
1030
1031 hark_vtxo_swap(wallet, participation, &mut new_vtxos, &funding_tx, unlock_hash).await
1032 .context("error forfeiting hArk VTXOs")
1033 .map_err(HarkForfeitError::SentForfeits)?;
1034
1035 Ok(HarkProgressResult::Ok { funding_tx, new_vtxos })
1036}
1037
1038async fn progress_attempt(
1039 state: &mut AttemptState,
1040 wallet: &Wallet,
1041 part: &RoundParticipation,
1042 event: &RoundEvent,
1043) -> AttemptProgressResult {
1044 match (state, event) {
1048
1049 (
1050 AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, unlock_hash },
1051 RoundEvent::VtxoProposal(e),
1052 ) => {
1053 trace!("Received VtxoProposal: {:#?}", e);
1054
1055 let secret_nonces = if let Some(first) = cosign_keys.first() {
1058 match wallet.inner.round_secret_nonces.take(&first.public_key()) {
1059 Some(n) => n,
1060 None => return AttemptProgressResult::Failed(anyhow!(
1061 "secret cosign nonces unavailable (likely after a restart); \
1062 abandoning round attempt to avoid nonce reuse",
1063 )),
1064 }
1065 } else {
1066 vec![]
1067 };
1068
1069 match sign_vtxo_tree(
1070 wallet,
1071 part,
1072 &cosign_keys,
1073 secret_nonces,
1074 &e.unsigned_round_tx,
1075 &e.vtxos_spec,
1076 &e.cosign_agg_nonces,
1077 ).await {
1078 Ok(()) => {
1079 AttemptProgressResult::Updated {
1080 new_state: AttemptState::AwaitingFinishedRound {
1081 unsigned_round_tx: e.unsigned_round_tx.clone(),
1082 vtxos_spec: e.vtxos_spec.clone(),
1083 unlock_hash: *unlock_hash,
1084 },
1085 }
1086 },
1087 Err(e) => {
1088 trace!("Error signing VTXO tree: {:#}", e);
1089 AttemptProgressResult::Failed(e)
1090 },
1091 }
1092 },
1093
1094 (
1095 AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash },
1096 RoundEvent::Finished(RoundFinished { cosign_sigs, signed_round_tx, .. }),
1097 ) => {
1098 if unsigned_round_tx.compute_txid() != signed_round_tx.compute_txid() {
1099 return AttemptProgressResult::Failed(anyhow!(
1100 "signed funding tx ({}) doesn't match tx received before ({})",
1101 signed_round_tx.compute_txid(), unsigned_round_tx.compute_txid(),
1102 ));
1103 }
1104
1105 if let Err(e) = wallet.inner.chain.broadcast_tx(&signed_round_tx).await {
1106 warn!("Failed to broadcast signed round tx: {:#}", e);
1107 }
1108
1109 match construct_new_vtxos(
1110 part, unsigned_round_tx, vtxos_spec, cosign_sigs,
1111 ).await {
1112 Ok(v) => AttemptProgressResult::Finished {
1113 funding_tx: signed_round_tx.clone(),
1114 vtxos: v,
1115 unlock_hash: *unlock_hash,
1116 },
1117 Err(e) => AttemptProgressResult::Failed(anyhow!(
1118 "failed to construct new VTXOs for round: {:#}", e,
1119 )),
1120 }
1121 },
1122
1123 (state, RoundEvent::Finished(RoundFinished { .. })) => {
1124 AttemptProgressResult::Failed(anyhow!(
1125 "unexpectedly received a finished round while we were in state {}",
1126 state.kind(),
1127 ))
1128 },
1129
1130 (state, _) => {
1131 trace!("Ignoring round event {} in state {}", event.kind(), state.kind());
1132 AttemptProgressResult::NotUpdated
1133 },
1134 }
1135}
1136
1137async fn sign_vtxo_tree(
1138 wallet: &Wallet,
1139 participation: &RoundParticipation,
1140 cosign_keys: &[Keypair],
1141 secret_nonces: Vec<Vec<SecretNonce>>,
1142 unsigned_round_tx: &Transaction,
1143 vtxo_tree: &VtxoTreeSpec,
1144 cosign_agg_nonces: &[musig::AggregatedNonce],
1145) -> anyhow::Result<()> {
1146 let (mut srv, _) = wallet.require_server().await.context("server not available")?;
1147
1148 let vtxos_utxo = OutPoint::new(unsigned_round_tx.compute_txid(), ROUND_TX_VTXO_TREE_VOUT);
1149
1150 let mut my_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1152 for vtxo_req in vtxo_tree.iter_vtxos() {
1153 if let Some(i) = my_vtxos.iter().position(|v| {
1154 v.policy == vtxo_req.vtxo.policy && v.amount == vtxo_req.vtxo.amount
1155 }) {
1156 my_vtxos.swap_remove(i);
1157 }
1158 }
1159 if !my_vtxos.is_empty() {
1160 bail!("server didn't include all of our vtxos, missing: {:?}", my_vtxos);
1161 }
1162
1163 let unsigned_vtxos = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1164 trace!("Sending vtxo signatures to server...");
1165 for ((req, key), sec) in participation.outputs.iter().zip(cosign_keys).zip(secret_nonces) {
1170 let leaf_idx = unsigned_vtxos.spec.leaf_idx_of_req(req).expect("req included");
1171 let part_sigs = unsigned_vtxos.cosign_branch(
1172 &cosign_agg_nonces, leaf_idx, key, sec,
1173 ).context("failed to cosign branch: our request not part of tree")?;
1174
1175 info!("Sending {} partial vtxo cosign signatures for pk {}",
1176 part_sigs.len(), key.public_key(),
1177 );
1178
1179 srv.client.provide_vtxo_signatures(protos::VtxoSignaturesRequest {
1180 pubkey: key.public_key().serialize().to_vec(),
1181 signatures: part_sigs.iter().map(|s| s.serialize().to_vec()).collect(),
1182 }).await.context("error sending vtxo signatures")?;
1183 }
1184 trace!("Done sending vtxo signatures to server");
1185
1186 Ok(())
1187}
1188
1189async fn construct_new_vtxos(
1190 participation: &RoundParticipation,
1191 unsigned_round_tx: &Transaction,
1192 vtxo_tree: &VtxoTreeSpec,
1193 vtxo_cosign_sigs: &[schnorr::Signature],
1194) -> anyhow::Result<Vec<Vtxo<Full>>> {
1195 let round_txid = unsigned_round_tx.compute_txid();
1196 let vtxos_utxo = OutPoint::new(round_txid, ROUND_TX_VTXO_TREE_VOUT);
1197 let vtxo_tree = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1198
1199 if vtxo_tree.verify_cosign_sigs(&vtxo_cosign_sigs).is_err() {
1201 bail!("Received incorrect vtxo cosign signatures from server");
1203 }
1204
1205 let signed_vtxos = vtxo_tree
1206 .into_signed_tree(vtxo_cosign_sigs.to_vec())
1207 .into_cached_tree();
1208
1209 let mut expected_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1210 let total_nb_expected_vtxos = expected_vtxos.len();
1211
1212 let mut new_vtxos = vec![];
1213 for (idx, req) in signed_vtxos.spec.spec.vtxos.iter().enumerate() {
1214 if let Some(expected_idx) = expected_vtxos.iter().position(|r| **r == req.vtxo) {
1215 let vtxo = signed_vtxos.build_vtxo(idx);
1216
1217 check_vtxo_fails_hash_lock(unsigned_round_tx, &vtxo)
1220 .context("constructed invalid vtxo from tree")?;
1221
1222 info!("New VTXO from round: {} ({}, {})",
1223 vtxo.id(), vtxo.amount(), vtxo.policy_type(),
1224 );
1225
1226 new_vtxos.push(vtxo);
1227 expected_vtxos.swap_remove(expected_idx);
1228 }
1229 }
1230
1231 if !expected_vtxos.is_empty() {
1232 if expected_vtxos.len() == total_nb_expected_vtxos {
1233 bail!("None of our VTXOs were present in round!");
1235 } else {
1236 bail!("Server included some of our VTXOs but not all: {} missing: {:?}",
1237 expected_vtxos.len(), expected_vtxos,
1238 );
1239 }
1240 }
1241 Ok(new_vtxos)
1242}
1243
1244async fn persist_round_success(
1246 wallet: &Wallet,
1247 participation: &RoundParticipation,
1248 movement_id: Option<MovementId>,
1249 new_vtxos: &[Vtxo<Full>],
1250 funding_tx: &Transaction,
1251) -> anyhow::Result<()> {
1252 debug!("Persisting newly finished round. {} new vtxos, movement ID {:?}",
1253 new_vtxos.len(), movement_id,
1254 );
1255
1256 let store_result = wallet.store_spendable_vtxos(new_vtxos).await
1260 .context("failed to store new VTXOs");
1261 let spent_result = wallet.mark_vtxos_as_spent(&participation.inputs).await
1262 .context("failed to mark input VTXOs as spent");
1263 let update_result = if let Some(mid) = movement_id {
1264 wallet.inner.movements.finish_movement_with_update(
1265 mid,
1266 MovementStatus::Successful,
1267 MovementUpdate::new()
1268 .produced_vtxos(new_vtxos)
1269 .metadata([("funding_txid".into(), serde_json::to_value(funding_tx.compute_txid())?)]),
1270 ).await.context("failed to mark movement as finished")
1271 } else {
1272 Ok(())
1273 };
1274
1275 store_result?;
1276 spent_result?;
1277 update_result?;
1278
1279 Ok(())
1280}
1281
1282async fn persist_round_failure(
1283 wallet: &Wallet,
1284 participation: &RoundParticipation,
1285 movement_id: Option<MovementId>,
1286) -> anyhow::Result<()> {
1287 debug!("Attempting to persist the failure of a round with the movement ID {:?}", movement_id);
1288 let unlock_result = wallet.unlock_vtxos(&participation.inputs).await;
1289 let finish_result = if let Some(movement_id) = movement_id {
1290 wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
1291 } else {
1292 Ok(())
1293 };
1294 if let Err(e) = &finish_result {
1295 error!("Failed to mark movement as failed: {:#}", e);
1296 }
1297 match (unlock_result, finish_result) {
1298 (Ok(()), Ok(())) => Ok(()),
1299 (Err(e), _) => Err(e),
1300 (_, Err(e)) => Err(anyhow!("Failed to mark movement as failed: {:#}", e)),
1301 }
1302}
1303
1304async fn update_funding_txid(
1305 wallet: &Wallet,
1306 movement_id: MovementId,
1307 funding_txid: Txid,
1308) -> anyhow::Result<()> {
1309 wallet.inner.movements.update_movement(
1310 movement_id,
1311 MovementUpdate::new()
1312 .metadata([("funding_txid".into(), serde_json::to_value(&funding_txid)?)])
1313 ).await.context("Unable to update funding txid of round")
1314}
1315
1316#[derive(Default)]
1325pub struct RoundSecretNonces {
1326 inner: parking_lot::Mutex<HashMap<bitcoin::secp256k1::PublicKey, Vec<Vec<SecretNonce>>>>,
1327}
1328
1329impl RoundSecretNonces {
1330 pub fn new() -> Self {
1331 Self { inner: parking_lot::Mutex::new(HashMap::new()) }
1332 }
1333
1334 pub fn stash(
1336 &self,
1337 first_cosign_pubkey: bitcoin::secp256k1::PublicKey,
1338 nonces: Vec<Vec<SecretNonce>>,
1339 ) {
1340 self.inner.lock().insert(first_cosign_pubkey, nonces);
1341 }
1342
1343 pub fn take(
1346 &self,
1347 first_cosign_pubkey: &bitcoin::secp256k1::PublicKey,
1348 ) -> Option<Vec<Vec<SecretNonce>>> {
1349 self.inner.lock().remove(first_cosign_pubkey)
1350 }
1351
1352 pub fn forget(&self, first_cosign_pubkey: &bitcoin::secp256k1::PublicKey) {
1356 self.inner.lock().remove(first_cosign_pubkey);
1357 }
1358}
1359
1360impl Wallet {
1361 pub async fn lock_wait_round_state(&self, id: RoundStateId) -> anyhow::Result<Option<StoredRoundState>> {
1366 let guard = self.inner.lock_manager.lock(
1367 &format!("{}.round.{}", self.fingerprint(), id),
1368 ROUND_LOCK_TIMEOUT,
1369 ).await.with_context(|| format!(
1370 "timed out waiting for lock on round state {} (wallet {})",
1371 id, self.fingerprint(),
1372 ))?;
1373
1374 if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1375 return Ok(Some(state.lock(guard)));
1376 }
1377
1378 Ok(None)
1379 }
1380
1381 pub async fn next_round_start_time(&self) -> anyhow::Result<SystemTime> {
1383 let (mut srv, _) = self.require_server().await?;
1384 let ts = srv.client.next_round_time(protos::Empty {}).await?.into_inner().timestamp;
1385 Ok(UNIX_EPOCH.checked_add(Duration::from_secs(ts)).context("invalid timestamp")?)
1386 }
1387
1388 pub async fn join_next_round(
1397 &self,
1398 participation: RoundParticipation,
1399 movement_kind: Option<RoundMovement>,
1400 ) -> anyhow::Result<StoredRoundState> {
1401 let movement = if let Some(kind) = movement_kind {
1402 Some(self.inner.movements.new_guarded_movement_with_update(
1403 Subsystem::ROUND,
1404 kind.to_string(),
1405 OnDropStatus::Failed,
1406 participation.to_movement_update()?
1407 ).await?)
1408 } else {
1409 None
1410 };
1411 let movement_id = movement.as_ref().map(|m| m.id());
1412 let input_vtxos = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1413 let state = RoundState::new_interactive(participation, movement_id);
1414
1415 self.lock_vtxos(&input_vtxos, movement_id.map(|m| m.into())).await
1416 .context("failed to lock input VTXOs")?;
1417
1418 match (async || {
1419 let id = self.inner.db.store_round_state(&state).await?;
1420 Ok(self.lock_wait_round_state(id).await?
1421 .context("failed to lock fresh round state")?)
1422 })().await {
1423 Ok(state) => {
1424 if let Some(mut m) = movement {
1425 m.stop();
1426 }
1427 Ok(state)
1428 },
1429 Err(e) => {
1430 self.unlock_vtxos(&input_vtxos).await
1431 .context("failed to unlock input VTXOs")?;
1432 if let Some(mut m) = movement {
1433 m.fail().await.context("failed to mark movement as failed")?;
1434 }
1435 Err(e)
1436 },
1437 }
1438 }
1439
1440 pub async fn join_delegated_round(
1446 &self,
1447 participation: RoundParticipation,
1448 movement_kind: Option<RoundMovement>,
1449 scheduled_height: Option<BlockHeight>,
1450 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1451 let movement = if let Some(kind) = movement_kind {
1452 Some(self.inner.movements.new_guarded_movement_with_update(
1453 Subsystem::ROUND,
1454 kind.to_string(),
1455 OnDropStatus::Failed,
1456 participation.to_movement_update()?,
1457 ).await?)
1458 } else {
1459 None
1460 };
1461 let movement_id = movement.as_ref().map(|m| m.id());
1462
1463 match self.join_delegated_round_inner(participation, movement_id, scheduled_height).await {
1464 Ok(state) => {
1465 if let Some(mut m) = movement {
1466 m.stop();
1467 }
1468 Ok(state)
1469 },
1470 Err(e) => {
1471 if let Some(mut m) = movement {
1472 m.fail().await.context("error marking movement as failed")?;
1473 }
1474 Err(e)
1475 },
1476 }
1477 }
1478
1479 pub async fn join_next_round_delegated(
1482 &self,
1483 participation: RoundParticipation,
1484 movement_kind: Option<RoundMovement>,
1485 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1486 self.join_delegated_round(participation, movement_kind, None).await
1487 }
1488
1489 async fn join_delegated_round_inner(
1494 &self,
1495 participation: RoundParticipation,
1496 movement_id: Option<MovementId>,
1497 scheduled_height: Option<BlockHeight>,
1498 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1499 let (mut srv, _) = self.require_server().await?;
1500
1501 let unblinded_mailbox_id = self.mailbox_identifier();
1503
1504 self.register_vtxo_transactions_with_server(&participation.inputs).await
1506 .context("failed to register input vtxo transactions with server")?;
1507
1508 let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
1510 for vtxo in participation.inputs.iter() {
1511 let keypair = self.get_vtxo_key(vtxo).await
1512 .context("failed to get vtxo keypair")?;
1513 input_vtxos.push(protos::InputVtxo {
1514 vtxo_id: vtxo.id().to_bytes().to_vec(),
1515 attestation: {
1516 let attestation = DelegatedRoundParticipationAttestation::new(
1517 vtxo.id(), &participation.outputs, &keypair,
1518 );
1519 attestation.serialize()
1520 },
1521 });
1522 }
1523
1524 let vtxo_requests = participation.outputs.iter()
1526 .map(|req|
1527 protos::VtxoRequest {
1528 policy: req.policy.serialize(),
1529 amount: req.amount.to_sat(),
1530 })
1531 .collect::<Vec<_>>();
1532
1533 let resp = srv.client.submit_round_participation(protos::RoundParticipationRequest {
1535 input_vtxos,
1536 vtxo_requests,
1537 unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
1538 scheduled_height,
1539 }).await.context("error submitting round participation to server")?.into_inner();
1540
1541 let unlock_hash = UnlockHash::from_bytes(resp.unlock_hash)
1542 .context("invalid unlock hash from server")?;
1543
1544 let state = RoundState::new_delegated(participation, unlock_hash, movement_id);
1545
1546 info!("Delegated round participation submitted, it will automatically execute \
1547 when you next sync your wallet after the round happened \
1548 (and has sufficient confirmations).",
1549 );
1550
1551 let id = self.inner.db.store_round_state(&state).await?;
1552 Ok(StoredRoundState::new(id, state))
1553 }
1554
1555 pub(crate) async fn join_attempt_interactive(
1563 &self,
1564 participation: RoundParticipation,
1565 attempt: &RoundAttempt,
1566 movement_kind: Option<RoundMovement>,
1567 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1568 let movement = if let Some(kind) = movement_kind {
1569 Some(self.inner.movements.new_guarded_movement_with_update(
1570 Subsystem::ROUND,
1571 kind.to_string(),
1572 OnDropStatus::Failed,
1573 participation.to_movement_update()?,
1574 ).await?)
1575 } else {
1576 None
1577 };
1578 let movement_id = movement.as_ref().map(|m| m.id());
1579
1580 let input_ids = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1581 self.lock_vtxos(&input_ids, movement_id.map(|m| m.into())).await
1582 .context("error locking input VTXOs")?;
1583
1584 match self.join_attempt_interactive_inner(participation, attempt, movement_id).await {
1585 Ok(state) => {
1586 if let Some(mut m) = movement {
1587 m.stop();
1588 }
1589 Ok(state)
1590 },
1591 Err(e) => {
1592 self.unlock_vtxos(&input_ids).await
1593 .context("error unlocking input VTXOs")?;
1594 if let Some(mut m) = movement {
1595 m.fail().await.context("error marking movement as failed")?;
1596 }
1597 Err(e)
1598 },
1599 }
1600 }
1601
1602 async fn join_attempt_interactive_inner(
1603 &self,
1604 participation: RoundParticipation,
1605 attempt: &RoundAttempt,
1606 movement_id: Option<MovementId>,
1607 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1608 let attempt_state = start_attempt(self, &participation, attempt).await?;
1612
1613 let mut state = RoundState::new_interactive(participation, movement_id);
1614 state.flow = RoundFlowState::InteractiveOngoing {
1615 round_seq: attempt.round_seq,
1616 attempt_seq: attempt.attempt_seq,
1617 state: attempt_state,
1618 };
1619
1620 let id = self.inner.db.store_round_state(&state).await?;
1621 Ok(StoredRoundState::new(id, state))
1622 }
1623
1624 pub async fn pending_round_state_ids(&self) -> anyhow::Result<Vec<RoundStateId>> {
1626 self.inner.db.get_pending_round_state_ids().await
1627 }
1628
1629 pub async fn pending_round_states(&self) -> anyhow::Result<Vec<StoredRoundState<Unlocked>>> {
1631 let ids = self.inner.db.get_pending_round_state_ids().await?;
1632 let mut states = Vec::with_capacity(ids.len());
1633 for id in ids {
1634 if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1635 states.push(state);
1636 }
1637 }
1638 Ok(states)
1639 }
1640
1641 pub async fn pending_round_balance(&self) -> anyhow::Result<Amount> {
1643 let mut ret = Amount::ZERO;
1644 for round in self.pending_round_states().await? {
1645 ret += round.state().pending_balance();
1646 }
1647 Ok(ret)
1648 }
1649
1650 pub async fn pending_round_input_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1655 let mut ret = Vec::new();
1656 for round in self.pending_round_states().await? {
1657 let inputs = round.state().locked_pending_inputs();
1658 ret.reserve(inputs.len());
1659 for input in inputs {
1660 let v = self.get_vtxo_by_id(input.id()).await
1661 .context("unknown round input VTXO")?;
1662 ret.push(v);
1663 }
1664 }
1665 Ok(ret)
1666 }
1667
1668 pub async fn sync_pending_rounds(&self) -> anyhow::Result<HashMap<RoundStateId, RoundStatus>> {
1670 let states = self.pending_round_states().await?;
1671 if states.is_empty() {
1672 return Ok(HashMap::new());
1673 }
1674
1675 debug!("Syncing {} pending round states...", states.len());
1676
1677 let ret = Arc::new(parking_lot::Mutex::new(HashMap::with_capacity(states.len())));
1678 tokio_stream::iter(states).for_each_concurrent(10, |state| {
1679 let ret = ret.clone();
1680 async move {
1681 if state.state().ongoing_participation() {
1683 return;
1684 }
1685
1686 let mut state = match self.lock_wait_round_state(state.id()).await {
1687 Ok(Some(state)) => state,
1688 Ok(None) => return,
1689 Err(e) => {
1690 warn!("Error locking round state: {:#}", e);
1691 return;
1692 },
1693 };
1694
1695 let status = match state.state_mut().sync(self).await {
1696 Ok(s) => s,
1697 Err(e) => {
1698 warn!("Error syncing round: {:#}", e);
1699 return;
1700 },
1701 };
1702 trace!("Synced round #{}, status: {:?}", state.id(), status);
1703 match status {
1704 RoundStatus::Confirmed { funding_txid } => {
1705 info!("Round confirmed. Funding tx {}", funding_txid);
1706 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1707 warn!("Error removing confirmed round state from db: {:#}", e);
1708 }
1709 },
1710 RoundStatus::Unconfirmed { funding_txid } => {
1711 info!("Waiting for confirmations for round funding tx {}", funding_txid);
1712 if let Err(e) = self.inner.db.update_round_state(&state).await {
1713 warn!("Error updating pending round state in db: {:#}", e);
1714 }
1715 },
1716 RoundStatus::Pending => {
1717 if let Err(e) = self.inner.db.update_round_state(&state).await {
1718 warn!("Error updating pending round state in db: {:#}", e);
1719 }
1720 },
1721 RoundStatus::Failed { ref error } => {
1722 error!("Round failed: {}", error);
1723 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1724 warn!("Error removing failed round state from db: {:#}", e);
1725 }
1726 },
1727 RoundStatus::Canceled => {
1728 error!("Round canceled");
1729 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1730 warn!("Error removing canceled round state from db: {:#}", e);
1731 }
1732 },
1733 }
1734 ret.lock().insert(state.id(), status);
1735 }
1736 }).await;
1737
1738 Ok(Arc::into_inner(ret).expect("only ref left").into_inner())
1739 }
1740
1741 async fn get_last_round_event(&self) -> anyhow::Result<RoundEvent> {
1743 let (mut srv, _) = self.require_server().await?;
1744 let e = srv.client.last_round_event(protos::Empty {}).await?.into_inner();
1745 Ok(RoundEvent::try_from(e).context("invalid event format from server")?)
1746 }
1747
1748 async fn inner_process_event(
1749 &self,
1750 state: &mut StoredRoundState,
1751 event: Option<&RoundEvent>,
1752 ) {
1753 if let Some(event) = event && state.state().ongoing_participation() {
1754 let updated = state.state_mut().process_event(self, &event).await;
1755 if updated {
1756 if let Err(e) = self.inner.db.update_round_state(&state).await {
1757 error!("Error storing round state #{} after progress: {:#}", state.id(), e);
1758 }
1759 }
1760 }
1761
1762 match state.state_mut().sync(self).await {
1763 Err(e) => warn!("Error syncing round #{}: {:#}", state.id(), e),
1764 Ok(s) if s.is_final() => {
1765 info!("Round #{} finished with result: {:?}", state.id(), s);
1766 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1767 warn!("Failed to remove finished round #{} from db: {:#}", state.id(), e);
1768 }
1769 },
1770 Ok(s) => {
1771 trace!("Round state #{} is now in state {:?}", state.id(), s);
1772 if let Err(e) = self.inner.db.update_round_state(&state).await {
1773 warn!("Error storing round state #{}: {:#}", state.id(), e);
1774 }
1775 },
1776 }
1777 }
1778
1779 pub async fn progress_pending_rounds(
1784 &self,
1785 last_round_event: Option<&RoundEvent>,
1786 ) -> anyhow::Result<()> {
1787 let states = self.pending_round_states().await?;
1788 if states.is_empty() {
1789 return Ok(());
1790 }
1791
1792 info!("Processing {} rounds...", states.len());
1793
1794 let mut last_round_event = last_round_event.map(|e| Cow::Borrowed(e));
1795
1796 let has_ongoing_participation = states.iter()
1797 .any(|s| s.state().ongoing_participation());
1798 if has_ongoing_participation && last_round_event.is_none() {
1799 match self.get_last_round_event().await {
1800 Ok(e) => last_round_event = Some(Cow::Owned(e)),
1801 Err(e) => {
1802 warn!("Error fetching round event, \
1803 failed to progress ongoing rounds: {:#}", e);
1804 },
1805 }
1806 }
1807
1808 let event = last_round_event.as_ref().map(|c| c.as_ref());
1809
1810 let futs = states.into_iter().map(async |state| {
1811 let locked = self.lock_wait_round_state(state.id()).await?;
1812 if let Some(mut locked) = locked {
1813 self.inner_process_event(&mut locked, event).await;
1814 }
1815 Ok::<_, anyhow::Error>(())
1816 });
1817
1818 futures::future::join_all(futs).await;
1819
1820 Ok(())
1821 }
1822
1823 pub async fn subscribe_round_events(&self)
1824 -> anyhow::Result<impl Stream<Item = anyhow::Result<RoundEvent>> + Unpin>
1825 {
1826 let (mut srv, _) = self.require_server().await?;
1827 let mut req = tonic::IntoRequest::into_request(protos::Empty {});
1828 req.set_timeout(SUBSCRIBE_REQUEST_TIMEOUT);
1829 let events = srv.client.subscribe_rounds(req).await?
1830 .into_inner().map(|m| {
1831 let m = m.context("received error on event stream")?;
1832 let e = RoundEvent::try_from(m.clone())
1833 .with_context(|| format!("error converting rpc round event: {:?}", m))?;
1834 trace!("Received round event: {}", e);
1835 Ok::<_, anyhow::Error>(e)
1836 });
1837 Ok(events)
1838 }
1839
1840 pub async fn participate_ongoing_rounds(&self) -> anyhow::Result<()> {
1845 let mut events = self.subscribe_round_events().await?;
1846
1847 loop {
1848 let state_ids = self.pending_round_states().await?.iter()
1851 .filter(|s| s.state().ongoing_participation())
1852 .map(|s| s.id())
1853 .collect::<Vec<_>>();
1854
1855 if state_ids.is_empty() {
1856 info!("All rounds handled");
1857 return Ok(());
1858 }
1859
1860 let event = events.next().await
1861 .context("events stream broke")?
1862 .context("error on event stream")?;
1863
1864 let futs = state_ids.into_iter().map(async |state| {
1865 let locked = self.lock_wait_round_state(state).await?;
1866 if let Some(mut locked) = locked {
1867 self.inner_process_event(&mut locked, Some(&event)).await;
1868 }
1869 Ok::<_, anyhow::Error>(())
1870 });
1871
1872 futures::future::join_all(futs).await;
1873 }
1874 }
1875
1876 pub async fn cancel_all_pending_rounds(&self) -> anyhow::Result<()> {
1881 let state_ids = self.inner.db.get_pending_round_state_ids().await?;
1883
1884 let futures = state_ids.into_iter().map(|state_id| {
1885 async move {
1886 let mut state = match self.lock_wait_round_state(state_id).await {
1888 Ok(Some(s)) => s,
1889 Ok(None) => return,
1890 Err(e) => return warn!("Error loading round state #{}: {:#}", state_id, e),
1891 };
1892
1893 match state.state_mut().try_cancel(self).await {
1894 Ok(true) => {
1895 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1896 warn!("Error removing canceled round state from db: {:#}", e);
1897 }
1898 },
1899 Ok(false) => {},
1900 Err(e) => warn!("Error trying to cancel round #{}: {:#}", state_id, e),
1901 }
1902 }
1903 });
1904
1905 join_all(futures).await;
1906
1907 Ok(())
1908 }
1909
1910 pub async fn cancel_pending_round(&self, id: RoundStateId) -> anyhow::Result<()> {
1912 let mut state = self.lock_wait_round_state(id).await?
1913 .context("round state not found")?;
1914
1915 if state.state_mut().try_cancel(self).await.context("failed to cancel round")? {
1916 self.inner.db.remove_round_state(&state).await
1917 .context("error removing canceled round state from db")?;
1918 } else {
1919 bail!("failed to cancel round");
1920 }
1921
1922 Ok(())
1923 }
1924
1925 pub(crate) async fn participate_round(
1932 &self,
1933 participation: RoundParticipation,
1934 movement_kind: Option<RoundMovement>,
1935 ) -> anyhow::Result<RoundStatus> {
1936 let state = self.join_next_round(participation, movement_kind).await?;
1937
1938 info!("Waiting for a round start...");
1939 let mut events = self.subscribe_round_events().await?;
1940
1941 self.drive_round_state(state, &mut events).await
1942 }
1943
1944 pub(crate) async fn drive_round_state<S>(
1952 &self,
1953 mut state: StoredRoundState,
1954 events: &mut S,
1955 ) -> anyhow::Result<RoundStatus>
1956 where
1957 S: Stream<Item = anyhow::Result<RoundEvent>> + Unpin,
1958 {
1959 loop {
1960 if !state.state().ongoing_participation() {
1961 let status = state.state_mut().sync(self).await?;
1962 match status {
1963 RoundStatus::Failed { error } => bail!("round failed: {}", error),
1964 RoundStatus::Canceled => bail!("round canceled"),
1965 status => return Ok(status),
1966 }
1967 }
1968
1969 let event = events.next().await
1970 .context("events stream broke")?
1971 .context("error on event stream")?;
1972 if state.state_mut().process_event(self, &event).await {
1973 self.inner.db.update_round_state(&state).await?;
1974 }
1975 }
1976 }
1977}
1978
1979#[cfg(test)]
1980mod test {
1981 use super::*;
1982
1983 use bitcoin::secp256k1::Secp256k1;
1984
1985 fn pubkey() -> bitcoin::secp256k1::PublicKey {
1986 let secp = Secp256k1::new();
1987 Keypair::new(&secp, &mut rand::thread_rng()).public_key()
1988 }
1989
1990 fn nonces() -> Vec<Vec<SecretNonce>> {
1991 let secp = Secp256k1::new();
1992 let key = Keypair::new(&secp, &mut rand::thread_rng());
1993 vec![vec![musig::nonce_pair(&key).0, musig::nonce_pair(&key).0]]
1997 }
1998
1999 #[test]
2000 fn stash_and_take() {
2001 let store = RoundSecretNonces::new();
2002 let k = pubkey();
2003 store.stash(k, nonces());
2004
2005 assert!(store.take(&k).is_some());
2006 }
2007
2008 #[test]
2009 fn cannot_take_twice() {
2010 let store = RoundSecretNonces::new();
2011 let k = pubkey();
2012 store.stash(k, nonces());
2013
2014 assert!(store.take(&k).is_some());
2015 assert!(store.take(&k).is_none());
2016 }
2017
2018 #[test]
2019 fn cannot_take_after_forget() {
2020 let store = RoundSecretNonces::new();
2021 let k = pubkey();
2022 store.stash(k, nonces());
2023 store.forget(&k);
2024
2025 assert!(store.take(&k).is_none());
2026 }
2027
2028 #[test]
2029 fn stash_overrides_stash() {
2030 let secp = Secp256k1::new();
2031 let key = Keypair::new(&secp, &mut rand::thread_rng());
2032 let nonces_1 = vec![vec![musig::nonce_pair(&key).0]];
2033 let nonces_2 = vec![];
2034
2035 let store = RoundSecretNonces::new();
2036 store.stash(key.public_key(), nonces_1);
2037 store.stash(key.public_key(), nonces_2);
2038
2039 let taken = store.take(&key.public_key()).expect("nonces present");
2040 assert_eq!(taken.len(), 0);
2041 }
2042}