1use std::collections::HashMap;
6use std::iter;
7use std::borrow::Cow;
8use std::convert::Infallible;
9use std::sync::Arc;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12use anyhow::Context;
13use ark::vtxo::{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
42const ROUND_LOCK_TIMEOUT: Duration = Duration::from_secs(10);
45use crate::subsystem::{RoundMovement, Subsystem};
46
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct RoundParticipation {
51 #[serde(with = "ark::encode::serde::vec")]
52 pub inputs: Vec<Vtxo<Full>>,
53 pub outputs: Vec<VtxoRequest>,
56 #[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 Confirmed {
79 funding_txid: Txid,
80 },
81 Unconfirmed {
83 funding_txid: Txid,
84 },
85 Pending,
87 Failed {
89 error: String,
90 },
91 Canceled,
93}
94
95impl RoundStatus {
96 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 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
119pub struct RoundState {
132 pub(crate) done: bool,
134
135 pub(crate) participation: RoundParticipation,
137
138 pub(crate) flow: RoundFlowState,
140
141 pub(crate) new_vtxos: Vec<Vtxo<Full>>,
146
147 pub(crate) sent_forfeit_sigs: bool,
154
155 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 pub fn participation(&self) -> &RoundParticipation {
191 &self.participation
192 }
193
194 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 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 pub async fn try_cancel(&mut self, wallet: &Wallet) -> anyhow::Result<bool> {
232 let ret = match self.flow {
233 RoundFlowState::NonInteractivePending { .. } => {
234 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 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 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 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 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 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 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 Err(HarkForfeitError::Err(e)) => {
447 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 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 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 pub fn locked_pending_inputs(&self) -> &[Vtxo<Full>] {
512 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 &[]
523 } else {
524 &self.participation.inputs
525 },
526 RoundFlowState::Failed { .. }
527 | RoundFlowState::Canceled
528 => {
529 &[]
531 },
532 }
533 }
534
535 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
559pub enum RoundFlowState {
564 NonInteractivePending {
566 unlock_hash: UnlockHash,
567 },
568
569 InteractivePending,
571 InteractiveOngoing {
573 round_seq: RoundSeq,
574 attempt_seq: usize,
575 state: AttemptState,
576 },
577
578 Finished {
580 funding_tx: Transaction,
581 unlock_hash: UnlockHash,
582 },
583
584 Failed {
586 error: String,
587 },
588
589 Canceled,
591}
592
593pub 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 fn kind(&self) -> &'static str {
613 match self {
614 Self::AwaitingAttempt => "AwaitingAttempt",
615 Self::AwaitingUnsignedVtxoTree { .. } => "AwaitingUnsignedVtxoTree",
616 Self::AwaitingFinishedRound { .. } => "AwaitingFinishedRound",
617 }
618 }
619}
620
621enum AttemptProgressResult {
623 Finished {
624 funding_tx: Transaction,
625 vtxos: Vec<Vtxo<Full>>,
626 unlock_hash: UnlockHash,
627 },
628 Failed(anyhow::Error),
629 Updated {
635 new_state: AttemptState,
636 },
637 NotUpdated,
638}
639
640async 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 let cosign_keys = iter::repeat_with(|| Keypair::new(&SECP, &mut rand::thread_rng()))
650 .take(participation.outputs.len())
651 .collect::<Vec<_>>();
652
653 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 debug!("Submitting payment request with {} inputs and {} vtxo outputs",
672 participation.inputs.len(), participation.outputs.len(),
673 );
674
675 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 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 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#[derive(Debug, thiserror::Error)]
731enum HarkForfeitError {
732 #[error("error after forfeits were sent")]
734 SentForfeits(#[source] anyhow::Error),
735 #[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
765async 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 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 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 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 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 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 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 check_vtxo_fails_hash_lock(funding_tx, vtxo)?;
899 }
900
901 Ok(())
902}
903
904async 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 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 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 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_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 match (state, event) {
1050
1051 (
1052 AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, unlock_hash },
1053 RoundEvent::VtxoProposal(e),
1054 ) => {
1055 trace!("Received VtxoProposal: {:#?}", e);
1056
1057 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 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 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 if vtxo_tree.verify_cosign_sigs(&vtxo_cosign_sigs).is_err() {
1203 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 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 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
1246async 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 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#[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 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 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 pub fn forget(&self, first_cosign_pubkey: &bitcoin::secp256k1::PublicKey) {
1358 self.inner.lock().remove(first_cosign_pubkey);
1359 }
1360}
1361
1362impl Wallet {
1363 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 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 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 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 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 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 let unblinded_mailbox_id = self.mailbox_identifier();
1505
1506 self.register_vtxo_transactions_with_server(&participation.inputs).await
1508 .context("failed to register input vtxo transactions with server")?;
1509
1510 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn participate_ongoing_rounds(&self) -> anyhow::Result<()> {
1847 let mut events = self.subscribe_round_events().await?;
1848
1849 loop {
1850 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 pub async fn cancel_all_pending_rounds(&self) -> anyhow::Result<()> {
1883 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 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 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 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 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 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 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}