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;
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, unlock_hash, scheduled_height,
458 self.sent_forfeit_sigs,
459 ).await {
460 Ok(HarkProgressResult::RoundPending) => Ok(RoundStatus::Pending),
461 Ok(HarkProgressResult::RoundNotFound) => {
464 info!("Server reports round participation not found (no forfeits sent)");
465 self.flow = RoundFlowState::Failed {
466 error: "server reports round participation not found".into(),
467 };
468 if let Some(movement_id) = self.movement_id {
469 wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
470 .context("failed to mark refresh movement as failed")?;
471 }
472 Ok(RoundStatus::Failed {
473 error: "server reports round participation not found".into(),
474 })
475 },
476 Ok(HarkProgressResult::Ok { funding_tx, new_vtxos }) => {
477 let funding_txid = funding_tx.compute_txid();
478 self.new_vtxos = new_vtxos;
479 self.flow = RoundFlowState::Finished {
480 funding_tx: funding_tx.clone(),
481 unlock_hash: unlock_hash,
482 };
483
484 persist_round_success(
485 wallet,
486 &self.participation,
487 self.movement_id,
488 &self.new_vtxos,
489 &funding_tx,
490 ).await.context("failed to store successful round in DB!")?;
491
492 self.done = true;
493
494 Ok(RoundStatus::Confirmed { funding_txid })
495 },
496 Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid }) => {
497 if let Some(mid) = self.movement_id {
498 update_funding_txid(wallet, mid, funding_txid).await
499 .context("failed to update funding txid in DB")?;
500 }
501 Ok(RoundStatus::Unconfirmed { funding_txid })
502 },
503
504 Err(HarkForfeitError::Err(e)) => {
507 Err(e.context("error progressing delegated round"))
511 },
512 Err(HarkForfeitError::SentForfeits(e)) => {
513 self.sent_forfeit_sigs = true;
514 Err(e.context("error progressing delegated round \
515 after sending forfeit tx signatures"))
516 },
517 }
518 },
519 RoundFlowState::Finished { ref funding_tx, unlock_hash } => {
521 let funding_txid = funding_tx.compute_txid();
522 let confirmed = check_funding_tx_confirmations(
523 wallet, funding_txid, &funding_tx,
524 ).await.context("error checking funding tx confirmations")?;
525 if !confirmed {
526 trace!("Funding tx {} not yet deeply enough confirmed", funding_txid);
527 return Ok(RoundStatus::Unconfirmed { funding_txid });
528 }
529
530 match hark_vtxo_swap(
531 wallet, &self.participation, &mut self.new_vtxos, &funding_tx, unlock_hash,
532 self.sent_forfeit_sigs,
533 ).await {
534 Ok(()) => {
535 persist_round_success(
536 wallet,
537 &self.participation,
538 self.movement_id,
539 &self.new_vtxos,
540 &funding_tx,
541 ).await.context("failed to store successful round in DB!")?;
542
543 self.done = true;
544
545 Ok(RoundStatus::Confirmed { funding_txid })
546 },
547 Err(HarkForfeitError::Err(e)) => {
548 Err(e.context("error forfeiting VTXOs after round"))
549 },
550 Err(HarkForfeitError::SentForfeits(e)) => {
551 self.sent_forfeit_sigs = true;
552 Err(e.context("error after having signed and sent \
553 forfeit signatures to server"))
554 },
555 }
556 },
557 }
558 }
559
560 pub fn output_vtxos(&self) -> Option<&[Vtxo<Full>]> {
563 if self.new_vtxos.is_empty() {
564 None
565 } else {
566 Some(&self.new_vtxos)
567 }
568 }
569
570 pub fn locked_pending_inputs(&self) -> &[Vtxo<Full>] {
573 match self.flow {
575 RoundFlowState::NonInteractivePending { .. }
576 | RoundFlowState::InteractivePending
577 | RoundFlowState::InteractiveOngoing { .. }
578 => {
579 &self.participation.inputs
580 },
581 RoundFlowState::Finished { .. } => if self.done {
582 &[]
584 } else {
585 &self.participation.inputs
586 },
587 RoundFlowState::Failed { .. }
588 | RoundFlowState::Canceled
589 => {
590 &[]
592 },
593 }
594 }
595
596 pub fn pending_balance(&self) -> Amount {
600 if self.done {
601 return Amount::ZERO;
602 }
603
604 match self.flow {
605 RoundFlowState::NonInteractivePending { .. }
606 | RoundFlowState::InteractivePending
607 | RoundFlowState::InteractiveOngoing { .. }
608 | RoundFlowState::Finished { .. }
609 => {
610 self.participation.outputs.iter().map(|o| o.amount).sum()
611 },
612 RoundFlowState::Failed { .. } | RoundFlowState::Canceled => {
613 Amount::ZERO
614 },
615 }
616 }
617
618}
619
620pub enum RoundFlowState {
625 NonInteractivePending {
627 unlock_hash: UnlockHash,
628 scheduled_height: Option<BlockHeight>,
632 },
633
634 InteractivePending,
636 InteractiveOngoing {
638 round_seq: RoundSeq,
639 attempt_seq: usize,
640 state: AttemptState,
641 },
642
643 Finished {
645 funding_tx: Transaction,
646 unlock_hash: UnlockHash,
647 },
648
649 Failed {
651 error: String,
652 },
653
654 Canceled,
656}
657
658pub enum AttemptState {
663 AwaitingAttempt,
664 AwaitingUnsignedVtxoTree {
665 cosign_keys: Vec<Keypair>,
666 unlock_hash: UnlockHash,
667 },
668 AwaitingFinishedRound {
669 unsigned_round_tx: Transaction,
670 vtxos_spec: VtxoTreeSpec,
671 unlock_hash: UnlockHash,
672 },
673}
674
675impl AttemptState {
676 fn kind(&self) -> &'static str {
678 match self {
679 Self::AwaitingAttempt => "AwaitingAttempt",
680 Self::AwaitingUnsignedVtxoTree { .. } => "AwaitingUnsignedVtxoTree",
681 Self::AwaitingFinishedRound { .. } => "AwaitingFinishedRound",
682 }
683 }
684}
685
686enum AttemptProgressResult {
688 Finished {
689 funding_tx: Transaction,
690 vtxos: Vec<Vtxo<Full>>,
691 unlock_hash: UnlockHash,
692 },
693 Failed(anyhow::Error),
694 Updated {
700 new_state: AttemptState,
701 },
702 NotUpdated,
703}
704
705async fn start_attempt(
707 wallet: &Wallet,
708 participation: &RoundParticipation,
709 event: &RoundAttempt,
710) -> anyhow::Result<AttemptState> {
711 let (mut srv, ark_info) = wallet.require_server().await.context("server not available")?;
712
713 let cosign_keys = iter::repeat_with(|| Keypair::new(&SECP, &mut rand::thread_rng()))
715 .take(participation.outputs.len())
716 .collect::<Vec<_>>();
717
718 let cosign_nonces = cosign_keys.iter()
721 .map(|key| {
722 let mut secs = Vec::with_capacity(ark_info.nb_round_nonces);
723 let mut pubs = Vec::with_capacity(ark_info.nb_round_nonces);
724 for _ in 0..ark_info.nb_round_nonces {
725 let (s, p) = musig::nonce_pair(key);
726 secs.push(s);
727 pubs.push(p);
728 }
729 (secs, pubs)
730 })
731 .take(participation.outputs.len())
732 .collect::<Vec<(Vec<SecretNonce>, Vec<PublicNonce>)>>();
733
734
735 debug!("Submitting payment request with {} inputs and {} vtxo outputs",
737 participation.inputs.len(), participation.outputs.len(),
738 );
739
740 let unblinded_mailbox_id = wallet.mailbox_identifier();
742 let signed_reqs = participation.outputs.iter()
743 .zip(cosign_keys.iter())
744 .zip(cosign_nonces.iter())
745 .map(|((req, cosign_key), (_sec, pub_nonces))| {
746 SignedVtxoRequest {
747 vtxo: req.clone(),
748 cosign_pubkey: cosign_key.public_key(),
749 nonces: pub_nonces.clone(),
750 }
751 })
752 .collect::<Vec<_>>();
753
754 let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
755 for vtxo in participation.inputs.iter() {
756 let keypair = wallet.get_vtxo_key(vtxo).await
757 .map_err(HarkForfeitError::Err)?;
758 input_vtxos.push(protos::InputVtxo {
759 vtxo_id: vtxo.id().to_bytes().to_vec(),
760 attestation: {
761 let attestation = RoundAttemptAttestation::new(
762 event.challenge, vtxo.id(), &signed_reqs, &keypair,
763 );
764 attestation.serialize()
765 },
766 });
767 }
768
769 wallet.register_vtxo_transactions_with_server(&participation.inputs).await
771 .map_err(HarkForfeitError::Err)?;
772
773 let resp = srv.client.submit_payment(protos::SubmitPaymentRequest {
774 input_vtxos: input_vtxos,
775 vtxo_requests: signed_reqs.into_iter().map(Into::into).collect(),
776 #[allow(deprecated)]
777 offboard_requests: vec![],
778 unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
779 }).await.context("Ark server refused our payment submission")?;
780 let unlock_hash = UnlockHash::from_bytes(&resp.into_inner().unlock_hash)?;
781
782 if let Some(k) = cosign_keys.first() {
785 wallet.inner.round_secret_nonces.stash(
786 k.public_key(),
787 cosign_nonces.into_iter().map(|(sec, _pub)| sec).collect(),
788 );
789 }
790
791 Ok(AttemptState::AwaitingUnsignedVtxoTree { unlock_hash, cosign_keys })
792}
793
794#[derive(Debug, thiserror::Error)]
796enum HarkForfeitError {
797 #[error("error after forfeits were sent")]
799 SentForfeits(#[source] anyhow::Error),
800 #[error("error before forfeits were sent")]
802 Err(#[source] anyhow::Error),
803}
804
805async fn hark_cosign_leaf(
806 wallet: &Wallet,
807 srv: &mut ServerConnection,
808 funding_tx: &Transaction,
809 vtxo: &mut Vtxo<Full>,
810) -> anyhow::Result<()> {
811 let key = wallet.pubkey_keypair(&vtxo.user_pubkey()).await
812 .context("error fetching keypair").map_err(HarkForfeitError::Err)?
813 .with_context(|| format!(
814 "keypair {} not found for VTXO {}", vtxo.user_pubkey(), vtxo.id(),
815 ))?.1;
816 let (ctx, cosign_req) = LeafVtxoCosignContext::new(vtxo, funding_tx, &key)
817 .with_context(|| format!("can't cosign leaf of VTXO {}", vtxo.id()))?;
818 let cosign_resp = srv.client.request_leaf_vtxo_cosign(
819 protos::LeafVtxoCosignRequest::from(cosign_req),
820 ).await
821 .with_context(|| format!("error requesting leaf cosign for vtxo {}", vtxo.id()))?
822 .into_inner().try_into()
823 .context("bad leaf vtxo cosign response")?;
824 ensure!(ctx.finalize(vtxo, cosign_resp),
825 "failed to finalize VTXO leaf signature for VTXO {}", vtxo.id(),
826 );
827
828 Ok(())
829}
830
831async fn hark_vtxo_swap(
841 wallet: &Wallet,
842 participation: &RoundParticipation,
843 output_vtxos: &mut [Vtxo<Full>],
844 funding_tx: &Transaction,
845 unlock_hash: UnlockHash,
846 sent_forfeit_sigs: bool,
847) -> Result<(), HarkForfeitError> {
848 let (mut srv, _) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
849
850 wallet.register_vtxo_transactions_with_server(&participation.inputs).await
852 .context("couldn't send our input vtxo transactions to server")
853 .map_err(HarkForfeitError::Err)?;
854
855 for vtxo in output_vtxos.iter_mut() {
857 hark_cosign_leaf(wallet, &mut srv, funding_tx, vtxo).await
858 .map_err(HarkForfeitError::Err)?;
859 }
860
861 if !sent_forfeit_sigs {
864 let tip = wallet.inner.chain.tip().await
867 .context("chain source error")
868 .map_err(HarkForfeitError::Err)?;
869 if participation.inputs.iter().any(|v| v.expiry_height() > tip) {
870 let max_input_exit_delta = participation.inputs.iter().map(|v| v.exit_delta()).max()
871 .expect("minimum one input");
872 check_output_vtxos_exitable(
873 output_vtxos, tip, wallet.inner.config.vtxo_exit_margin, max_input_exit_delta,
874 )
875 .context("refusing to forfeit our input VTXOs")
876 .map_err(HarkForfeitError::Err)?;
877 }
878 }
879
880 let mut server_nonces = Vec::with_capacity(participation.inputs.len());
885 for inputs in participation.inputs.chunks(MAX_NB_FORFEIT_NONCE_IDS) {
886 let nonces = srv.client.request_forfeit_nonces(protos::ForfeitNoncesRequest {
887 unlock_hash: unlock_hash.to_byte_array().to_vec(),
888 vtxo_ids: inputs.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
889 }).await
890 .context("request forfeits nonces call failed")
891 .map_err(HarkForfeitError::Err)?
892 .into_inner().public_nonces.into_iter()
893 .map(|b| musig::PublicNonce::from_bytes(b))
894 .collect::<Result<Vec<_>, _>>()
895 .context("invalid forfeit nonces")
896 .map_err(HarkForfeitError::Err)?;
897
898 if nonces.len() != inputs.len() {
899 return Err(HarkForfeitError::Err(anyhow!(
900 "server sent {} nonce pairs, expected {}",
901 nonces.len(), inputs.len(),
902 )));
903 }
904 server_nonces.extend(nonces);
905 }
906
907 let mut forfeit_bundles = Vec::with_capacity(participation.inputs.len());
908 for (input, nonces) in participation.inputs.iter().zip(server_nonces.into_iter()) {
909 let user_key = wallet.pubkey_keypair(&input.user_pubkey()).await
910 .ok().flatten().with_context(|| format!(
911 "failed to fetch keypair for vtxo user pubkey {}", input.user_pubkey(),
912 )).map_err(HarkForfeitError::Err)?.1;
913 forfeit_bundles.push(HashLockedForfeitBundle::new(
914 input, unlock_hash, &user_key, &nonces,
915 ))
916 }
917
918 let preimage = srv.client.forfeit_vtxos(protos::ForfeitVtxosRequest {
919 forfeit_bundles: forfeit_bundles.iter().map(|b| b.serialize()).collect(),
920 }).await
921 .context("forfeit vtxos call failed")
922 .map_err(HarkForfeitError::SentForfeits)?
923 .into_inner().unlock_preimage.as_slice().try_into()
924 .context("invalid preimage length")
925 .map_err(HarkForfeitError::SentForfeits)?;
926
927 for vtxo in output_vtxos.iter_mut() {
928 if !vtxo.provide_unlock_preimage(preimage) {
929 return Err(HarkForfeitError::SentForfeits(anyhow!(
930 "invalid preimage {} for vtxo {} with supposed unlock hash {}",
931 preimage.as_hex(), vtxo.id(), unlock_hash,
932 )));
933 }
934
935 vtxo.validate(&funding_tx).with_context(|| format!(
937 "new VTXO {} does not pass validation after hArk forfeit protocol", vtxo.id(),
938 )).map_err(HarkForfeitError::SentForfeits)?;
939 }
940
941 wallet.register_vtxo_transactions_with_server(output_vtxos).await
943 .context("couldn't register output vtxo transactions with server")
944 .map_err(HarkForfeitError::SentForfeits)?;
945
946 Ok(())
947}
948
949fn check_vtxo_fails_hash_lock(funding_tx: &Transaction, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
950 vtxo.validate_unsigned(funding_tx).with_context(|| format!(
954 "new VTXO {} failed unsigned validation", vtxo.id(),
955 ))?;
956
957 match vtxo.validate(funding_tx) {
958 Err(VtxoValidationError::GenesisTransition {
962 genesis_idx, genesis_len, transition_kind, ..
963 }) if genesis_idx + 1 == genesis_len
964 && (transition_kind == TransitionKind::HashLockedCosigned.as_str()
965 || transition_kind == TransitionKind::HashLockedCosigned_v0.as_str()) => Ok(()),
966 Ok(()) => Err(anyhow!("new un-unlocked VTXO should fail validation but doesn't: {}",
967 vtxo.serialize_hex(),
968 )),
969 Err(e) => Err(anyhow!("new VTXO {} failed validation: {:#}", vtxo.id(), e)),
970 }
971}
972
973fn min_exitable_output_vtxo_expiry_height(
981 tip: BlockHeight,
982 exit_margin: BlockDelta,
983 exit_delta: BlockDelta,
984) -> BlockHeight {
985 tip.saturating_add(2 * exit_margin as BlockHeight)
986 .saturating_add(exit_delta as BlockHeight)
987}
988
989fn check_output_vtxos_exitable(
994 vtxos: &[Vtxo<Full>],
995 tip: BlockHeight,
996 exit_margin: BlockDelta,
997 max_input_exit_delta: BlockDelta,
998) -> anyhow::Result<()> {
999 let min_expiry_height = min_exitable_output_vtxo_expiry_height(tip, exit_margin, max_input_exit_delta);
1000 for vtxo in vtxos {
1001 ensure!(vtxo.expiry_height() >= min_expiry_height,
1002 "VTXO {} expires at height {}, which doesn't leave us room for a \
1003 unilateral exit (tip {}, exit margin {})",
1004 vtxo.id(), vtxo.expiry_height(), tip, exit_margin,
1005 );
1006 }
1007 Ok(())
1008}
1009
1010fn check_round_matches_participation(
1011 part: &RoundParticipation,
1012 new_vtxos: &[Vtxo<Full>],
1013 funding_tx: &Transaction,
1014 ark_info: &ArkInfo,
1015 scheduled_height: Option<BlockHeight>,
1016 tip: BlockHeight,
1017 exit_margin: BlockDelta,
1018) -> anyhow::Result<()> {
1019 ensure!(new_vtxos.len() == part.outputs.len(),
1020 "unexpected number of VTXOs: got {}, expected {}", new_vtxos.len(), part.outputs.len(),
1021 );
1022
1023 for (idx, vtxo) in new_vtxos.iter().enumerate() {
1027 ensure!(new_vtxos[idx + 1..].iter().all(|v| v.id() != vtxo.id()),
1028 "server delivered duplicate VTXO {}", vtxo.id(),
1029 );
1030 }
1031
1032 let expired_inputs = part.inputs.iter().all(|v| v.expiry_height() <= tip);
1036 let max_input_exit_delta = part.inputs.iter().map(|v| v.exit_delta()).max()
1037 .expect("min one input");
1038 let min_exitable = min_exitable_output_vtxo_expiry_height(tip, exit_margin, max_input_exit_delta);
1039 let min_scheduled = scheduled_height.map(|h| h.saturating_add(1));
1040 let min_expiry_height = match (expired_inputs, min_scheduled) {
1041 (false, Some(h)) => h.max(min_exitable),
1042 (false, None) => min_exitable,
1043 (true, Some(h)) => h,
1044 (true, None) => 0,
1045 };
1046
1047 for (vtxo, req) in new_vtxos.iter().zip(&part.outputs) {
1048 ensure!(vtxo.amount() == req.amount,
1049 "unexpected VTXO amount: got {}, expected {}", vtxo.amount(), req.amount,
1050 );
1051 ensure!(*vtxo.policy() == req.policy,
1052 "unexpected VTXO policy: got {:?}, expected {:?}", vtxo.policy(), req.policy,
1053 );
1054
1055 validate_vtxo_tree_params(
1058 vtxo.server_pubkey(), vtxo.exit_delta(), vtxo.expiry_height(),
1059 ark_info.server_pubkey, ark_info.vtxo_exit_delta, min_expiry_height,
1060 )?;
1061
1062 check_vtxo_fails_hash_lock(funding_tx, vtxo)?;
1064 }
1065
1066 Ok(())
1067}
1068
1069async fn check_funding_tx_confirmations(
1079 wallet: &Wallet,
1080 funding_txid: Txid,
1081 funding_tx: &Transaction,
1082) -> anyhow::Result<bool> {
1083 let tip = wallet.inner.chain.tip().await.context("chain source error")?;
1084 let conf_height = tip - wallet.inner.config.round_tx_required_confirmations + 1;
1085 let tx_status = wallet.inner.chain.tx_status(funding_txid).await.context("chain source error")?;
1086 trace!("Round funding tx {} confirmation status: {:?} (tip={})",
1087 funding_txid, tx_status, tip,
1088 );
1089 match tx_status {
1090 TxStatus::Confirmed(b) if b.height <= conf_height => Ok(true),
1091 TxStatus::Mempool | TxStatus::Confirmed(_) => {
1092 if wallet.inner.config.round_tx_required_confirmations == 0 {
1093 debug!("Accepting round funding tx without confirmations because of configuration");
1094 Ok(true)
1095 } else {
1096 trace!("Hark round funding tx not confirmed (deep enough) yet: {:?}", tx_status);
1097 Ok(false)
1098 }
1099 },
1100 TxStatus::NotFound => {
1101 if let Err(e) = wallet.inner.chain.broadcast_tx(&funding_tx).await {
1106 Err(anyhow!("hark funding tx {} server sent us is rejected by mempool (hex={}): {:#}",
1107 funding_txid, serialize_hex(funding_tx), e,
1108 ))
1109 } else {
1110 trace!("hark funding tx {} was not in mempool but we broadcast it", funding_txid);
1111 Ok(false)
1112 }
1113 },
1114 }
1115}
1116
1117enum HarkProgressResult {
1118 RoundPending,
1119 RoundNotFound,
1120 FundingTxUnconfirmed {
1121 funding_txid: Txid,
1122 },
1123 Ok {
1124 funding_tx: Transaction,
1125 new_vtxos: Vec<Vtxo<Full>>,
1126 },
1127}
1128
1129async fn progress_delegated(
1130 wallet: &Wallet,
1131 participation: &RoundParticipation,
1132 unlock_hash: UnlockHash,
1133 scheduled_height: Option<BlockHeight>,
1134 sent_forfeit_sigs: bool,
1135) -> Result<HarkProgressResult, HarkForfeitError> {
1136 let (mut srv, ark_info) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
1137
1138 let resp = match srv.client.round_participation_status(protos::RoundParticipationStatusRequest {
1139 unlock_hash: unlock_hash.to_byte_array().to_vec(),
1140 }).await {
1141 Ok(resp) => resp.into_inner(),
1142 Err(err) if err.code() == tonic::Code::NotFound => {
1143 return Ok(HarkProgressResult::RoundNotFound);
1144 },
1145 Err(err) => {
1146 return Err(HarkForfeitError::Err(
1147 anyhow::Error::from(err).context("error checking round participation status"),
1148 ));
1149 },
1150 };
1151 let status = protos::RoundParticipationStatus::try_from(resp.status)
1152 .context("unknown status from server")
1153 .map_err(HarkForfeitError::Err) ?;
1154
1155 if status == protos::RoundParticipationStatus::RoundPartPending {
1156 trace!("Hark round still pending");
1157 return Ok(HarkProgressResult::RoundPending);
1158 }
1159
1160 if status == protos::RoundParticipationStatus::RoundPartReleased {
1165 let preimage = resp.unlock_preimage.as_ref().map(|p| p.as_hex());
1166 warn!("Server says preimage was already released for hArk participation \
1167 with unlock hash {}. Supposed preimage: {:?}", unlock_hash, preimage,
1168 );
1169 }
1170
1171 let funding_tx_bytes = resp.round_funding_tx
1172 .context("funding txid should be provided when status is not pending")
1173 .map_err(HarkForfeitError::Err)?;
1174 let funding_tx = deserialize::<Transaction>(&funding_tx_bytes)
1175 .context("invalid funding txid")
1176 .map_err(HarkForfeitError::Err)?;
1177 let funding_txid = funding_tx.compute_txid();
1178 trace!("Funding tx for round participation with unlock hash {}: {} ({})",
1179 unlock_hash, funding_tx.compute_txid(), funding_tx_bytes.as_hex(),
1180 );
1181
1182 match check_funding_tx_confirmations(wallet, funding_txid, &funding_tx).await {
1184 Ok(true) => {},
1185 Ok(false) => return Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid }),
1186 Err(e) => return Err(HarkForfeitError::Err(e.context("checking funding tx confirmations"))),
1187 }
1188
1189 let mut new_vtxos = resp.output_vtxos.into_iter()
1190 .map(|v| <Vtxo<Full>>::deserialize(&v))
1191 .collect::<Result<Vec<_>, _>>()
1192 .context("invalid output VTXOs from server")
1193 .map_err(HarkForfeitError::Err)?;
1194
1195 let tip = wallet.inner.chain.tip().await
1198 .context("chain source error")
1199 .map_err(HarkForfeitError::Err)?;
1200 check_round_matches_participation(
1201 participation, &new_vtxos, &funding_tx, &ark_info, scheduled_height,
1202 tip, wallet.inner.config.vtxo_exit_margin,
1203 )
1204 .context("new VTXOs received from server don't match our participation")
1205 .map_err(HarkForfeitError::Err)?;
1206
1207 hark_vtxo_swap(
1211 wallet, participation, &mut new_vtxos, &funding_tx, unlock_hash, sent_forfeit_sigs,
1212 ).await.map_err(|e| match e {
1213 HarkForfeitError::Err(e) =>
1214 HarkForfeitError::Err(e.context("error forfeiting hArk VTXOs")),
1215 HarkForfeitError::SentForfeits(e) =>
1216 HarkForfeitError::SentForfeits(e.context("error forfeiting hArk VTXOs")),
1217 })?;
1218
1219 Ok(HarkProgressResult::Ok { funding_tx, new_vtxos })
1220}
1221
1222async fn progress_attempt(
1223 state: &mut AttemptState,
1224 wallet: &Wallet,
1225 part: &RoundParticipation,
1226 event: &RoundEvent,
1227) -> AttemptProgressResult {
1228 match (state, event) {
1232
1233 (
1234 AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, unlock_hash },
1235 RoundEvent::VtxoProposal(e),
1236 ) => {
1237 trace!("Received VtxoProposal: {:#?}", e);
1238
1239 let secret_nonces = if let Some(first) = cosign_keys.first() {
1242 match wallet.inner.round_secret_nonces.take(&first.public_key()) {
1243 Some(n) => n,
1244 None => return AttemptProgressResult::Failed(anyhow!(
1245 "secret cosign nonces unavailable (likely after a restart); \
1246 abandoning round attempt to avoid nonce reuse",
1247 )),
1248 }
1249 } else {
1250 vec![]
1251 };
1252
1253 match sign_vtxo_tree(
1254 wallet,
1255 part,
1256 &cosign_keys,
1257 secret_nonces,
1258 &e.unsigned_round_tx,
1259 &e.vtxos_spec,
1260 &e.cosign_agg_nonces,
1261 *unlock_hash,
1262 ).await {
1263 Ok(()) => {
1264 AttemptProgressResult::Updated {
1265 new_state: AttemptState::AwaitingFinishedRound {
1266 unsigned_round_tx: e.unsigned_round_tx.clone(),
1267 vtxos_spec: e.vtxos_spec.clone(),
1268 unlock_hash: *unlock_hash,
1269 },
1270 }
1271 },
1272 Err(e) => {
1273 trace!("Error signing VTXO tree: {:#}", e);
1274 AttemptProgressResult::Failed(e)
1275 },
1276 }
1277 },
1278
1279 (
1280 AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash },
1281 RoundEvent::Finished(RoundFinished { cosign_sigs, signed_round_tx, .. }),
1282 ) => {
1283 if unsigned_round_tx.compute_txid() != signed_round_tx.compute_txid() {
1284 return AttemptProgressResult::Failed(anyhow!(
1285 "signed funding tx ({}) doesn't match tx received before ({})",
1286 signed_round_tx.compute_txid(), unsigned_round_tx.compute_txid(),
1287 ));
1288 }
1289
1290 if let Err(e) = wallet.inner.chain.broadcast_tx(&signed_round_tx).await {
1291 warn!("Failed to broadcast signed round tx: {:#}", e);
1292 }
1293
1294 match construct_new_vtxos(
1295 part, unsigned_round_tx, vtxos_spec, cosign_sigs,
1296 ).await {
1297 Ok(v) => AttemptProgressResult::Finished {
1298 funding_tx: signed_round_tx.clone(),
1299 vtxos: v,
1300 unlock_hash: *unlock_hash,
1301 },
1302 Err(e) => AttemptProgressResult::Failed(anyhow!(
1303 "failed to construct new VTXOs for round: {:#}", e,
1304 )),
1305 }
1306 },
1307
1308 (state, RoundEvent::Finished(RoundFinished { .. })) => {
1309 AttemptProgressResult::Failed(anyhow!(
1310 "unexpectedly received a finished round while we were in state {}",
1311 state.kind(),
1312 ))
1313 },
1314
1315 (state, _) => {
1316 trace!("Ignoring round event {} in state {}", event.kind(), state.kind());
1317 AttemptProgressResult::NotUpdated
1318 },
1319 }
1320}
1321
1322async fn sign_vtxo_tree(
1323 wallet: &Wallet,
1324 participation: &RoundParticipation,
1325 cosign_keys: &[Keypair],
1326 secret_nonces: Vec<Vec<SecretNonce>>,
1327 unsigned_round_tx: &Transaction,
1328 vtxo_tree: &VtxoTreeSpec,
1329 cosign_agg_nonces: &[musig::AggregatedNonce],
1330 unlock_hash: UnlockHash,
1331) -> anyhow::Result<()> {
1332 let (mut srv, ark_info) = wallet.require_server().await.context("server not available")?;
1333
1334 let vtxos_utxo = OutPoint::new(unsigned_round_tx.compute_txid(), ROUND_TX_VTXO_TREE_VOUT);
1335
1336 let tip = wallet.inner.chain.tip().await.context("chain source error")?;
1339 let min_expiry_height = tip
1340 .saturating_add(ark_info.vtxo_lifetime as BlockHeight)
1341 .saturating_sub(VTXO_EXPIRY_HEIGHT_BUFFER);
1342 validate_vtxo_tree_params(
1343 vtxo_tree.server_pubkey, vtxo_tree.exit_delta, vtxo_tree.expiry_height,
1344 ark_info.server_pubkey, ark_info.vtxo_exit_delta, min_expiry_height,
1345 )?;
1346
1347 let mut my_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1349 for vtxo_req in vtxo_tree.iter_vtxos() {
1350 if let Some(i) = my_vtxos.iter().position(|v| {
1351 v.policy == vtxo_req.vtxo.policy && v.amount == vtxo_req.vtxo.amount
1352 }) {
1353 my_vtxos.swap_remove(i);
1354 }
1355 }
1356 if !my_vtxos.is_empty() {
1357 bail!("server didn't include all of our vtxos, missing: {:?}", my_vtxos);
1358 }
1359
1360 let unsigned_vtxos = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1361 trace!("Sending vtxo signatures to server...");
1362 let leaf_idxs = unsigned_vtxos.spec.leaf_idxs_for_participation(
1363 unlock_hash, participation.outputs.iter().map(|o| o),
1364 ).context("our outputs not part of tree")?;
1365 for ((leaf_idx, key), sec) in leaf_idxs.into_iter().zip(cosign_keys).zip(secret_nonces) {
1370 let part_sigs = unsigned_vtxos.cosign_branch(
1371 &cosign_agg_nonces, leaf_idx, key, sec,
1372 ).context("failed to cosign branch: our request not part of tree")?;
1373
1374 info!("Sending {} partial vtxo cosign signatures for pk {}",
1375 part_sigs.len(), key.public_key(),
1376 );
1377
1378 srv.client.provide_vtxo_signatures(protos::VtxoSignaturesRequest {
1379 pubkey: key.public_key().serialize().to_vec(),
1380 signatures: part_sigs.iter().map(|s| s.serialize().to_vec()).collect(),
1381 }).await.context("error sending vtxo signatures")?;
1382 }
1383 trace!("Done sending vtxo signatures to server");
1384
1385 Ok(())
1386}
1387
1388async fn construct_new_vtxos(
1389 participation: &RoundParticipation,
1390 unsigned_round_tx: &Transaction,
1391 vtxo_tree: &VtxoTreeSpec,
1392 vtxo_cosign_sigs: &[schnorr::Signature],
1393) -> anyhow::Result<Vec<Vtxo<Full>>> {
1394 let round_txid = unsigned_round_tx.compute_txid();
1395 let vtxos_utxo = OutPoint::new(round_txid, ROUND_TX_VTXO_TREE_VOUT);
1396 let vtxo_tree = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1397
1398 if vtxo_tree.verify_cosign_sigs(&vtxo_cosign_sigs).is_err() {
1400 bail!("Received incorrect vtxo cosign signatures from server");
1402 }
1403
1404 let signed_vtxos = vtxo_tree
1405 .into_signed_tree(vtxo_cosign_sigs.to_vec())
1406 .into_cached_tree();
1407
1408 let mut expected_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1409 let total_nb_expected_vtxos = expected_vtxos.len();
1410
1411 let mut new_vtxos = vec![];
1412 for (idx, req) in signed_vtxos.spec.spec.vtxos.iter().enumerate() {
1413 if let Some(expected_idx) = expected_vtxos.iter().position(|r| **r == req.vtxo) {
1414 let vtxo = signed_vtxos.build_vtxo(idx);
1415
1416 check_vtxo_fails_hash_lock(unsigned_round_tx, &vtxo)
1419 .context("constructed invalid vtxo from tree")?;
1420
1421 info!("New VTXO from round: {} ({}, {})",
1422 vtxo.id(), vtxo.amount(), vtxo.policy_type(),
1423 );
1424
1425 new_vtxos.push(vtxo);
1426 expected_vtxos.swap_remove(expected_idx);
1427 }
1428 }
1429
1430 if !expected_vtxos.is_empty() {
1431 if expected_vtxos.len() == total_nb_expected_vtxos {
1432 bail!("None of our VTXOs were present in round!");
1434 } else {
1435 bail!("Server included some of our VTXOs but not all: {} missing: {:?}",
1436 expected_vtxos.len(), expected_vtxos,
1437 );
1438 }
1439 }
1440 Ok(new_vtxos)
1441}
1442
1443async fn persist_round_success(
1445 wallet: &Wallet,
1446 participation: &RoundParticipation,
1447 movement_id: Option<MovementId>,
1448 new_vtxos: &[Vtxo<Full>],
1449 funding_tx: &Transaction,
1450) -> anyhow::Result<()> {
1451 debug!("Persisting newly finished round. {} new vtxos, movement ID {:?}",
1452 new_vtxos.len(), movement_id,
1453 );
1454
1455 let store_result = wallet.store_spendable_vtxos(new_vtxos).await
1459 .context("failed to store new VTXOs");
1460 let spent_result = wallet.mark_vtxos_as_spent(&participation.inputs).await
1461 .context("failed to mark input VTXOs as spent");
1462 let update_result = if let Some(mid) = movement_id {
1463 wallet.inner.movements.finish_movement_with_update(
1464 mid,
1465 MovementStatus::Successful,
1466 MovementUpdate::new()
1467 .produced_vtxos(new_vtxos)
1468 .metadata([("funding_txid".into(), serde_json::to_value(funding_tx.compute_txid())?)]),
1469 ).await.context("failed to mark movement as finished")
1470 } else {
1471 Ok(())
1472 };
1473
1474 store_result?;
1475 spent_result?;
1476 update_result?;
1477
1478 Ok(())
1479}
1480
1481async fn persist_round_failure(
1482 wallet: &Wallet,
1483 participation: &RoundParticipation,
1484 movement_id: Option<MovementId>,
1485) -> anyhow::Result<()> {
1486 debug!("Attempting to persist the failure of a round with the movement ID {:?}", movement_id);
1487 let unlock_result = wallet.unlock_vtxos(
1488 &participation.inputs, movement_id.map(|m| m.into()),
1489 ).await;
1490 let finish_result = if let Some(movement_id) = movement_id {
1491 wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
1492 } else {
1493 Ok(())
1494 };
1495 if let Err(e) = &finish_result {
1496 error!("Failed to mark movement as failed: {:#}", e);
1497 }
1498 match (unlock_result, finish_result) {
1499 (Ok(()), Ok(())) => Ok(()),
1500 (Err(e), _) => Err(e),
1501 (_, Err(e)) => Err(anyhow!("Failed to mark movement as failed: {:#}", e)),
1502 }
1503}
1504
1505async fn update_funding_txid(
1506 wallet: &Wallet,
1507 movement_id: MovementId,
1508 funding_txid: Txid,
1509) -> anyhow::Result<()> {
1510 wallet.inner.movements.update_movement(
1511 movement_id,
1512 MovementUpdate::new()
1513 .metadata([("funding_txid".into(), serde_json::to_value(&funding_txid)?)])
1514 ).await.context("Unable to update funding txid of round")
1515}
1516
1517#[derive(Default)]
1526pub struct RoundSecretNonces {
1527 inner: parking_lot::Mutex<HashMap<bitcoin::secp256k1::PublicKey, Vec<Vec<SecretNonce>>>>,
1528}
1529
1530impl RoundSecretNonces {
1531 pub fn new() -> Self {
1532 Self { inner: parking_lot::Mutex::new(HashMap::new()) }
1533 }
1534
1535 pub fn stash(
1537 &self,
1538 first_cosign_pubkey: bitcoin::secp256k1::PublicKey,
1539 nonces: Vec<Vec<SecretNonce>>,
1540 ) {
1541 self.inner.lock().insert(first_cosign_pubkey, nonces);
1542 }
1543
1544 pub fn take(
1547 &self,
1548 first_cosign_pubkey: &bitcoin::secp256k1::PublicKey,
1549 ) -> Option<Vec<Vec<SecretNonce>>> {
1550 self.inner.lock().remove(first_cosign_pubkey)
1551 }
1552
1553 pub fn forget(&self, first_cosign_pubkey: &bitcoin::secp256k1::PublicKey) {
1557 self.inner.lock().remove(first_cosign_pubkey);
1558 }
1559}
1560
1561impl Wallet {
1562 pub async fn lock_wait_round_state(&self, id: RoundStateId) -> anyhow::Result<Option<StoredRoundState>> {
1567 let guard = self.inner.lock_manager.lock(
1568 &format!("{}.round.{}", self.fingerprint(), id),
1569 ROUND_LOCK_TIMEOUT,
1570 ).await.with_context(|| format!(
1571 "timed out waiting for lock on round state {} (wallet {})",
1572 id, self.fingerprint(),
1573 ))?;
1574
1575 if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1576 return Ok(Some(state.lock(guard)));
1577 }
1578
1579 Ok(None)
1580 }
1581
1582 pub async fn next_round_start_time(&self) -> anyhow::Result<SystemTime> {
1584 let (mut srv, _) = self.require_server().await?;
1585 let ts = srv.client.next_round_time(protos::Empty {}).await?.into_inner().timestamp;
1586 Ok(UNIX_EPOCH.checked_add(Duration::from_secs(ts)).context("invalid timestamp")?)
1587 }
1588
1589 pub async fn join_next_round(
1598 &self,
1599 participation: RoundParticipation,
1600 movement_kind: Option<RoundMovement>,
1601 ) -> anyhow::Result<StoredRoundState> {
1602 let movement = if let Some(kind) = movement_kind {
1603 Some(self.inner.movements.new_guarded_movement_with_update(
1604 Subsystem::ROUND,
1605 kind.to_string(),
1606 OnDropStatus::Failed,
1607 participation.to_movement_update()?
1608 ).await?)
1609 } else {
1610 None
1611 };
1612 let movement_id = movement.as_ref().map(|m| m.id());
1613 let input_vtxos = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1614 let state = RoundState::new_interactive(participation, movement_id);
1615
1616 self.lock_vtxos(&input_vtxos, movement_id.map(|m| m.into())).await
1617 .context("failed to lock input VTXOs")?;
1618
1619 match (async || {
1620 let id = self.inner.db.store_round_state(&state).await?;
1621 Ok(self.lock_wait_round_state(id).await?
1622 .context("failed to lock fresh round state")?)
1623 })().await {
1624 Ok(state) => {
1625 if let Some(mut m) = movement {
1626 m.stop();
1627 }
1628 Ok(state)
1629 },
1630 Err(e) => {
1631 self.unlock_vtxos(&input_vtxos, movement_id.map(|m| m.into())).await
1632 .context("failed to unlock input VTXOs")?;
1633 if let Some(mut m) = movement {
1634 m.fail().await.context("failed to mark movement as failed")?;
1635 }
1636 Err(e)
1637 },
1638 }
1639 }
1640
1641 pub async fn join_delegated_round(
1647 &self,
1648 participation: RoundParticipation,
1649 movement_kind: Option<RoundMovement>,
1650 scheduled_height: Option<BlockHeight>,
1651 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1652 let movement = if let Some(kind) = movement_kind {
1653 Some(self.inner.movements.new_guarded_movement_with_update(
1654 Subsystem::ROUND,
1655 kind.to_string(),
1656 OnDropStatus::Failed,
1657 participation.to_movement_update()?,
1658 ).await?)
1659 } else {
1660 None
1661 };
1662 let movement_id = movement.as_ref().map(|m| m.id());
1663
1664 match self.join_delegated_round_inner(participation, movement_id, scheduled_height).await {
1665 Ok(state) => {
1666 if let Some(mut m) = movement {
1667 m.stop();
1668 }
1669 Ok(state)
1670 },
1671 Err(e) => {
1672 if let Some(mut m) = movement {
1673 m.fail().await.context("error marking movement as failed")?;
1674 }
1675 Err(e)
1676 },
1677 }
1678 }
1679
1680 pub async fn join_next_round_delegated(
1683 &self,
1684 participation: RoundParticipation,
1685 movement_kind: Option<RoundMovement>,
1686 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1687 self.join_delegated_round(participation, movement_kind, None).await
1688 }
1689
1690 async fn join_delegated_round_inner(
1695 &self,
1696 participation: RoundParticipation,
1697 movement_id: Option<MovementId>,
1698 scheduled_height: Option<BlockHeight>,
1699 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1700 let (mut srv, _) = self.require_server().await?;
1701
1702 let unblinded_mailbox_id = self.mailbox_identifier();
1704
1705 self.register_vtxo_transactions_with_server(&participation.inputs).await
1707 .context("failed to register input vtxo transactions with server")?;
1708
1709 let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
1711 for vtxo in participation.inputs.iter() {
1712 let keypair = self.get_vtxo_key(vtxo).await
1713 .context("failed to get vtxo keypair")?;
1714 input_vtxos.push(protos::InputVtxo {
1715 vtxo_id: vtxo.id().to_bytes().to_vec(),
1716 attestation: {
1717 let attestation = DelegatedRoundParticipationAttestation::new(
1718 vtxo.id(), &participation.outputs, &keypair,
1719 );
1720 attestation.serialize()
1721 },
1722 });
1723 }
1724
1725 let vtxo_requests = participation.outputs.iter()
1727 .map(|req|
1728 protos::VtxoRequest {
1729 policy: req.policy.serialize(),
1730 amount: req.amount.to_sat(),
1731 })
1732 .collect::<Vec<_>>();
1733
1734 let resp = srv.client.submit_round_participation(protos::RoundParticipationRequest {
1736 input_vtxos,
1737 vtxo_requests,
1738 unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
1739 scheduled_height,
1740 }).await.context("error submitting round participation to server")?.into_inner();
1741
1742 let unlock_hash = UnlockHash::from_bytes(resp.unlock_hash)
1743 .context("invalid unlock hash from server")?;
1744
1745 let state = RoundState::new_delegated(
1746 participation, unlock_hash, scheduled_height, movement_id,
1747 );
1748
1749 info!("Delegated round participation submitted, it will automatically execute \
1750 when you next sync your wallet after the round happened \
1751 (and has sufficient confirmations).",
1752 );
1753
1754 let id = self.inner.db.store_round_state(&state).await?;
1755 Ok(StoredRoundState::new(id, state))
1756 }
1757
1758 pub(crate) async fn join_attempt_interactive(
1766 &self,
1767 participation: RoundParticipation,
1768 attempt: &RoundAttempt,
1769 movement_kind: Option<RoundMovement>,
1770 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1771 let movement = if let Some(kind) = movement_kind {
1772 Some(self.inner.movements.new_guarded_movement_with_update(
1773 Subsystem::ROUND,
1774 kind.to_string(),
1775 OnDropStatus::Failed,
1776 participation.to_movement_update()?,
1777 ).await?)
1778 } else {
1779 None
1780 };
1781 let movement_id = movement.as_ref().map(|m| m.id());
1782
1783 let input_ids = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1784 self.lock_vtxos(&input_ids, movement_id.map(|m| m.into())).await
1785 .context("error locking input VTXOs")?;
1786
1787 match self.join_attempt_interactive_inner(participation, attempt, movement_id).await {
1788 Ok(state) => {
1789 if let Some(mut m) = movement {
1790 m.stop();
1791 }
1792 Ok(state)
1793 },
1794 Err(e) => {
1795 self.unlock_vtxos(&input_ids, movement_id.map(|m| m.into())).await
1796 .context("error unlocking input VTXOs")?;
1797 if let Some(mut m) = movement {
1798 m.fail().await.context("error marking movement as failed")?;
1799 }
1800 Err(e)
1801 },
1802 }
1803 }
1804
1805 async fn join_attempt_interactive_inner(
1806 &self,
1807 participation: RoundParticipation,
1808 attempt: &RoundAttempt,
1809 movement_id: Option<MovementId>,
1810 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1811 let attempt_state = start_attempt(self, &participation, attempt).await?;
1815
1816 let mut state = RoundState::new_interactive(participation, movement_id);
1817 state.flow = RoundFlowState::InteractiveOngoing {
1818 round_seq: attempt.round_seq,
1819 attempt_seq: attempt.attempt_seq,
1820 state: attempt_state,
1821 };
1822
1823 let id = self.inner.db.store_round_state(&state).await?;
1824 Ok(StoredRoundState::new(id, state))
1825 }
1826
1827 pub async fn pending_round_state_ids(&self) -> anyhow::Result<Vec<RoundStateId>> {
1829 self.inner.db.get_pending_round_state_ids().await
1830 }
1831
1832 pub async fn pending_round_states(&self) -> anyhow::Result<Vec<StoredRoundState<Unlocked>>> {
1834 let ids = self.inner.db.get_pending_round_state_ids().await?;
1835 let mut states = Vec::with_capacity(ids.len());
1836 for id in ids {
1837 if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1838 states.push(state);
1839 }
1840 }
1841 Ok(states)
1842 }
1843
1844 pub async fn pending_round_balance(&self) -> anyhow::Result<Amount> {
1846 let mut ret = Amount::ZERO;
1847 for round in self.pending_round_states().await? {
1848 ret += round.state().pending_balance();
1849 }
1850 Ok(ret)
1851 }
1852
1853 pub async fn pending_round_input_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1858 let mut ret = Vec::new();
1859 for round in self.pending_round_states().await? {
1860 let inputs = round.state().locked_pending_inputs();
1861 ret.reserve(inputs.len());
1862 for input in inputs {
1863 let v = self.get_vtxo_by_id(input.id()).await
1864 .context("unknown round input VTXO")?;
1865 ret.push(v);
1866 }
1867 }
1868 Ok(ret)
1869 }
1870
1871 pub async fn sync_pending_rounds(&self) -> anyhow::Result<HashMap<RoundStateId, RoundStatus>> {
1873 let states = self.pending_round_states().await?;
1874 if states.is_empty() {
1875 return Ok(HashMap::new());
1876 }
1877
1878 debug!("Syncing {} pending round states...", states.len());
1879
1880 let ret = Arc::new(parking_lot::Mutex::new(HashMap::with_capacity(states.len())));
1881 tokio_stream::iter(states).for_each_concurrent(10, |state| {
1882 let ret = ret.clone();
1883 async move {
1884 if state.state().ongoing_participation() {
1886 return;
1887 }
1888
1889 let mut state = match self.lock_wait_round_state(state.id()).await {
1890 Ok(Some(state)) => state,
1891 Ok(None) => return,
1892 Err(e) => {
1893 warn!("Error locking round state: {:#}", e);
1894 return;
1895 },
1896 };
1897
1898 let status = match state.state_mut().sync(self).await {
1899 Ok(s) => s,
1900 Err(e) => {
1901 warn!("Error syncing round: {:#}", e);
1902 return;
1903 },
1904 };
1905 trace!("Synced round #{}, status: {:?}", state.id(), status);
1906 match status {
1907 RoundStatus::Confirmed { funding_txid } => {
1908 info!("Round confirmed. Funding tx {}", funding_txid);
1909 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1910 warn!("Error removing confirmed round state from db: {:#}", e);
1911 }
1912 },
1913 RoundStatus::Unconfirmed { funding_txid } => {
1914 info!("Waiting for confirmations for round funding tx {}", funding_txid);
1915 if let Err(e) = self.inner.db.update_round_state(&state).await {
1916 warn!("Error updating pending round state in db: {:#}", e);
1917 }
1918 },
1919 RoundStatus::Pending => {
1920 if let Err(e) = self.inner.db.update_round_state(&state).await {
1921 warn!("Error updating pending round state in db: {:#}", e);
1922 }
1923 },
1924 RoundStatus::Failed { ref error } => {
1925 error!("Round failed: {}", error);
1926 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1927 warn!("Error removing failed round state from db: {:#}", e);
1928 }
1929 },
1930 RoundStatus::Canceled => {
1931 error!("Round canceled");
1932 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1933 warn!("Error removing canceled round state from db: {:#}", e);
1934 }
1935 },
1936 }
1937 ret.lock().insert(state.id(), status);
1938 }
1939 }).await;
1940
1941 Ok(Arc::into_inner(ret).expect("only ref left").into_inner())
1942 }
1943
1944 async fn get_last_round_event(&self) -> anyhow::Result<RoundEvent> {
1946 let (mut srv, _) = self.require_server().await?;
1947 let e = srv.client.last_round_event(protos::Empty {}).await?.into_inner();
1948 Ok(RoundEvent::try_from(e).context("invalid event format from server")?)
1949 }
1950
1951 async fn inner_process_event(
1952 &self,
1953 state: &mut StoredRoundState,
1954 event: Option<&RoundEvent>,
1955 ) {
1956 if let Some(event) = event && state.state().ongoing_participation() {
1957 let updated = state.state_mut().process_event(self, &event).await;
1958 if updated {
1959 if let Err(e) = self.inner.db.update_round_state(&state).await {
1960 error!("Error storing round state #{} after progress: {:#}", state.id(), e);
1961 }
1962 }
1963 }
1964
1965 match state.state_mut().sync(self).await {
1966 Err(e) => warn!("Error syncing round #{}: {:#}", state.id(), e),
1967 Ok(s) if s.is_final() => {
1968 info!("Round #{} finished with result: {:?}", state.id(), s);
1969 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1970 warn!("Failed to remove finished round #{} from db: {:#}", state.id(), e);
1971 }
1972 },
1973 Ok(s) => {
1974 trace!("Round state #{} is now in state {:?}", state.id(), s);
1975 if let Err(e) = self.inner.db.update_round_state(&state).await {
1976 warn!("Error storing round state #{}: {:#}", state.id(), e);
1977 }
1978 },
1979 }
1980 }
1981
1982 pub async fn progress_pending_rounds(
1987 &self,
1988 last_round_event: Option<&RoundEvent>,
1989 ) -> anyhow::Result<()> {
1990 let states = self.pending_round_states().await?;
1991 if states.is_empty() {
1992 return Ok(());
1993 }
1994
1995 info!("Processing {} rounds...", states.len());
1996
1997 let mut last_round_event = last_round_event.map(|e| Cow::Borrowed(e));
1998
1999 let has_ongoing_participation = states.iter()
2000 .any(|s| s.state().ongoing_participation());
2001 if has_ongoing_participation && last_round_event.is_none() {
2002 match self.get_last_round_event().await {
2003 Ok(e) => last_round_event = Some(Cow::Owned(e)),
2004 Err(e) => {
2005 warn!("Error fetching round event, \
2006 failed to progress ongoing rounds: {:#}", e);
2007 },
2008 }
2009 }
2010
2011 let event = last_round_event.as_ref().map(|c| c.as_ref());
2012
2013 let futs = states.into_iter().map(async |state| {
2014 let locked = self.lock_wait_round_state(state.id()).await?;
2015 if let Some(mut locked) = locked {
2016 self.inner_process_event(&mut locked, event).await;
2017 }
2018 Ok::<_, anyhow::Error>(())
2019 });
2020
2021 futures::future::join_all(futs).await;
2022
2023 Ok(())
2024 }
2025
2026 pub async fn subscribe_round_events(&self)
2027 -> anyhow::Result<impl Stream<Item = anyhow::Result<RoundEvent>> + Unpin + use<>>
2028 {
2029 let (mut srv, _) = self.require_server().await?;
2030 let mut req = tonic::IntoRequest::into_request(protos::Empty {});
2031 req.set_timeout(SUBSCRIBE_REQUEST_TIMEOUT);
2032 let events = srv.client.subscribe_rounds(req).await?
2033 .into_inner().map(|m| {
2034 let m = m.context("received error on event stream")?;
2035 let e = RoundEvent::try_from(m.clone())
2036 .with_context(|| format!("error converting rpc round event: {:?}", m))?;
2037 trace!("Received round event: {}", e);
2038 Ok::<_, anyhow::Error>(e)
2039 });
2040 Ok(events)
2041 }
2042
2043 pub async fn participate_ongoing_rounds(&self) -> anyhow::Result<()> {
2048 let mut events = self.subscribe_round_events().await?;
2049
2050 loop {
2051 let state_ids = self.pending_round_states().await?.iter()
2054 .filter(|s| s.state().ongoing_participation())
2055 .map(|s| s.id())
2056 .collect::<Vec<_>>();
2057
2058 if state_ids.is_empty() {
2059 info!("All rounds handled");
2060 return Ok(());
2061 }
2062
2063 let event = events.next().await
2064 .context("events stream broke")?
2065 .context("error on event stream")?;
2066
2067 let futs = state_ids.into_iter().map(async |state| {
2068 let locked = self.lock_wait_round_state(state).await?;
2069 if let Some(mut locked) = locked {
2070 self.inner_process_event(&mut locked, Some(&event)).await;
2071 }
2072 Ok::<_, anyhow::Error>(())
2073 });
2074
2075 futures::future::join_all(futs).await;
2076 }
2077 }
2078
2079 pub async fn cancel_all_pending_rounds(&self) -> anyhow::Result<()> {
2084 let state_ids = self.inner.db.get_pending_round_state_ids().await?;
2086
2087 let futures = state_ids.into_iter().map(|state_id| {
2088 async move {
2089 let mut state = match self.lock_wait_round_state(state_id).await {
2091 Ok(Some(s)) => s,
2092 Ok(None) => return,
2093 Err(e) => return warn!("Error loading round state #{}: {:#}", state_id, e),
2094 };
2095
2096 match state.state_mut().try_cancel(self).await {
2097 Ok(true) => {
2098 if let Err(e) = self.inner.db.remove_round_state(&state).await {
2099 warn!("Error removing canceled round state from db: {:#}", e);
2100 }
2101 },
2102 Ok(false) => {},
2103 Err(e) => warn!("Error trying to cancel round #{}: {:#}", state_id, e),
2104 }
2105 }
2106 });
2107
2108 join_all(futures).await;
2109
2110 Ok(())
2111 }
2112
2113 pub async fn cancel_pending_round(&self, id: RoundStateId) -> anyhow::Result<()> {
2115 let mut state = self.lock_wait_round_state(id).await?
2116 .context("round state not found")?;
2117
2118 if state.state_mut().try_cancel(self).await.context("failed to cancel round")? {
2119 self.inner.db.remove_round_state(&state).await
2120 .context("error removing canceled round state from db")?;
2121 } else {
2122 bail!("failed to cancel round");
2123 }
2124
2125 Ok(())
2126 }
2127
2128 pub(crate) async fn participate_round(
2135 &self,
2136 participation: RoundParticipation,
2137 movement_kind: Option<RoundMovement>,
2138 ) -> anyhow::Result<RoundStatus> {
2139 let state = self.join_next_round(participation, movement_kind).await?;
2140
2141 info!("Waiting for a round start...");
2142 let mut events = self.subscribe_round_events().await?;
2143
2144 self.drive_round_state(state, &mut events).await
2145 }
2146
2147 pub(crate) async fn drive_round_state<S>(
2155 &self,
2156 mut state: StoredRoundState,
2157 events: &mut S,
2158 ) -> anyhow::Result<RoundStatus>
2159 where
2160 S: Stream<Item = anyhow::Result<RoundEvent>> + Unpin,
2161 {
2162 loop {
2163 if !state.state().ongoing_participation() {
2164 let status = state.state_mut().sync(self).await?;
2165 match status {
2166 RoundStatus::Failed { error } => bail!("round failed: {}", error),
2167 RoundStatus::Canceled => bail!("round canceled"),
2168 status => return Ok(status),
2169 }
2170 }
2171
2172 let event = events.next().await
2173 .context("events stream broke")?
2174 .context("error on event stream")?;
2175 if state.state_mut().process_event(self, &event).await {
2176 self.inner.db.update_round_state(&state).await?;
2177 }
2178 }
2179 }
2180}
2181
2182#[cfg(test)]
2183mod test {
2184 use super::*;
2185
2186 use bitcoin::secp256k1::Secp256k1;
2187
2188 use ark::VtxoPolicy;
2189 use ark::tree::signed::{HashlockVersion, UnlockPreimage};
2190
2191 fn pubkey() -> bitcoin::secp256k1::PublicKey {
2192 let secp = Secp256k1::new();
2193 Keypair::new(&secp, &mut rand::thread_rng()).public_key()
2194 }
2195
2196 fn nonces() -> Vec<Vec<SecretNonce>> {
2197 let secp = Secp256k1::new();
2198 let key = Keypair::new(&secp, &mut rand::thread_rng());
2199 vec![vec![musig::nonce_pair(&key).0, musig::nonce_pair(&key).0]]
2203 }
2204
2205 #[test]
2206 fn accepts_hash_locked_leaves_of_both_versions() {
2207 let secp = Secp256k1::new();
2208 let mut rng = rand::thread_rng();
2209 let user_key = Keypair::new(&secp, &mut rng);
2210 let user_cosign_key = Keypair::new(&secp, &mut rng);
2211 let server_key = Keypair::new(&secp, &mut rng);
2212 let server_cosign_key = Keypair::new(&secp, &mut rng);
2213
2214 let preimage: UnlockPreimage = rand::random();
2215 let unlock_hash = UnlockHash::hash(&preimage);
2216
2217 let outputs = (0..2u64).map(|i| VtxoRequest {
2218 amount: Amount::from_sat(10_000 + i),
2219 policy: VtxoPolicy::new_pubkey(user_key.public_key()),
2220 }).collect::<Vec<_>>();
2221
2222 for version in [HashlockVersion::V0, HashlockVersion::V1] {
2226 let (tree, funding_tx) = ark::test_util::build_signed_tree(
2227 version, outputs.iter().cloned(),
2228 &user_cosign_key, &server_key, &server_cosign_key, unlock_hash,
2229 );
2230 for vtxo in tree.into_cached_tree().output_vtxos() {
2231 check_vtxo_fails_hash_lock(&funding_tx, &vtxo).unwrap_or_else(|e| panic!(
2232 "locked {:?} leaf vtxo should be accepted: {:#}", version, e,
2233 ));
2234 }
2235 }
2236 }
2237
2238 #[test]
2239 fn rejects_locked_round_vtxo_with_tampered_point() {
2240 let secp = Secp256k1::new();
2241 let mut rng = rand::thread_rng();
2242 let user_key = Keypair::new(&secp, &mut rng);
2243 let user_cosign_key = Keypair::new(&secp, &mut rng);
2244 let server_key = Keypair::new(&secp, &mut rng);
2245 let server_cosign_key = Keypair::new(&secp, &mut rng);
2246 let preimage: UnlockPreimage = rand::random();
2247 let unlock_hash = UnlockHash::hash(&preimage);
2248 let outputs = (0..2u64).map(|i| VtxoRequest {
2249 amount: Amount::from_sat(10_000 + i),
2250 policy: VtxoPolicy::new_pubkey(user_key.public_key()),
2251 }).collect::<Vec<_>>();
2252
2253 let (tree, funding_tx) = ark::test_util::build_signed_tree(
2254 HashlockVersion::V1, outputs,
2255 &user_cosign_key, &server_key, &server_cosign_key, unlock_hash,
2256 );
2257 let vtxo = tree.into_cached_tree().output_vtxos().next().unwrap();
2258
2259 let mut encoded = vtxo.serialize();
2262 let vout_offset = encoded.len() - 4;
2263 encoded[vout_offset] = 1;
2264 let tampered = Vtxo::<Full>::deserialize(&encoded).unwrap();
2265
2266 assert!(tampered.validate_unsigned(&funding_tx).is_err());
2267 assert!(check_vtxo_fails_hash_lock(&funding_tx, &tampered).is_err(),
2268 "tampered point must be rejected before forfeits are sent");
2269 }
2270
2271 #[test]
2272 fn refuses_vtxos_without_room_for_unilateral_exit() {
2273 let secp = Secp256k1::new();
2274 let mut rng = rand::thread_rng();
2275 let user_key = Keypair::new(&secp, &mut rng);
2276 let user_cosign_key = Keypair::new(&secp, &mut rng);
2277 let server_key = Keypair::new(&secp, &mut rng);
2278 let server_cosign_key = Keypair::new(&secp, &mut rng);
2279
2280 let preimage: UnlockPreimage = rand::random();
2281 let unlock_hash = UnlockHash::hash(&preimage);
2282
2283 let outputs = (0..2u64).map(|i| VtxoRequest {
2284 amount: Amount::from_sat(10_000 + i),
2285 policy: VtxoPolicy::new_pubkey(user_key.public_key()),
2286 }).collect::<Vec<_>>();
2287
2288 let (tree, _funding_tx) = ark::test_util::build_signed_tree(
2289 HashlockVersion::V1, outputs,
2290 &user_cosign_key, &server_key, &server_cosign_key, unlock_hash,
2291 );
2292 let vtxos = tree.into_cached_tree().output_vtxos().collect::<Vec<_>>();
2293
2294 check_output_vtxos_exitable(&vtxos, 100_970, 12, 6)
2298 .expect("vtxos with room for a unilateral exit should be accepted");
2299 assert!(check_output_vtxos_exitable(&vtxos, 100_971, 12, 6).is_err(),
2300 "vtxos without room for a unilateral exit must be rejected");
2301 }
2302
2303 #[test]
2304 fn stash_and_take() {
2305 let store = RoundSecretNonces::new();
2306 let k = pubkey();
2307 store.stash(k, nonces());
2308
2309 assert!(store.take(&k).is_some());
2310 }
2311
2312 #[test]
2313 fn cannot_take_twice() {
2314 let store = RoundSecretNonces::new();
2315 let k = pubkey();
2316 store.stash(k, nonces());
2317
2318 assert!(store.take(&k).is_some());
2319 assert!(store.take(&k).is_none());
2320 }
2321
2322 #[test]
2323 fn cannot_take_after_forget() {
2324 let store = RoundSecretNonces::new();
2325 let k = pubkey();
2326 store.stash(k, nonces());
2327 store.forget(&k);
2328
2329 assert!(store.take(&k).is_none());
2330 }
2331
2332 #[test]
2333 fn stash_overrides_stash() {
2334 let secp = Secp256k1::new();
2335 let key = Keypair::new(&secp, &mut rand::thread_rng());
2336 let nonces_1 = vec![vec![musig::nonce_pair(&key).0]];
2337 let nonces_2 = vec![];
2338
2339 let store = RoundSecretNonces::new();
2340 store.stash(key.public_key(), nonces_1);
2341 store.stash(key.public_key(), nonces_2);
2342
2343 let taken = store.take(&key.public_key()).expect("nonces present");
2344 assert_eq!(taken.len(), 0);
2345 }
2346}