1use std::collections::HashMap;
6use std::fmt;
7use std::iter;
8use std::borrow::Cow;
9use std::convert::Infallible;
10use std::sync::Arc;
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12
13use anyhow::Context;
14use ark::vtxo::{TransitionKind, VtxoValidationError};
15use bdk_esplora::esplora_client::Amount;
16use bip39::rand;
17use bitcoin::{OutPoint, SignedAmount, Transaction, Txid};
18use bitcoin::consensus::encode::{deserialize, serialize_hex};
19use bitcoin::hashes::Hash;
20use bitcoin::hex::DisplayHex;
21use bitcoin::key::Keypair;
22use bitcoin::secp256k1::schnorr;
23use futures::future::join_all;
24use futures::{Stream, StreamExt};
25use log::{debug, error, info, trace, warn};
26
27use ark::{ArkInfo, ProtocolEncoding, SignedVtxoRequest, Vtxo, VtxoRequest};
28use ark::vtxo::Full;
29use ark::attestations::{DelegatedRoundParticipationAttestation, RoundAttemptAttestation};
30use ark::forfeit::HashLockedForfeitBundle;
31use ark::musig::{self, PublicNonce, SecretNonce};
32use ark::rounds::{RoundAttempt, RoundEvent, RoundFinished, RoundSeq, ROUND_TX_VTXO_TREE_VOUT};
33use ark::tree::signed::{LeafVtxoCosignContext, UnlockHash, VtxoTreeSpec};
34use bitcoin_ext::{BlockDelta, BlockHeight, TxStatus};
35use server_rpc::{protos, ServerConnection, TryFromBytes, MAX_NB_FORFEIT_NONCE_IDS};
36
37use crate::movement::manager::OnDropStatus;
38use crate::{Wallet, WalletVtxo, SECP, SUBSCRIBE_REQUEST_TIMEOUT};
39use crate::movement::{MovementId, MovementStatus};
40use crate::movement::update::MovementUpdate;
41use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
42use crate::subsystem::{RoundMovement, Subsystem};
43use crate::vtxo::{validate_vtxo_tree_params, VtxoLockHolder, VtxoState};
44
45const ROUND_LOCK_TIMEOUT: Duration = Duration::from_secs(10);
48
49const VTXO_EXPIRY_HEIGHT_BUFFER: BlockHeight = 6;
52
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct RoundParticipation {
57 #[serde(with = "ark::encode::serde::vec")]
58 pub inputs: Vec<Vtxo<Full>>,
59 pub outputs: Vec<VtxoRequest>,
62 #[serde(default, skip_serializing_if = "Option::is_none", with = "ark::encode::serde::opt")]
64 pub unblinded_mailbox_id: Option<ark::mailbox::MailboxIdentifier>,
65}
66
67impl RoundParticipation {
68 pub fn to_movement_update(&self) -> anyhow::Result<MovementUpdate> {
69 let input_amount = self.inputs.iter().map(|i| i.amount()).sum::<Amount>();
70 let output_amount = self.outputs.iter().map(|r| r.amount).sum::<Amount>();
71 let fee = input_amount - output_amount;
72 Ok(MovementUpdate::new()
73 .consumed_vtxos(&self.inputs)
74 .intended_balance(SignedAmount::ZERO)
75 .effective_balance( - fee.to_signed()?)
76 .fee(fee)
77 )
78 }
79}
80
81#[derive(Debug, Clone)]
82pub enum RoundStatus {
83 Confirmed {
85 funding_txid: Txid,
86 },
87 Unconfirmed {
89 funding_txid: Txid,
90 },
91 Pending,
93 Failed {
95 error: String,
96 },
97 Canceled,
99}
100
101impl RoundStatus {
102 pub fn is_final(&self) -> bool {
104 match self {
105 Self::Confirmed { .. } => true,
106 Self::Unconfirmed { .. } => false,
107 Self::Pending => false,
108 Self::Failed { .. } => true,
109 Self::Canceled => true,
110 }
111 }
112
113 pub fn is_success(&self) -> bool {
115 match self {
116 Self::Confirmed { .. } => true,
117 Self::Unconfirmed { .. } => true,
118 Self::Pending => false,
119 Self::Failed { .. } => false,
120 Self::Canceled => false,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
127pub enum RoundFlowKind {
128 DelegatedPending,
130 Pending,
132 Ongoing,
134 AwaitingConfirmations,
136 Failed,
138 Canceled,
140}
141
142impl fmt::Display for RoundFlowKind {
143 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
144 match self {
145 Self::DelegatedPending => f.write_str("delegated-pending"),
146 Self::Pending => f.write_str("pending"),
147 Self::Ongoing => f.write_str("ongoing"),
148 Self::AwaitingConfirmations => f.write_str("awaiting-confirmations"),
149 Self::Failed => f.write_str("failed"),
150 Self::Canceled => f.write_str("canceled"),
151 }
152 }
153}
154
155pub struct RoundState {
168 pub(crate) done: bool,
170
171 pub(crate) participation: RoundParticipation,
173
174 pub(crate) flow: RoundFlowState,
176
177 pub(crate) new_vtxos: Vec<Vtxo<Full>>,
182
183 pub(crate) sent_forfeit_sigs: bool,
190
191 pub(crate) movement_id: Option<MovementId>,
193}
194
195impl RoundState {
196 fn new_interactive(
197 participation: RoundParticipation,
198 movement_id: Option<MovementId>,
199 ) -> Self {
200 Self {
201 participation,
202 movement_id,
203 flow: RoundFlowState::InteractivePending,
204 new_vtxos: Vec::new(),
205 sent_forfeit_sigs: false,
206 done: false,
207 }
208 }
209
210 fn new_delegated(
211 participation: RoundParticipation,
212 unlock_hash: UnlockHash,
213 scheduled_height: Option<BlockHeight>,
214 movement_id: Option<MovementId>,
215 ) -> Self {
216 Self {
217 participation,
218 movement_id,
219 flow: RoundFlowState::NonInteractivePending { unlock_hash, scheduled_height },
220 new_vtxos: Vec::new(),
221 sent_forfeit_sigs: false,
222 done: false,
223 }
224 }
225
226 pub fn participation(&self) -> &RoundParticipation {
228 &self.participation
229 }
230
231 pub fn flow_kind(&self) -> RoundFlowKind {
233 match self.flow {
234 RoundFlowState::NonInteractivePending { .. } => RoundFlowKind::DelegatedPending,
235 RoundFlowState::InteractivePending => RoundFlowKind::Pending,
236 RoundFlowState::InteractiveOngoing { .. } => RoundFlowKind::Ongoing,
237 RoundFlowState::Finished { .. } => RoundFlowKind::AwaitingConfirmations,
238 RoundFlowState::Failed { .. } => RoundFlowKind::Failed,
239 RoundFlowState::Canceled => RoundFlowKind::Canceled,
240 }
241 }
242
243 pub fn scheduled_height(&self) -> Option<BlockHeight> {
245 match self.flow {
246 RoundFlowState::NonInteractivePending { scheduled_height, .. } => scheduled_height,
247 _ => None,
248 }
249 }
250
251 pub fn unlock_hash(&self) -> Option<UnlockHash> {
253 match self.flow {
254 RoundFlowState::NonInteractivePending { unlock_hash, .. } => Some(unlock_hash),
255 RoundFlowState::InteractivePending => None,
256 RoundFlowState::InteractiveOngoing { .. } => None,
257 RoundFlowState::Failed { .. } => None,
258 RoundFlowState::Canceled => None,
259 RoundFlowState::Finished { unlock_hash, .. } => Some(unlock_hash),
260 }
261 }
262
263 pub fn funding_tx(&self) -> Option<&Transaction> {
264 match self.flow {
265 RoundFlowState::NonInteractivePending { .. } => None,
266 RoundFlowState::InteractivePending => None,
267 RoundFlowState::InteractiveOngoing { .. } => None,
268 RoundFlowState::Failed { .. } => None,
269 RoundFlowState::Canceled => None,
270 RoundFlowState::Finished { ref funding_tx, .. } => Some(funding_tx),
271 }
272 }
273
274 pub fn ongoing_participation(&self) -> bool {
276 match self.flow {
277 RoundFlowState::NonInteractivePending { .. } => false,
278 RoundFlowState::InteractivePending => true,
279 RoundFlowState::InteractiveOngoing { .. } => true,
280 RoundFlowState::Failed { .. } => false,
281 RoundFlowState::Canceled => false,
282 RoundFlowState::Finished { .. } => false,
283 }
284 }
285
286 pub async fn try_cancel(&mut self, wallet: &Wallet) -> anyhow::Result<bool> {
289 let ret = match self.flow {
290 RoundFlowState::NonInteractivePending { .. } => {
291 bail!("it is currently not yet possible to cancel pending delegated rounds");
293 },
294 RoundFlowState::Canceled => true,
295 RoundFlowState::Failed { .. } => true,
296 RoundFlowState::InteractivePending | RoundFlowState::InteractiveOngoing { .. } => {
297 self.flow = RoundFlowState::Canceled;
298 true
299 },
300 RoundFlowState::Finished { .. } => false,
301 };
302 if ret {
303 persist_round_failure(wallet, &self.participation, self.movement_id).await
304 .context("failed to persist round failure for cancelation")?;
305 }
306 Ok(ret)
307 }
308
309 async fn try_start_attempt(
310 &mut self,
311 wallet: &Wallet,
312 attempt: &RoundAttempt,
313 ) {
314 if let RoundFlowState::InteractiveOngoing {
317 state: AttemptState::AwaitingUnsignedVtxoTree { ref cosign_keys, .. },
318 ..
319 } = self.flow {
320 if let Some(k) = cosign_keys.first() {
321 wallet.inner.round_secret_nonces.forget(&k.public_key());
322 }
323 }
324
325 match start_attempt(wallet, &self.participation, attempt).await {
326 Ok(state) => {
327 self.flow = RoundFlowState::InteractiveOngoing {
328 round_seq: attempt.round_seq,
329 attempt_seq: attempt.attempt_seq,
330 state: state,
331 };
332 },
333 Err(e) => {
334 self.flow = RoundFlowState::Failed {
335 error: format!("{:#}", e),
336 };
337 },
338 }
339 }
340
341 pub async fn process_event(
343 &mut self,
344 wallet: &Wallet,
345 event: &RoundEvent,
346 ) -> bool {
347 let _: Infallible = match self.flow {
348 RoundFlowState::InteractivePending => {
349 if let RoundEvent::Attempt(e) = event && e.attempt_seq == 0 {
350 trace!("Joining round attempt {}:{}", e.round_seq, e.attempt_seq);
351 self.try_start_attempt(wallet, e).await;
352 return true;
353 } else {
354 trace!("Ignoring {} event (seq {}:{}), waiting for round to start",
355 event.kind(), event.round_seq(), event.attempt_seq(),
356 );
357 return false;
358 }
359 },
360 RoundFlowState::InteractiveOngoing { round_seq, attempt_seq, ref mut state } => {
361 if let RoundEvent::Failed(e) = event && e.round_seq == round_seq {
364 warn!("Round {} failed by server", round_seq);
365 self.flow = RoundFlowState::Failed {
366 error: format!("round {} failed by server", round_seq),
367 };
368 return true;
369 }
370
371 if event.round_seq() > round_seq {
372 self.flow = RoundFlowState::Failed {
375 error: format!("round {} started while we were on {}",
376 event.round_seq(), round_seq,
377 ),
378 };
379 return true;
380 }
381
382 if event.attempt_seq() < attempt_seq {
383 trace!("ignoring replayed message from old attempt");
384 return false;
385 }
386
387 if let RoundEvent::Attempt(e) = event && e.attempt_seq > attempt_seq {
388 trace!("Joining new round attempt {}:{}", e.round_seq, e.attempt_seq);
389 self.try_start_attempt(wallet, e).await;
390 return true;
391 }
392 trace!("Processing event {} for round attempt {}:{} in state {}",
393 event.kind(), round_seq, attempt_seq, state.kind(),
394 );
395
396 return match progress_attempt(state, wallet, &self.participation, event).await {
397 AttemptProgressResult::NotUpdated => false,
398 AttemptProgressResult::Updated { new_state } => {
399 *state = new_state;
400 true
401 },
402 AttemptProgressResult::Failed(e) => {
403 warn!("Round failed with error: {:#}", e);
404 self.flow = RoundFlowState::Failed {
405 error: format!("{:#}", e),
406 };
407 true
408 },
409 AttemptProgressResult::Finished { funding_tx, vtxos, unlock_hash } => {
410 self.new_vtxos = vtxos;
411 let funding_txid = funding_tx.compute_txid();
412 self.flow = RoundFlowState::Finished { funding_tx, unlock_hash };
413 if let Some(mid) = self.movement_id {
414 if let Err(e) = update_funding_txid(wallet, mid, funding_txid).await {
415 warn!("Error updating the round funding txid: {:#}", e);
416 }
417 }
418 true
419 },
420 };
421 },
422 RoundFlowState::NonInteractivePending { .. }
423 | RoundFlowState::Finished { .. }
424 | RoundFlowState::Failed { .. }
425 | RoundFlowState::Canceled => return false,
426 };
427 }
428
429 pub async fn sync(&mut self, wallet: &Wallet) -> anyhow::Result<RoundStatus> {
434 match self.flow {
435 RoundFlowState::Finished { ref funding_tx, .. } if self.done => {
436 Ok(RoundStatus::Confirmed {
437 funding_txid: funding_tx.compute_txid(),
438 })
439 },
440
441 RoundFlowState::InteractivePending | RoundFlowState::InteractiveOngoing { .. } => {
442 Ok(RoundStatus::Pending)
443 },
444 RoundFlowState::Failed { ref error } => {
445 persist_round_failure(wallet, &self.participation, self.movement_id).await
446 .context("failed to persist round failure")?;
447 Ok(RoundStatus::Failed { error: error.clone() })
448 },
449 RoundFlowState::Canceled => {
450 persist_round_failure(wallet, &self.participation, self.movement_id).await
451 .context("failed to persist round failure")?;
452 Ok(RoundStatus::Canceled)
453 },
454
455 RoundFlowState::NonInteractivePending { unlock_hash, scheduled_height } => {
456 match progress_delegated(
457 wallet, &self.participation, self.movement_id, unlock_hash, scheduled_height,
458 self.sent_forfeit_sigs,
459 ).await {
460 Ok(HarkProgressResult::RoundPending) => Ok(RoundStatus::Pending),
461 Ok(HarkProgressResult::RoundNotFound) => {
462 info!("Server reports round participation not found (no forfeits sent)");
467 wallet.unlock_vtxos(
468 &self.participation.inputs, self.movement_id.map(|m| m.into()),
469 ).await.context("failed to unlock delegated round inputs")?;
470 self.flow = RoundFlowState::Failed {
471 error: "server reports round participation not found".into(),
472 };
473 if let Some(movement_id) = self.movement_id {
474 wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
475 .context("failed to mark refresh movement as failed")?;
476 }
477 Ok(RoundStatus::Failed {
478 error: "server reports round participation not found".into(),
479 })
480 },
481 Ok(HarkProgressResult::Ok { funding_tx, new_vtxos }) => {
482 let funding_txid = funding_tx.compute_txid();
483 self.new_vtxos = new_vtxos;
484 self.flow = RoundFlowState::Finished {
485 funding_tx: funding_tx.clone(),
486 unlock_hash: unlock_hash,
487 };
488
489 persist_round_success(
490 wallet,
491 &self.participation,
492 self.movement_id,
493 &self.new_vtxos,
494 &funding_tx,
495 ).await.context("failed to store successful round in DB!")?;
496
497 self.done = true;
498
499 Ok(RoundStatus::Confirmed { funding_txid })
500 },
501 Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid }) => {
502 if let Some(mid) = self.movement_id {
503 update_funding_txid(wallet, mid, funding_txid).await
504 .context("failed to update funding txid in DB")?;
505 }
506 Ok(RoundStatus::Unconfirmed { funding_txid })
507 },
508
509 Err(HarkForfeitError::Err(e)) => {
512 Err(e.context("error progressing delegated round"))
516 },
517 Err(HarkForfeitError::SentForfeits(e)) => {
518 self.sent_forfeit_sigs = true;
519 Err(e.context("error progressing delegated round \
520 after sending forfeit tx signatures"))
521 },
522 }
523 },
524 RoundFlowState::Finished { ref funding_tx, unlock_hash } => {
526 let funding_txid = funding_tx.compute_txid();
527 let confirmed = check_funding_tx_confirmations(
528 wallet, funding_txid, &funding_tx,
529 ).await.context("error checking funding tx confirmations")?;
530 if !confirmed {
531 trace!("Funding tx {} not yet deeply enough confirmed", funding_txid);
532 return Ok(RoundStatus::Unconfirmed { funding_txid });
533 }
534
535 match hark_vtxo_swap(
536 wallet, &self.participation, &mut self.new_vtxos, &funding_tx, unlock_hash,
537 self.sent_forfeit_sigs,
538 ).await {
539 Ok(()) => {
540 persist_round_success(
541 wallet,
542 &self.participation,
543 self.movement_id,
544 &self.new_vtxos,
545 &funding_tx,
546 ).await.context("failed to store successful round in DB!")?;
547
548 self.done = true;
549
550 Ok(RoundStatus::Confirmed { funding_txid })
551 },
552 Err(HarkForfeitError::Err(e)) => {
553 Err(e.context("error forfeiting VTXOs after round"))
554 },
555 Err(HarkForfeitError::SentForfeits(e)) => {
556 self.sent_forfeit_sigs = true;
557 Err(e.context("error after having signed and sent \
558 forfeit signatures to server"))
559 },
560 }
561 },
562 }
563 }
564
565 pub fn output_vtxos(&self) -> Option<&[Vtxo<Full>]> {
568 if self.new_vtxos.is_empty() {
569 None
570 } else {
571 Some(&self.new_vtxos)
572 }
573 }
574
575 pub fn locked_pending_inputs(&self) -> &[Vtxo<Full>] {
578 match self.flow {
580 RoundFlowState::NonInteractivePending { .. }
581 | RoundFlowState::InteractivePending
582 | RoundFlowState::InteractiveOngoing { .. }
583 => {
584 &self.participation.inputs
585 },
586 RoundFlowState::Finished { .. } => if self.done {
587 &[]
589 } else {
590 &self.participation.inputs
591 },
592 RoundFlowState::Failed { .. }
593 | RoundFlowState::Canceled
594 => {
595 &[]
597 },
598 }
599 }
600
601 pub fn pending_balance(&self) -> Amount {
605 if self.done {
606 return Amount::ZERO;
607 }
608
609 match self.flow {
610 RoundFlowState::NonInteractivePending { .. }
611 | RoundFlowState::InteractivePending
612 | RoundFlowState::InteractiveOngoing { .. }
613 | RoundFlowState::Finished { .. }
614 => {
615 self.participation.outputs.iter().map(|o| o.amount).sum()
616 },
617 RoundFlowState::Failed { .. } | RoundFlowState::Canceled => {
618 Amount::ZERO
619 },
620 }
621 }
622
623}
624
625pub enum RoundFlowState {
630 NonInteractivePending {
632 unlock_hash: UnlockHash,
633 scheduled_height: Option<BlockHeight>,
637 },
638
639 InteractivePending,
641 InteractiveOngoing {
643 round_seq: RoundSeq,
644 attempt_seq: usize,
645 state: AttemptState,
646 },
647
648 Finished {
650 funding_tx: Transaction,
651 unlock_hash: UnlockHash,
652 },
653
654 Failed {
656 error: String,
657 },
658
659 Canceled,
661}
662
663pub enum AttemptState {
668 AwaitingAttempt,
669 AwaitingUnsignedVtxoTree {
670 cosign_keys: Vec<Keypair>,
671 unlock_hash: UnlockHash,
672 },
673 AwaitingFinishedRound {
674 unsigned_round_tx: Transaction,
675 vtxos_spec: VtxoTreeSpec,
676 unlock_hash: UnlockHash,
677 },
678}
679
680impl AttemptState {
681 fn kind(&self) -> &'static str {
683 match self {
684 Self::AwaitingAttempt => "AwaitingAttempt",
685 Self::AwaitingUnsignedVtxoTree { .. } => "AwaitingUnsignedVtxoTree",
686 Self::AwaitingFinishedRound { .. } => "AwaitingFinishedRound",
687 }
688 }
689}
690
691enum AttemptProgressResult {
693 Finished {
694 funding_tx: Transaction,
695 vtxos: Vec<Vtxo<Full>>,
696 unlock_hash: UnlockHash,
697 },
698 Failed(anyhow::Error),
699 Updated {
705 new_state: AttemptState,
706 },
707 NotUpdated,
708}
709
710async fn start_attempt(
712 wallet: &Wallet,
713 participation: &RoundParticipation,
714 event: &RoundAttempt,
715) -> anyhow::Result<AttemptState> {
716 let (mut srv, ark_info) = wallet.require_server().await.context("server not available")?;
717
718 let cosign_keys = iter::repeat_with(|| Keypair::new(&SECP, &mut rand::thread_rng()))
720 .take(participation.outputs.len())
721 .collect::<Vec<_>>();
722
723 let cosign_nonces = cosign_keys.iter()
726 .map(|key| {
727 let mut secs = Vec::with_capacity(ark_info.nb_round_nonces);
728 let mut pubs = Vec::with_capacity(ark_info.nb_round_nonces);
729 for _ in 0..ark_info.nb_round_nonces {
730 let (s, p) = musig::nonce_pair(key);
731 secs.push(s);
732 pubs.push(p);
733 }
734 (secs, pubs)
735 })
736 .take(participation.outputs.len())
737 .collect::<Vec<(Vec<SecretNonce>, Vec<PublicNonce>)>>();
738
739
740 debug!("Submitting payment request with {} inputs and {} vtxo outputs",
742 participation.inputs.len(), participation.outputs.len(),
743 );
744
745 let unblinded_mailbox_id = wallet.mailbox_identifier();
747 let signed_reqs = participation.outputs.iter()
748 .zip(cosign_keys.iter())
749 .zip(cosign_nonces.iter())
750 .map(|((req, cosign_key), (_sec, pub_nonces))| {
751 SignedVtxoRequest {
752 vtxo: req.clone(),
753 cosign_pubkey: cosign_key.public_key(),
754 nonces: pub_nonces.clone(),
755 }
756 })
757 .collect::<Vec<_>>();
758
759 let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
760 for vtxo in participation.inputs.iter() {
761 let keypair = wallet.get_vtxo_key(vtxo).await
762 .map_err(HarkForfeitError::Err)?;
763 input_vtxos.push(protos::InputVtxo {
764 vtxo_id: vtxo.id().to_bytes().to_vec(),
765 attestation: {
766 let attestation = RoundAttemptAttestation::new(
767 event.challenge, vtxo.id(), &signed_reqs, &keypair,
768 );
769 attestation.serialize()
770 },
771 });
772 }
773
774 wallet.register_vtxo_transactions_with_server(&participation.inputs).await
776 .map_err(HarkForfeitError::Err)?;
777
778 let resp = srv.client.submit_payment(protos::SubmitPaymentRequest {
779 input_vtxos: input_vtxos,
780 vtxo_requests: signed_reqs.into_iter().map(Into::into).collect(),
781 #[allow(deprecated)]
782 offboard_requests: vec![],
783 unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
784 }).await.context("Ark server refused our payment submission")?;
785 let unlock_hash = UnlockHash::from_bytes(&resp.into_inner().unlock_hash)?;
786
787 if let Some(k) = cosign_keys.first() {
790 wallet.inner.round_secret_nonces.stash(
791 k.public_key(),
792 cosign_nonces.into_iter().map(|(sec, _pub)| sec).collect(),
793 );
794 }
795
796 Ok(AttemptState::AwaitingUnsignedVtxoTree { unlock_hash, cosign_keys })
797}
798
799#[derive(Debug, thiserror::Error)]
801enum HarkForfeitError {
802 #[error("error after forfeits were sent")]
804 SentForfeits(#[source] anyhow::Error),
805 #[error("error before forfeits were sent")]
807 Err(#[source] anyhow::Error),
808}
809
810async fn hark_cosign_leaf(
811 wallet: &Wallet,
812 srv: &mut ServerConnection,
813 funding_tx: &Transaction,
814 vtxo: &mut Vtxo<Full>,
815) -> anyhow::Result<()> {
816 let key = wallet.pubkey_keypair(&vtxo.user_pubkey()).await
817 .context("error fetching keypair").map_err(HarkForfeitError::Err)?
818 .with_context(|| format!(
819 "keypair {} not found for VTXO {}", vtxo.user_pubkey(), vtxo.id(),
820 ))?.1;
821 let (ctx, cosign_req) = LeafVtxoCosignContext::new(vtxo, funding_tx, &key)
822 .with_context(|| format!("can't cosign leaf of VTXO {}", vtxo.id()))?;
823 let cosign_resp = srv.client.request_leaf_vtxo_cosign(
824 protos::LeafVtxoCosignRequest::from(cosign_req),
825 ).await
826 .with_context(|| format!("error requesting leaf cosign for vtxo {}", vtxo.id()))?
827 .into_inner().try_into()
828 .context("bad leaf vtxo cosign response")?;
829 ensure!(ctx.finalize(vtxo, cosign_resp),
830 "failed to finalize VTXO leaf signature for VTXO {}", vtxo.id(),
831 );
832
833 Ok(())
834}
835
836async fn hark_vtxo_swap(
846 wallet: &Wallet,
847 participation: &RoundParticipation,
848 output_vtxos: &mut [Vtxo<Full>],
849 funding_tx: &Transaction,
850 unlock_hash: UnlockHash,
851 sent_forfeit_sigs: bool,
852) -> Result<(), HarkForfeitError> {
853 let (mut srv, _) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
854
855 wallet.register_vtxo_transactions_with_server(&participation.inputs).await
857 .context("couldn't send our input vtxo transactions to server")
858 .map_err(HarkForfeitError::Err)?;
859
860 for vtxo in output_vtxos.iter_mut() {
862 hark_cosign_leaf(wallet, &mut srv, funding_tx, vtxo).await
863 .map_err(HarkForfeitError::Err)?;
864 }
865
866 if !sent_forfeit_sigs {
869 let tip = wallet.inner.chain.tip().await
872 .context("chain source error")
873 .map_err(HarkForfeitError::Err)?;
874 if participation.inputs.iter().any(|v| v.expiry_height() > tip) {
875 let max_input_exit_delta = participation.inputs.iter().map(|v| v.exit_delta()).max()
876 .expect("minimum one input");
877 check_output_vtxos_exitable(
878 output_vtxos, tip, wallet.inner.config.vtxo_exit_margin, max_input_exit_delta,
879 )
880 .context("refusing to forfeit our input VTXOs")
881 .map_err(HarkForfeitError::Err)?;
882 }
883 }
884
885 let mut server_nonces = Vec::with_capacity(participation.inputs.len());
890 for inputs in participation.inputs.chunks(MAX_NB_FORFEIT_NONCE_IDS) {
891 let nonces = srv.client.request_forfeit_nonces(protos::ForfeitNoncesRequest {
892 unlock_hash: unlock_hash.to_byte_array().to_vec(),
893 vtxo_ids: inputs.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
894 }).await
895 .context("request forfeits nonces call failed")
896 .map_err(HarkForfeitError::Err)?
897 .into_inner().public_nonces.into_iter()
898 .map(|b| musig::PublicNonce::from_bytes(b))
899 .collect::<Result<Vec<_>, _>>()
900 .context("invalid forfeit nonces")
901 .map_err(HarkForfeitError::Err)?;
902
903 if nonces.len() != inputs.len() {
904 return Err(HarkForfeitError::Err(anyhow!(
905 "server sent {} nonce pairs, expected {}",
906 nonces.len(), inputs.len(),
907 )));
908 }
909 server_nonces.extend(nonces);
910 }
911
912 let mut forfeit_bundles = Vec::with_capacity(participation.inputs.len());
913 for (input, nonces) in participation.inputs.iter().zip(server_nonces.into_iter()) {
914 let user_key = wallet.pubkey_keypair(&input.user_pubkey()).await
915 .ok().flatten().with_context(|| format!(
916 "failed to fetch keypair for vtxo user pubkey {}", input.user_pubkey(),
917 )).map_err(HarkForfeitError::Err)?.1;
918 forfeit_bundles.push(HashLockedForfeitBundle::new(
919 input, unlock_hash, &user_key, &nonces,
920 ))
921 }
922
923 let preimage = srv.client.forfeit_vtxos(protos::ForfeitVtxosRequest {
924 forfeit_bundles: forfeit_bundles.iter().map(|b| b.serialize()).collect(),
925 }).await
926 .context("forfeit vtxos call failed")
927 .map_err(HarkForfeitError::SentForfeits)?
928 .into_inner().unlock_preimage.as_slice().try_into()
929 .context("invalid preimage length")
930 .map_err(HarkForfeitError::SentForfeits)?;
931
932 for vtxo in output_vtxos.iter_mut() {
933 if !vtxo.provide_unlock_preimage(preimage) {
934 return Err(HarkForfeitError::SentForfeits(anyhow!(
935 "invalid preimage for vtxo {} with supposed unlock hash {}",
936 vtxo.id(), unlock_hash,
937 )));
938 }
939
940 vtxo.validate(&funding_tx).with_context(|| format!(
942 "new VTXO {} does not pass validation after hArk forfeit protocol", vtxo.id(),
943 )).map_err(HarkForfeitError::SentForfeits)?;
944 }
945
946 wallet.register_vtxo_transactions_with_server(output_vtxos).await
948 .context("couldn't register output vtxo transactions with server")
949 .map_err(HarkForfeitError::SentForfeits)?;
950
951 Ok(())
952}
953
954fn check_vtxo_fails_hash_lock(funding_tx: &Transaction, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
955 vtxo.validate_unsigned(funding_tx).with_context(|| format!(
959 "new VTXO {} failed unsigned validation", vtxo.id(),
960 ))?;
961
962 match vtxo.validate(funding_tx) {
963 Err(VtxoValidationError::GenesisTransition {
967 genesis_idx, genesis_len, transition_kind, ..
968 }) if genesis_idx + 1 == genesis_len
969 && (transition_kind == TransitionKind::HashLockedCosigned.as_str()
970 || transition_kind == TransitionKind::HashLockedCosigned_v0.as_str()) => Ok(()),
971 Ok(()) => Err(anyhow!("new un-unlocked VTXO should fail validation but doesn't: {}",
972 vtxo.serialize_hex(),
973 )),
974 Err(e) => Err(anyhow!("new VTXO {} failed validation: {:#}", vtxo.id(), e)),
975 }
976}
977
978fn min_exitable_output_vtxo_expiry_height(
986 tip: BlockHeight,
987 exit_margin: BlockDelta,
988 exit_delta: BlockDelta,
989) -> BlockHeight {
990 tip.saturating_add(2 * exit_margin as BlockHeight)
991 .saturating_add(exit_delta as BlockHeight)
992}
993
994fn check_output_vtxos_exitable(
999 vtxos: &[Vtxo<Full>],
1000 tip: BlockHeight,
1001 exit_margin: BlockDelta,
1002 max_input_exit_delta: BlockDelta,
1003) -> anyhow::Result<()> {
1004 let min_expiry_height = min_exitable_output_vtxo_expiry_height(tip, exit_margin, max_input_exit_delta);
1005 for vtxo in vtxos {
1006 ensure!(vtxo.expiry_height() >= min_expiry_height,
1007 "VTXO {} expires at height {}, which doesn't leave us room for a \
1008 unilateral exit (tip {}, exit margin {})",
1009 vtxo.id(), vtxo.expiry_height(), tip, exit_margin,
1010 );
1011 }
1012 Ok(())
1013}
1014
1015fn check_round_matches_participation(
1016 part: &RoundParticipation,
1017 new_vtxos: &[Vtxo<Full>],
1018 funding_tx: &Transaction,
1019 ark_info: &ArkInfo,
1020 scheduled_height: Option<BlockHeight>,
1021 tip: BlockHeight,
1022 exit_margin: BlockDelta,
1023) -> anyhow::Result<()> {
1024 ensure!(new_vtxos.len() == part.outputs.len(),
1025 "unexpected number of VTXOs: got {}, expected {}", new_vtxos.len(), part.outputs.len(),
1026 );
1027
1028 for (idx, vtxo) in new_vtxos.iter().enumerate() {
1032 ensure!(new_vtxos[idx + 1..].iter().all(|v| v.id() != vtxo.id()),
1033 "server delivered duplicate VTXO {}", vtxo.id(),
1034 );
1035 }
1036
1037 let expired_inputs = part.inputs.iter().all(|v| v.expiry_height() <= tip);
1041 let max_input_exit_delta = part.inputs.iter().map(|v| v.exit_delta()).max()
1042 .expect("min one input");
1043 let min_exitable = min_exitable_output_vtxo_expiry_height(tip, exit_margin, max_input_exit_delta);
1044 let min_scheduled = scheduled_height.map(|h| h.saturating_add(1));
1045 let min_expiry_height = match (expired_inputs, min_scheduled) {
1046 (false, Some(h)) => h.max(min_exitable),
1047 (false, None) => min_exitable,
1048 (true, Some(h)) => h,
1049 (true, None) => 0,
1050 };
1051
1052 for (vtxo, req) in new_vtxos.iter().zip(&part.outputs) {
1053 ensure!(vtxo.amount() == req.amount,
1054 "unexpected VTXO amount: got {}, expected {}", vtxo.amount(), req.amount,
1055 );
1056 ensure!(*vtxo.policy() == req.policy,
1057 "unexpected VTXO policy: got {:?}, expected {:?}", vtxo.policy(), req.policy,
1058 );
1059
1060 validate_vtxo_tree_params(
1063 vtxo.server_pubkey(), vtxo.exit_delta(), vtxo.expiry_height(),
1064 ark_info.server_pubkey, ark_info.vtxo_exit_delta, min_expiry_height,
1065 )?;
1066
1067 check_vtxo_fails_hash_lock(funding_tx, vtxo)?;
1069 }
1070
1071 Ok(())
1072}
1073
1074async fn check_funding_tx_confirmations(
1084 wallet: &Wallet,
1085 funding_txid: Txid,
1086 funding_tx: &Transaction,
1087) -> anyhow::Result<bool> {
1088 let tip = wallet.inner.chain.tip().await.context("chain source error")?;
1089 let conf_height = tip - wallet.inner.config.round_tx_required_confirmations + 1;
1090 let tx_status = wallet.inner.chain.tx_status(funding_txid).await.context("chain source error")?;
1091 trace!("Round funding tx {} confirmation status: {:?} (tip={})",
1092 funding_txid, tx_status, tip,
1093 );
1094 match tx_status {
1095 TxStatus::Confirmed(b) if b.height <= conf_height => Ok(true),
1096 TxStatus::Mempool | TxStatus::Confirmed(_) => {
1097 if wallet.inner.config.round_tx_required_confirmations == 0 {
1098 debug!("Accepting round funding tx without confirmations because of configuration");
1099 Ok(true)
1100 } else {
1101 trace!("Hark round funding tx not confirmed (deep enough) yet: {:?}", tx_status);
1102 Ok(false)
1103 }
1104 },
1105 TxStatus::NotFound => {
1106 if let Err(e) = wallet.inner.chain.broadcast_tx(&funding_tx).await {
1111 Err(anyhow!("hark funding tx {} server sent us is rejected by mempool (hex={}): {:#}",
1112 funding_txid, serialize_hex(funding_tx), e,
1113 ))
1114 } else {
1115 trace!("hark funding tx {} was not in mempool but we broadcast it", funding_txid);
1116 Ok(false)
1117 }
1118 },
1119 }
1120}
1121
1122enum HarkProgressResult {
1123 RoundPending,
1124 RoundNotFound,
1125 FundingTxUnconfirmed {
1126 funding_txid: Txid,
1127 },
1128 Ok {
1129 funding_tx: Transaction,
1130 new_vtxos: Vec<Vtxo<Full>>,
1131 },
1132}
1133
1134async fn progress_delegated(
1135 wallet: &Wallet,
1136 participation: &RoundParticipation,
1137 movement_id: Option<MovementId>,
1138 unlock_hash: UnlockHash,
1139 scheduled_height: Option<BlockHeight>,
1140 sent_forfeit_sigs: bool,
1141) -> Result<HarkProgressResult, HarkForfeitError> {
1142 let (mut srv, ark_info) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
1143
1144 let resp = match srv.client.round_participation_status(protos::RoundParticipationStatusRequest {
1145 unlock_hash: unlock_hash.to_byte_array().to_vec(),
1146 }).await {
1147 Ok(resp) => resp.into_inner(),
1148 Err(err) if err.code() == tonic::Code::NotFound => {
1149 return Ok(HarkProgressResult::RoundNotFound);
1150 },
1151 Err(err) => {
1152 return Err(HarkForfeitError::Err(
1153 anyhow::Error::from(err).context("error checking round participation status"),
1154 ));
1155 },
1156 };
1157 let status = protos::RoundParticipationStatus::try_from(resp.status)
1158 .context("unknown status from server")
1159 .map_err(HarkForfeitError::Err) ?;
1160
1161 if status == protos::RoundParticipationStatus::RoundPartPending {
1162 trace!("Hark round still pending");
1163 return Ok(HarkProgressResult::RoundPending);
1164 }
1165
1166 if status == protos::RoundParticipationStatus::RoundPartReleased {
1171 let preimage = resp.unlock_preimage.as_ref().map(|p| p.as_hex());
1172 warn!("Server says preimage was already released for hArk participation \
1173 with unlock hash {}. Supposed preimage: {:?}", unlock_hash, preimage,
1174 );
1175 }
1176
1177 let funding_tx_bytes = resp.round_funding_tx
1178 .context("funding txid should be provided when status is not pending")
1179 .map_err(HarkForfeitError::Err)?;
1180 let funding_tx = deserialize::<Transaction>(&funding_tx_bytes)
1181 .context("invalid funding txid")
1182 .map_err(HarkForfeitError::Err)?;
1183 let funding_txid = funding_tx.compute_txid();
1184 trace!("Funding tx for round participation with unlock hash {}: {} ({})",
1185 unlock_hash, funding_tx.compute_txid(), funding_tx_bytes.as_hex(),
1186 );
1187
1188 let confirmed = check_funding_tx_confirmations(wallet, funding_txid, &funding_tx).await
1190 .context("checking funding tx confirmations")
1191 .map_err(HarkForfeitError::Err)?;
1192
1193 wallet.lock_vtxos(&participation.inputs, movement_id.map(|m| m.into())).await
1197 .context("failed to lock inputs of issued delegated round")
1198 .map_err(HarkForfeitError::Err)?;
1199
1200 if !confirmed {
1201 return Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid });
1202 }
1203
1204 let mut new_vtxos = resp.output_vtxos.into_iter()
1205 .map(|v| <Vtxo<Full>>::deserialize(&v))
1206 .collect::<Result<Vec<_>, _>>()
1207 .context("invalid output VTXOs from server")
1208 .map_err(HarkForfeitError::Err)?;
1209
1210 let tip = wallet.inner.chain.tip().await
1213 .context("chain source error")
1214 .map_err(HarkForfeitError::Err)?;
1215 check_round_matches_participation(
1216 participation, &new_vtxos, &funding_tx, &ark_info, scheduled_height,
1217 tip, wallet.inner.config.vtxo_exit_margin,
1218 )
1219 .context("new VTXOs received from server don't match our participation")
1220 .map_err(HarkForfeitError::Err)?;
1221
1222 hark_vtxo_swap(
1226 wallet, participation, &mut new_vtxos, &funding_tx, unlock_hash, sent_forfeit_sigs,
1227 ).await.map_err(|e| match e {
1228 HarkForfeitError::Err(e) =>
1229 HarkForfeitError::Err(e.context("error forfeiting hArk VTXOs")),
1230 HarkForfeitError::SentForfeits(e) =>
1231 HarkForfeitError::SentForfeits(e.context("error forfeiting hArk VTXOs")),
1232 })?;
1233
1234 Ok(HarkProgressResult::Ok { funding_tx, new_vtxos })
1235}
1236
1237async fn progress_attempt(
1238 state: &mut AttemptState,
1239 wallet: &Wallet,
1240 part: &RoundParticipation,
1241 event: &RoundEvent,
1242) -> AttemptProgressResult {
1243 match (state, event) {
1247
1248 (
1249 AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, unlock_hash },
1250 RoundEvent::VtxoProposal(e),
1251 ) => {
1252 trace!("Received VtxoProposal: {:#?}", e);
1253
1254 let secret_nonces = if let Some(first) = cosign_keys.first() {
1257 match wallet.inner.round_secret_nonces.take(&first.public_key()) {
1258 Some(n) => n,
1259 None => return AttemptProgressResult::Failed(anyhow!(
1260 "secret cosign nonces unavailable (likely after a restart); \
1261 abandoning round attempt to avoid nonce reuse",
1262 )),
1263 }
1264 } else {
1265 vec![]
1266 };
1267
1268 match sign_vtxo_tree(
1269 wallet,
1270 part,
1271 &cosign_keys,
1272 secret_nonces,
1273 &e.unsigned_round_tx,
1274 &e.vtxos_spec,
1275 &e.cosign_agg_nonces,
1276 *unlock_hash,
1277 ).await {
1278 Ok(()) => {
1279 AttemptProgressResult::Updated {
1280 new_state: AttemptState::AwaitingFinishedRound {
1281 unsigned_round_tx: e.unsigned_round_tx.clone(),
1282 vtxos_spec: e.vtxos_spec.clone(),
1283 unlock_hash: *unlock_hash,
1284 },
1285 }
1286 },
1287 Err(e) => {
1288 trace!("Error signing VTXO tree: {:#}", e);
1289 AttemptProgressResult::Failed(e)
1290 },
1291 }
1292 },
1293
1294 (
1295 AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash },
1296 RoundEvent::Finished(RoundFinished { cosign_sigs, signed_round_tx, .. }),
1297 ) => {
1298 if unsigned_round_tx.compute_txid() != signed_round_tx.compute_txid() {
1299 return AttemptProgressResult::Failed(anyhow!(
1300 "signed funding tx ({}) doesn't match tx received before ({})",
1301 signed_round_tx.compute_txid(), unsigned_round_tx.compute_txid(),
1302 ));
1303 }
1304
1305 if let Err(e) = wallet.inner.chain.broadcast_tx(&signed_round_tx).await {
1306 warn!("Failed to broadcast signed round tx: {:#}", e);
1307 }
1308
1309 match construct_new_vtxos(
1310 part, unsigned_round_tx, vtxos_spec, cosign_sigs,
1311 ).await {
1312 Ok(v) => AttemptProgressResult::Finished {
1313 funding_tx: signed_round_tx.clone(),
1314 vtxos: v,
1315 unlock_hash: *unlock_hash,
1316 },
1317 Err(e) => AttemptProgressResult::Failed(anyhow!(
1318 "failed to construct new VTXOs for round: {:#}", e,
1319 )),
1320 }
1321 },
1322
1323 (state, RoundEvent::Finished(RoundFinished { .. })) => {
1324 AttemptProgressResult::Failed(anyhow!(
1325 "unexpectedly received a finished round while we were in state {}",
1326 state.kind(),
1327 ))
1328 },
1329
1330 (state, _) => {
1331 trace!("Ignoring round event {} in state {}", event.kind(), state.kind());
1332 AttemptProgressResult::NotUpdated
1333 },
1334 }
1335}
1336
1337async fn sign_vtxo_tree(
1338 wallet: &Wallet,
1339 participation: &RoundParticipation,
1340 cosign_keys: &[Keypair],
1341 secret_nonces: Vec<Vec<SecretNonce>>,
1342 unsigned_round_tx: &Transaction,
1343 vtxo_tree: &VtxoTreeSpec,
1344 cosign_agg_nonces: &[musig::AggregatedNonce],
1345 unlock_hash: UnlockHash,
1346) -> anyhow::Result<()> {
1347 let (mut srv, ark_info) = wallet.require_server().await.context("server not available")?;
1348
1349 let vtxos_utxo = OutPoint::new(unsigned_round_tx.compute_txid(), ROUND_TX_VTXO_TREE_VOUT);
1350
1351 let tip = wallet.inner.chain.tip().await.context("chain source error")?;
1354 let min_expiry_height = tip
1355 .saturating_add(ark_info.vtxo_lifetime as BlockHeight)
1356 .saturating_sub(VTXO_EXPIRY_HEIGHT_BUFFER);
1357 validate_vtxo_tree_params(
1358 vtxo_tree.server_pubkey, vtxo_tree.exit_delta, vtxo_tree.expiry_height,
1359 ark_info.server_pubkey, ark_info.vtxo_exit_delta, min_expiry_height,
1360 )?;
1361
1362 let mut my_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1364 for vtxo_req in vtxo_tree.iter_vtxos() {
1365 if let Some(i) = my_vtxos.iter().position(|v| {
1366 v.policy == vtxo_req.vtxo.policy && v.amount == vtxo_req.vtxo.amount
1367 }) {
1368 my_vtxos.swap_remove(i);
1369 }
1370 }
1371 if !my_vtxos.is_empty() {
1372 bail!("server didn't include all of our vtxos, missing: {:?}", my_vtxos);
1373 }
1374
1375 let unsigned_vtxos = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1376 trace!("Sending vtxo signatures to server...");
1377 let leaf_idxs = unsigned_vtxos.spec.leaf_idxs_for_participation(
1378 unlock_hash, participation.outputs.iter().map(|o| o),
1379 ).context("our outputs not part of tree")?;
1380 for ((leaf_idx, key), sec) in leaf_idxs.into_iter().zip(cosign_keys).zip(secret_nonces) {
1385 let part_sigs = unsigned_vtxos.cosign_branch(
1386 &cosign_agg_nonces, leaf_idx, key, sec,
1387 ).context("failed to cosign branch: our request not part of tree")?;
1388
1389 info!("Sending {} partial vtxo cosign signatures for pk {}",
1390 part_sigs.len(), key.public_key(),
1391 );
1392
1393 srv.client.provide_vtxo_signatures(protos::VtxoSignaturesRequest {
1394 pubkey: key.public_key().serialize().to_vec(),
1395 signatures: part_sigs.iter().map(|s| s.serialize().to_vec()).collect(),
1396 }).await.context("error sending vtxo signatures")?;
1397 }
1398 trace!("Done sending vtxo signatures to server");
1399
1400 Ok(())
1401}
1402
1403async fn construct_new_vtxos(
1404 participation: &RoundParticipation,
1405 unsigned_round_tx: &Transaction,
1406 vtxo_tree: &VtxoTreeSpec,
1407 vtxo_cosign_sigs: &[schnorr::Signature],
1408) -> anyhow::Result<Vec<Vtxo<Full>>> {
1409 let round_txid = unsigned_round_tx.compute_txid();
1410 let vtxos_utxo = OutPoint::new(round_txid, ROUND_TX_VTXO_TREE_VOUT);
1411 let vtxo_tree = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1412
1413 if vtxo_tree.verify_cosign_sigs(&vtxo_cosign_sigs).is_err() {
1415 bail!("Received incorrect vtxo cosign signatures from server");
1417 }
1418
1419 let signed_vtxos = vtxo_tree
1420 .into_signed_tree(vtxo_cosign_sigs.to_vec())
1421 .into_cached_tree();
1422
1423 let mut expected_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1424 let total_nb_expected_vtxos = expected_vtxos.len();
1425
1426 let mut new_vtxos = vec![];
1427 for (idx, req) in signed_vtxos.spec.spec.vtxos.iter().enumerate() {
1428 if let Some(expected_idx) = expected_vtxos.iter().position(|r| **r == req.vtxo) {
1429 let vtxo = signed_vtxos.build_vtxo(idx);
1430
1431 check_vtxo_fails_hash_lock(unsigned_round_tx, &vtxo)
1434 .context("constructed invalid vtxo from tree")?;
1435
1436 info!("New VTXO from round: {} ({}, {})",
1437 vtxo.id(), vtxo.amount(), vtxo.policy_type(),
1438 );
1439
1440 new_vtxos.push(vtxo);
1441 expected_vtxos.swap_remove(expected_idx);
1442 }
1443 }
1444
1445 if !expected_vtxos.is_empty() {
1446 if expected_vtxos.len() == total_nb_expected_vtxos {
1447 bail!("None of our VTXOs were present in round!");
1449 } else {
1450 bail!("Server included some of our VTXOs but not all: {} missing: {:?}",
1451 expected_vtxos.len(), expected_vtxos,
1452 );
1453 }
1454 }
1455 Ok(new_vtxos)
1456}
1457
1458async fn persist_round_success(
1460 wallet: &Wallet,
1461 participation: &RoundParticipation,
1462 movement_id: Option<MovementId>,
1463 new_vtxos: &[Vtxo<Full>],
1464 funding_tx: &Transaction,
1465) -> anyhow::Result<()> {
1466 debug!("Persisting newly finished round. {} new vtxos, movement ID {:?}",
1467 new_vtxos.len(), movement_id,
1468 );
1469
1470 let store_result = wallet.store_spendable_vtxos(new_vtxos).await
1474 .context("failed to store new VTXOs");
1475 let spent_result = wallet.mark_vtxos_as_spent(&participation.inputs).await
1476 .context("failed to mark input VTXOs as spent");
1477 let update_result = if let Some(mid) = movement_id {
1478 wallet.inner.movements.finish_movement_with_update(
1479 mid,
1480 MovementStatus::Successful,
1481 MovementUpdate::new()
1482 .produced_vtxos(new_vtxos)
1483 .metadata([("funding_txid".into(), serde_json::to_value(funding_tx.compute_txid())?)]),
1484 ).await.context("failed to mark movement as finished")
1485 } else {
1486 Ok(())
1487 };
1488
1489 store_result?;
1490 spent_result?;
1491 update_result?;
1492
1493 Ok(())
1494}
1495
1496async fn persist_round_failure(
1497 wallet: &Wallet,
1498 participation: &RoundParticipation,
1499 movement_id: Option<MovementId>,
1500) -> anyhow::Result<()> {
1501 debug!("Attempting to persist the failure of a round with the movement ID {:?}", movement_id);
1502 let unlock_result = wallet.unlock_vtxos(
1503 &participation.inputs, movement_id.map(|m| m.into()),
1504 ).await;
1505 let finish_result = if let Some(movement_id) = movement_id {
1506 wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
1507 } else {
1508 Ok(())
1509 };
1510 if let Err(e) = &finish_result {
1511 error!("Failed to mark movement as failed: {:#}", e);
1512 }
1513 match (unlock_result, finish_result) {
1514 (Ok(()), Ok(())) => Ok(()),
1515 (Err(e), _) => Err(e),
1516 (_, Err(e)) => Err(anyhow!("Failed to mark movement as failed: {:#}", e)),
1517 }
1518}
1519
1520async fn update_funding_txid(
1521 wallet: &Wallet,
1522 movement_id: MovementId,
1523 funding_txid: Txid,
1524) -> anyhow::Result<()> {
1525 wallet.inner.movements.update_movement(
1526 movement_id,
1527 MovementUpdate::new()
1528 .metadata([("funding_txid".into(), serde_json::to_value(&funding_txid)?)])
1529 ).await.context("Unable to update funding txid of round")
1530}
1531
1532#[derive(Default)]
1541pub struct RoundSecretNonces {
1542 inner: parking_lot::Mutex<HashMap<bitcoin::secp256k1::PublicKey, Vec<Vec<SecretNonce>>>>,
1543}
1544
1545impl RoundSecretNonces {
1546 pub fn new() -> Self {
1547 Self { inner: parking_lot::Mutex::new(HashMap::new()) }
1548 }
1549
1550 pub fn stash(
1552 &self,
1553 first_cosign_pubkey: bitcoin::secp256k1::PublicKey,
1554 nonces: Vec<Vec<SecretNonce>>,
1555 ) {
1556 self.inner.lock().insert(first_cosign_pubkey, nonces);
1557 }
1558
1559 pub fn take(
1562 &self,
1563 first_cosign_pubkey: &bitcoin::secp256k1::PublicKey,
1564 ) -> Option<Vec<Vec<SecretNonce>>> {
1565 self.inner.lock().remove(first_cosign_pubkey)
1566 }
1567
1568 pub fn forget(&self, first_cosign_pubkey: &bitcoin::secp256k1::PublicKey) {
1572 self.inner.lock().remove(first_cosign_pubkey);
1573 }
1574}
1575
1576impl Wallet {
1577 pub async fn lock_wait_round_state(&self, id: RoundStateId) -> anyhow::Result<Option<StoredRoundState>> {
1582 let guard = self.inner.lock_manager.lock(
1583 &format!("{}.round.{}", self.fingerprint(), id),
1584 ROUND_LOCK_TIMEOUT,
1585 ).await.with_context(|| format!(
1586 "timed out waiting for lock on round state {} (wallet {})",
1587 id, self.fingerprint(),
1588 ))?;
1589
1590 if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1591 return Ok(Some(state.lock(guard)));
1592 }
1593
1594 Ok(None)
1595 }
1596
1597 pub async fn next_round_start_time(&self) -> anyhow::Result<SystemTime> {
1599 let (mut srv, _) = self.require_server().await?;
1600 let ts = srv.client.next_round_time(protos::Empty {}).await?.into_inner().timestamp;
1601 Ok(UNIX_EPOCH.checked_add(Duration::from_secs(ts)).context("invalid timestamp")?)
1602 }
1603
1604 pub async fn join_next_round(
1613 &self,
1614 participation: RoundParticipation,
1615 movement_kind: Option<RoundMovement>,
1616 ) -> anyhow::Result<StoredRoundState> {
1617 let movement = if let Some(kind) = movement_kind {
1618 Some(self.inner.movements.new_guarded_movement_with_update(
1619 Subsystem::ROUND,
1620 kind.to_string(),
1621 OnDropStatus::Failed,
1622 participation.to_movement_update()?
1623 ).await?)
1624 } else {
1625 None
1626 };
1627 let movement_id = movement.as_ref().map(|m| m.id());
1628 let input_vtxos = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1629 let state = RoundState::new_interactive(participation, movement_id);
1630
1631 self.lock_vtxos(&input_vtxos, movement_id.map(|m| m.into())).await
1632 .context("failed to lock input VTXOs")?;
1633
1634 match (async || {
1635 let id = self.inner.db.store_round_state(&state).await?;
1636 Ok(self.lock_wait_round_state(id).await?
1637 .context("failed to lock fresh round state")?)
1638 })().await {
1639 Ok(state) => {
1640 if let Some(mut m) = movement {
1641 m.stop();
1642 }
1643 Ok(state)
1644 },
1645 Err(e) => {
1646 self.unlock_vtxos(&input_vtxos, movement_id.map(|m| m.into())).await
1647 .context("failed to unlock input VTXOs")?;
1648 if let Some(mut m) = movement {
1649 m.fail().await.context("failed to mark movement as failed")?;
1650 }
1651 Err(e)
1652 },
1653 }
1654 }
1655
1656 pub async fn join_delegated_round(
1662 &self,
1663 participation: RoundParticipation,
1664 movement_kind: Option<RoundMovement>,
1665 scheduled_height: Option<BlockHeight>,
1666 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1667 let movement = if let Some(kind) = movement_kind {
1668 Some(self.inner.movements.new_guarded_movement_with_update(
1669 Subsystem::ROUND,
1670 kind.to_string(),
1671 OnDropStatus::Failed,
1672 participation.to_movement_update()?,
1673 ).await?)
1674 } else {
1675 None
1676 };
1677 let movement_id = movement.as_ref().map(|m| m.id());
1678
1679 match self.join_delegated_round_inner(participation, movement_id, scheduled_height).await {
1680 Ok(state) => {
1681 if let Some(mut m) = movement {
1682 m.stop();
1683 }
1684 Ok(state)
1685 },
1686 Err(e) => {
1687 if let Some(mut m) = movement {
1688 m.fail().await.context("error marking movement as failed")?;
1689 }
1690 Err(e)
1691 },
1692 }
1693 }
1694
1695 pub async fn join_next_round_delegated(
1698 &self,
1699 participation: RoundParticipation,
1700 movement_kind: Option<RoundMovement>,
1701 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1702 self.join_delegated_round(participation, movement_kind, None).await
1703 }
1704
1705 async fn join_delegated_round_inner(
1710 &self,
1711 participation: RoundParticipation,
1712 movement_id: Option<MovementId>,
1713 scheduled_height: Option<BlockHeight>,
1714 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1715 let (mut srv, _) = self.require_server().await?;
1716
1717 let unblinded_mailbox_id = self.mailbox_identifier();
1719
1720 self.register_vtxo_transactions_with_server(&participation.inputs).await
1722 .context("failed to register input vtxo transactions with server")?;
1723
1724 let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
1726 for vtxo in participation.inputs.iter() {
1727 let keypair = self.get_vtxo_key(vtxo).await
1728 .context("failed to get vtxo keypair")?;
1729 input_vtxos.push(protos::InputVtxo {
1730 vtxo_id: vtxo.id().to_bytes().to_vec(),
1731 attestation: {
1732 let attestation = DelegatedRoundParticipationAttestation::new(
1733 vtxo.id(), &participation.outputs, &keypair,
1734 );
1735 attestation.serialize()
1736 },
1737 });
1738 }
1739
1740 let vtxo_requests = participation.outputs.iter()
1742 .map(|req|
1743 protos::VtxoRequest {
1744 policy: req.policy.serialize(),
1745 amount: req.amount.to_sat(),
1746 })
1747 .collect::<Vec<_>>();
1748
1749 let resp = srv.client.submit_round_participation(protos::RoundParticipationRequest {
1751 input_vtxos,
1752 vtxo_requests,
1753 unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
1754 scheduled_height,
1755 }).await.context("error submitting round participation to server")?.into_inner();
1756
1757 let unlock_hash = UnlockHash::from_bytes(resp.unlock_hash)
1758 .context("invalid unlock hash from server")?;
1759
1760 let state = RoundState::new_delegated(
1761 participation, unlock_hash, scheduled_height, movement_id,
1762 );
1763
1764 info!("Delegated round participation submitted, it will automatically execute \
1765 when you next sync your wallet after the round happened \
1766 (and has sufficient confirmations).",
1767 );
1768
1769 let id = self.inner.db.store_round_state(&state).await?;
1770 Ok(StoredRoundState::new(id, state))
1771 }
1772
1773 pub(crate) async fn join_attempt_interactive(
1781 &self,
1782 participation: RoundParticipation,
1783 attempt: &RoundAttempt,
1784 movement_kind: Option<RoundMovement>,
1785 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1786 let movement = if let Some(kind) = movement_kind {
1787 Some(self.inner.movements.new_guarded_movement_with_update(
1788 Subsystem::ROUND,
1789 kind.to_string(),
1790 OnDropStatus::Failed,
1791 participation.to_movement_update()?,
1792 ).await?)
1793 } else {
1794 None
1795 };
1796 let movement_id = movement.as_ref().map(|m| m.id());
1797
1798 let input_ids = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1799 self.lock_vtxos(&input_ids, movement_id.map(|m| m.into())).await
1800 .context("error locking input VTXOs")?;
1801
1802 match self.join_attempt_interactive_inner(participation, attempt, movement_id).await {
1803 Ok(state) => {
1804 if let Some(mut m) = movement {
1805 m.stop();
1806 }
1807 Ok(state)
1808 },
1809 Err(e) => {
1810 self.unlock_vtxos(&input_ids, movement_id.map(|m| m.into())).await
1811 .context("error unlocking input VTXOs")?;
1812 if let Some(mut m) = movement {
1813 m.fail().await.context("error marking movement as failed")?;
1814 }
1815 Err(e)
1816 },
1817 }
1818 }
1819
1820 async fn join_attempt_interactive_inner(
1821 &self,
1822 participation: RoundParticipation,
1823 attempt: &RoundAttempt,
1824 movement_id: Option<MovementId>,
1825 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1826 let attempt_state = start_attempt(self, &participation, attempt).await?;
1830
1831 let mut state = RoundState::new_interactive(participation, movement_id);
1832 state.flow = RoundFlowState::InteractiveOngoing {
1833 round_seq: attempt.round_seq,
1834 attempt_seq: attempt.attempt_seq,
1835 state: attempt_state,
1836 };
1837
1838 let id = self.inner.db.store_round_state(&state).await?;
1839 Ok(StoredRoundState::new(id, state))
1840 }
1841
1842 pub async fn pending_round_state_ids(&self) -> anyhow::Result<Vec<RoundStateId>> {
1844 self.inner.db.get_pending_round_state_ids().await
1845 }
1846
1847 pub async fn pending_round_states(&self) -> anyhow::Result<Vec<StoredRoundState<Unlocked>>> {
1849 let ids = self.inner.db.get_pending_round_state_ids().await?;
1850 let mut states = Vec::with_capacity(ids.len());
1851 for id in ids {
1852 if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1853 states.push(state);
1854 }
1855 }
1856 Ok(states)
1857 }
1858
1859 pub async fn pending_round_balance(&self) -> anyhow::Result<Amount> {
1861 let mut ret = Amount::ZERO;
1862 for round in self.pending_round_states().await? {
1863 ret += round.state().pending_balance();
1864 }
1865 Ok(ret)
1866 }
1867
1868 pub async fn pending_round_input_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1875 let mut ret = Vec::new();
1876 for round in self.pending_round_states().await? {
1877 let holder = round.state().movement_id.map(|id| VtxoLockHolder::Movement { id });
1878 let inputs = round.state().locked_pending_inputs();
1879 ret.reserve(inputs.len());
1880 for input in inputs {
1881 let v = self.get_vtxo_by_id(input.id()).await
1882 .context("unknown round input VTXO")?;
1883 if matches!(&v.state, VtxoState::Locked { holder: h } if *h == holder) {
1884 ret.push(v);
1885 }
1886 }
1887 }
1888 Ok(ret)
1889 }
1890
1891 pub async fn sync_pending_rounds(&self) -> anyhow::Result<HashMap<RoundStateId, RoundStatus>> {
1893 let states = self.pending_round_states().await?;
1894 if states.is_empty() {
1895 return Ok(HashMap::new());
1896 }
1897
1898 debug!("Syncing {} pending round states...", states.len());
1899
1900 let ret = Arc::new(parking_lot::Mutex::new(HashMap::with_capacity(states.len())));
1901 tokio_stream::iter(states).for_each_concurrent(10, |state| {
1902 let ret = ret.clone();
1903 async move {
1904 if state.state().ongoing_participation() {
1906 return;
1907 }
1908
1909 let mut state = match self.lock_wait_round_state(state.id()).await {
1910 Ok(Some(state)) => state,
1911 Ok(None) => return,
1912 Err(e) => {
1913 warn!("Error locking round state: {:#}", e);
1914 return;
1915 },
1916 };
1917
1918 let status = match state.state_mut().sync(self).await {
1919 Ok(s) => s,
1920 Err(e) => {
1921 warn!("Error syncing round: {:#}", e);
1922 return;
1923 },
1924 };
1925 trace!("Synced round #{}, status: {:?}", state.id(), status);
1926 match status {
1927 RoundStatus::Confirmed { funding_txid } => {
1928 info!("Round confirmed. Funding tx {}", funding_txid);
1929 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1930 warn!("Error removing confirmed round state from db: {:#}", e);
1931 }
1932 },
1933 RoundStatus::Unconfirmed { funding_txid } => {
1934 info!("Waiting for confirmations for round funding tx {}", funding_txid);
1935 if let Err(e) = self.inner.db.update_round_state(&state).await {
1936 warn!("Error updating pending round state in db: {:#}", e);
1937 }
1938 },
1939 RoundStatus::Pending => {
1940 if let Err(e) = self.inner.db.update_round_state(&state).await {
1941 warn!("Error updating pending round state in db: {:#}", e);
1942 }
1943 },
1944 RoundStatus::Failed { ref error } => {
1945 error!("Round failed: {}", error);
1946 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1947 warn!("Error removing failed round state from db: {:#}", e);
1948 }
1949 },
1950 RoundStatus::Canceled => {
1951 error!("Round canceled");
1952 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1953 warn!("Error removing canceled round state from db: {:#}", e);
1954 }
1955 },
1956 }
1957 ret.lock().insert(state.id(), status);
1958 }
1959 }).await;
1960
1961 Ok(Arc::into_inner(ret).expect("only ref left").into_inner())
1962 }
1963
1964 async fn get_last_round_event(&self) -> anyhow::Result<RoundEvent> {
1966 let (mut srv, _) = self.require_server().await?;
1967 let e = srv.client.last_round_event(protos::Empty {}).await?.into_inner();
1968 Ok(RoundEvent::try_from(e).context("invalid event format from server")?)
1969 }
1970
1971 async fn inner_process_event(
1972 &self,
1973 state: &mut StoredRoundState,
1974 event: Option<&RoundEvent>,
1975 ) {
1976 if let Some(event) = event && state.state().ongoing_participation() {
1977 let updated = state.state_mut().process_event(self, &event).await;
1978 if updated {
1979 if let Err(e) = self.inner.db.update_round_state(&state).await {
1980 error!("Error storing round state #{} after progress: {:#}", state.id(), e);
1981 }
1982 }
1983 }
1984
1985 match state.state_mut().sync(self).await {
1986 Err(e) => warn!("Error syncing round #{}: {:#}", state.id(), e),
1987 Ok(s) if s.is_final() => {
1988 info!("Round #{} finished with result: {:?}", state.id(), s);
1989 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1990 warn!("Failed to remove finished round #{} from db: {:#}", state.id(), e);
1991 }
1992 },
1993 Ok(s) => {
1994 trace!("Round state #{} is now in state {:?}", state.id(), s);
1995 if let Err(e) = self.inner.db.update_round_state(&state).await {
1996 warn!("Error storing round state #{}: {:#}", state.id(), e);
1997 }
1998 },
1999 }
2000 }
2001
2002 pub async fn progress_pending_rounds(
2007 &self,
2008 last_round_event: Option<&RoundEvent>,
2009 ) -> anyhow::Result<()> {
2010 let states = self.pending_round_states().await?;
2011 if states.is_empty() {
2012 return Ok(());
2013 }
2014
2015 info!("Processing {} rounds...", states.len());
2016
2017 let mut last_round_event = last_round_event.map(|e| Cow::Borrowed(e));
2018
2019 let has_ongoing_participation = states.iter()
2020 .any(|s| s.state().ongoing_participation());
2021 if has_ongoing_participation && last_round_event.is_none() {
2022 match self.get_last_round_event().await {
2023 Ok(e) => last_round_event = Some(Cow::Owned(e)),
2024 Err(e) => {
2025 warn!("Error fetching round event, \
2026 failed to progress ongoing rounds: {:#}", e);
2027 },
2028 }
2029 }
2030
2031 let event = last_round_event.as_ref().map(|c| c.as_ref());
2032
2033 let futs = states.into_iter().map(async |state| {
2034 let locked = self.lock_wait_round_state(state.id()).await?;
2035 if let Some(mut locked) = locked {
2036 self.inner_process_event(&mut locked, event).await;
2037 }
2038 Ok::<_, anyhow::Error>(())
2039 });
2040
2041 futures::future::join_all(futs).await;
2042
2043 Ok(())
2044 }
2045
2046 pub async fn subscribe_round_events(&self)
2047 -> anyhow::Result<impl Stream<Item = anyhow::Result<RoundEvent>> + Unpin + use<>>
2048 {
2049 let (mut srv, _) = self.require_server().await?;
2050 let mut req = tonic::IntoRequest::into_request(protos::Empty {});
2051 req.set_timeout(SUBSCRIBE_REQUEST_TIMEOUT);
2052 let events = srv.client.subscribe_rounds(req).await?
2053 .into_inner().map(|m| {
2054 let m = m.context("received error on event stream")?;
2055 let e = RoundEvent::try_from(m.clone())
2056 .with_context(|| format!("error converting rpc round event: {:?}", m))?;
2057 trace!("Received round event: {}", e);
2058 Ok::<_, anyhow::Error>(e)
2059 });
2060 Ok(events)
2061 }
2062
2063 pub async fn participate_ongoing_rounds(&self) -> anyhow::Result<()> {
2068 let mut events = self.subscribe_round_events().await?;
2069
2070 loop {
2071 let state_ids = self.pending_round_states().await?.iter()
2074 .filter(|s| s.state().ongoing_participation())
2075 .map(|s| s.id())
2076 .collect::<Vec<_>>();
2077
2078 if state_ids.is_empty() {
2079 info!("All rounds handled");
2080 return Ok(());
2081 }
2082
2083 let event = events.next().await
2084 .context("events stream broke")?
2085 .context("error on event stream")?;
2086
2087 let futs = state_ids.into_iter().map(async |state| {
2088 let locked = self.lock_wait_round_state(state).await?;
2089 if let Some(mut locked) = locked {
2090 self.inner_process_event(&mut locked, Some(&event)).await;
2091 }
2092 Ok::<_, anyhow::Error>(())
2093 });
2094
2095 futures::future::join_all(futs).await;
2096 }
2097 }
2098
2099 pub async fn cancel_all_pending_rounds(&self) -> anyhow::Result<()> {
2104 let state_ids = self.inner.db.get_pending_round_state_ids().await?;
2106
2107 let futures = state_ids.into_iter().map(|state_id| {
2108 async move {
2109 let mut state = match self.lock_wait_round_state(state_id).await {
2111 Ok(Some(s)) => s,
2112 Ok(None) => return,
2113 Err(e) => return warn!("Error loading round state #{}: {:#}", state_id, e),
2114 };
2115
2116 match state.state_mut().try_cancel(self).await {
2117 Ok(true) => {
2118 if let Err(e) = self.inner.db.remove_round_state(&state).await {
2119 warn!("Error removing canceled round state from db: {:#}", e);
2120 }
2121 },
2122 Ok(false) => {},
2123 Err(e) => warn!("Error trying to cancel round #{}: {:#}", state_id, e),
2124 }
2125 }
2126 });
2127
2128 join_all(futures).await;
2129
2130 Ok(())
2131 }
2132
2133 pub async fn cancel_pending_round(&self, id: RoundStateId) -> anyhow::Result<()> {
2135 let mut state = self.lock_wait_round_state(id).await?
2136 .context("round state not found")?;
2137
2138 if state.state_mut().try_cancel(self).await.context("failed to cancel round")? {
2139 self.inner.db.remove_round_state(&state).await
2140 .context("error removing canceled round state from db")?;
2141 } else {
2142 bail!("failed to cancel round");
2143 }
2144
2145 Ok(())
2146 }
2147
2148 pub(crate) async fn participate_round(
2155 &self,
2156 participation: RoundParticipation,
2157 movement_kind: Option<RoundMovement>,
2158 ) -> anyhow::Result<RoundStatus> {
2159 let state = self.join_next_round(participation, movement_kind).await?;
2160
2161 info!("Waiting for a round start...");
2162 let mut events = self.subscribe_round_events().await?;
2163
2164 self.drive_round_state(state, &mut events).await
2165 }
2166
2167 pub(crate) async fn drive_round_state<S>(
2175 &self,
2176 mut state: StoredRoundState,
2177 events: &mut S,
2178 ) -> anyhow::Result<RoundStatus>
2179 where
2180 S: Stream<Item = anyhow::Result<RoundEvent>> + Unpin,
2181 {
2182 loop {
2183 if !state.state().ongoing_participation() {
2184 let status = state.state_mut().sync(self).await?;
2185 match status {
2186 RoundStatus::Failed { error } => bail!("round failed: {}", error),
2187 RoundStatus::Canceled => bail!("round canceled"),
2188 status => return Ok(status),
2189 }
2190 }
2191
2192 let event = events.next().await
2193 .context("events stream broke")?
2194 .context("error on event stream")?;
2195 if state.state_mut().process_event(self, &event).await {
2196 self.inner.db.update_round_state(&state).await?;
2197 }
2198 }
2199 }
2200}
2201
2202#[cfg(test)]
2203mod test {
2204 use super::*;
2205
2206 use bitcoin::secp256k1::Secp256k1;
2207
2208 use ark::VtxoPolicy;
2209 use ark::tree::signed::{HashlockVersion, UnlockPreimage};
2210
2211 fn pubkey() -> bitcoin::secp256k1::PublicKey {
2212 let secp = Secp256k1::new();
2213 Keypair::new(&secp, &mut rand::thread_rng()).public_key()
2214 }
2215
2216 fn nonces() -> Vec<Vec<SecretNonce>> {
2217 let secp = Secp256k1::new();
2218 let key = Keypair::new(&secp, &mut rand::thread_rng());
2219 vec![vec![musig::nonce_pair(&key).0, musig::nonce_pair(&key).0]]
2223 }
2224
2225 #[test]
2226 fn accepts_hash_locked_leaves_of_both_versions() {
2227 let secp = Secp256k1::new();
2228 let mut rng = rand::thread_rng();
2229 let user_key = Keypair::new(&secp, &mut rng);
2230 let user_cosign_key = Keypair::new(&secp, &mut rng);
2231 let server_key = Keypair::new(&secp, &mut rng);
2232 let server_cosign_key = Keypair::new(&secp, &mut rng);
2233
2234 let preimage: UnlockPreimage = rand::random();
2235 let unlock_hash = UnlockHash::hash(&preimage);
2236
2237 let outputs = (0..2u64).map(|i| VtxoRequest {
2238 amount: Amount::from_sat(10_000 + i),
2239 policy: VtxoPolicy::new_pubkey(user_key.public_key()),
2240 }).collect::<Vec<_>>();
2241
2242 for version in [HashlockVersion::V0, HashlockVersion::V1] {
2246 let (tree, funding_tx) = ark::test_util::build_signed_tree(
2247 version, outputs.iter().cloned(),
2248 &user_cosign_key, &server_key, &server_cosign_key, unlock_hash,
2249 );
2250 for vtxo in tree.into_cached_tree().output_vtxos() {
2251 check_vtxo_fails_hash_lock(&funding_tx, &vtxo).unwrap_or_else(|e| panic!(
2252 "locked {:?} leaf vtxo should be accepted: {:#}", version, e,
2253 ));
2254 }
2255 }
2256 }
2257
2258 #[test]
2259 fn rejects_locked_round_vtxo_with_tampered_point() {
2260 let secp = Secp256k1::new();
2261 let mut rng = rand::thread_rng();
2262 let user_key = Keypair::new(&secp, &mut rng);
2263 let user_cosign_key = Keypair::new(&secp, &mut rng);
2264 let server_key = Keypair::new(&secp, &mut rng);
2265 let server_cosign_key = Keypair::new(&secp, &mut rng);
2266 let preimage: UnlockPreimage = rand::random();
2267 let unlock_hash = UnlockHash::hash(&preimage);
2268 let outputs = (0..2u64).map(|i| VtxoRequest {
2269 amount: Amount::from_sat(10_000 + i),
2270 policy: VtxoPolicy::new_pubkey(user_key.public_key()),
2271 }).collect::<Vec<_>>();
2272
2273 let (tree, funding_tx) = ark::test_util::build_signed_tree(
2274 HashlockVersion::V1, outputs,
2275 &user_cosign_key, &server_key, &server_cosign_key, unlock_hash,
2276 );
2277 let vtxo = tree.into_cached_tree().output_vtxos().next().unwrap();
2278
2279 let mut encoded = vtxo.serialize();
2282 let vout_offset = encoded.len() - 4;
2283 encoded[vout_offset] = 1;
2284 let tampered = Vtxo::<Full>::deserialize(&encoded).unwrap();
2285
2286 assert!(tampered.validate_unsigned(&funding_tx).is_err());
2287 assert!(check_vtxo_fails_hash_lock(&funding_tx, &tampered).is_err(),
2288 "tampered point must be rejected before forfeits are sent");
2289 }
2290
2291 #[test]
2292 fn refuses_vtxos_without_room_for_unilateral_exit() {
2293 let secp = Secp256k1::new();
2294 let mut rng = rand::thread_rng();
2295 let user_key = Keypair::new(&secp, &mut rng);
2296 let user_cosign_key = Keypair::new(&secp, &mut rng);
2297 let server_key = Keypair::new(&secp, &mut rng);
2298 let server_cosign_key = Keypair::new(&secp, &mut rng);
2299
2300 let preimage: UnlockPreimage = rand::random();
2301 let unlock_hash = UnlockHash::hash(&preimage);
2302
2303 let outputs = (0..2u64).map(|i| VtxoRequest {
2304 amount: Amount::from_sat(10_000 + i),
2305 policy: VtxoPolicy::new_pubkey(user_key.public_key()),
2306 }).collect::<Vec<_>>();
2307
2308 let (tree, _funding_tx) = ark::test_util::build_signed_tree(
2309 HashlockVersion::V1, outputs,
2310 &user_cosign_key, &server_key, &server_cosign_key, unlock_hash,
2311 );
2312 let vtxos = tree.into_cached_tree().output_vtxos().collect::<Vec<_>>();
2313
2314 check_output_vtxos_exitable(&vtxos, 100_970, 12, 6)
2318 .expect("vtxos with room for a unilateral exit should be accepted");
2319 assert!(check_output_vtxos_exitable(&vtxos, 100_971, 12, 6).is_err(),
2320 "vtxos without room for a unilateral exit must be rejected");
2321 }
2322
2323 #[test]
2324 fn stash_and_take() {
2325 let store = RoundSecretNonces::new();
2326 let k = pubkey();
2327 store.stash(k, nonces());
2328
2329 assert!(store.take(&k).is_some());
2330 }
2331
2332 #[test]
2333 fn cannot_take_twice() {
2334 let store = RoundSecretNonces::new();
2335 let k = pubkey();
2336 store.stash(k, nonces());
2337
2338 assert!(store.take(&k).is_some());
2339 assert!(store.take(&k).is_none());
2340 }
2341
2342 #[test]
2343 fn cannot_take_after_forget() {
2344 let store = RoundSecretNonces::new();
2345 let k = pubkey();
2346 store.stash(k, nonces());
2347 store.forget(&k);
2348
2349 assert!(store.take(&k).is_none());
2350 }
2351
2352 #[test]
2353 fn stash_overrides_stash() {
2354 let secp = Secp256k1::new();
2355 let key = Keypair::new(&secp, &mut rand::thread_rng());
2356 let nonces_1 = vec![vec![musig::nonce_pair(&key).0]];
2357 let nonces_2 = vec![];
2358
2359 let store = RoundSecretNonces::new();
2360 store.stash(key.public_key(), nonces_1);
2361 store.stash(key.public_key(), nonces_2);
2362
2363 let taken = store.take(&key.public_key()).expect("nonces present");
2364 assert_eq!(taken.len(), 0);
2365 }
2366}