Skip to main content

bark/round/
mod.rs

1//!
2//! Round State Machine
3//!
4
5use 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
42/// How long [`Wallet::lock_wait_round_state`] waits for a contended
43/// round lock before giving up. Long enough to outlast a normal round.
44const ROUND_LOCK_TIMEOUT: Duration = Duration::from_secs(10);
45use crate::subsystem::{RoundMovement, Subsystem};
46
47
48/// The type string for the hArk leaf transition
49const HARK_TRANSITION_KIND: &str = "hash-locked-cosigned";
50
51/// Struct to communicate your specific participation for an Ark round.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct RoundParticipation {
54	#[serde(with = "ark::encode::serde::vec")]
55	pub inputs: Vec<Vtxo<Full>>,
56	/// The output VTXOs that we request in the round,
57	/// including change
58	pub outputs: Vec<VtxoRequest>,
59	/// Optional mailbox identifier for round completion notification
60	#[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	/// The round was successful and is fully confirmed
81	Confirmed {
82		funding_txid: Txid,
83	},
84	/// Round successful but not fully confirmed
85	Unconfirmed {
86		funding_txid: Txid,
87	},
88	/// Round didn't finish yet
89	Pending,
90	/// The round failed
91	Failed {
92		error: String,
93	},
94	/// User canceled the round
95	Canceled,
96}
97
98impl RoundStatus {
99	/// Whether this is the final state and it won't change anymore
100	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	/// Whether it looks like the round succeeded
111	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
122/// State of the progress of a round participation
123///
124/// An instance of this struct is kept all the way from the intention of joining
125/// the next round, until either the round fully confirms or it fails and we are
126/// sure it won't have any effect on our wallet.
127///
128/// As soon as we have signed forfeit txs for the round, we keep track of this
129/// round attempt until we see another attempt we participated in confirm or
130/// we gain confidence that the failed attempt will never confirm.
131//
132//TODO(stevenroose) move the id in here and have the state persist itself with the wallet
133// to have better control. this way we can touch db before we sent forfeit sigs
134pub struct RoundState {
135	/// Round is fully done
136	pub(crate) done: bool,
137
138	/// Our participation in this round
139	pub(crate) participation: RoundParticipation,
140
141	/// The flow of the round in case it is still ongoing with the server
142	pub(crate) flow: RoundFlowState,
143
144	/// The new output vtxos of this round participation
145	///
146	/// After we finish the interactive part, we fill this with the uncompleted
147	/// VTXOs which we then try to complete with the unlock preimage.
148	pub(crate) new_vtxos: Vec<Vtxo<Full>>,
149
150	/// Whether we sent our forfeit signatures to the server
151	///
152	/// If we did this and the server refused to reveal our new VTXOs,
153	/// we will be forced to exit.
154	//TODO(stevenroose) implement exit when this is true and we can't make progress
155	// probably based on the input vtxos becoming close to expiry
156	pub(crate) sent_forfeit_sigs: bool,
157
158	/// The ID of the [Movement] associated with this round
159	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	/// Our participation in this round
193	pub fn participation(&self) -> &RoundParticipation {
194		&self.participation
195	}
196
197	/// the unlock hash if already known
198	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	/// Whether the interactive part of the round is still ongoing
221	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	/// Tries to cancel the round and returns whether it was succesfully canceled
233	/// or if it was already canceled or failed
234	pub async fn try_cancel(&mut self, wallet: &Wallet) -> anyhow::Result<bool> {
235		let ret = match self.flow {
236			RoundFlowState::NonInteractivePending { .. } => {
237				//TODO(stevenroose) we have to cancel with server
238				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		// Drop the previous attempt's stashed nonces: the new attempt
261		// regenerates cosign keys, so the old key becomes unreachable.
262		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	/// Processes the given event and returns true if some update was made to the state
288	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				// here we catch the cases where we're in a wrong flow
308
309				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					// new round started, we don't support multiple parallel rounds,
319					// this means we failed
320					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	/// Sync the round's status and return it
376	///
377	/// When success or failure is returned, the round state can be eliminated
378	//TODO(stevenroose) make RoundState manage its own db record
379	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					// We don't lock inputs on delegated rounds, so if we don't find it,
405					// we just mark the refresh movement as failed
406					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					//TODO(stevenroose) should we mark as failed for these cases?
448
449					Err(HarkForfeitError::Err(e)) => {
450						//TODO(stevenroose) we failed here but we might actualy be able to
451						// succeed if we retry. should we implement some kind of limited
452						// retry after which we mark as failed?
453						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			// interactive part finished, but didn't forfeit yet
463			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	/// Once we know the signed round funding tx, this returns the output VTXOs
503	/// for this round.
504	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	/// Returns the input VTXOs that are locked in this round, but only
513	/// if no output VTXOs were issued yet.
514	pub fn locked_pending_inputs(&self) -> &[Vtxo<Full>] {
515		//TODO(stevenroose) consider if we can't just drop the state after forfeit exchange
516		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				// inputs already unlocked
525				&[]
526			} else {
527				&self.participation.inputs
528			},
529			RoundFlowState::Failed { .. }
530				| RoundFlowState::Canceled
531			=> {
532				// inputs already unlocked
533				&[]
534			},
535		}
536	}
537
538	/// The balance pending in this round
539	///
540	/// This becomes zero once the new round VTXOs are unlocked.
541	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
562/// The state of the process flow of a round
563///
564/// This tracks the progress of the interactive part of the round, from
565/// waiting to start until finishing either succesfully or with a failure.
566pub enum RoundFlowState {
567	/// We don't do flow and we just wait for the round to finish
568	NonInteractivePending {
569		unlock_hash: UnlockHash,
570	},
571
572	/// Waiting for round to happen
573	InteractivePending,
574	/// Interactive part ongoing
575	InteractiveOngoing {
576		round_seq: RoundSeq,
577		attempt_seq: usize,
578		state: AttemptState,
579	},
580
581	/// Interactive part finished, waiting for confirmation
582	Finished {
583		funding_tx: Transaction,
584		unlock_hash: UnlockHash,
585	},
586
587	/// Failed during round
588	Failed {
589		error: String,
590	},
591
592	/// User canceled round
593	Canceled,
594}
595
596/// The state of a single round attempt
597///
598/// For each attempt that we participate in, we keep the state of our concrete
599/// participation.
600pub 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	/// The state kind represented as a string
615	fn kind(&self) -> &'static str {
616		match self {
617			Self::AwaitingAttempt => "AwaitingAttempt",
618			Self::AwaitingUnsignedVtxoTree { .. } => "AwaitingUnsignedVtxoTree",
619			Self::AwaitingFinishedRound { .. } => "AwaitingFinishedRound",
620		}
621	}
622}
623
624/// Result from trying to progress an ongoing round attempt
625enum AttemptProgressResult {
626	Finished {
627		funding_tx: Transaction,
628		vtxos: Vec<Vtxo<Full>>,
629		unlock_hash: UnlockHash,
630	},
631	Failed(anyhow::Error),
632	/// When the state changes, this variant is returned
633	///
634	/// If during the processing, we have signed any forfeit txs and tried
635	/// sending them to the server, the [UnconfirmedRound] instance is returned
636	/// so that it can be stored in the state.
637	Updated {
638		new_state: AttemptState,
639	},
640	NotUpdated,
641}
642
643/// Participate in the new round attempt by submitting our round participation
644async 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	// Assign cosign pubkeys to the payment requests.
652	let cosign_keys = iter::repeat_with(|| Keypair::new(&SECP, &mut rand::thread_rng()))
653		.take(participation.outputs.len())
654		.collect::<Vec<_>>();
655
656	// Prepare round participation info.
657	// For each of our requested vtxo output, we need a set of public and secret nonces.
658	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	// The round has now started. We can submit our payment.
674	debug!("Submitting payment request with {} inputs and {} vtxo outputs",
675		participation.inputs.len(), participation.outputs.len(),
676	);
677
678	// Build signed requests with mailbox IDs
679	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	// Register VTXO transaction chains with server before round participation
708	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	// Stash nonces in memory only. Empty `cosign_keys` means no VTXO
721	// outputs (offboard-only) — nothing to stash.
722	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/// just an internal type; need Error trait to work with anyhow
733#[derive(Debug, thiserror::Error)]
734enum HarkForfeitError {
735	/// An error happened after we sent forfeit signatures to the server
736	#[error("error after forfeits were sent")]
737	SentForfeits(#[source] anyhow::Error),
738	/// An error happened before we sent forfeit signatures to the server
739	#[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
768/// Finish the hArk VTXO swap protocol
769///
770/// This includes:
771/// - requesting cosignature of the locked hArk leaves
772/// - sending forfeit txs to the server in return for the unlock preimage
773///
774/// NB all the actions in this function are idempotent, meaning that the server
775/// allows them to be done multiple times. this means that if this function calls
776/// fails, it's safe to just call it again at a later time
777async 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	// before we start make sure the server has our input vtxo signatures
787	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	// first get the leaves signed
792	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	// then do the forfeit dance
798
799	// the server caps the number of vtxo ids per nonces request, so for
800	// large participations we request the nonces in chunks
801	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		// then validate the vtxo works
853		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	// then register the output vtxos with the server
859	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		// We accept the VTXO if only the hArk transition (last) failure happens
896		check_vtxo_fails_hash_lock(funding_tx, vtxo)?;
897	}
898
899	Ok(())
900}
901
902/// Check the confirmation status of a funding tx
903///
904/// Returns true if the funding tx is confirmed deeply enough for us to accept it.
905/// The required number of confirmations depends on the wallet's configuration.
906///
907/// Returns false if the funding tx seems valid but not confirmed yet.
908///
909/// Returns an error if the chain source fails or if we can't submit the tx to the
910/// mempool, suggesting it might be double spent.
911async 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			// let's try to submit it to our mempool
935			//TODO(stevenroose) change this to an explicit "testmempoolaccept" so that we can
936			// reliably distinguish the cases of our chain source having issues and the tx
937			// actually being rejected which suggests the round was double-spent
938			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	// Since we got here, we clearly don't think we're finished.
992	// So even if the server thinks we did the dance before, we need the
993	// cosignature on the leaf tx so we need to do the dance again.
994	// "Guilty feet have got no rhythm."
995	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	// Check the confirmation status of the funding tx
1014	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 that the vtxos match our participation in the exact order
1027	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	// we will match only the states and messages required to make progress,
1045	// all else we ignore, except an unexpected finish
1046
1047	match (state, event) {
1048
1049		(
1050			AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, unlock_hash },
1051			RoundEvent::VtxoProposal(e),
1052		) => {
1053			trace!("Received VtxoProposal: {:#?}", e);
1054
1055			// Missing nonces means we restarted before signing —
1056			// abandon the attempt rather than reuse on retry.
1057			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	// Check that the proposal contains our inputs.
1151	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	// Sequential: SecretNonce is consume-once and not Clone, so we move
1166	// one Vec<SecretNonce> into cosign_branch per output. Going parallel
1167	// would require sharing the server connection across futures, which
1168	// isn't worth the complexity for a per-output RPC.
1169	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	// Validate the vtxo tree and cosign signatures.
1200	if vtxo_tree.verify_cosign_sigs(&vtxo_cosign_sigs).is_err() {
1201		// bad server!
1202		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			// validate the received vtxos
1218			// This is more like a sanity check since we crafted them ourselves.
1219			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			// we must have done something wrong
1234			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
1244//TODO(stevenroose) should be made idempotent
1245async 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	// we first try all actions that need to happen and only afterwards return errors
1257	// so that we achieve maximum success
1258
1259	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/// In-memory store for MuSig2 secret cosign nonces used during round
1317/// signing. Entries are keyed by the first cosign pubkey of each round
1318/// attempt — that pubkey is freshly generated in `start_attempt`,
1319/// uniquely identifies the attempt within a process, and is reachable
1320/// from the persisted `AttemptState::AwaitingUnsignedVtxoTree`.
1321///
1322/// Nonces never touch disk: persisting them risks signing twice with
1323/// the same nonce, which is unsafe with MuSig2.
1324#[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	/// Insert nonces under the given key, replacing any previous entry.
1335	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	/// Remove and return the nonces stashed under the given key.
1344	/// `None` after a process restart or if the entry was never stashed.
1345	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	/// Drop the entry under the given key without returning its
1353	/// contents. Use when a stashed attempt is being replaced by a new
1354	/// one and its key would otherwise be unreachable.
1355	pub fn forget(&self, first_cosign_pubkey: &bitcoin::secp256k1::PublicKey) {
1356		self.inner.lock().remove(first_cosign_pubkey);
1357	}
1358}
1359
1360impl Wallet {
1361	/// Load and lock a single given round state (by id), waiting for the lock.
1362	///
1363	/// Returns `Some(state)` if the round state is found and locked, `None`
1364	/// if it is not found after acquiring the lock.
1365	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	/// Ask the server when the next round is scheduled to start
1382	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	/// Start a new round participation
1389	///
1390	/// This function will store the state in the db and mark the VTXOs as locked.
1391	///
1392	/// ### Return
1393	///
1394	/// - By default, the returned state will be locked to prevent race conditions.
1395	/// To unlock the state, [StoredRoundState::unlock()] can be called.
1396	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	/// Join a round in delegated mode.
1441	///
1442	/// When `scheduled_height` is set, the server won't include the participation in a round
1443	/// before the chain tip reaches it. When `None`, it is eligible for the next round (see
1444	/// [Wallet::join_next_round_delegated]).
1445	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	/// Join the next delegated round, i.e. [Wallet::join_delegated_round] with no scheduled
1480	/// height, so the participation is eligible for the very next round.
1481	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	/// Join a round in delegated mode.
1490	///
1491	/// When `scheduled_height` is set, the server won't include the participation in a round
1492	/// before the chain tip reaches it. When `None`, it is eligible for the next round.
1493	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		// Get mailbox identifier for VTXO delivery
1502		let unblinded_mailbox_id = self.mailbox_identifier();
1503
1504		// Register VTXO transaction chains with server before round participation
1505		self.register_vtxo_transactions_with_server(&participation.inputs).await
1506			.context("failed to register input vtxo transactions with server")?;
1507
1508		// Generate attestations for input vtxos
1509		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		// Build proto VtxoRequests
1525		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		// Submit participation to server and get unlock_hash
1534		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	/// Join an already-started round attempt interactively, submitting our
1556	/// participation synchronously.
1557	///
1558	/// Unlike [Wallet::join_next_round] — which stores a pending participation
1559	/// and waits for a round to start before submitting inside the round state
1560	/// machine — this submits to the in-flight `attempt` right away. This allows
1561	/// us to react to any unspendable VTXOs and exclude them from the refresh.
1562	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		// Submit synchronously to the in-flight attempt. On rejection the
1609		// tonic::Status (carrying the unusable input ids in its `identifiers`
1610		// metadata) propagates up the error chain untouched.
1611		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	/// Get all pending round states
1625	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	/// Get all pending round states
1630	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	/// Balance locked in pending rounds
1642	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	/// Returns all VTXOs that are locked in a pending round
1651	///
1652	/// This excludes all input VTXOs for which the output VTXOs have already
1653	/// been created.
1654	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	/// Sync pending rounds that have finished but are waiting for confirmations
1669	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				// not processing events here
1682				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	/// Fetch last round event from server
1742	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	/// Try to make incremental progress on all pending round states
1780	///
1781	/// If the `last_round_event` argument is not provided, it will be fetched
1782	/// from the server.
1783	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	/// A blocking call that will try to perform a full round participation
1841	/// for all ongoing rounds
1842	///
1843	/// Returns only once there is no ongoing rounds anymore.
1844	pub async fn participate_ongoing_rounds(&self) -> anyhow::Result<()> {
1845		let mut events = self.subscribe_round_events().await?;
1846
1847		loop {
1848			// NB: we need to load all ongoing rounds on every iteration here
1849			// because some might be finished by another call
1850			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	/// Will cancel all pending rounds that can safely be canceled
1877	///
1878	/// All rounds that have not started yet can safely be canceled,
1879	/// as well as rounds where we have not yet signed any forfeit txs.
1880	pub async fn cancel_all_pending_rounds(&self) -> anyhow::Result<()> {
1881		// initial load to get all pending round states ids
1882		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				// wait for lock and load again to ensure most recent state
1887				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	/// Try to cancel the given round
1911	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	/// Participate in a round
1926	///
1927	/// This function will start a new round participation and block until
1928	/// the round is finished.
1929	/// After this method returns the round state will be kept active until
1930	/// the round tx fully confirms.
1931	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	/// Drive an already-joined round state to its final [RoundStatus], blocking
1945	/// on `events` and persisting each update.
1946	///
1947	/// Shared by [Wallet::participate_round] and the blocking maintenance
1948	/// refresh: the latter submits its participation up-front (against an
1949	/// in-flight attempt) and then drives the resulting round to completion
1950	/// here.
1951	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		// Shape mirrors what start_attempt produces: outer Vec is one
1994		// entry per cosign keypair, inner Vec is the tree-depth set of
1995		// pre-generated nonces.
1996		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}