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::{TransitionKind, 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/// Struct to communicate your specific participation for an Ark round.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct RoundParticipation {
51	#[serde(with = "ark::encode::serde::vec")]
52	pub inputs: Vec<Vtxo<Full>>,
53	/// The output VTXOs that we request in the round,
54	/// including change
55	pub outputs: Vec<VtxoRequest>,
56	/// Optional mailbox identifier for round completion notification
57	#[serde(default, skip_serializing_if = "Option::is_none", with = "ark::encode::serde::opt")]
58	pub unblinded_mailbox_id: Option<ark::mailbox::MailboxIdentifier>,
59}
60
61impl RoundParticipation {
62	pub fn to_movement_update(&self) -> anyhow::Result<MovementUpdate> {
63		let input_amount = self.inputs.iter().map(|i| i.amount()).sum::<Amount>();
64		let output_amount = self.outputs.iter().map(|r| r.amount).sum::<Amount>();
65		let fee = input_amount - output_amount;
66		Ok(MovementUpdate::new()
67			.consumed_vtxos(&self.inputs)
68			.intended_balance(SignedAmount::ZERO)
69			.effective_balance( - fee.to_signed()?)
70			.fee(fee)
71		)
72	}
73}
74
75#[derive(Debug, Clone)]
76pub enum RoundStatus {
77	/// The round was successful and is fully confirmed
78	Confirmed {
79		funding_txid: Txid,
80	},
81	/// Round successful but not fully confirmed
82	Unconfirmed {
83		funding_txid: Txid,
84	},
85	/// Round didn't finish yet
86	Pending,
87	/// The round failed
88	Failed {
89		error: String,
90	},
91	/// User canceled the round
92	Canceled,
93}
94
95impl RoundStatus {
96	/// Whether this is the final state and it won't change anymore
97	pub fn is_final(&self) -> bool {
98		match self {
99			Self::Confirmed { .. } => true,
100			Self::Unconfirmed { .. } => false,
101			Self::Pending => false,
102			Self::Failed { .. } => true,
103			Self::Canceled => true,
104		}
105	}
106
107	/// Whether it looks like the round succeeded
108	pub fn is_success(&self) -> bool {
109		match self {
110			Self::Confirmed { .. } => true,
111			Self::Unconfirmed { .. } => true,
112			Self::Pending => false,
113			Self::Failed { .. } => false,
114			Self::Canceled => false,
115		}
116	}
117}
118
119/// State of the progress of a round participation
120///
121/// An instance of this struct is kept all the way from the intention of joining
122/// the next round, until either the round fully confirms or it fails and we are
123/// sure it won't have any effect on our wallet.
124///
125/// As soon as we have signed forfeit txs for the round, we keep track of this
126/// round attempt until we see another attempt we participated in confirm or
127/// we gain confidence that the failed attempt will never confirm.
128//
129//TODO(stevenroose) move the id in here and have the state persist itself with the wallet
130// to have better control. this way we can touch db before we sent forfeit sigs
131pub struct RoundState {
132	/// Round is fully done
133	pub(crate) done: bool,
134
135	/// Our participation in this round
136	pub(crate) participation: RoundParticipation,
137
138	/// The flow of the round in case it is still ongoing with the server
139	pub(crate) flow: RoundFlowState,
140
141	/// The new output vtxos of this round participation
142	///
143	/// After we finish the interactive part, we fill this with the uncompleted
144	/// VTXOs which we then try to complete with the unlock preimage.
145	pub(crate) new_vtxos: Vec<Vtxo<Full>>,
146
147	/// Whether we sent our forfeit signatures to the server
148	///
149	/// If we did this and the server refused to reveal our new VTXOs,
150	/// we will be forced to exit.
151	//TODO(stevenroose) implement exit when this is true and we can't make progress
152	// probably based on the input vtxos becoming close to expiry
153	pub(crate) sent_forfeit_sigs: bool,
154
155	/// The ID of the [Movement] associated with this round
156	pub(crate) movement_id: Option<MovementId>,
157}
158
159impl RoundState {
160	fn new_interactive(
161		participation: RoundParticipation,
162		movement_id: Option<MovementId>,
163	) -> Self {
164		Self {
165			participation,
166			movement_id,
167			flow: RoundFlowState::InteractivePending,
168			new_vtxos: Vec::new(),
169			sent_forfeit_sigs: false,
170			done: false,
171		}
172	}
173
174	fn new_delegated(
175		participation: RoundParticipation,
176		unlock_hash: UnlockHash,
177		movement_id: Option<MovementId>,
178	) -> Self {
179		Self {
180			participation,
181			movement_id,
182			flow: RoundFlowState::NonInteractivePending { unlock_hash },
183			new_vtxos: Vec::new(),
184			sent_forfeit_sigs: false,
185			done: false,
186		}
187	}
188
189	/// Our participation in this round
190	pub fn participation(&self) -> &RoundParticipation {
191		&self.participation
192	}
193
194	/// the unlock hash if already known
195	pub fn unlock_hash(&self) -> Option<UnlockHash> {
196		match self.flow {
197			RoundFlowState::NonInteractivePending { unlock_hash } => Some(unlock_hash),
198			RoundFlowState::InteractivePending => None,
199			RoundFlowState::InteractiveOngoing { .. } => None,
200			RoundFlowState::Failed { .. } => None,
201			RoundFlowState::Canceled => None,
202			RoundFlowState::Finished { unlock_hash, .. } => Some(unlock_hash),
203		}
204	}
205
206	pub fn funding_tx(&self) -> Option<&Transaction> {
207		match self.flow {
208			RoundFlowState::NonInteractivePending { .. } => None,
209			RoundFlowState::InteractivePending => None,
210			RoundFlowState::InteractiveOngoing { .. } => None,
211			RoundFlowState::Failed { .. } => None,
212			RoundFlowState::Canceled => None,
213			RoundFlowState::Finished { ref funding_tx, .. } => Some(funding_tx),
214		}
215	}
216
217	/// Whether the interactive part of the round is still ongoing
218	pub fn ongoing_participation(&self) -> bool {
219		match self.flow {
220			RoundFlowState::NonInteractivePending { .. } => false,
221			RoundFlowState::InteractivePending => true,
222			RoundFlowState::InteractiveOngoing { .. } => true,
223			RoundFlowState::Failed { .. } => false,
224			RoundFlowState::Canceled => false,
225			RoundFlowState::Finished { .. } => false,
226		}
227	}
228
229	/// Tries to cancel the round and returns whether it was succesfully canceled
230	/// or if it was already canceled or failed
231	pub async fn try_cancel(&mut self, wallet: &Wallet) -> anyhow::Result<bool> {
232		let ret = match self.flow {
233			RoundFlowState::NonInteractivePending { .. } => {
234				//TODO(stevenroose) we have to cancel with server
235				bail!("it is currently not yet possible to cancel pending delegated rounds");
236			},
237			RoundFlowState::Canceled => true,
238			RoundFlowState::Failed { .. } => true,
239			RoundFlowState::InteractivePending | RoundFlowState::InteractiveOngoing { .. } => {
240				self.flow = RoundFlowState::Canceled;
241				true
242			},
243			RoundFlowState::Finished { .. } => false,
244		};
245		if ret {
246			persist_round_failure(wallet, &self.participation, self.movement_id).await
247				.context("failed to persist round failure for cancelation")?;
248		}
249		Ok(ret)
250	}
251
252	async fn try_start_attempt(
253		&mut self,
254		wallet: &Wallet,
255		attempt: &RoundAttempt,
256	) {
257		// Drop the previous attempt's stashed nonces: the new attempt
258		// regenerates cosign keys, so the old key becomes unreachable.
259		if let RoundFlowState::InteractiveOngoing {
260			state: AttemptState::AwaitingUnsignedVtxoTree { ref cosign_keys, .. },
261			..
262		} = self.flow {
263			if let Some(k) = cosign_keys.first() {
264				wallet.inner.round_secret_nonces.forget(&k.public_key());
265			}
266		}
267
268		match start_attempt(wallet, &self.participation, attempt).await {
269			Ok(state) => {
270				self.flow = RoundFlowState::InteractiveOngoing {
271					round_seq: attempt.round_seq,
272					attempt_seq: attempt.attempt_seq,
273					state: state,
274				};
275			},
276			Err(e) => {
277				self.flow = RoundFlowState::Failed {
278					error: format!("{:#}", e),
279				};
280			},
281		}
282	}
283
284	/// Processes the given event and returns true if some update was made to the state
285	pub async fn process_event(
286		&mut self,
287		wallet: &Wallet,
288		event: &RoundEvent,
289	) -> bool {
290		let _: Infallible = match self.flow {
291			RoundFlowState::InteractivePending => {
292				if let RoundEvent::Attempt(e) = event && e.attempt_seq == 0 {
293					trace!("Joining round attempt {}:{}", e.round_seq, e.attempt_seq);
294					self.try_start_attempt(wallet, e).await;
295					return true;
296				} else {
297					trace!("Ignoring {} event (seq {}:{}), waiting for round to start",
298						event.kind(), event.round_seq(), event.attempt_seq(),
299					);
300					return false;
301				}
302			},
303			RoundFlowState::InteractiveOngoing { round_seq, attempt_seq, ref mut state } => {
304				// here we catch the cases where we're in a wrong flow
305
306				if let RoundEvent::Failed(e) = event && e.round_seq == round_seq {
307					warn!("Round {} failed by server", round_seq);
308					self.flow = RoundFlowState::Failed {
309						error: format!("round {} failed by server", round_seq),
310					};
311					return true;
312				}
313
314				if event.round_seq() > round_seq {
315					// new round started, we don't support multiple parallel rounds,
316					// this means we failed
317					self.flow = RoundFlowState::Failed {
318						error: format!("round {} started while we were on {}",
319							event.round_seq(), round_seq,
320						),
321					};
322					return true;
323				}
324
325				if event.attempt_seq() < attempt_seq {
326					trace!("ignoring replayed message from old attempt");
327					return false;
328				}
329
330				if let RoundEvent::Attempt(e) = event && e.attempt_seq > attempt_seq {
331					trace!("Joining new round attempt {}:{}", e.round_seq, e.attempt_seq);
332					self.try_start_attempt(wallet, e).await;
333					return true;
334				}
335				trace!("Processing event {} for round attempt {}:{} in state {}",
336					event.kind(), round_seq, attempt_seq, state.kind(),
337				);
338
339				return match progress_attempt(state, wallet, &self.participation, event).await {
340					AttemptProgressResult::NotUpdated => false,
341					AttemptProgressResult::Updated { new_state } => {
342						*state = new_state;
343						true
344					},
345					AttemptProgressResult::Failed(e) => {
346						warn!("Round failed with error: {:#}", e);
347						self.flow = RoundFlowState::Failed {
348							error: format!("{:#}", e),
349						};
350						true
351					},
352					AttemptProgressResult::Finished { funding_tx, vtxos, unlock_hash } => {
353						self.new_vtxos = vtxos;
354						let funding_txid = funding_tx.compute_txid();
355						self.flow = RoundFlowState::Finished { funding_tx, unlock_hash };
356						if let Some(mid) = self.movement_id {
357							if let Err(e) = update_funding_txid(wallet, mid, funding_txid).await {
358								warn!("Error updating the round funding txid: {:#}", e);
359							}
360						}
361						true
362					},
363				};
364			},
365			RoundFlowState::NonInteractivePending { .. }
366				| RoundFlowState::Finished { .. }
367				| RoundFlowState::Failed { .. }
368				| RoundFlowState::Canceled => return false,
369		};
370	}
371
372	/// Sync the round's status and return it
373	///
374	/// When success or failure is returned, the round state can be eliminated
375	//TODO(stevenroose) make RoundState manage its own db record
376	pub async fn sync(&mut self, wallet: &Wallet) -> anyhow::Result<RoundStatus> {
377		match self.flow {
378			RoundFlowState::Finished { ref funding_tx, .. } if self.done => {
379				Ok(RoundStatus::Confirmed {
380					funding_txid: funding_tx.compute_txid(),
381				})
382			},
383
384			RoundFlowState::InteractivePending | RoundFlowState::InteractiveOngoing { .. } => {
385				Ok(RoundStatus::Pending)
386			},
387			RoundFlowState::Failed { ref error } => {
388				persist_round_failure(wallet, &self.participation, self.movement_id).await
389					.context("failed to persist round failure")?;
390				Ok(RoundStatus::Failed { error: error.clone() })
391			},
392			RoundFlowState::Canceled => {
393				persist_round_failure(wallet, &self.participation, self.movement_id).await
394					.context("failed to persist round failure")?;
395				Ok(RoundStatus::Canceled)
396			},
397
398			RoundFlowState::NonInteractivePending { unlock_hash } => {
399				match progress_delegated(wallet, &self.participation, unlock_hash).await {
400					Ok(HarkProgressResult::RoundPending) => Ok(RoundStatus::Pending),
401					// We don't lock inputs on delegated rounds, so if we don't find it,
402					// we just mark the refresh movement as failed
403					Ok(HarkProgressResult::RoundNotFound) => {
404						info!("Server reports round participation not found (no forfeits sent)");
405						self.flow = RoundFlowState::Failed {
406							error: "server reports round participation not found".into(),
407						};
408						if let Some(movement_id) = self.movement_id {
409							wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
410								.context("failed to mark refresh movement as failed")?;
411						}
412						Ok(RoundStatus::Failed {
413							error: "server reports round participation not found".into(),
414						})
415					},
416					Ok(HarkProgressResult::Ok { funding_tx, new_vtxos }) => {
417						let funding_txid = funding_tx.compute_txid();
418						self.new_vtxos = new_vtxos;
419						self.flow = RoundFlowState::Finished {
420							funding_tx: funding_tx.clone(),
421							unlock_hash: unlock_hash,
422						};
423
424						persist_round_success(
425							wallet,
426							&self.participation,
427							self.movement_id,
428							&self.new_vtxos,
429							&funding_tx,
430						).await.context("failed to store successful round in DB!")?;
431
432						self.done = true;
433
434						Ok(RoundStatus::Confirmed { funding_txid })
435					},
436					Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid }) => {
437						if let Some(mid) = self.movement_id {
438							update_funding_txid(wallet, mid, funding_txid).await
439								.context("failed to update funding txid in DB")?;
440						}
441						Ok(RoundStatus::Unconfirmed { funding_txid })
442					},
443
444					//TODO(stevenroose) should we mark as failed for these cases?
445
446					Err(HarkForfeitError::Err(e)) => {
447						//TODO(stevenroose) we failed here but we might actualy be able to
448						// succeed if we retry. should we implement some kind of limited
449						// retry after which we mark as failed?
450						Err(e.context("error progressing delegated round"))
451					},
452					Err(HarkForfeitError::SentForfeits(e)) => {
453						self.sent_forfeit_sigs = true;
454						Err(e.context("error progressing delegated round \
455							after sending forfeit tx signatures"))
456					},
457				}
458			},
459			// interactive part finished, but didn't forfeit yet
460			RoundFlowState::Finished { ref funding_tx, unlock_hash } => {
461				let funding_txid = funding_tx.compute_txid();
462				let confirmed = check_funding_tx_confirmations(
463					wallet, funding_txid, &funding_tx,
464				).await.context("error checking funding tx confirmations")?;
465				if !confirmed {
466					trace!("Funding tx {} not yet deeply enough confirmed", funding_txid);
467					return Ok(RoundStatus::Unconfirmed { funding_txid });
468				}
469
470				match hark_vtxo_swap(
471					wallet, &self.participation, &mut self.new_vtxos, &funding_tx, unlock_hash,
472				).await {
473					Ok(()) => {
474						persist_round_success(
475							wallet,
476							&self.participation,
477							self.movement_id,
478							&self.new_vtxos,
479							&funding_tx,
480						).await.context("failed to store successful round in DB!")?;
481
482						self.done = true;
483
484						Ok(RoundStatus::Confirmed { funding_txid })
485					},
486					Err(HarkForfeitError::Err(e)) => {
487						Err(e.context("error forfeiting VTXOs after round"))
488					},
489					Err(HarkForfeitError::SentForfeits(e)) => {
490						self.sent_forfeit_sigs = true;
491						Err(e.context("error after having signed and sent \
492							forfeit signatures to server"))
493					},
494				}
495			},
496		}
497	}
498
499	/// Once we know the signed round funding tx, this returns the output VTXOs
500	/// for this round.
501	pub fn output_vtxos(&self) -> Option<&[Vtxo<Full>]> {
502		if self.new_vtxos.is_empty() {
503			None
504		} else {
505			Some(&self.new_vtxos)
506		}
507	}
508
509	/// Returns the input VTXOs that are locked in this round, but only
510	/// if no output VTXOs were issued yet.
511	pub fn locked_pending_inputs(&self) -> &[Vtxo<Full>] {
512		//TODO(stevenroose) consider if we can't just drop the state after forfeit exchange
513		match self.flow {
514			RoundFlowState::NonInteractivePending { .. }
515				| RoundFlowState::InteractivePending
516				| RoundFlowState::InteractiveOngoing { .. }
517			=> {
518				&self.participation.inputs
519			},
520			RoundFlowState::Finished { .. } => if self.done {
521				// inputs already unlocked
522				&[]
523			} else {
524				&self.participation.inputs
525			},
526			RoundFlowState::Failed { .. }
527				| RoundFlowState::Canceled
528			=> {
529				// inputs already unlocked
530				&[]
531			},
532		}
533	}
534
535	/// The balance pending in this round
536	///
537	/// This becomes zero once the new round VTXOs are unlocked.
538	pub fn pending_balance(&self) -> Amount {
539		if self.done {
540			return Amount::ZERO;
541		}
542
543		match self.flow {
544			RoundFlowState::NonInteractivePending { .. }
545				| RoundFlowState::InteractivePending
546				| RoundFlowState::InteractiveOngoing { .. }
547				| RoundFlowState::Finished { .. }
548			=> {
549				self.participation.outputs.iter().map(|o| o.amount).sum()
550			},
551			RoundFlowState::Failed { .. } | RoundFlowState::Canceled => {
552				Amount::ZERO
553			},
554		}
555	}
556
557}
558
559/// The state of the process flow of a round
560///
561/// This tracks the progress of the interactive part of the round, from
562/// waiting to start until finishing either succesfully or with a failure.
563pub enum RoundFlowState {
564	/// We don't do flow and we just wait for the round to finish
565	NonInteractivePending {
566		unlock_hash: UnlockHash,
567	},
568
569	/// Waiting for round to happen
570	InteractivePending,
571	/// Interactive part ongoing
572	InteractiveOngoing {
573		round_seq: RoundSeq,
574		attempt_seq: usize,
575		state: AttemptState,
576	},
577
578	/// Interactive part finished, waiting for confirmation
579	Finished {
580		funding_tx: Transaction,
581		unlock_hash: UnlockHash,
582	},
583
584	/// Failed during round
585	Failed {
586		error: String,
587	},
588
589	/// User canceled round
590	Canceled,
591}
592
593/// The state of a single round attempt
594///
595/// For each attempt that we participate in, we keep the state of our concrete
596/// participation.
597pub enum AttemptState {
598	AwaitingAttempt,
599	AwaitingUnsignedVtxoTree {
600		cosign_keys: Vec<Keypair>,
601		unlock_hash: UnlockHash,
602	},
603	AwaitingFinishedRound {
604		unsigned_round_tx: Transaction,
605		vtxos_spec: VtxoTreeSpec,
606		unlock_hash: UnlockHash,
607	},
608}
609
610impl AttemptState {
611	/// The state kind represented as a string
612	fn kind(&self) -> &'static str {
613		match self {
614			Self::AwaitingAttempt => "AwaitingAttempt",
615			Self::AwaitingUnsignedVtxoTree { .. } => "AwaitingUnsignedVtxoTree",
616			Self::AwaitingFinishedRound { .. } => "AwaitingFinishedRound",
617		}
618	}
619}
620
621/// Result from trying to progress an ongoing round attempt
622enum AttemptProgressResult {
623	Finished {
624		funding_tx: Transaction,
625		vtxos: Vec<Vtxo<Full>>,
626		unlock_hash: UnlockHash,
627	},
628	Failed(anyhow::Error),
629	/// When the state changes, this variant is returned
630	///
631	/// If during the processing, we have signed any forfeit txs and tried
632	/// sending them to the server, the [UnconfirmedRound] instance is returned
633	/// so that it can be stored in the state.
634	Updated {
635		new_state: AttemptState,
636	},
637	NotUpdated,
638}
639
640/// Participate in the new round attempt by submitting our round participation
641async fn start_attempt(
642	wallet: &Wallet,
643	participation: &RoundParticipation,
644	event: &RoundAttempt,
645) -> anyhow::Result<AttemptState> {
646	let (mut srv, ark_info) = wallet.require_server().await.context("server not available")?;
647
648	// Assign cosign pubkeys to the payment requests.
649	let cosign_keys = iter::repeat_with(|| Keypair::new(&SECP, &mut rand::thread_rng()))
650		.take(participation.outputs.len())
651		.collect::<Vec<_>>();
652
653	// Prepare round participation info.
654	// For each of our requested vtxo output, we need a set of public and secret nonces.
655	let cosign_nonces = cosign_keys.iter()
656		.map(|key| {
657			let mut secs = Vec::with_capacity(ark_info.nb_round_nonces);
658			let mut pubs = Vec::with_capacity(ark_info.nb_round_nonces);
659			for _ in 0..ark_info.nb_round_nonces {
660				let (s, p) = musig::nonce_pair(key);
661				secs.push(s);
662				pubs.push(p);
663			}
664			(secs, pubs)
665		})
666		.take(participation.outputs.len())
667		.collect::<Vec<(Vec<SecretNonce>, Vec<PublicNonce>)>>();
668
669
670	// The round has now started. We can submit our payment.
671	debug!("Submitting payment request with {} inputs and {} vtxo outputs",
672		participation.inputs.len(), participation.outputs.len(),
673	);
674
675	// Build signed requests with mailbox IDs
676	let unblinded_mailbox_id = wallet.mailbox_identifier();
677	let signed_reqs = participation.outputs.iter()
678		.zip(cosign_keys.iter())
679		.zip(cosign_nonces.iter())
680		.map(|((req, cosign_key), (_sec, pub_nonces))| {
681			SignedVtxoRequest {
682				vtxo: req.clone(),
683				cosign_pubkey: cosign_key.public_key(),
684				nonces: pub_nonces.clone(),
685			}
686		})
687		.collect::<Vec<_>>();
688
689	let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
690	for vtxo in participation.inputs.iter() {
691		let keypair = wallet.get_vtxo_key(vtxo).await
692			.map_err(HarkForfeitError::Err)?;
693		input_vtxos.push(protos::InputVtxo {
694			vtxo_id: vtxo.id().to_bytes().to_vec(),
695			attestation: {
696				let attestation = RoundAttemptAttestation::new(
697					event.challenge, vtxo.id(), &signed_reqs, &keypair,
698				);
699				attestation.serialize()
700			},
701		});
702	}
703
704	// Register VTXO transaction chains with server before round participation
705	wallet.register_vtxo_transactions_with_server(&participation.inputs).await
706		.map_err(HarkForfeitError::Err)?;
707
708	let resp = srv.client.submit_payment(protos::SubmitPaymentRequest {
709		input_vtxos: input_vtxos,
710		vtxo_requests: signed_reqs.into_iter().map(Into::into).collect(),
711		#[allow(deprecated)]
712		offboard_requests: vec![],
713		unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
714	}).await.context("Ark server refused our payment submission")?;
715	let unlock_hash = UnlockHash::from_bytes(&resp.into_inner().unlock_hash)?;
716
717	// Stash nonces in memory only. Empty `cosign_keys` means no VTXO
718	// outputs (offboard-only) — nothing to stash.
719	if let Some(k) = cosign_keys.first() {
720		wallet.inner.round_secret_nonces.stash(
721			k.public_key(),
722			cosign_nonces.into_iter().map(|(sec, _pub)| sec).collect(),
723		);
724	}
725
726	Ok(AttemptState::AwaitingUnsignedVtxoTree { unlock_hash, cosign_keys })
727}
728
729/// just an internal type; need Error trait to work with anyhow
730#[derive(Debug, thiserror::Error)]
731enum HarkForfeitError {
732	/// An error happened after we sent forfeit signatures to the server
733	#[error("error after forfeits were sent")]
734	SentForfeits(#[source] anyhow::Error),
735	/// An error happened before we sent forfeit signatures to the server
736	#[error("error before forfeits were sent")]
737	Err(#[source] anyhow::Error),
738}
739
740async fn hark_cosign_leaf(
741	wallet: &Wallet,
742	srv: &mut ServerConnection,
743	funding_tx: &Transaction,
744	vtxo: &mut Vtxo<Full>,
745) -> anyhow::Result<()> {
746	let key = wallet.pubkey_keypair(&vtxo.user_pubkey()).await
747		.context("error fetching keypair").map_err(HarkForfeitError::Err)?
748		.with_context(|| format!(
749			"keypair {} not found for VTXO {}", vtxo.user_pubkey(), vtxo.id(),
750		))?.1;
751	let (ctx, cosign_req) = LeafVtxoCosignContext::new(vtxo, funding_tx, &key);
752	let cosign_resp = srv.client.request_leaf_vtxo_cosign(
753		protos::LeafVtxoCosignRequest::from(cosign_req),
754	).await
755		.with_context(|| format!("error requesting leaf cosign for vtxo {}", vtxo.id()))?
756		.into_inner().try_into()
757		.context("bad leaf vtxo cosign response")?;
758	ensure!(ctx.finalize(vtxo, cosign_resp),
759		"failed to finalize VTXO leaf signature for VTXO {}", vtxo.id(),
760	);
761
762	Ok(())
763}
764
765/// Finish the hArk VTXO swap protocol
766///
767/// This includes:
768/// - requesting cosignature of the locked hArk leaves
769/// - sending forfeit txs to the server in return for the unlock preimage
770///
771/// NB all the actions in this function are idempotent, meaning that the server
772/// allows them to be done multiple times. this means that if this function calls
773/// fails, it's safe to just call it again at a later time
774async fn hark_vtxo_swap(
775	wallet: &Wallet,
776	participation: &RoundParticipation,
777	output_vtxos: &mut [Vtxo<Full>],
778	funding_tx: &Transaction,
779	unlock_hash: UnlockHash,
780) -> Result<(), HarkForfeitError> {
781	let (mut srv, _) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
782
783	// before we start make sure the server has our input vtxo signatures
784	wallet.register_vtxo_transactions_with_server(&participation.inputs).await
785		.context("couldn't send our input vtxo transactions to server")
786		.map_err(HarkForfeitError::Err)?;
787
788	// first get the leaves signed
789	for vtxo in output_vtxos.iter_mut() {
790		hark_cosign_leaf(wallet, &mut srv, funding_tx, vtxo).await
791			.map_err(HarkForfeitError::Err)?;
792	}
793
794	// then do the forfeit dance
795
796	// the server caps the number of vtxo ids per nonces request, so for
797	// large participations we request the nonces in chunks
798	let mut server_nonces = Vec::with_capacity(participation.inputs.len());
799	for inputs in participation.inputs.chunks(MAX_NB_FORFEIT_NONCE_IDS) {
800		let nonces = srv.client.request_forfeit_nonces(protos::ForfeitNoncesRequest {
801			unlock_hash: unlock_hash.to_byte_array().to_vec(),
802			vtxo_ids: inputs.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
803		}).await
804			.context("request forfeits nonces call failed")
805			.map_err(HarkForfeitError::Err)?
806			.into_inner().public_nonces.into_iter()
807			.map(|b| musig::PublicNonce::from_bytes(b))
808			.collect::<Result<Vec<_>, _>>()
809			.context("invalid forfeit nonces")
810			.map_err(HarkForfeitError::Err)?;
811
812		if nonces.len() != inputs.len() {
813			return Err(HarkForfeitError::Err(anyhow!(
814				"server sent {} nonce pairs, expected {}",
815				nonces.len(), inputs.len(),
816			)));
817		}
818		server_nonces.extend(nonces);
819	}
820
821	let mut forfeit_bundles = Vec::with_capacity(participation.inputs.len());
822	for (input, nonces) in participation.inputs.iter().zip(server_nonces.into_iter()) {
823		let user_key = wallet.pubkey_keypair(&input.user_pubkey()).await
824			.ok().flatten().with_context(|| format!(
825				"failed to fetch keypair for vtxo user pubkey {}", input.user_pubkey(),
826			)).map_err(HarkForfeitError::Err)?.1;
827		forfeit_bundles.push(HashLockedForfeitBundle::new(
828			input, unlock_hash, &user_key, &nonces,
829		))
830	}
831
832	let preimage = srv.client.forfeit_vtxos(protos::ForfeitVtxosRequest {
833		forfeit_bundles: forfeit_bundles.iter().map(|b| b.serialize()).collect(),
834	}).await
835		.context("forfeit vtxos call failed")
836		.map_err(HarkForfeitError::SentForfeits)?
837		.into_inner().unlock_preimage.as_slice().try_into()
838		.context("invalid preimage length")
839		.map_err(HarkForfeitError::SentForfeits)?;
840
841	for vtxo in output_vtxos.iter_mut() {
842		if !vtxo.provide_unlock_preimage(preimage) {
843			return Err(HarkForfeitError::SentForfeits(anyhow!(
844				"invalid preimage {} for vtxo {} with supposed unlock hash {}",
845				preimage.as_hex(), vtxo.id(), unlock_hash,
846			)));
847		}
848
849		// then validate the vtxo works
850		vtxo.validate(&funding_tx).with_context(|| format!(
851			"new VTXO {} does not pass validation after hArk forfeit protocol", vtxo.id(),
852		)).map_err(HarkForfeitError::SentForfeits)?;
853	}
854
855	// then register the output vtxos with the server
856	wallet.register_vtxo_transactions_with_server(output_vtxos).await
857		.context("couldn't register output vtxo transactions with server")
858		.map_err(HarkForfeitError::SentForfeits)?;
859
860	Ok(())
861}
862
863fn check_vtxo_fails_hash_lock(funding_tx: &Transaction, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
864	match vtxo.validate(funding_tx) {
865		// NB delegated participations from rounds that predate the v1
866		// hashlock clauses have leaves with the v0 transition, so both
867		// versions are acceptable here
868		Err(VtxoValidationError::GenesisTransition {
869			genesis_idx, genesis_len, transition_kind, ..
870		}) if genesis_idx + 1 == genesis_len
871			&& (transition_kind == TransitionKind::HashLockedCosigned.as_str()
872				|| transition_kind == TransitionKind::HashLockedCosigned_v0.as_str()) => Ok(()),
873		Ok(()) => Err(anyhow!("new un-unlocked VTXO should fail validation but doesn't: {}",
874			vtxo.serialize_hex(),
875		)),
876		Err(e) => Err(anyhow!("new VTXO {} failed validation: {:#}", vtxo.id(), e)),
877	}
878}
879
880fn check_round_matches_participation(
881	part: &RoundParticipation,
882	new_vtxos: &[Vtxo<Full>],
883	funding_tx: &Transaction,
884) -> anyhow::Result<()> {
885	ensure!(new_vtxos.len() == part.outputs.len(),
886		"unexpected number of VTXOs: got {}, expected {}", new_vtxos.len(), part.outputs.len(),
887	);
888
889	for (vtxo, req) in new_vtxos.iter().zip(&part.outputs) {
890		ensure!(vtxo.amount() == req.amount,
891			"unexpected VTXO amount: got {}, expected {}", vtxo.amount(), req.amount,
892		);
893		ensure!(*vtxo.policy() == req.policy,
894			"unexpected VTXO policy: got {:?}, expected {:?}", vtxo.policy(), req.policy,
895		);
896
897		// We accept the VTXO if only the hArk transition (last) failure happens
898		check_vtxo_fails_hash_lock(funding_tx, vtxo)?;
899	}
900
901	Ok(())
902}
903
904/// Check the confirmation status of a funding tx
905///
906/// Returns true if the funding tx is confirmed deeply enough for us to accept it.
907/// The required number of confirmations depends on the wallet's configuration.
908///
909/// Returns false if the funding tx seems valid but not confirmed yet.
910///
911/// Returns an error if the chain source fails or if we can't submit the tx to the
912/// mempool, suggesting it might be double spent.
913async fn check_funding_tx_confirmations(
914	wallet: &Wallet,
915	funding_txid: Txid,
916	funding_tx: &Transaction,
917) -> anyhow::Result<bool> {
918	let tip = wallet.inner.chain.tip().await.context("chain source error")?;
919	let conf_height = tip - wallet.inner.config.round_tx_required_confirmations + 1;
920	let tx_status = wallet.inner.chain.tx_status(funding_txid).await.context("chain source error")?;
921	trace!("Round funding tx {} confirmation status: {:?} (tip={})",
922		funding_txid, tx_status, tip,
923	);
924	match tx_status {
925		TxStatus::Confirmed(b) if b.height <= conf_height => Ok(true),
926		TxStatus::Mempool | TxStatus::Confirmed(_) => {
927			if wallet.inner.config.round_tx_required_confirmations == 0 {
928				debug!("Accepting round funding tx without confirmations because of configuration");
929				Ok(true)
930			} else {
931				trace!("Hark round funding tx not confirmed (deep enough) yet: {:?}", tx_status);
932				Ok(false)
933			}
934		},
935		TxStatus::NotFound => {
936			// let's try to submit it to our mempool
937			//TODO(stevenroose) change this to an explicit "testmempoolaccept" so that we can
938			// reliably distinguish the cases of our chain source having issues and the tx
939			// actually being rejected which suggests the round was double-spent
940			if let Err(e) = wallet.inner.chain.broadcast_tx(&funding_tx).await {
941				Err(anyhow!("hark funding tx {} server sent us is rejected by mempool (hex={}): {:#}",
942					funding_txid, serialize_hex(funding_tx), e,
943				))
944			} else {
945				trace!("hark funding tx {} was not in mempool but we broadcast it", funding_txid);
946				Ok(false)
947			}
948		},
949	}
950}
951
952enum HarkProgressResult {
953	RoundPending,
954	RoundNotFound,
955	FundingTxUnconfirmed {
956		funding_txid: Txid,
957	},
958	Ok {
959		funding_tx: Transaction,
960		new_vtxos: Vec<Vtxo<Full>>,
961	},
962}
963
964async fn progress_delegated(
965	wallet: &Wallet,
966	participation: &RoundParticipation,
967	unlock_hash: UnlockHash,
968) -> Result<HarkProgressResult, HarkForfeitError> {
969	let (mut srv, _) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
970
971	let resp = match srv.client.round_participation_status(protos::RoundParticipationStatusRequest {
972		unlock_hash: unlock_hash.to_byte_array().to_vec(),
973	}).await {
974		Ok(resp) => resp.into_inner(),
975		Err(err) if err.code() == tonic::Code::NotFound => {
976			return Ok(HarkProgressResult::RoundNotFound);
977		},
978		Err(err) => {
979			return Err(HarkForfeitError::Err(
980				anyhow::Error::from(err).context("error checking round participation status"),
981			));
982		},
983	};
984	let status = protos::RoundParticipationStatus::try_from(resp.status)
985		.context("unknown status from server")
986		.map_err(HarkForfeitError::Err)	?;
987
988	if status == protos::RoundParticipationStatus::RoundPartPending {
989		trace!("Hark round still pending");
990		return Ok(HarkProgressResult::RoundPending);
991	}
992
993	// Since we got here, we clearly don't think we're finished.
994	// So even if the server thinks we did the dance before, we need the
995	// cosignature on the leaf tx so we need to do the dance again.
996	// "Guilty feet have got no rhythm."
997	if status == protos::RoundParticipationStatus::RoundPartReleased {
998		let preimage = resp.unlock_preimage.as_ref().map(|p| p.as_hex());
999		warn!("Server says preimage was already released for hArk participation \
1000			with unlock hash {}. Supposed preimage: {:?}", unlock_hash, preimage,
1001		);
1002	}
1003
1004	let funding_tx_bytes = resp.round_funding_tx
1005		.context("funding txid should be provided when status is not pending")
1006		.map_err(HarkForfeitError::Err)?;
1007	let funding_tx = deserialize::<Transaction>(&funding_tx_bytes)
1008		.context("invalid funding txid")
1009		.map_err(HarkForfeitError::Err)?;
1010	let funding_txid = funding_tx.compute_txid();
1011	trace!("Funding tx for round participation with unlock hash {}: {} ({})",
1012		unlock_hash, funding_tx.compute_txid(), funding_tx_bytes.as_hex(),
1013	);
1014
1015	// Check the confirmation status of the funding tx
1016	match check_funding_tx_confirmations(wallet, funding_txid, &funding_tx).await {
1017		Ok(true) => {},
1018		Ok(false) => return Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid }),
1019		Err(e) => return Err(HarkForfeitError::Err(e.context("checking funding tx confirmations"))),
1020	}
1021
1022	let mut new_vtxos = resp.output_vtxos.into_iter()
1023		.map(|v| <Vtxo<Full>>::deserialize(&v))
1024		.collect::<Result<Vec<_>, _>>()
1025		.context("invalid output VTXOs from server")
1026		.map_err(HarkForfeitError::Err)?;
1027
1028	// Check that the vtxos match our participation in the exact order
1029	check_round_matches_participation(participation, &new_vtxos, &funding_tx)
1030		.context("new VTXOs received from server don't match our participation")
1031		.map_err(HarkForfeitError::Err)?;
1032
1033	hark_vtxo_swap(wallet, participation, &mut new_vtxos, &funding_tx, unlock_hash).await
1034		.context("error forfeiting hArk VTXOs")
1035		.map_err(HarkForfeitError::SentForfeits)?;
1036
1037	Ok(HarkProgressResult::Ok { funding_tx, new_vtxos })
1038}
1039
1040async fn progress_attempt(
1041	state: &mut AttemptState,
1042	wallet: &Wallet,
1043	part: &RoundParticipation,
1044	event: &RoundEvent,
1045) -> AttemptProgressResult {
1046	// we will match only the states and messages required to make progress,
1047	// all else we ignore, except an unexpected finish
1048
1049	match (state, event) {
1050
1051		(
1052			AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, unlock_hash },
1053			RoundEvent::VtxoProposal(e),
1054		) => {
1055			trace!("Received VtxoProposal: {:#?}", e);
1056
1057			// Missing nonces means we restarted before signing —
1058			// abandon the attempt rather than reuse on retry.
1059			let secret_nonces = if let Some(first) = cosign_keys.first() {
1060				match wallet.inner.round_secret_nonces.take(&first.public_key()) {
1061					Some(n) => n,
1062					None => return AttemptProgressResult::Failed(anyhow!(
1063						"secret cosign nonces unavailable (likely after a restart); \
1064						 abandoning round attempt to avoid nonce reuse",
1065					)),
1066				}
1067			} else {
1068				vec![]
1069			};
1070
1071			match sign_vtxo_tree(
1072				wallet,
1073				part,
1074				&cosign_keys,
1075				secret_nonces,
1076				&e.unsigned_round_tx,
1077				&e.vtxos_spec,
1078				&e.cosign_agg_nonces,
1079			).await {
1080				Ok(()) => {
1081					AttemptProgressResult::Updated {
1082						new_state: AttemptState::AwaitingFinishedRound {
1083							unsigned_round_tx: e.unsigned_round_tx.clone(),
1084							vtxos_spec: e.vtxos_spec.clone(),
1085							unlock_hash: *unlock_hash,
1086						},
1087					}
1088				},
1089				Err(e) => {
1090					trace!("Error signing VTXO tree: {:#}", e);
1091					AttemptProgressResult::Failed(e)
1092				},
1093			}
1094		},
1095
1096		(
1097			AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash },
1098			RoundEvent::Finished(RoundFinished { cosign_sigs, signed_round_tx, .. }),
1099		) => {
1100			if unsigned_round_tx.compute_txid() != signed_round_tx.compute_txid() {
1101				return AttemptProgressResult::Failed(anyhow!(
1102					"signed funding tx ({}) doesn't match tx received before ({})",
1103					signed_round_tx.compute_txid(), unsigned_round_tx.compute_txid(),
1104				));
1105			}
1106
1107			if let Err(e) = wallet.inner.chain.broadcast_tx(&signed_round_tx).await {
1108				warn!("Failed to broadcast signed round tx: {:#}", e);
1109			}
1110
1111			match construct_new_vtxos(
1112				part, unsigned_round_tx, vtxos_spec, cosign_sigs,
1113			).await {
1114				Ok(v) => AttemptProgressResult::Finished {
1115					funding_tx: signed_round_tx.clone(),
1116					vtxos: v,
1117					unlock_hash: *unlock_hash,
1118				},
1119				Err(e) => AttemptProgressResult::Failed(anyhow!(
1120					"failed to construct new VTXOs for round: {:#}", e,
1121				)),
1122			}
1123		},
1124
1125		(state, RoundEvent::Finished(RoundFinished { .. })) => {
1126			AttemptProgressResult::Failed(anyhow!(
1127				"unexpectedly received a finished round while we were in state {}",
1128				state.kind(),
1129			))
1130		},
1131
1132		(state, _) => {
1133			trace!("Ignoring round event {} in state {}", event.kind(), state.kind());
1134			AttemptProgressResult::NotUpdated
1135		},
1136	}
1137}
1138
1139async fn sign_vtxo_tree(
1140	wallet: &Wallet,
1141	participation: &RoundParticipation,
1142	cosign_keys: &[Keypair],
1143	secret_nonces: Vec<Vec<SecretNonce>>,
1144	unsigned_round_tx: &Transaction,
1145	vtxo_tree: &VtxoTreeSpec,
1146	cosign_agg_nonces: &[musig::AggregatedNonce],
1147) -> anyhow::Result<()> {
1148	let (mut srv, _) = wallet.require_server().await.context("server not available")?;
1149
1150	let vtxos_utxo = OutPoint::new(unsigned_round_tx.compute_txid(), ROUND_TX_VTXO_TREE_VOUT);
1151
1152	// Check that the proposal contains our inputs.
1153	let mut my_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1154	for vtxo_req in vtxo_tree.iter_vtxos() {
1155		if let Some(i) = my_vtxos.iter().position(|v| {
1156			v.policy == vtxo_req.vtxo.policy && v.amount == vtxo_req.vtxo.amount
1157		}) {
1158			my_vtxos.swap_remove(i);
1159		}
1160	}
1161	if !my_vtxos.is_empty() {
1162		bail!("server didn't include all of our vtxos, missing: {:?}", my_vtxos);
1163	}
1164
1165	let unsigned_vtxos = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1166	trace!("Sending vtxo signatures to server...");
1167	// Sequential: SecretNonce is consume-once and not Clone, so we move
1168	// one Vec<SecretNonce> into cosign_branch per output. Going parallel
1169	// would require sharing the server connection across futures, which
1170	// isn't worth the complexity for a per-output RPC.
1171	for ((req, key), sec) in participation.outputs.iter().zip(cosign_keys).zip(secret_nonces) {
1172		let leaf_idx = unsigned_vtxos.spec.leaf_idx_of_req(req).expect("req included");
1173		let part_sigs = unsigned_vtxos.cosign_branch(
1174			&cosign_agg_nonces, leaf_idx, key, sec,
1175		).context("failed to cosign branch: our request not part of tree")?;
1176
1177		info!("Sending {} partial vtxo cosign signatures for pk {}",
1178			part_sigs.len(), key.public_key(),
1179		);
1180
1181		srv.client.provide_vtxo_signatures(protos::VtxoSignaturesRequest {
1182			pubkey: key.public_key().serialize().to_vec(),
1183			signatures: part_sigs.iter().map(|s| s.serialize().to_vec()).collect(),
1184		}).await.context("error sending vtxo signatures")?;
1185	}
1186	trace!("Done sending vtxo signatures to server");
1187
1188	Ok(())
1189}
1190
1191async fn construct_new_vtxos(
1192	participation: &RoundParticipation,
1193	unsigned_round_tx: &Transaction,
1194	vtxo_tree: &VtxoTreeSpec,
1195	vtxo_cosign_sigs: &[schnorr::Signature],
1196) -> anyhow::Result<Vec<Vtxo<Full>>> {
1197	let round_txid = unsigned_round_tx.compute_txid();
1198	let vtxos_utxo = OutPoint::new(round_txid, ROUND_TX_VTXO_TREE_VOUT);
1199	let vtxo_tree = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1200
1201	// Validate the vtxo tree and cosign signatures.
1202	if vtxo_tree.verify_cosign_sigs(&vtxo_cosign_sigs).is_err() {
1203		// bad server!
1204		bail!("Received incorrect vtxo cosign signatures from server");
1205	}
1206
1207	let signed_vtxos = vtxo_tree
1208		.into_signed_tree(vtxo_cosign_sigs.to_vec())
1209		.into_cached_tree();
1210
1211	let mut expected_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1212	let total_nb_expected_vtxos = expected_vtxos.len();
1213
1214	let mut new_vtxos = vec![];
1215	for (idx, req) in signed_vtxos.spec.spec.vtxos.iter().enumerate() {
1216		if let Some(expected_idx) = expected_vtxos.iter().position(|r| **r == req.vtxo) {
1217			let vtxo = signed_vtxos.build_vtxo(idx);
1218
1219			// validate the received vtxos
1220			// This is more like a sanity check since we crafted them ourselves.
1221			check_vtxo_fails_hash_lock(unsigned_round_tx, &vtxo)
1222				.context("constructed invalid vtxo from tree")?;
1223
1224			info!("New VTXO from round: {} ({}, {})",
1225				vtxo.id(), vtxo.amount(), vtxo.policy_type(),
1226			);
1227
1228			new_vtxos.push(vtxo);
1229			expected_vtxos.swap_remove(expected_idx);
1230		}
1231	}
1232
1233	if !expected_vtxos.is_empty() {
1234		if expected_vtxos.len() == total_nb_expected_vtxos {
1235			// we must have done something wrong
1236			bail!("None of our VTXOs were present in round!");
1237		} else {
1238			bail!("Server included some of our VTXOs but not all: {} missing: {:?}",
1239				expected_vtxos.len(), expected_vtxos,
1240			);
1241		}
1242	}
1243	Ok(new_vtxos)
1244}
1245
1246//TODO(stevenroose) should be made idempotent
1247async fn persist_round_success(
1248	wallet: &Wallet,
1249	participation: &RoundParticipation,
1250	movement_id: Option<MovementId>,
1251	new_vtxos: &[Vtxo<Full>],
1252	funding_tx: &Transaction,
1253) -> anyhow::Result<()> {
1254	debug!("Persisting newly finished round. {} new vtxos, movement ID {:?}",
1255		new_vtxos.len(), movement_id,
1256	);
1257
1258	// we first try all actions that need to happen and only afterwards return errors
1259	// so that we achieve maximum success
1260
1261	let store_result = wallet.store_spendable_vtxos(new_vtxos).await
1262		.context("failed to store new VTXOs");
1263	let spent_result = wallet.mark_vtxos_as_spent(&participation.inputs).await
1264		.context("failed to mark input VTXOs as spent");
1265	let update_result = if let Some(mid) = movement_id {
1266		wallet.inner.movements.finish_movement_with_update(
1267			mid,
1268			MovementStatus::Successful,
1269			MovementUpdate::new()
1270				.produced_vtxos(new_vtxos)
1271				.metadata([("funding_txid".into(), serde_json::to_value(funding_tx.compute_txid())?)]),
1272		).await.context("failed to mark movement as finished")
1273	} else {
1274		Ok(())
1275	};
1276
1277	store_result?;
1278	spent_result?;
1279	update_result?;
1280
1281	Ok(())
1282}
1283
1284async fn persist_round_failure(
1285	wallet: &Wallet,
1286	participation: &RoundParticipation,
1287	movement_id: Option<MovementId>,
1288) -> anyhow::Result<()> {
1289	debug!("Attempting to persist the failure of a round with the movement ID {:?}", movement_id);
1290	let unlock_result = wallet.unlock_vtxos(&participation.inputs).await;
1291	let finish_result = if let Some(movement_id) = movement_id {
1292		wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
1293	} else {
1294		Ok(())
1295	};
1296	if let Err(e) = &finish_result {
1297		error!("Failed to mark movement as failed: {:#}", e);
1298	}
1299	match (unlock_result, finish_result) {
1300		(Ok(()), Ok(())) => Ok(()),
1301		(Err(e), _) => Err(e),
1302		(_, Err(e)) => Err(anyhow!("Failed to mark movement as failed: {:#}", e)),
1303	}
1304}
1305
1306async fn update_funding_txid(
1307	wallet: &Wallet,
1308	movement_id: MovementId,
1309	funding_txid: Txid,
1310) -> anyhow::Result<()> {
1311	wallet.inner.movements.update_movement(
1312		movement_id,
1313		MovementUpdate::new()
1314			.metadata([("funding_txid".into(), serde_json::to_value(&funding_txid)?)])
1315	).await.context("Unable to update funding txid of round")
1316}
1317
1318/// In-memory store for MuSig2 secret cosign nonces used during round
1319/// signing. Entries are keyed by the first cosign pubkey of each round
1320/// attempt — that pubkey is freshly generated in `start_attempt`,
1321/// uniquely identifies the attempt within a process, and is reachable
1322/// from the persisted `AttemptState::AwaitingUnsignedVtxoTree`.
1323///
1324/// Nonces never touch disk: persisting them risks signing twice with
1325/// the same nonce, which is unsafe with MuSig2.
1326#[derive(Default)]
1327pub struct RoundSecretNonces {
1328	inner: parking_lot::Mutex<HashMap<bitcoin::secp256k1::PublicKey, Vec<Vec<SecretNonce>>>>,
1329}
1330
1331impl RoundSecretNonces {
1332	pub fn new() -> Self {
1333		Self { inner: parking_lot::Mutex::new(HashMap::new()) }
1334	}
1335
1336	/// Insert nonces under the given key, replacing any previous entry.
1337	pub fn stash(
1338		&self,
1339		first_cosign_pubkey: bitcoin::secp256k1::PublicKey,
1340		nonces: Vec<Vec<SecretNonce>>,
1341	) {
1342		self.inner.lock().insert(first_cosign_pubkey, nonces);
1343	}
1344
1345	/// Remove and return the nonces stashed under the given key.
1346	/// `None` after a process restart or if the entry was never stashed.
1347	pub fn take(
1348		&self,
1349		first_cosign_pubkey: &bitcoin::secp256k1::PublicKey,
1350	) -> Option<Vec<Vec<SecretNonce>>> {
1351		self.inner.lock().remove(first_cosign_pubkey)
1352	}
1353
1354	/// Drop the entry under the given key without returning its
1355	/// contents. Use when a stashed attempt is being replaced by a new
1356	/// one and its key would otherwise be unreachable.
1357	pub fn forget(&self, first_cosign_pubkey: &bitcoin::secp256k1::PublicKey) {
1358		self.inner.lock().remove(first_cosign_pubkey);
1359	}
1360}
1361
1362impl Wallet {
1363	/// Load and lock a single given round state (by id), waiting for the lock.
1364	///
1365	/// Returns `Some(state)` if the round state is found and locked, `None`
1366	/// if it is not found after acquiring the lock.
1367	pub async fn lock_wait_round_state(&self, id: RoundStateId) -> anyhow::Result<Option<StoredRoundState>> {
1368		let guard = self.inner.lock_manager.lock(
1369			&format!("{}.round.{}", self.fingerprint(), id),
1370			ROUND_LOCK_TIMEOUT,
1371		).await.with_context(|| format!(
1372			"timed out waiting for lock on round state {} (wallet {})",
1373			id, self.fingerprint(),
1374		))?;
1375
1376		if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1377			return Ok(Some(state.lock(guard)));
1378		}
1379
1380		Ok(None)
1381	}
1382
1383	/// Ask the server when the next round is scheduled to start
1384	pub async fn next_round_start_time(&self) -> anyhow::Result<SystemTime> {
1385		let (mut srv, _) = self.require_server().await?;
1386		let ts = srv.client.next_round_time(protos::Empty {}).await?.into_inner().timestamp;
1387		Ok(UNIX_EPOCH.checked_add(Duration::from_secs(ts)).context("invalid timestamp")?)
1388	}
1389
1390	/// Start a new round participation
1391	///
1392	/// This function will store the state in the db and mark the VTXOs as locked.
1393	///
1394	/// ### Return
1395	///
1396	/// - By default, the returned state will be locked to prevent race conditions.
1397	/// To unlock the state, [StoredRoundState::unlock()] can be called.
1398	pub async fn join_next_round(
1399		&self,
1400		participation: RoundParticipation,
1401		movement_kind: Option<RoundMovement>,
1402	) -> anyhow::Result<StoredRoundState> {
1403		let movement = if let Some(kind) = movement_kind {
1404			Some(self.inner.movements.new_guarded_movement_with_update(
1405				Subsystem::ROUND,
1406				kind.to_string(),
1407				OnDropStatus::Failed,
1408				participation.to_movement_update()?
1409			).await?)
1410		} else {
1411			None
1412		};
1413		let movement_id = movement.as_ref().map(|m| m.id());
1414		let input_vtxos = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1415		let state = RoundState::new_interactive(participation, movement_id);
1416
1417		self.lock_vtxos(&input_vtxos, movement_id.map(|m| m.into())).await
1418			.context("failed to lock input VTXOs")?;
1419
1420		match (async || {
1421			let id = self.inner.db.store_round_state(&state).await?;
1422			Ok(self.lock_wait_round_state(id).await?
1423				.context("failed to lock fresh round state")?)
1424		})().await {
1425			Ok(state) => {
1426				if let Some(mut m) = movement {
1427					m.stop();
1428				}
1429				Ok(state)
1430			},
1431			Err(e) => {
1432				self.unlock_vtxos(&input_vtxos).await
1433					.context("failed to unlock input VTXOs")?;
1434				if let Some(mut m) = movement {
1435					m.fail().await.context("failed to mark movement as failed")?;
1436				}
1437				Err(e)
1438			},
1439		}
1440	}
1441
1442	/// Join a round in delegated mode.
1443	///
1444	/// When `scheduled_height` is set, the server won't include the participation in a round
1445	/// before the chain tip reaches it. When `None`, it is eligible for the next round (see
1446	/// [Wallet::join_next_round_delegated]).
1447	pub async fn join_delegated_round(
1448		&self,
1449		participation: RoundParticipation,
1450		movement_kind: Option<RoundMovement>,
1451		scheduled_height: Option<BlockHeight>,
1452	) -> anyhow::Result<StoredRoundState<Unlocked>> {
1453		let movement = if let Some(kind) = movement_kind {
1454			Some(self.inner.movements.new_guarded_movement_with_update(
1455				Subsystem::ROUND,
1456				kind.to_string(),
1457				OnDropStatus::Failed,
1458				participation.to_movement_update()?,
1459			).await?)
1460		} else {
1461			None
1462		};
1463		let movement_id = movement.as_ref().map(|m| m.id());
1464
1465		match self.join_delegated_round_inner(participation, movement_id, scheduled_height).await {
1466			Ok(state) => {
1467				if let Some(mut m) = movement {
1468					m.stop();
1469				}
1470				Ok(state)
1471			},
1472			Err(e) => {
1473				if let Some(mut m) = movement {
1474					m.fail().await.context("error marking movement as failed")?;
1475				}
1476				Err(e)
1477			},
1478		}
1479	}
1480
1481	/// Join the next delegated round, i.e. [Wallet::join_delegated_round] with no scheduled
1482	/// height, so the participation is eligible for the very next round.
1483	pub async fn join_next_round_delegated(
1484		&self,
1485		participation: RoundParticipation,
1486		movement_kind: Option<RoundMovement>,
1487	) -> anyhow::Result<StoredRoundState<Unlocked>> {
1488		self.join_delegated_round(participation, movement_kind, None).await
1489	}
1490
1491	/// Join a round in delegated mode.
1492	///
1493	/// When `scheduled_height` is set, the server won't include the participation in a round
1494	/// before the chain tip reaches it. When `None`, it is eligible for the next round.
1495	async fn join_delegated_round_inner(
1496		&self,
1497		participation: RoundParticipation,
1498		movement_id: Option<MovementId>,
1499		scheduled_height: Option<BlockHeight>,
1500	) -> anyhow::Result<StoredRoundState<Unlocked>> {
1501		let (mut srv, _) = self.require_server().await?;
1502
1503		// Get mailbox identifier for VTXO delivery
1504		let unblinded_mailbox_id = self.mailbox_identifier();
1505
1506		// Register VTXO transaction chains with server before round participation
1507		self.register_vtxo_transactions_with_server(&participation.inputs).await
1508			.context("failed to register input vtxo transactions with server")?;
1509
1510		// Generate attestations for input vtxos
1511		let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
1512		for vtxo in participation.inputs.iter() {
1513			let keypair = self.get_vtxo_key(vtxo).await
1514				.context("failed to get vtxo keypair")?;
1515			input_vtxos.push(protos::InputVtxo {
1516				vtxo_id: vtxo.id().to_bytes().to_vec(),
1517				attestation: {
1518					let attestation = DelegatedRoundParticipationAttestation::new(
1519						vtxo.id(), &participation.outputs, &keypair,
1520					);
1521					attestation.serialize()
1522				},
1523			});
1524		}
1525
1526		// Build proto VtxoRequests
1527		let vtxo_requests = participation.outputs.iter()
1528			.map(|req|
1529				protos::VtxoRequest {
1530					policy: req.policy.serialize(),
1531					amount: req.amount.to_sat(),
1532			})
1533			.collect::<Vec<_>>();
1534
1535		// Submit participation to server and get unlock_hash
1536		let resp = srv.client.submit_round_participation(protos::RoundParticipationRequest {
1537			input_vtxos,
1538			vtxo_requests,
1539			unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
1540			scheduled_height,
1541		}).await.context("error submitting round participation to server")?.into_inner();
1542
1543		let unlock_hash = UnlockHash::from_bytes(resp.unlock_hash)
1544			.context("invalid unlock hash from server")?;
1545
1546		let state = RoundState::new_delegated(participation, unlock_hash, movement_id);
1547
1548		info!("Delegated round participation submitted, it will automatically execute \
1549			when you next sync your wallet after the round happened \
1550			(and has sufficient confirmations).",
1551		);
1552
1553		let id = self.inner.db.store_round_state(&state).await?;
1554		Ok(StoredRoundState::new(id, state))
1555	}
1556
1557	/// Join an already-started round attempt interactively, submitting our
1558	/// participation synchronously.
1559	///
1560	/// Unlike [Wallet::join_next_round] — which stores a pending participation
1561	/// and waits for a round to start before submitting inside the round state
1562	/// machine — this submits to the in-flight `attempt` right away. This allows
1563	/// us to react to any unspendable VTXOs and exclude them from the refresh.
1564	pub(crate) async fn join_attempt_interactive(
1565		&self,
1566		participation: RoundParticipation,
1567		attempt: &RoundAttempt,
1568		movement_kind: Option<RoundMovement>,
1569	) -> anyhow::Result<StoredRoundState<Unlocked>> {
1570		let movement = if let Some(kind) = movement_kind {
1571			Some(self.inner.movements.new_guarded_movement_with_update(
1572				Subsystem::ROUND,
1573				kind.to_string(),
1574				OnDropStatus::Failed,
1575				participation.to_movement_update()?,
1576			).await?)
1577		} else {
1578			None
1579		};
1580		let movement_id = movement.as_ref().map(|m| m.id());
1581
1582		let input_ids = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1583		self.lock_vtxos(&input_ids, movement_id.map(|m| m.into())).await
1584			.context("error locking input VTXOs")?;
1585
1586		match self.join_attempt_interactive_inner(participation, attempt, movement_id).await {
1587			Ok(state) => {
1588				if let Some(mut m) = movement {
1589					m.stop();
1590				}
1591				Ok(state)
1592			},
1593			Err(e) => {
1594				self.unlock_vtxos(&input_ids).await
1595					.context("error unlocking input VTXOs")?;
1596				if let Some(mut m) = movement {
1597					m.fail().await.context("error marking movement as failed")?;
1598				}
1599				Err(e)
1600			},
1601		}
1602	}
1603
1604	async fn join_attempt_interactive_inner(
1605		&self,
1606		participation: RoundParticipation,
1607		attempt: &RoundAttempt,
1608		movement_id: Option<MovementId>,
1609	) -> anyhow::Result<StoredRoundState<Unlocked>> {
1610		// Submit synchronously to the in-flight attempt. On rejection the
1611		// tonic::Status (carrying the unusable input ids in its `identifiers`
1612		// metadata) propagates up the error chain untouched.
1613		let attempt_state = start_attempt(self, &participation, attempt).await?;
1614
1615		let mut state = RoundState::new_interactive(participation, movement_id);
1616		state.flow = RoundFlowState::InteractiveOngoing {
1617			round_seq: attempt.round_seq,
1618			attempt_seq: attempt.attempt_seq,
1619			state: attempt_state,
1620		};
1621
1622		let id = self.inner.db.store_round_state(&state).await?;
1623		Ok(StoredRoundState::new(id, state))
1624	}
1625
1626	/// Get all pending round states
1627	pub async fn pending_round_state_ids(&self) -> anyhow::Result<Vec<RoundStateId>> {
1628		self.inner.db.get_pending_round_state_ids().await
1629	}
1630
1631	/// Get all pending round states
1632	pub async fn pending_round_states(&self) -> anyhow::Result<Vec<StoredRoundState<Unlocked>>> {
1633		let ids = self.inner.db.get_pending_round_state_ids().await?;
1634		let mut states = Vec::with_capacity(ids.len());
1635		for id in ids {
1636			if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1637				states.push(state);
1638			}
1639		}
1640		Ok(states)
1641	}
1642
1643	/// Balance locked in pending rounds
1644	pub async fn pending_round_balance(&self) -> anyhow::Result<Amount> {
1645		let mut ret = Amount::ZERO;
1646		for round in self.pending_round_states().await? {
1647			ret += round.state().pending_balance();
1648		}
1649		Ok(ret)
1650	}
1651
1652	/// Returns all VTXOs that are locked in a pending round
1653	///
1654	/// This excludes all input VTXOs for which the output VTXOs have already
1655	/// been created.
1656	pub async fn pending_round_input_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1657		let mut ret = Vec::new();
1658		for round in self.pending_round_states().await? {
1659			let inputs = round.state().locked_pending_inputs();
1660			ret.reserve(inputs.len());
1661			for input in inputs {
1662				let v = self.get_vtxo_by_id(input.id()).await
1663					.context("unknown round input VTXO")?;
1664				ret.push(v);
1665			}
1666		}
1667		Ok(ret)
1668	}
1669
1670	/// Sync pending rounds that have finished but are waiting for confirmations
1671	pub async fn sync_pending_rounds(&self) -> anyhow::Result<HashMap<RoundStateId, RoundStatus>> {
1672		let states = self.pending_round_states().await?;
1673		if states.is_empty() {
1674			return Ok(HashMap::new());
1675		}
1676
1677		debug!("Syncing {} pending round states...", states.len());
1678
1679		let ret = Arc::new(parking_lot::Mutex::new(HashMap::with_capacity(states.len())));
1680		tokio_stream::iter(states).for_each_concurrent(10, |state| {
1681			let ret = ret.clone();
1682			async move {
1683				// not processing events here
1684				if state.state().ongoing_participation() {
1685					return;
1686				}
1687
1688				let mut state = match self.lock_wait_round_state(state.id()).await {
1689					Ok(Some(state)) => state,
1690					Ok(None) => return,
1691					Err(e) => {
1692						warn!("Error locking round state: {:#}", e);
1693						return;
1694					},
1695				};
1696
1697				let status = match state.state_mut().sync(self).await {
1698					Ok(s) => s,
1699					Err(e) => {
1700						warn!("Error syncing round: {:#}", e);
1701						return;
1702					},
1703				};
1704				trace!("Synced round #{}, status: {:?}", state.id(), status);
1705				match status {
1706					RoundStatus::Confirmed { funding_txid } => {
1707						info!("Round confirmed. Funding tx {}", funding_txid);
1708						if let Err(e) = self.inner.db.remove_round_state(&state).await {
1709							warn!("Error removing confirmed round state from db: {:#}", e);
1710						}
1711					},
1712					RoundStatus::Unconfirmed { funding_txid } => {
1713						info!("Waiting for confirmations for round funding tx {}", funding_txid);
1714						if let Err(e) = self.inner.db.update_round_state(&state).await {
1715							warn!("Error updating pending round state in db: {:#}", e);
1716						}
1717					},
1718					RoundStatus::Pending => {
1719						if let Err(e) = self.inner.db.update_round_state(&state).await {
1720							warn!("Error updating pending round state in db: {:#}", e);
1721						}
1722					},
1723					RoundStatus::Failed { ref error } => {
1724						error!("Round failed: {}", error);
1725						if let Err(e) = self.inner.db.remove_round_state(&state).await {
1726							warn!("Error removing failed round state from db: {:#}", e);
1727						}
1728					},
1729					RoundStatus::Canceled => {
1730						error!("Round canceled");
1731						if let Err(e) = self.inner.db.remove_round_state(&state).await {
1732							warn!("Error removing canceled round state from db: {:#}", e);
1733						}
1734					},
1735				}
1736				ret.lock().insert(state.id(), status);
1737			}
1738		}).await;
1739
1740		Ok(Arc::into_inner(ret).expect("only ref left").into_inner())
1741	}
1742
1743	/// Fetch last round event from server
1744	async fn get_last_round_event(&self) -> anyhow::Result<RoundEvent> {
1745		let (mut srv, _) = self.require_server().await?;
1746		let e = srv.client.last_round_event(protos::Empty {}).await?.into_inner();
1747		Ok(RoundEvent::try_from(e).context("invalid event format from server")?)
1748	}
1749
1750	async fn inner_process_event(
1751		&self,
1752		state: &mut StoredRoundState,
1753		event: Option<&RoundEvent>,
1754	) {
1755		if let Some(event) = event && state.state().ongoing_participation() {
1756			let updated = state.state_mut().process_event(self, &event).await;
1757			if updated {
1758				if let Err(e) = self.inner.db.update_round_state(&state).await {
1759					error!("Error storing round state #{} after progress: {:#}", state.id(), e);
1760				}
1761			}
1762		}
1763
1764		match state.state_mut().sync(self).await {
1765			Err(e) => warn!("Error syncing round #{}: {:#}", state.id(), e),
1766			Ok(s) if s.is_final() => {
1767				info!("Round #{} finished with result: {:?}", state.id(), s);
1768				if let Err(e) = self.inner.db.remove_round_state(&state).await {
1769					warn!("Failed to remove finished round #{} from db: {:#}", state.id(), e);
1770				}
1771			},
1772			Ok(s) => {
1773				trace!("Round state #{} is now in state {:?}", state.id(), s);
1774				if let Err(e) = self.inner.db.update_round_state(&state).await {
1775					warn!("Error storing round state #{}: {:#}", state.id(), e);
1776				}
1777			},
1778		}
1779	}
1780
1781	/// Try to make incremental progress on all pending round states
1782	///
1783	/// If the `last_round_event` argument is not provided, it will be fetched
1784	/// from the server.
1785	pub async fn progress_pending_rounds(
1786		&self,
1787		last_round_event: Option<&RoundEvent>,
1788	) -> anyhow::Result<()> {
1789		let states = self.pending_round_states().await?;
1790		if states.is_empty() {
1791			return Ok(());
1792		}
1793
1794		info!("Processing {} rounds...", states.len());
1795
1796		let mut last_round_event = last_round_event.map(|e| Cow::Borrowed(e));
1797
1798		let has_ongoing_participation = states.iter()
1799			.any(|s| s.state().ongoing_participation());
1800		if has_ongoing_participation && last_round_event.is_none() {
1801			match self.get_last_round_event().await {
1802				Ok(e) => last_round_event = Some(Cow::Owned(e)),
1803				Err(e) => {
1804					warn!("Error fetching round event, \
1805						failed to progress ongoing rounds: {:#}", e);
1806				},
1807			}
1808		}
1809
1810		let event = last_round_event.as_ref().map(|c| c.as_ref());
1811
1812		let futs = states.into_iter().map(async |state| {
1813			let locked = self.lock_wait_round_state(state.id()).await?;
1814			if let Some(mut locked) = locked {
1815				self.inner_process_event(&mut locked, event).await;
1816			}
1817			Ok::<_, anyhow::Error>(())
1818		});
1819
1820		futures::future::join_all(futs).await;
1821
1822		Ok(())
1823	}
1824
1825	pub async fn subscribe_round_events(&self)
1826		-> anyhow::Result<impl Stream<Item = anyhow::Result<RoundEvent>> + Unpin>
1827	{
1828		let (mut srv, _) = self.require_server().await?;
1829		let mut req = tonic::IntoRequest::into_request(protos::Empty {});
1830		req.set_timeout(SUBSCRIBE_REQUEST_TIMEOUT);
1831		let events = srv.client.subscribe_rounds(req).await?
1832			.into_inner().map(|m| {
1833				let m = m.context("received error on event stream")?;
1834				let e = RoundEvent::try_from(m.clone())
1835					.with_context(|| format!("error converting rpc round event: {:?}", m))?;
1836				trace!("Received round event: {}", e);
1837				Ok::<_, anyhow::Error>(e)
1838			});
1839		Ok(events)
1840	}
1841
1842	/// A blocking call that will try to perform a full round participation
1843	/// for all ongoing rounds
1844	///
1845	/// Returns only once there is no ongoing rounds anymore.
1846	pub async fn participate_ongoing_rounds(&self) -> anyhow::Result<()> {
1847		let mut events = self.subscribe_round_events().await?;
1848
1849		loop {
1850			// NB: we need to load all ongoing rounds on every iteration here
1851			// because some might be finished by another call
1852			let state_ids = self.pending_round_states().await?.iter()
1853				.filter(|s| s.state().ongoing_participation())
1854				.map(|s| s.id())
1855				.collect::<Vec<_>>();
1856
1857			if state_ids.is_empty() {
1858				info!("All rounds handled");
1859				return Ok(());
1860			}
1861
1862			let event = events.next().await
1863				.context("events stream broke")?
1864				.context("error on event stream")?;
1865
1866			let futs = state_ids.into_iter().map(async |state| {
1867				let locked = self.lock_wait_round_state(state).await?;
1868				if let Some(mut locked) = locked {
1869					self.inner_process_event(&mut locked, Some(&event)).await;
1870				}
1871				Ok::<_, anyhow::Error>(())
1872			});
1873
1874			futures::future::join_all(futs).await;
1875		}
1876	}
1877
1878	/// Will cancel all pending rounds that can safely be canceled
1879	///
1880	/// All rounds that have not started yet can safely be canceled,
1881	/// as well as rounds where we have not yet signed any forfeit txs.
1882	pub async fn cancel_all_pending_rounds(&self) -> anyhow::Result<()> {
1883		// initial load to get all pending round states ids
1884		let state_ids = self.inner.db.get_pending_round_state_ids().await?;
1885
1886		let futures = state_ids.into_iter().map(|state_id| {
1887			async move {
1888				// wait for lock and load again to ensure most recent state
1889				let mut state = match self.lock_wait_round_state(state_id).await {
1890					Ok(Some(s)) => s,
1891					Ok(None) => return,
1892					Err(e) => return warn!("Error loading round state #{}: {:#}", state_id, e),
1893				};
1894
1895				match state.state_mut().try_cancel(self).await {
1896					Ok(true) => {
1897						if let Err(e) = self.inner.db.remove_round_state(&state).await {
1898							warn!("Error removing canceled round state from db: {:#}", e);
1899						}
1900					},
1901					Ok(false) => {},
1902					Err(e) => warn!("Error trying to cancel round #{}: {:#}", state_id, e),
1903				}
1904			}
1905		});
1906
1907		join_all(futures).await;
1908
1909		Ok(())
1910	}
1911
1912	/// Try to cancel the given round
1913	pub async fn cancel_pending_round(&self, id: RoundStateId) -> anyhow::Result<()> {
1914		let mut state = self.lock_wait_round_state(id).await?
1915			.context("round state not found")?;
1916
1917		if state.state_mut().try_cancel(self).await.context("failed to cancel round")? {
1918			self.inner.db.remove_round_state(&state).await
1919				.context("error removing canceled round state from db")?;
1920		} else {
1921			bail!("failed to cancel round");
1922		}
1923
1924		Ok(())
1925	}
1926
1927	/// Participate in a round
1928	///
1929	/// This function will start a new round participation and block until
1930	/// the round is finished.
1931	/// After this method returns the round state will be kept active until
1932	/// the round tx fully confirms.
1933	pub(crate) async fn participate_round(
1934		&self,
1935		participation: RoundParticipation,
1936		movement_kind: Option<RoundMovement>,
1937	) -> anyhow::Result<RoundStatus> {
1938		let state = self.join_next_round(participation, movement_kind).await?;
1939
1940		info!("Waiting for a round start...");
1941		let mut events = self.subscribe_round_events().await?;
1942
1943		self.drive_round_state(state, &mut events).await
1944	}
1945
1946	/// Drive an already-joined round state to its final [RoundStatus], blocking
1947	/// on `events` and persisting each update.
1948	///
1949	/// Shared by [Wallet::participate_round] and the blocking maintenance
1950	/// refresh: the latter submits its participation up-front (against an
1951	/// in-flight attempt) and then drives the resulting round to completion
1952	/// here.
1953	pub(crate) async fn drive_round_state<S>(
1954		&self,
1955		mut state: StoredRoundState,
1956		events: &mut S,
1957	) -> anyhow::Result<RoundStatus>
1958	where
1959		S: Stream<Item = anyhow::Result<RoundEvent>> + Unpin,
1960	{
1961		loop {
1962			if !state.state().ongoing_participation() {
1963				let status = state.state_mut().sync(self).await?;
1964				match status {
1965					RoundStatus::Failed { error } => bail!("round failed: {}", error),
1966					RoundStatus::Canceled => bail!("round canceled"),
1967					status => return Ok(status),
1968				}
1969			}
1970
1971			let event = events.next().await
1972				.context("events stream broke")?
1973				.context("error on event stream")?;
1974			if state.state_mut().process_event(self, &event).await {
1975				self.inner.db.update_round_state(&state).await?;
1976			}
1977		}
1978	}
1979}
1980
1981#[cfg(test)]
1982mod test {
1983	use super::*;
1984
1985	use bitcoin::secp256k1::Secp256k1;
1986
1987	use ark::VtxoPolicy;
1988	use ark::tree::signed::{HashlockVersion, UnlockPreimage};
1989
1990	fn pubkey() -> bitcoin::secp256k1::PublicKey {
1991		let secp = Secp256k1::new();
1992		Keypair::new(&secp, &mut rand::thread_rng()).public_key()
1993	}
1994
1995	fn nonces() -> Vec<Vec<SecretNonce>> {
1996		let secp = Secp256k1::new();
1997		let key = Keypair::new(&secp, &mut rand::thread_rng());
1998		// Shape mirrors what start_attempt produces: outer Vec is one
1999		// entry per cosign keypair, inner Vec is the tree-depth set of
2000		// pre-generated nonces.
2001		vec![vec![musig::nonce_pair(&key).0, musig::nonce_pair(&key).0]]
2002	}
2003
2004	#[test]
2005	fn accepts_hash_locked_leaves_of_both_versions() {
2006		let secp = Secp256k1::new();
2007		let mut rng = rand::thread_rng();
2008		let user_key = Keypair::new(&secp, &mut rng);
2009		let user_cosign_key = Keypair::new(&secp, &mut rng);
2010		let server_key = Keypair::new(&secp, &mut rng);
2011		let server_cosign_key = Keypair::new(&secp, &mut rng);
2012
2013		let preimage: UnlockPreimage = rand::random();
2014		let unlock_hash = UnlockHash::hash(&preimage);
2015
2016		let outputs = (0..2u64).map(|i| VtxoRequest {
2017			amount: Amount::from_sat(10_000 + i),
2018			policy: VtxoPolicy::new_pubkey(user_key.public_key()),
2019		}).collect::<Vec<_>>();
2020
2021		// Delegated participations from rounds that predate the v1 hashlock
2022		// clauses have leaves with the v0 policy and genesis transition, so
2023		// the check must accept the still-locked leaves of both versions.
2024		for version in [HashlockVersion::V0, HashlockVersion::V1] {
2025			let (tree, funding_tx) = ark::test_util::build_signed_tree(
2026				version, outputs.iter().cloned(),
2027				&user_cosign_key, &server_key, &server_cosign_key, unlock_hash,
2028			);
2029			for vtxo in tree.into_cached_tree().output_vtxos() {
2030				check_vtxo_fails_hash_lock(&funding_tx, &vtxo).unwrap_or_else(|e| panic!(
2031					"locked {:?} leaf vtxo should be accepted: {:#}", version, e,
2032				));
2033			}
2034		}
2035	}
2036
2037	#[test]
2038	fn stash_and_take() {
2039		let store = RoundSecretNonces::new();
2040		let k = pubkey();
2041		store.stash(k, nonces());
2042
2043		assert!(store.take(&k).is_some());
2044	}
2045
2046	#[test]
2047	fn cannot_take_twice() {
2048		let store = RoundSecretNonces::new();
2049		let k = pubkey();
2050		store.stash(k, nonces());
2051
2052		assert!(store.take(&k).is_some());
2053		assert!(store.take(&k).is_none());
2054	}
2055
2056	#[test]
2057	fn cannot_take_after_forget() {
2058		let store = RoundSecretNonces::new();
2059		let k = pubkey();
2060		store.stash(k, nonces());
2061		store.forget(&k);
2062
2063		assert!(store.take(&k).is_none());
2064	}
2065
2066	#[test]
2067	fn stash_overrides_stash() {
2068		let secp = Secp256k1::new();
2069		let key = Keypair::new(&secp, &mut rand::thread_rng());
2070		let nonces_1 = vec![vec![musig::nonce_pair(&key).0]];
2071		let nonces_2 = vec![];
2072
2073		let store = RoundSecretNonces::new();
2074		store.stash(key.public_key(), nonces_1);
2075		store.stash(key.public_key(), nonces_2);
2076
2077		let taken = store.take(&key.public_key()).expect("nonces present");
2078		assert_eq!(taken.len(), 0);
2079	}
2080}