1use std::collections::HashMap;
6use std::iter;
7use std::borrow::Cow;
8use std::convert::Infallible;
9use std::sync::Arc;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12use anyhow::Context;
13use ark::vtxo::{TransitionKind, VtxoValidationError};
14use bdk_esplora::esplora_client::Amount;
15use bip39::rand;
16use bitcoin::{OutPoint, SignedAmount, Transaction, Txid};
17use bitcoin::consensus::encode::{deserialize, serialize_hex};
18use bitcoin::hashes::Hash;
19use bitcoin::hex::DisplayHex;
20use bitcoin::key::Keypair;
21use bitcoin::secp256k1::{schnorr, PublicKey};
22use futures::future::join_all;
23use futures::{Stream, StreamExt};
24use log::{debug, error, info, trace, warn};
25
26use ark::{ArkInfo, ProtocolEncoding, SignedVtxoRequest, Vtxo, VtxoRequest};
27use ark::vtxo::Full;
28use ark::attestations::{DelegatedRoundParticipationAttestation, RoundAttemptAttestation};
29use ark::forfeit::HashLockedForfeitBundle;
30use ark::musig::{self, PublicNonce, SecretNonce};
31use ark::rounds::{RoundAttempt, RoundEvent, RoundFinished, RoundSeq, ROUND_TX_VTXO_TREE_VOUT};
32use ark::tree::signed::{LeafVtxoCosignContext, UnlockHash, VtxoTreeSpec};
33use bitcoin_ext::{BlockDelta, BlockHeight, TxStatus};
34use server_rpc::{protos, ServerConnection, TryFromBytes, MAX_NB_FORFEIT_NONCE_IDS};
35
36use crate::movement::manager::OnDropStatus;
37use crate::{Wallet, WalletVtxo, SECP, SUBSCRIBE_REQUEST_TIMEOUT};
38use crate::movement::{MovementId, MovementStatus};
39use crate::movement::update::MovementUpdate;
40use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
41use crate::subsystem::{RoundMovement, Subsystem};
42
43const ROUND_LOCK_TIMEOUT: Duration = Duration::from_secs(10);
46
47const VTXO_EXPIRY_HEIGHT_BUFFER: BlockHeight = 6;
50
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct RoundParticipation {
55 #[serde(with = "ark::encode::serde::vec")]
56 pub inputs: Vec<Vtxo<Full>>,
57 pub outputs: Vec<VtxoRequest>,
60 #[serde(default, skip_serializing_if = "Option::is_none", with = "ark::encode::serde::opt")]
62 pub unblinded_mailbox_id: Option<ark::mailbox::MailboxIdentifier>,
63}
64
65impl RoundParticipation {
66 pub fn to_movement_update(&self) -> anyhow::Result<MovementUpdate> {
67 let input_amount = self.inputs.iter().map(|i| i.amount()).sum::<Amount>();
68 let output_amount = self.outputs.iter().map(|r| r.amount).sum::<Amount>();
69 let fee = input_amount - output_amount;
70 Ok(MovementUpdate::new()
71 .consumed_vtxos(&self.inputs)
72 .intended_balance(SignedAmount::ZERO)
73 .effective_balance( - fee.to_signed()?)
74 .fee(fee)
75 )
76 }
77}
78
79#[derive(Debug, Clone)]
80pub enum RoundStatus {
81 Confirmed {
83 funding_txid: Txid,
84 },
85 Unconfirmed {
87 funding_txid: Txid,
88 },
89 Pending,
91 Failed {
93 error: String,
94 },
95 Canceled,
97}
98
99impl RoundStatus {
100 pub fn is_final(&self) -> bool {
102 match self {
103 Self::Confirmed { .. } => true,
104 Self::Unconfirmed { .. } => false,
105 Self::Pending => false,
106 Self::Failed { .. } => true,
107 Self::Canceled => true,
108 }
109 }
110
111 pub fn is_success(&self) -> bool {
113 match self {
114 Self::Confirmed { .. } => true,
115 Self::Unconfirmed { .. } => true,
116 Self::Pending => false,
117 Self::Failed { .. } => false,
118 Self::Canceled => false,
119 }
120 }
121}
122
123pub struct RoundState {
136 pub(crate) done: bool,
138
139 pub(crate) participation: RoundParticipation,
141
142 pub(crate) flow: RoundFlowState,
144
145 pub(crate) new_vtxos: Vec<Vtxo<Full>>,
150
151 pub(crate) sent_forfeit_sigs: bool,
158
159 pub(crate) movement_id: Option<MovementId>,
161}
162
163impl RoundState {
164 fn new_interactive(
165 participation: RoundParticipation,
166 movement_id: Option<MovementId>,
167 ) -> Self {
168 Self {
169 participation,
170 movement_id,
171 flow: RoundFlowState::InteractivePending,
172 new_vtxos: Vec::new(),
173 sent_forfeit_sigs: false,
174 done: false,
175 }
176 }
177
178 fn new_delegated(
179 participation: RoundParticipation,
180 unlock_hash: UnlockHash,
181 scheduled_height: Option<BlockHeight>,
182 movement_id: Option<MovementId>,
183 ) -> Self {
184 Self {
185 participation,
186 movement_id,
187 flow: RoundFlowState::NonInteractivePending { unlock_hash, scheduled_height },
188 new_vtxos: Vec::new(),
189 sent_forfeit_sigs: false,
190 done: false,
191 }
192 }
193
194 pub fn participation(&self) -> &RoundParticipation {
196 &self.participation
197 }
198
199 pub fn unlock_hash(&self) -> Option<UnlockHash> {
201 match self.flow {
202 RoundFlowState::NonInteractivePending { unlock_hash, .. } => Some(unlock_hash),
203 RoundFlowState::InteractivePending => None,
204 RoundFlowState::InteractiveOngoing { .. } => None,
205 RoundFlowState::Failed { .. } => None,
206 RoundFlowState::Canceled => None,
207 RoundFlowState::Finished { unlock_hash, .. } => Some(unlock_hash),
208 }
209 }
210
211 pub fn funding_tx(&self) -> Option<&Transaction> {
212 match self.flow {
213 RoundFlowState::NonInteractivePending { .. } => None,
214 RoundFlowState::InteractivePending => None,
215 RoundFlowState::InteractiveOngoing { .. } => None,
216 RoundFlowState::Failed { .. } => None,
217 RoundFlowState::Canceled => None,
218 RoundFlowState::Finished { ref funding_tx, .. } => Some(funding_tx),
219 }
220 }
221
222 pub fn ongoing_participation(&self) -> bool {
224 match self.flow {
225 RoundFlowState::NonInteractivePending { .. } => false,
226 RoundFlowState::InteractivePending => true,
227 RoundFlowState::InteractiveOngoing { .. } => true,
228 RoundFlowState::Failed { .. } => false,
229 RoundFlowState::Canceled => false,
230 RoundFlowState::Finished { .. } => false,
231 }
232 }
233
234 pub async fn try_cancel(&mut self, wallet: &Wallet) -> anyhow::Result<bool> {
237 let ret = match self.flow {
238 RoundFlowState::NonInteractivePending { .. } => {
239 bail!("it is currently not yet possible to cancel pending delegated rounds");
241 },
242 RoundFlowState::Canceled => true,
243 RoundFlowState::Failed { .. } => true,
244 RoundFlowState::InteractivePending | RoundFlowState::InteractiveOngoing { .. } => {
245 self.flow = RoundFlowState::Canceled;
246 true
247 },
248 RoundFlowState::Finished { .. } => false,
249 };
250 if ret {
251 persist_round_failure(wallet, &self.participation, self.movement_id).await
252 .context("failed to persist round failure for cancelation")?;
253 }
254 Ok(ret)
255 }
256
257 async fn try_start_attempt(
258 &mut self,
259 wallet: &Wallet,
260 attempt: &RoundAttempt,
261 ) {
262 if let RoundFlowState::InteractiveOngoing {
265 state: AttemptState::AwaitingUnsignedVtxoTree { ref cosign_keys, .. },
266 ..
267 } = self.flow {
268 if let Some(k) = cosign_keys.first() {
269 wallet.inner.round_secret_nonces.forget(&k.public_key());
270 }
271 }
272
273 match start_attempt(wallet, &self.participation, attempt).await {
274 Ok(state) => {
275 self.flow = RoundFlowState::InteractiveOngoing {
276 round_seq: attempt.round_seq,
277 attempt_seq: attempt.attempt_seq,
278 state: state,
279 };
280 },
281 Err(e) => {
282 self.flow = RoundFlowState::Failed {
283 error: format!("{:#}", e),
284 };
285 },
286 }
287 }
288
289 pub async fn process_event(
291 &mut self,
292 wallet: &Wallet,
293 event: &RoundEvent,
294 ) -> bool {
295 let _: Infallible = match self.flow {
296 RoundFlowState::InteractivePending => {
297 if let RoundEvent::Attempt(e) = event && e.attempt_seq == 0 {
298 trace!("Joining round attempt {}:{}", e.round_seq, e.attempt_seq);
299 self.try_start_attempt(wallet, e).await;
300 return true;
301 } else {
302 trace!("Ignoring {} event (seq {}:{}), waiting for round to start",
303 event.kind(), event.round_seq(), event.attempt_seq(),
304 );
305 return false;
306 }
307 },
308 RoundFlowState::InteractiveOngoing { round_seq, attempt_seq, ref mut state } => {
309 if let RoundEvent::Failed(e) = event && e.round_seq == round_seq {
312 warn!("Round {} failed by server", round_seq);
313 self.flow = RoundFlowState::Failed {
314 error: format!("round {} failed by server", round_seq),
315 };
316 return true;
317 }
318
319 if event.round_seq() > round_seq {
320 self.flow = RoundFlowState::Failed {
323 error: format!("round {} started while we were on {}",
324 event.round_seq(), round_seq,
325 ),
326 };
327 return true;
328 }
329
330 if event.attempt_seq() < attempt_seq {
331 trace!("ignoring replayed message from old attempt");
332 return false;
333 }
334
335 if let RoundEvent::Attempt(e) = event && e.attempt_seq > attempt_seq {
336 trace!("Joining new round attempt {}:{}", e.round_seq, e.attempt_seq);
337 self.try_start_attempt(wallet, e).await;
338 return true;
339 }
340 trace!("Processing event {} for round attempt {}:{} in state {}",
341 event.kind(), round_seq, attempt_seq, state.kind(),
342 );
343
344 return match progress_attempt(state, wallet, &self.participation, event).await {
345 AttemptProgressResult::NotUpdated => false,
346 AttemptProgressResult::Updated { new_state } => {
347 *state = new_state;
348 true
349 },
350 AttemptProgressResult::Failed(e) => {
351 warn!("Round failed with error: {:#}", e);
352 self.flow = RoundFlowState::Failed {
353 error: format!("{:#}", e),
354 };
355 true
356 },
357 AttemptProgressResult::Finished { funding_tx, vtxos, unlock_hash } => {
358 self.new_vtxos = vtxos;
359 let funding_txid = funding_tx.compute_txid();
360 self.flow = RoundFlowState::Finished { funding_tx, unlock_hash };
361 if let Some(mid) = self.movement_id {
362 if let Err(e) = update_funding_txid(wallet, mid, funding_txid).await {
363 warn!("Error updating the round funding txid: {:#}", e);
364 }
365 }
366 true
367 },
368 };
369 },
370 RoundFlowState::NonInteractivePending { .. }
371 | RoundFlowState::Finished { .. }
372 | RoundFlowState::Failed { .. }
373 | RoundFlowState::Canceled => return false,
374 };
375 }
376
377 pub async fn sync(&mut self, wallet: &Wallet) -> anyhow::Result<RoundStatus> {
382 match self.flow {
383 RoundFlowState::Finished { ref funding_tx, .. } if self.done => {
384 Ok(RoundStatus::Confirmed {
385 funding_txid: funding_tx.compute_txid(),
386 })
387 },
388
389 RoundFlowState::InteractivePending | RoundFlowState::InteractiveOngoing { .. } => {
390 Ok(RoundStatus::Pending)
391 },
392 RoundFlowState::Failed { ref error } => {
393 persist_round_failure(wallet, &self.participation, self.movement_id).await
394 .context("failed to persist round failure")?;
395 Ok(RoundStatus::Failed { error: error.clone() })
396 },
397 RoundFlowState::Canceled => {
398 persist_round_failure(wallet, &self.participation, self.movement_id).await
399 .context("failed to persist round failure")?;
400 Ok(RoundStatus::Canceled)
401 },
402
403 RoundFlowState::NonInteractivePending { unlock_hash, scheduled_height } => {
404 match progress_delegated(
405 wallet, &self.participation, unlock_hash, scheduled_height,
406 ).await {
407 Ok(HarkProgressResult::RoundPending) => Ok(RoundStatus::Pending),
408 Ok(HarkProgressResult::RoundNotFound) => {
411 info!("Server reports round participation not found (no forfeits sent)");
412 self.flow = RoundFlowState::Failed {
413 error: "server reports round participation not found".into(),
414 };
415 if let Some(movement_id) = self.movement_id {
416 wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
417 .context("failed to mark refresh movement as failed")?;
418 }
419 Ok(RoundStatus::Failed {
420 error: "server reports round participation not found".into(),
421 })
422 },
423 Ok(HarkProgressResult::Ok { funding_tx, new_vtxos }) => {
424 let funding_txid = funding_tx.compute_txid();
425 self.new_vtxos = new_vtxos;
426 self.flow = RoundFlowState::Finished {
427 funding_tx: funding_tx.clone(),
428 unlock_hash: unlock_hash,
429 };
430
431 persist_round_success(
432 wallet,
433 &self.participation,
434 self.movement_id,
435 &self.new_vtxos,
436 &funding_tx,
437 ).await.context("failed to store successful round in DB!")?;
438
439 self.done = true;
440
441 Ok(RoundStatus::Confirmed { funding_txid })
442 },
443 Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid }) => {
444 if let Some(mid) = self.movement_id {
445 update_funding_txid(wallet, mid, funding_txid).await
446 .context("failed to update funding txid in DB")?;
447 }
448 Ok(RoundStatus::Unconfirmed { funding_txid })
449 },
450
451 Err(HarkForfeitError::Err(e)) => {
454 Err(e.context("error progressing delegated round"))
458 },
459 Err(HarkForfeitError::SentForfeits(e)) => {
460 self.sent_forfeit_sigs = true;
461 Err(e.context("error progressing delegated round \
462 after sending forfeit tx signatures"))
463 },
464 }
465 },
466 RoundFlowState::Finished { ref funding_tx, unlock_hash } => {
468 let funding_txid = funding_tx.compute_txid();
469 let confirmed = check_funding_tx_confirmations(
470 wallet, funding_txid, &funding_tx,
471 ).await.context("error checking funding tx confirmations")?;
472 if !confirmed {
473 trace!("Funding tx {} not yet deeply enough confirmed", funding_txid);
474 return Ok(RoundStatus::Unconfirmed { funding_txid });
475 }
476
477 match hark_vtxo_swap(
478 wallet, &self.participation, &mut self.new_vtxos, &funding_tx, unlock_hash,
479 ).await {
480 Ok(()) => {
481 persist_round_success(
482 wallet,
483 &self.participation,
484 self.movement_id,
485 &self.new_vtxos,
486 &funding_tx,
487 ).await.context("failed to store successful round in DB!")?;
488
489 self.done = true;
490
491 Ok(RoundStatus::Confirmed { funding_txid })
492 },
493 Err(HarkForfeitError::Err(e)) => {
494 Err(e.context("error forfeiting VTXOs after round"))
495 },
496 Err(HarkForfeitError::SentForfeits(e)) => {
497 self.sent_forfeit_sigs = true;
498 Err(e.context("error after having signed and sent \
499 forfeit signatures to server"))
500 },
501 }
502 },
503 }
504 }
505
506 pub fn output_vtxos(&self) -> Option<&[Vtxo<Full>]> {
509 if self.new_vtxos.is_empty() {
510 None
511 } else {
512 Some(&self.new_vtxos)
513 }
514 }
515
516 pub fn locked_pending_inputs(&self) -> &[Vtxo<Full>] {
519 match self.flow {
521 RoundFlowState::NonInteractivePending { .. }
522 | RoundFlowState::InteractivePending
523 | RoundFlowState::InteractiveOngoing { .. }
524 => {
525 &self.participation.inputs
526 },
527 RoundFlowState::Finished { .. } => if self.done {
528 &[]
530 } else {
531 &self.participation.inputs
532 },
533 RoundFlowState::Failed { .. }
534 | RoundFlowState::Canceled
535 => {
536 &[]
538 },
539 }
540 }
541
542 pub fn pending_balance(&self) -> Amount {
546 if self.done {
547 return Amount::ZERO;
548 }
549
550 match self.flow {
551 RoundFlowState::NonInteractivePending { .. }
552 | RoundFlowState::InteractivePending
553 | RoundFlowState::InteractiveOngoing { .. }
554 | RoundFlowState::Finished { .. }
555 => {
556 self.participation.outputs.iter().map(|o| o.amount).sum()
557 },
558 RoundFlowState::Failed { .. } | RoundFlowState::Canceled => {
559 Amount::ZERO
560 },
561 }
562 }
563
564}
565
566pub enum RoundFlowState {
571 NonInteractivePending {
573 unlock_hash: UnlockHash,
574 scheduled_height: Option<BlockHeight>,
578 },
579
580 InteractivePending,
582 InteractiveOngoing {
584 round_seq: RoundSeq,
585 attempt_seq: usize,
586 state: AttemptState,
587 },
588
589 Finished {
591 funding_tx: Transaction,
592 unlock_hash: UnlockHash,
593 },
594
595 Failed {
597 error: String,
598 },
599
600 Canceled,
602}
603
604pub enum AttemptState {
609 AwaitingAttempt,
610 AwaitingUnsignedVtxoTree {
611 cosign_keys: Vec<Keypair>,
612 unlock_hash: UnlockHash,
613 },
614 AwaitingFinishedRound {
615 unsigned_round_tx: Transaction,
616 vtxos_spec: VtxoTreeSpec,
617 unlock_hash: UnlockHash,
618 },
619}
620
621impl AttemptState {
622 fn kind(&self) -> &'static str {
624 match self {
625 Self::AwaitingAttempt => "AwaitingAttempt",
626 Self::AwaitingUnsignedVtxoTree { .. } => "AwaitingUnsignedVtxoTree",
627 Self::AwaitingFinishedRound { .. } => "AwaitingFinishedRound",
628 }
629 }
630}
631
632enum AttemptProgressResult {
634 Finished {
635 funding_tx: Transaction,
636 vtxos: Vec<Vtxo<Full>>,
637 unlock_hash: UnlockHash,
638 },
639 Failed(anyhow::Error),
640 Updated {
646 new_state: AttemptState,
647 },
648 NotUpdated,
649}
650
651async fn start_attempt(
653 wallet: &Wallet,
654 participation: &RoundParticipation,
655 event: &RoundAttempt,
656) -> anyhow::Result<AttemptState> {
657 let (mut srv, ark_info) = wallet.require_server().await.context("server not available")?;
658
659 let cosign_keys = iter::repeat_with(|| Keypair::new(&SECP, &mut rand::thread_rng()))
661 .take(participation.outputs.len())
662 .collect::<Vec<_>>();
663
664 let cosign_nonces = cosign_keys.iter()
667 .map(|key| {
668 let mut secs = Vec::with_capacity(ark_info.nb_round_nonces);
669 let mut pubs = Vec::with_capacity(ark_info.nb_round_nonces);
670 for _ in 0..ark_info.nb_round_nonces {
671 let (s, p) = musig::nonce_pair(key);
672 secs.push(s);
673 pubs.push(p);
674 }
675 (secs, pubs)
676 })
677 .take(participation.outputs.len())
678 .collect::<Vec<(Vec<SecretNonce>, Vec<PublicNonce>)>>();
679
680
681 debug!("Submitting payment request with {} inputs and {} vtxo outputs",
683 participation.inputs.len(), participation.outputs.len(),
684 );
685
686 let unblinded_mailbox_id = wallet.mailbox_identifier();
688 let signed_reqs = participation.outputs.iter()
689 .zip(cosign_keys.iter())
690 .zip(cosign_nonces.iter())
691 .map(|((req, cosign_key), (_sec, pub_nonces))| {
692 SignedVtxoRequest {
693 vtxo: req.clone(),
694 cosign_pubkey: cosign_key.public_key(),
695 nonces: pub_nonces.clone(),
696 }
697 })
698 .collect::<Vec<_>>();
699
700 let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
701 for vtxo in participation.inputs.iter() {
702 let keypair = wallet.get_vtxo_key(vtxo).await
703 .map_err(HarkForfeitError::Err)?;
704 input_vtxos.push(protos::InputVtxo {
705 vtxo_id: vtxo.id().to_bytes().to_vec(),
706 attestation: {
707 let attestation = RoundAttemptAttestation::new(
708 event.challenge, vtxo.id(), &signed_reqs, &keypair,
709 );
710 attestation.serialize()
711 },
712 });
713 }
714
715 wallet.register_vtxo_transactions_with_server(&participation.inputs).await
717 .map_err(HarkForfeitError::Err)?;
718
719 let resp = srv.client.submit_payment(protos::SubmitPaymentRequest {
720 input_vtxos: input_vtxos,
721 vtxo_requests: signed_reqs.into_iter().map(Into::into).collect(),
722 #[allow(deprecated)]
723 offboard_requests: vec![],
724 unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
725 }).await.context("Ark server refused our payment submission")?;
726 let unlock_hash = UnlockHash::from_bytes(&resp.into_inner().unlock_hash)?;
727
728 if let Some(k) = cosign_keys.first() {
731 wallet.inner.round_secret_nonces.stash(
732 k.public_key(),
733 cosign_nonces.into_iter().map(|(sec, _pub)| sec).collect(),
734 );
735 }
736
737 Ok(AttemptState::AwaitingUnsignedVtxoTree { unlock_hash, cosign_keys })
738}
739
740#[derive(Debug, thiserror::Error)]
742enum HarkForfeitError {
743 #[error("error after forfeits were sent")]
745 SentForfeits(#[source] anyhow::Error),
746 #[error("error before forfeits were sent")]
748 Err(#[source] anyhow::Error),
749}
750
751async fn hark_cosign_leaf(
752 wallet: &Wallet,
753 srv: &mut ServerConnection,
754 funding_tx: &Transaction,
755 vtxo: &mut Vtxo<Full>,
756) -> anyhow::Result<()> {
757 let key = wallet.pubkey_keypair(&vtxo.user_pubkey()).await
758 .context("error fetching keypair").map_err(HarkForfeitError::Err)?
759 .with_context(|| format!(
760 "keypair {} not found for VTXO {}", vtxo.user_pubkey(), vtxo.id(),
761 ))?.1;
762 let (ctx, cosign_req) = LeafVtxoCosignContext::new(vtxo, funding_tx, &key);
763 let cosign_resp = srv.client.request_leaf_vtxo_cosign(
764 protos::LeafVtxoCosignRequest::from(cosign_req),
765 ).await
766 .with_context(|| format!("error requesting leaf cosign for vtxo {}", vtxo.id()))?
767 .into_inner().try_into()
768 .context("bad leaf vtxo cosign response")?;
769 ensure!(ctx.finalize(vtxo, cosign_resp),
770 "failed to finalize VTXO leaf signature for VTXO {}", vtxo.id(),
771 );
772
773 Ok(())
774}
775
776async fn hark_vtxo_swap(
786 wallet: &Wallet,
787 participation: &RoundParticipation,
788 output_vtxos: &mut [Vtxo<Full>],
789 funding_tx: &Transaction,
790 unlock_hash: UnlockHash,
791) -> Result<(), HarkForfeitError> {
792 let (mut srv, _) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
793
794 wallet.register_vtxo_transactions_with_server(&participation.inputs).await
796 .context("couldn't send our input vtxo transactions to server")
797 .map_err(HarkForfeitError::Err)?;
798
799 for vtxo in output_vtxos.iter_mut() {
801 hark_cosign_leaf(wallet, &mut srv, funding_tx, vtxo).await
802 .map_err(HarkForfeitError::Err)?;
803 }
804
805 let mut server_nonces = Vec::with_capacity(participation.inputs.len());
810 for inputs in participation.inputs.chunks(MAX_NB_FORFEIT_NONCE_IDS) {
811 let nonces = srv.client.request_forfeit_nonces(protos::ForfeitNoncesRequest {
812 unlock_hash: unlock_hash.to_byte_array().to_vec(),
813 vtxo_ids: inputs.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
814 }).await
815 .context("request forfeits nonces call failed")
816 .map_err(HarkForfeitError::Err)?
817 .into_inner().public_nonces.into_iter()
818 .map(|b| musig::PublicNonce::from_bytes(b))
819 .collect::<Result<Vec<_>, _>>()
820 .context("invalid forfeit nonces")
821 .map_err(HarkForfeitError::Err)?;
822
823 if nonces.len() != inputs.len() {
824 return Err(HarkForfeitError::Err(anyhow!(
825 "server sent {} nonce pairs, expected {}",
826 nonces.len(), inputs.len(),
827 )));
828 }
829 server_nonces.extend(nonces);
830 }
831
832 let mut forfeit_bundles = Vec::with_capacity(participation.inputs.len());
833 for (input, nonces) in participation.inputs.iter().zip(server_nonces.into_iter()) {
834 let user_key = wallet.pubkey_keypair(&input.user_pubkey()).await
835 .ok().flatten().with_context(|| format!(
836 "failed to fetch keypair for vtxo user pubkey {}", input.user_pubkey(),
837 )).map_err(HarkForfeitError::Err)?.1;
838 forfeit_bundles.push(HashLockedForfeitBundle::new(
839 input, unlock_hash, &user_key, &nonces,
840 ))
841 }
842
843 let preimage = srv.client.forfeit_vtxos(protos::ForfeitVtxosRequest {
844 forfeit_bundles: forfeit_bundles.iter().map(|b| b.serialize()).collect(),
845 }).await
846 .context("forfeit vtxos call failed")
847 .map_err(HarkForfeitError::SentForfeits)?
848 .into_inner().unlock_preimage.as_slice().try_into()
849 .context("invalid preimage length")
850 .map_err(HarkForfeitError::SentForfeits)?;
851
852 for vtxo in output_vtxos.iter_mut() {
853 if !vtxo.provide_unlock_preimage(preimage) {
854 return Err(HarkForfeitError::SentForfeits(anyhow!(
855 "invalid preimage {} for vtxo {} with supposed unlock hash {}",
856 preimage.as_hex(), vtxo.id(), unlock_hash,
857 )));
858 }
859
860 vtxo.validate(&funding_tx).with_context(|| format!(
862 "new VTXO {} does not pass validation after hArk forfeit protocol", vtxo.id(),
863 )).map_err(HarkForfeitError::SentForfeits)?;
864 }
865
866 wallet.register_vtxo_transactions_with_server(output_vtxos).await
868 .context("couldn't register output vtxo transactions with server")
869 .map_err(HarkForfeitError::SentForfeits)?;
870
871 Ok(())
872}
873
874fn check_vtxo_fails_hash_lock(funding_tx: &Transaction, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
875 match vtxo.validate(funding_tx) {
876 Err(VtxoValidationError::GenesisTransition {
880 genesis_idx, genesis_len, transition_kind, ..
881 }) if genesis_idx + 1 == genesis_len
882 && (transition_kind == TransitionKind::HashLockedCosigned.as_str()
883 || transition_kind == TransitionKind::HashLockedCosigned_v0.as_str()) => Ok(()),
884 Ok(()) => Err(anyhow!("new un-unlocked VTXO should fail validation but doesn't: {}",
885 vtxo.serialize_hex(),
886 )),
887 Err(e) => Err(anyhow!("new VTXO {} failed validation: {:#}", vtxo.id(), e)),
888 }
889}
890
891fn validate_vtxo_tree_params(
897 server_pubkey: PublicKey,
898 exit_delta: BlockDelta,
899 expiry_height: BlockHeight,
900 expected_server_pubkey: PublicKey,
901 expected_exit_delta: BlockDelta,
902 min_expiry_height: BlockHeight,
903) -> anyhow::Result<()> {
904 ensure!(server_pubkey == expected_server_pubkey,
905 "round VTXO tree uses an unexpected server pubkey: got {}, expected {}",
906 server_pubkey, expected_server_pubkey,
907 );
908
909 ensure!(exit_delta == expected_exit_delta,
910 "round VTXO tree uses an unexpected exit delta: got {}, expected {}",
911 exit_delta, expected_exit_delta,
912 );
913
914 ensure!(expiry_height >= min_expiry_height,
915 "round VTXO tree expiry height {} is below the required minimum {}; \
916 server may be trying to sweep before we can exit",
917 expiry_height, min_expiry_height,
918 );
919
920 Ok(())
921}
922
923fn check_round_matches_participation(
924 part: &RoundParticipation,
925 new_vtxos: &[Vtxo<Full>],
926 funding_tx: &Transaction,
927 ark_info: &ArkInfo,
928 scheduled_height: Option<BlockHeight>,
929) -> anyhow::Result<()> {
930 ensure!(new_vtxos.len() == part.outputs.len(),
931 "unexpected number of VTXOs: got {}, expected {}", new_vtxos.len(), part.outputs.len(),
932 );
933
934 let min_expiry_height = match scheduled_height {
935 Some(h) => h.saturating_add(1),
936 None => part.inputs.iter()
937 .map(|v| v.expiry_height()).max().unwrap_or(1)
938 .saturating_add(1),
939 };
940
941 for (vtxo, req) in new_vtxos.iter().zip(&part.outputs) {
942 ensure!(vtxo.amount() == req.amount,
943 "unexpected VTXO amount: got {}, expected {}", vtxo.amount(), req.amount,
944 );
945 ensure!(*vtxo.policy() == req.policy,
946 "unexpected VTXO policy: got {:?}, expected {:?}", vtxo.policy(), req.policy,
947 );
948
949 validate_vtxo_tree_params(
952 vtxo.server_pubkey(), vtxo.exit_delta(), vtxo.expiry_height(),
953 ark_info.server_pubkey, ark_info.vtxo_exit_delta, min_expiry_height,
954 )?;
955
956 check_vtxo_fails_hash_lock(funding_tx, vtxo)?;
958 }
959
960 Ok(())
961}
962
963async fn check_funding_tx_confirmations(
973 wallet: &Wallet,
974 funding_txid: Txid,
975 funding_tx: &Transaction,
976) -> anyhow::Result<bool> {
977 let tip = wallet.inner.chain.tip().await.context("chain source error")?;
978 let conf_height = tip - wallet.inner.config.round_tx_required_confirmations + 1;
979 let tx_status = wallet.inner.chain.tx_status(funding_txid).await.context("chain source error")?;
980 trace!("Round funding tx {} confirmation status: {:?} (tip={})",
981 funding_txid, tx_status, tip,
982 );
983 match tx_status {
984 TxStatus::Confirmed(b) if b.height <= conf_height => Ok(true),
985 TxStatus::Mempool | TxStatus::Confirmed(_) => {
986 if wallet.inner.config.round_tx_required_confirmations == 0 {
987 debug!("Accepting round funding tx without confirmations because of configuration");
988 Ok(true)
989 } else {
990 trace!("Hark round funding tx not confirmed (deep enough) yet: {:?}", tx_status);
991 Ok(false)
992 }
993 },
994 TxStatus::NotFound => {
995 if let Err(e) = wallet.inner.chain.broadcast_tx(&funding_tx).await {
1000 Err(anyhow!("hark funding tx {} server sent us is rejected by mempool (hex={}): {:#}",
1001 funding_txid, serialize_hex(funding_tx), e,
1002 ))
1003 } else {
1004 trace!("hark funding tx {} was not in mempool but we broadcast it", funding_txid);
1005 Ok(false)
1006 }
1007 },
1008 }
1009}
1010
1011enum HarkProgressResult {
1012 RoundPending,
1013 RoundNotFound,
1014 FundingTxUnconfirmed {
1015 funding_txid: Txid,
1016 },
1017 Ok {
1018 funding_tx: Transaction,
1019 new_vtxos: Vec<Vtxo<Full>>,
1020 },
1021}
1022
1023async fn progress_delegated(
1024 wallet: &Wallet,
1025 participation: &RoundParticipation,
1026 unlock_hash: UnlockHash,
1027 scheduled_height: Option<BlockHeight>,
1028) -> Result<HarkProgressResult, HarkForfeitError> {
1029 let (mut srv, ark_info) = wallet.require_server().await.map_err(HarkForfeitError::Err)?;
1030
1031 let resp = match srv.client.round_participation_status(protos::RoundParticipationStatusRequest {
1032 unlock_hash: unlock_hash.to_byte_array().to_vec(),
1033 }).await {
1034 Ok(resp) => resp.into_inner(),
1035 Err(err) if err.code() == tonic::Code::NotFound => {
1036 return Ok(HarkProgressResult::RoundNotFound);
1037 },
1038 Err(err) => {
1039 return Err(HarkForfeitError::Err(
1040 anyhow::Error::from(err).context("error checking round participation status"),
1041 ));
1042 },
1043 };
1044 let status = protos::RoundParticipationStatus::try_from(resp.status)
1045 .context("unknown status from server")
1046 .map_err(HarkForfeitError::Err) ?;
1047
1048 if status == protos::RoundParticipationStatus::RoundPartPending {
1049 trace!("Hark round still pending");
1050 return Ok(HarkProgressResult::RoundPending);
1051 }
1052
1053 if status == protos::RoundParticipationStatus::RoundPartReleased {
1058 let preimage = resp.unlock_preimage.as_ref().map(|p| p.as_hex());
1059 warn!("Server says preimage was already released for hArk participation \
1060 with unlock hash {}. Supposed preimage: {:?}", unlock_hash, preimage,
1061 );
1062 }
1063
1064 let funding_tx_bytes = resp.round_funding_tx
1065 .context("funding txid should be provided when status is not pending")
1066 .map_err(HarkForfeitError::Err)?;
1067 let funding_tx = deserialize::<Transaction>(&funding_tx_bytes)
1068 .context("invalid funding txid")
1069 .map_err(HarkForfeitError::Err)?;
1070 let funding_txid = funding_tx.compute_txid();
1071 trace!("Funding tx for round participation with unlock hash {}: {} ({})",
1072 unlock_hash, funding_tx.compute_txid(), funding_tx_bytes.as_hex(),
1073 );
1074
1075 match check_funding_tx_confirmations(wallet, funding_txid, &funding_tx).await {
1077 Ok(true) => {},
1078 Ok(false) => return Ok(HarkProgressResult::FundingTxUnconfirmed { funding_txid }),
1079 Err(e) => return Err(HarkForfeitError::Err(e.context("checking funding tx confirmations"))),
1080 }
1081
1082 let mut new_vtxos = resp.output_vtxos.into_iter()
1083 .map(|v| <Vtxo<Full>>::deserialize(&v))
1084 .collect::<Result<Vec<_>, _>>()
1085 .context("invalid output VTXOs from server")
1086 .map_err(HarkForfeitError::Err)?;
1087
1088 check_round_matches_participation(
1091 participation, &new_vtxos, &funding_tx, &ark_info, scheduled_height,
1092 )
1093 .context("new VTXOs received from server don't match our participation")
1094 .map_err(HarkForfeitError::Err)?;
1095
1096 hark_vtxo_swap(wallet, participation, &mut new_vtxos, &funding_tx, unlock_hash).await
1097 .context("error forfeiting hArk VTXOs")
1098 .map_err(HarkForfeitError::SentForfeits)?;
1099
1100 Ok(HarkProgressResult::Ok { funding_tx, new_vtxos })
1101}
1102
1103async fn progress_attempt(
1104 state: &mut AttemptState,
1105 wallet: &Wallet,
1106 part: &RoundParticipation,
1107 event: &RoundEvent,
1108) -> AttemptProgressResult {
1109 match (state, event) {
1113
1114 (
1115 AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, unlock_hash },
1116 RoundEvent::VtxoProposal(e),
1117 ) => {
1118 trace!("Received VtxoProposal: {:#?}", e);
1119
1120 let secret_nonces = if let Some(first) = cosign_keys.first() {
1123 match wallet.inner.round_secret_nonces.take(&first.public_key()) {
1124 Some(n) => n,
1125 None => return AttemptProgressResult::Failed(anyhow!(
1126 "secret cosign nonces unavailable (likely after a restart); \
1127 abandoning round attempt to avoid nonce reuse",
1128 )),
1129 }
1130 } else {
1131 vec![]
1132 };
1133
1134 match sign_vtxo_tree(
1135 wallet,
1136 part,
1137 &cosign_keys,
1138 secret_nonces,
1139 &e.unsigned_round_tx,
1140 &e.vtxos_spec,
1141 &e.cosign_agg_nonces,
1142 ).await {
1143 Ok(()) => {
1144 AttemptProgressResult::Updated {
1145 new_state: AttemptState::AwaitingFinishedRound {
1146 unsigned_round_tx: e.unsigned_round_tx.clone(),
1147 vtxos_spec: e.vtxos_spec.clone(),
1148 unlock_hash: *unlock_hash,
1149 },
1150 }
1151 },
1152 Err(e) => {
1153 trace!("Error signing VTXO tree: {:#}", e);
1154 AttemptProgressResult::Failed(e)
1155 },
1156 }
1157 },
1158
1159 (
1160 AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash },
1161 RoundEvent::Finished(RoundFinished { cosign_sigs, signed_round_tx, .. }),
1162 ) => {
1163 if unsigned_round_tx.compute_txid() != signed_round_tx.compute_txid() {
1164 return AttemptProgressResult::Failed(anyhow!(
1165 "signed funding tx ({}) doesn't match tx received before ({})",
1166 signed_round_tx.compute_txid(), unsigned_round_tx.compute_txid(),
1167 ));
1168 }
1169
1170 if let Err(e) = wallet.inner.chain.broadcast_tx(&signed_round_tx).await {
1171 warn!("Failed to broadcast signed round tx: {:#}", e);
1172 }
1173
1174 match construct_new_vtxos(
1175 part, unsigned_round_tx, vtxos_spec, cosign_sigs,
1176 ).await {
1177 Ok(v) => AttemptProgressResult::Finished {
1178 funding_tx: signed_round_tx.clone(),
1179 vtxos: v,
1180 unlock_hash: *unlock_hash,
1181 },
1182 Err(e) => AttemptProgressResult::Failed(anyhow!(
1183 "failed to construct new VTXOs for round: {:#}", e,
1184 )),
1185 }
1186 },
1187
1188 (state, RoundEvent::Finished(RoundFinished { .. })) => {
1189 AttemptProgressResult::Failed(anyhow!(
1190 "unexpectedly received a finished round while we were in state {}",
1191 state.kind(),
1192 ))
1193 },
1194
1195 (state, _) => {
1196 trace!("Ignoring round event {} in state {}", event.kind(), state.kind());
1197 AttemptProgressResult::NotUpdated
1198 },
1199 }
1200}
1201
1202async fn sign_vtxo_tree(
1203 wallet: &Wallet,
1204 participation: &RoundParticipation,
1205 cosign_keys: &[Keypair],
1206 secret_nonces: Vec<Vec<SecretNonce>>,
1207 unsigned_round_tx: &Transaction,
1208 vtxo_tree: &VtxoTreeSpec,
1209 cosign_agg_nonces: &[musig::AggregatedNonce],
1210) -> anyhow::Result<()> {
1211 let (mut srv, ark_info) = wallet.require_server().await.context("server not available")?;
1212
1213 let vtxos_utxo = OutPoint::new(unsigned_round_tx.compute_txid(), ROUND_TX_VTXO_TREE_VOUT);
1214
1215 let tip = wallet.inner.chain.tip().await.context("chain source error")?;
1218 let min_expiry_height = tip
1219 .saturating_add(ark_info.vtxo_lifetime as BlockHeight)
1220 .saturating_sub(VTXO_EXPIRY_HEIGHT_BUFFER);
1221 validate_vtxo_tree_params(
1222 vtxo_tree.server_pubkey, vtxo_tree.exit_delta, vtxo_tree.expiry_height,
1223 ark_info.server_pubkey, ark_info.vtxo_exit_delta, min_expiry_height,
1224 )?;
1225
1226 let mut my_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1228 for vtxo_req in vtxo_tree.iter_vtxos() {
1229 if let Some(i) = my_vtxos.iter().position(|v| {
1230 v.policy == vtxo_req.vtxo.policy && v.amount == vtxo_req.vtxo.amount
1231 }) {
1232 my_vtxos.swap_remove(i);
1233 }
1234 }
1235 if !my_vtxos.is_empty() {
1236 bail!("server didn't include all of our vtxos, missing: {:?}", my_vtxos);
1237 }
1238
1239 let unsigned_vtxos = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1240 trace!("Sending vtxo signatures to server...");
1241 for ((req, key), sec) in participation.outputs.iter().zip(cosign_keys).zip(secret_nonces) {
1246 let leaf_idx = unsigned_vtxos.spec.leaf_idx_of_req(req).expect("req included");
1247 let part_sigs = unsigned_vtxos.cosign_branch(
1248 &cosign_agg_nonces, leaf_idx, key, sec,
1249 ).context("failed to cosign branch: our request not part of tree")?;
1250
1251 info!("Sending {} partial vtxo cosign signatures for pk {}",
1252 part_sigs.len(), key.public_key(),
1253 );
1254
1255 srv.client.provide_vtxo_signatures(protos::VtxoSignaturesRequest {
1256 pubkey: key.public_key().serialize().to_vec(),
1257 signatures: part_sigs.iter().map(|s| s.serialize().to_vec()).collect(),
1258 }).await.context("error sending vtxo signatures")?;
1259 }
1260 trace!("Done sending vtxo signatures to server");
1261
1262 Ok(())
1263}
1264
1265async fn construct_new_vtxos(
1266 participation: &RoundParticipation,
1267 unsigned_round_tx: &Transaction,
1268 vtxo_tree: &VtxoTreeSpec,
1269 vtxo_cosign_sigs: &[schnorr::Signature],
1270) -> anyhow::Result<Vec<Vtxo<Full>>> {
1271 let round_txid = unsigned_round_tx.compute_txid();
1272 let vtxos_utxo = OutPoint::new(round_txid, ROUND_TX_VTXO_TREE_VOUT);
1273 let vtxo_tree = vtxo_tree.clone().into_unsigned_tree(vtxos_utxo);
1274
1275 if vtxo_tree.verify_cosign_sigs(&vtxo_cosign_sigs).is_err() {
1277 bail!("Received incorrect vtxo cosign signatures from server");
1279 }
1280
1281 let signed_vtxos = vtxo_tree
1282 .into_signed_tree(vtxo_cosign_sigs.to_vec())
1283 .into_cached_tree();
1284
1285 let mut expected_vtxos = participation.outputs.iter().collect::<Vec<_>>();
1286 let total_nb_expected_vtxos = expected_vtxos.len();
1287
1288 let mut new_vtxos = vec![];
1289 for (idx, req) in signed_vtxos.spec.spec.vtxos.iter().enumerate() {
1290 if let Some(expected_idx) = expected_vtxos.iter().position(|r| **r == req.vtxo) {
1291 let vtxo = signed_vtxos.build_vtxo(idx);
1292
1293 check_vtxo_fails_hash_lock(unsigned_round_tx, &vtxo)
1296 .context("constructed invalid vtxo from tree")?;
1297
1298 info!("New VTXO from round: {} ({}, {})",
1299 vtxo.id(), vtxo.amount(), vtxo.policy_type(),
1300 );
1301
1302 new_vtxos.push(vtxo);
1303 expected_vtxos.swap_remove(expected_idx);
1304 }
1305 }
1306
1307 if !expected_vtxos.is_empty() {
1308 if expected_vtxos.len() == total_nb_expected_vtxos {
1309 bail!("None of our VTXOs were present in round!");
1311 } else {
1312 bail!("Server included some of our VTXOs but not all: {} missing: {:?}",
1313 expected_vtxos.len(), expected_vtxos,
1314 );
1315 }
1316 }
1317 Ok(new_vtxos)
1318}
1319
1320async fn persist_round_success(
1322 wallet: &Wallet,
1323 participation: &RoundParticipation,
1324 movement_id: Option<MovementId>,
1325 new_vtxos: &[Vtxo<Full>],
1326 funding_tx: &Transaction,
1327) -> anyhow::Result<()> {
1328 debug!("Persisting newly finished round. {} new vtxos, movement ID {:?}",
1329 new_vtxos.len(), movement_id,
1330 );
1331
1332 let store_result = wallet.store_spendable_vtxos(new_vtxos).await
1336 .context("failed to store new VTXOs");
1337 let spent_result = wallet.mark_vtxos_as_spent(&participation.inputs).await
1338 .context("failed to mark input VTXOs as spent");
1339 let update_result = if let Some(mid) = movement_id {
1340 wallet.inner.movements.finish_movement_with_update(
1341 mid,
1342 MovementStatus::Successful,
1343 MovementUpdate::new()
1344 .produced_vtxos(new_vtxos)
1345 .metadata([("funding_txid".into(), serde_json::to_value(funding_tx.compute_txid())?)]),
1346 ).await.context("failed to mark movement as finished")
1347 } else {
1348 Ok(())
1349 };
1350
1351 store_result?;
1352 spent_result?;
1353 update_result?;
1354
1355 Ok(())
1356}
1357
1358async fn persist_round_failure(
1359 wallet: &Wallet,
1360 participation: &RoundParticipation,
1361 movement_id: Option<MovementId>,
1362) -> anyhow::Result<()> {
1363 debug!("Attempting to persist the failure of a round with the movement ID {:?}", movement_id);
1364 let unlock_result = wallet.unlock_vtxos(&participation.inputs).await;
1365 let finish_result = if let Some(movement_id) = movement_id {
1366 wallet.inner.movements.finish_movement(movement_id, MovementStatus::Failed).await
1367 } else {
1368 Ok(())
1369 };
1370 if let Err(e) = &finish_result {
1371 error!("Failed to mark movement as failed: {:#}", e);
1372 }
1373 match (unlock_result, finish_result) {
1374 (Ok(()), Ok(())) => Ok(()),
1375 (Err(e), _) => Err(e),
1376 (_, Err(e)) => Err(anyhow!("Failed to mark movement as failed: {:#}", e)),
1377 }
1378}
1379
1380async fn update_funding_txid(
1381 wallet: &Wallet,
1382 movement_id: MovementId,
1383 funding_txid: Txid,
1384) -> anyhow::Result<()> {
1385 wallet.inner.movements.update_movement(
1386 movement_id,
1387 MovementUpdate::new()
1388 .metadata([("funding_txid".into(), serde_json::to_value(&funding_txid)?)])
1389 ).await.context("Unable to update funding txid of round")
1390}
1391
1392#[derive(Default)]
1401pub struct RoundSecretNonces {
1402 inner: parking_lot::Mutex<HashMap<bitcoin::secp256k1::PublicKey, Vec<Vec<SecretNonce>>>>,
1403}
1404
1405impl RoundSecretNonces {
1406 pub fn new() -> Self {
1407 Self { inner: parking_lot::Mutex::new(HashMap::new()) }
1408 }
1409
1410 pub fn stash(
1412 &self,
1413 first_cosign_pubkey: bitcoin::secp256k1::PublicKey,
1414 nonces: Vec<Vec<SecretNonce>>,
1415 ) {
1416 self.inner.lock().insert(first_cosign_pubkey, nonces);
1417 }
1418
1419 pub fn take(
1422 &self,
1423 first_cosign_pubkey: &bitcoin::secp256k1::PublicKey,
1424 ) -> Option<Vec<Vec<SecretNonce>>> {
1425 self.inner.lock().remove(first_cosign_pubkey)
1426 }
1427
1428 pub fn forget(&self, first_cosign_pubkey: &bitcoin::secp256k1::PublicKey) {
1432 self.inner.lock().remove(first_cosign_pubkey);
1433 }
1434}
1435
1436impl Wallet {
1437 pub async fn lock_wait_round_state(&self, id: RoundStateId) -> anyhow::Result<Option<StoredRoundState>> {
1442 let guard = self.inner.lock_manager.lock(
1443 &format!("{}.round.{}", self.fingerprint(), id),
1444 ROUND_LOCK_TIMEOUT,
1445 ).await.with_context(|| format!(
1446 "timed out waiting for lock on round state {} (wallet {})",
1447 id, self.fingerprint(),
1448 ))?;
1449
1450 if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1451 return Ok(Some(state.lock(guard)));
1452 }
1453
1454 Ok(None)
1455 }
1456
1457 pub async fn next_round_start_time(&self) -> anyhow::Result<SystemTime> {
1459 let (mut srv, _) = self.require_server().await?;
1460 let ts = srv.client.next_round_time(protos::Empty {}).await?.into_inner().timestamp;
1461 Ok(UNIX_EPOCH.checked_add(Duration::from_secs(ts)).context("invalid timestamp")?)
1462 }
1463
1464 pub async fn join_next_round(
1473 &self,
1474 participation: RoundParticipation,
1475 movement_kind: Option<RoundMovement>,
1476 ) -> anyhow::Result<StoredRoundState> {
1477 let movement = if let Some(kind) = movement_kind {
1478 Some(self.inner.movements.new_guarded_movement_with_update(
1479 Subsystem::ROUND,
1480 kind.to_string(),
1481 OnDropStatus::Failed,
1482 participation.to_movement_update()?
1483 ).await?)
1484 } else {
1485 None
1486 };
1487 let movement_id = movement.as_ref().map(|m| m.id());
1488 let input_vtxos = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1489 let state = RoundState::new_interactive(participation, movement_id);
1490
1491 self.lock_vtxos(&input_vtxos, movement_id.map(|m| m.into())).await
1492 .context("failed to lock input VTXOs")?;
1493
1494 match (async || {
1495 let id = self.inner.db.store_round_state(&state).await?;
1496 Ok(self.lock_wait_round_state(id).await?
1497 .context("failed to lock fresh round state")?)
1498 })().await {
1499 Ok(state) => {
1500 if let Some(mut m) = movement {
1501 m.stop();
1502 }
1503 Ok(state)
1504 },
1505 Err(e) => {
1506 self.unlock_vtxos(&input_vtxos).await
1507 .context("failed to unlock input VTXOs")?;
1508 if let Some(mut m) = movement {
1509 m.fail().await.context("failed to mark movement as failed")?;
1510 }
1511 Err(e)
1512 },
1513 }
1514 }
1515
1516 pub async fn join_delegated_round(
1522 &self,
1523 participation: RoundParticipation,
1524 movement_kind: Option<RoundMovement>,
1525 scheduled_height: Option<BlockHeight>,
1526 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1527 let movement = if let Some(kind) = movement_kind {
1528 Some(self.inner.movements.new_guarded_movement_with_update(
1529 Subsystem::ROUND,
1530 kind.to_string(),
1531 OnDropStatus::Failed,
1532 participation.to_movement_update()?,
1533 ).await?)
1534 } else {
1535 None
1536 };
1537 let movement_id = movement.as_ref().map(|m| m.id());
1538
1539 match self.join_delegated_round_inner(participation, movement_id, scheduled_height).await {
1540 Ok(state) => {
1541 if let Some(mut m) = movement {
1542 m.stop();
1543 }
1544 Ok(state)
1545 },
1546 Err(e) => {
1547 if let Some(mut m) = movement {
1548 m.fail().await.context("error marking movement as failed")?;
1549 }
1550 Err(e)
1551 },
1552 }
1553 }
1554
1555 pub async fn join_next_round_delegated(
1558 &self,
1559 participation: RoundParticipation,
1560 movement_kind: Option<RoundMovement>,
1561 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1562 self.join_delegated_round(participation, movement_kind, None).await
1563 }
1564
1565 async fn join_delegated_round_inner(
1570 &self,
1571 participation: RoundParticipation,
1572 movement_id: Option<MovementId>,
1573 scheduled_height: Option<BlockHeight>,
1574 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1575 let (mut srv, _) = self.require_server().await?;
1576
1577 let unblinded_mailbox_id = self.mailbox_identifier();
1579
1580 self.register_vtxo_transactions_with_server(&participation.inputs).await
1582 .context("failed to register input vtxo transactions with server")?;
1583
1584 let mut input_vtxos = Vec::with_capacity(participation.inputs.len());
1586 for vtxo in participation.inputs.iter() {
1587 let keypair = self.get_vtxo_key(vtxo).await
1588 .context("failed to get vtxo keypair")?;
1589 input_vtxos.push(protos::InputVtxo {
1590 vtxo_id: vtxo.id().to_bytes().to_vec(),
1591 attestation: {
1592 let attestation = DelegatedRoundParticipationAttestation::new(
1593 vtxo.id(), &participation.outputs, &keypair,
1594 );
1595 attestation.serialize()
1596 },
1597 });
1598 }
1599
1600 let vtxo_requests = participation.outputs.iter()
1602 .map(|req|
1603 protos::VtxoRequest {
1604 policy: req.policy.serialize(),
1605 amount: req.amount.to_sat(),
1606 })
1607 .collect::<Vec<_>>();
1608
1609 let resp = srv.client.submit_round_participation(protos::RoundParticipationRequest {
1611 input_vtxos,
1612 vtxo_requests,
1613 unblinded_mailbox_id: Some(unblinded_mailbox_id.serialize()),
1614 scheduled_height,
1615 }).await.context("error submitting round participation to server")?.into_inner();
1616
1617 let unlock_hash = UnlockHash::from_bytes(resp.unlock_hash)
1618 .context("invalid unlock hash from server")?;
1619
1620 let state = RoundState::new_delegated(
1621 participation, unlock_hash, scheduled_height, movement_id,
1622 );
1623
1624 info!("Delegated round participation submitted, it will automatically execute \
1625 when you next sync your wallet after the round happened \
1626 (and has sufficient confirmations).",
1627 );
1628
1629 let id = self.inner.db.store_round_state(&state).await?;
1630 Ok(StoredRoundState::new(id, state))
1631 }
1632
1633 pub(crate) async fn join_attempt_interactive(
1641 &self,
1642 participation: RoundParticipation,
1643 attempt: &RoundAttempt,
1644 movement_kind: Option<RoundMovement>,
1645 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1646 let movement = if let Some(kind) = movement_kind {
1647 Some(self.inner.movements.new_guarded_movement_with_update(
1648 Subsystem::ROUND,
1649 kind.to_string(),
1650 OnDropStatus::Failed,
1651 participation.to_movement_update()?,
1652 ).await?)
1653 } else {
1654 None
1655 };
1656 let movement_id = movement.as_ref().map(|m| m.id());
1657
1658 let input_ids = participation.inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
1659 self.lock_vtxos(&input_ids, movement_id.map(|m| m.into())).await
1660 .context("error locking input VTXOs")?;
1661
1662 match self.join_attempt_interactive_inner(participation, attempt, movement_id).await {
1663 Ok(state) => {
1664 if let Some(mut m) = movement {
1665 m.stop();
1666 }
1667 Ok(state)
1668 },
1669 Err(e) => {
1670 self.unlock_vtxos(&input_ids).await
1671 .context("error unlocking input VTXOs")?;
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 async fn join_attempt_interactive_inner(
1681 &self,
1682 participation: RoundParticipation,
1683 attempt: &RoundAttempt,
1684 movement_id: Option<MovementId>,
1685 ) -> anyhow::Result<StoredRoundState<Unlocked>> {
1686 let attempt_state = start_attempt(self, &participation, attempt).await?;
1690
1691 let mut state = RoundState::new_interactive(participation, movement_id);
1692 state.flow = RoundFlowState::InteractiveOngoing {
1693 round_seq: attempt.round_seq,
1694 attempt_seq: attempt.attempt_seq,
1695 state: attempt_state,
1696 };
1697
1698 let id = self.inner.db.store_round_state(&state).await?;
1699 Ok(StoredRoundState::new(id, state))
1700 }
1701
1702 pub async fn pending_round_state_ids(&self) -> anyhow::Result<Vec<RoundStateId>> {
1704 self.inner.db.get_pending_round_state_ids().await
1705 }
1706
1707 pub async fn pending_round_states(&self) -> anyhow::Result<Vec<StoredRoundState<Unlocked>>> {
1709 let ids = self.inner.db.get_pending_round_state_ids().await?;
1710 let mut states = Vec::with_capacity(ids.len());
1711 for id in ids {
1712 if let Some(state) = self.inner.db.get_round_state_by_id(id).await? {
1713 states.push(state);
1714 }
1715 }
1716 Ok(states)
1717 }
1718
1719 pub async fn pending_round_balance(&self) -> anyhow::Result<Amount> {
1721 let mut ret = Amount::ZERO;
1722 for round in self.pending_round_states().await? {
1723 ret += round.state().pending_balance();
1724 }
1725 Ok(ret)
1726 }
1727
1728 pub async fn pending_round_input_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1733 let mut ret = Vec::new();
1734 for round in self.pending_round_states().await? {
1735 let inputs = round.state().locked_pending_inputs();
1736 ret.reserve(inputs.len());
1737 for input in inputs {
1738 let v = self.get_vtxo_by_id(input.id()).await
1739 .context("unknown round input VTXO")?;
1740 ret.push(v);
1741 }
1742 }
1743 Ok(ret)
1744 }
1745
1746 pub async fn sync_pending_rounds(&self) -> anyhow::Result<HashMap<RoundStateId, RoundStatus>> {
1748 let states = self.pending_round_states().await?;
1749 if states.is_empty() {
1750 return Ok(HashMap::new());
1751 }
1752
1753 debug!("Syncing {} pending round states...", states.len());
1754
1755 let ret = Arc::new(parking_lot::Mutex::new(HashMap::with_capacity(states.len())));
1756 tokio_stream::iter(states).for_each_concurrent(10, |state| {
1757 let ret = ret.clone();
1758 async move {
1759 if state.state().ongoing_participation() {
1761 return;
1762 }
1763
1764 let mut state = match self.lock_wait_round_state(state.id()).await {
1765 Ok(Some(state)) => state,
1766 Ok(None) => return,
1767 Err(e) => {
1768 warn!("Error locking round state: {:#}", e);
1769 return;
1770 },
1771 };
1772
1773 let status = match state.state_mut().sync(self).await {
1774 Ok(s) => s,
1775 Err(e) => {
1776 warn!("Error syncing round: {:#}", e);
1777 return;
1778 },
1779 };
1780 trace!("Synced round #{}, status: {:?}", state.id(), status);
1781 match status {
1782 RoundStatus::Confirmed { funding_txid } => {
1783 info!("Round confirmed. Funding tx {}", funding_txid);
1784 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1785 warn!("Error removing confirmed round state from db: {:#}", e);
1786 }
1787 },
1788 RoundStatus::Unconfirmed { funding_txid } => {
1789 info!("Waiting for confirmations for round funding tx {}", funding_txid);
1790 if let Err(e) = self.inner.db.update_round_state(&state).await {
1791 warn!("Error updating pending round state in db: {:#}", e);
1792 }
1793 },
1794 RoundStatus::Pending => {
1795 if let Err(e) = self.inner.db.update_round_state(&state).await {
1796 warn!("Error updating pending round state in db: {:#}", e);
1797 }
1798 },
1799 RoundStatus::Failed { ref error } => {
1800 error!("Round failed: {}", error);
1801 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1802 warn!("Error removing failed round state from db: {:#}", e);
1803 }
1804 },
1805 RoundStatus::Canceled => {
1806 error!("Round canceled");
1807 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1808 warn!("Error removing canceled round state from db: {:#}", e);
1809 }
1810 },
1811 }
1812 ret.lock().insert(state.id(), status);
1813 }
1814 }).await;
1815
1816 Ok(Arc::into_inner(ret).expect("only ref left").into_inner())
1817 }
1818
1819 async fn get_last_round_event(&self) -> anyhow::Result<RoundEvent> {
1821 let (mut srv, _) = self.require_server().await?;
1822 let e = srv.client.last_round_event(protos::Empty {}).await?.into_inner();
1823 Ok(RoundEvent::try_from(e).context("invalid event format from server")?)
1824 }
1825
1826 async fn inner_process_event(
1827 &self,
1828 state: &mut StoredRoundState,
1829 event: Option<&RoundEvent>,
1830 ) {
1831 if let Some(event) = event && state.state().ongoing_participation() {
1832 let updated = state.state_mut().process_event(self, &event).await;
1833 if updated {
1834 if let Err(e) = self.inner.db.update_round_state(&state).await {
1835 error!("Error storing round state #{} after progress: {:#}", state.id(), e);
1836 }
1837 }
1838 }
1839
1840 match state.state_mut().sync(self).await {
1841 Err(e) => warn!("Error syncing round #{}: {:#}", state.id(), e),
1842 Ok(s) if s.is_final() => {
1843 info!("Round #{} finished with result: {:?}", state.id(), s);
1844 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1845 warn!("Failed to remove finished round #{} from db: {:#}", state.id(), e);
1846 }
1847 },
1848 Ok(s) => {
1849 trace!("Round state #{} is now in state {:?}", state.id(), s);
1850 if let Err(e) = self.inner.db.update_round_state(&state).await {
1851 warn!("Error storing round state #{}: {:#}", state.id(), e);
1852 }
1853 },
1854 }
1855 }
1856
1857 pub async fn progress_pending_rounds(
1862 &self,
1863 last_round_event: Option<&RoundEvent>,
1864 ) -> anyhow::Result<()> {
1865 let states = self.pending_round_states().await?;
1866 if states.is_empty() {
1867 return Ok(());
1868 }
1869
1870 info!("Processing {} rounds...", states.len());
1871
1872 let mut last_round_event = last_round_event.map(|e| Cow::Borrowed(e));
1873
1874 let has_ongoing_participation = states.iter()
1875 .any(|s| s.state().ongoing_participation());
1876 if has_ongoing_participation && last_round_event.is_none() {
1877 match self.get_last_round_event().await {
1878 Ok(e) => last_round_event = Some(Cow::Owned(e)),
1879 Err(e) => {
1880 warn!("Error fetching round event, \
1881 failed to progress ongoing rounds: {:#}", e);
1882 },
1883 }
1884 }
1885
1886 let event = last_round_event.as_ref().map(|c| c.as_ref());
1887
1888 let futs = states.into_iter().map(async |state| {
1889 let locked = self.lock_wait_round_state(state.id()).await?;
1890 if let Some(mut locked) = locked {
1891 self.inner_process_event(&mut locked, event).await;
1892 }
1893 Ok::<_, anyhow::Error>(())
1894 });
1895
1896 futures::future::join_all(futs).await;
1897
1898 Ok(())
1899 }
1900
1901 pub async fn subscribe_round_events(&self)
1902 -> anyhow::Result<impl Stream<Item = anyhow::Result<RoundEvent>> + Unpin + use<>>
1903 {
1904 let (mut srv, _) = self.require_server().await?;
1905 let mut req = tonic::IntoRequest::into_request(protos::Empty {});
1906 req.set_timeout(SUBSCRIBE_REQUEST_TIMEOUT);
1907 let events = srv.client.subscribe_rounds(req).await?
1908 .into_inner().map(|m| {
1909 let m = m.context("received error on event stream")?;
1910 let e = RoundEvent::try_from(m.clone())
1911 .with_context(|| format!("error converting rpc round event: {:?}", m))?;
1912 trace!("Received round event: {}", e);
1913 Ok::<_, anyhow::Error>(e)
1914 });
1915 Ok(events)
1916 }
1917
1918 pub async fn participate_ongoing_rounds(&self) -> anyhow::Result<()> {
1923 let mut events = self.subscribe_round_events().await?;
1924
1925 loop {
1926 let state_ids = self.pending_round_states().await?.iter()
1929 .filter(|s| s.state().ongoing_participation())
1930 .map(|s| s.id())
1931 .collect::<Vec<_>>();
1932
1933 if state_ids.is_empty() {
1934 info!("All rounds handled");
1935 return Ok(());
1936 }
1937
1938 let event = events.next().await
1939 .context("events stream broke")?
1940 .context("error on event stream")?;
1941
1942 let futs = state_ids.into_iter().map(async |state| {
1943 let locked = self.lock_wait_round_state(state).await?;
1944 if let Some(mut locked) = locked {
1945 self.inner_process_event(&mut locked, Some(&event)).await;
1946 }
1947 Ok::<_, anyhow::Error>(())
1948 });
1949
1950 futures::future::join_all(futs).await;
1951 }
1952 }
1953
1954 pub async fn cancel_all_pending_rounds(&self) -> anyhow::Result<()> {
1959 let state_ids = self.inner.db.get_pending_round_state_ids().await?;
1961
1962 let futures = state_ids.into_iter().map(|state_id| {
1963 async move {
1964 let mut state = match self.lock_wait_round_state(state_id).await {
1966 Ok(Some(s)) => s,
1967 Ok(None) => return,
1968 Err(e) => return warn!("Error loading round state #{}: {:#}", state_id, e),
1969 };
1970
1971 match state.state_mut().try_cancel(self).await {
1972 Ok(true) => {
1973 if let Err(e) = self.inner.db.remove_round_state(&state).await {
1974 warn!("Error removing canceled round state from db: {:#}", e);
1975 }
1976 },
1977 Ok(false) => {},
1978 Err(e) => warn!("Error trying to cancel round #{}: {:#}", state_id, e),
1979 }
1980 }
1981 });
1982
1983 join_all(futures).await;
1984
1985 Ok(())
1986 }
1987
1988 pub async fn cancel_pending_round(&self, id: RoundStateId) -> anyhow::Result<()> {
1990 let mut state = self.lock_wait_round_state(id).await?
1991 .context("round state not found")?;
1992
1993 if state.state_mut().try_cancel(self).await.context("failed to cancel round")? {
1994 self.inner.db.remove_round_state(&state).await
1995 .context("error removing canceled round state from db")?;
1996 } else {
1997 bail!("failed to cancel round");
1998 }
1999
2000 Ok(())
2001 }
2002
2003 pub(crate) async fn participate_round(
2010 &self,
2011 participation: RoundParticipation,
2012 movement_kind: Option<RoundMovement>,
2013 ) -> anyhow::Result<RoundStatus> {
2014 let state = self.join_next_round(participation, movement_kind).await?;
2015
2016 info!("Waiting for a round start...");
2017 let mut events = self.subscribe_round_events().await?;
2018
2019 self.drive_round_state(state, &mut events).await
2020 }
2021
2022 pub(crate) async fn drive_round_state<S>(
2030 &self,
2031 mut state: StoredRoundState,
2032 events: &mut S,
2033 ) -> anyhow::Result<RoundStatus>
2034 where
2035 S: Stream<Item = anyhow::Result<RoundEvent>> + Unpin,
2036 {
2037 loop {
2038 if !state.state().ongoing_participation() {
2039 let status = state.state_mut().sync(self).await?;
2040 match status {
2041 RoundStatus::Failed { error } => bail!("round failed: {}", error),
2042 RoundStatus::Canceled => bail!("round canceled"),
2043 status => return Ok(status),
2044 }
2045 }
2046
2047 let event = events.next().await
2048 .context("events stream broke")?
2049 .context("error on event stream")?;
2050 if state.state_mut().process_event(self, &event).await {
2051 self.inner.db.update_round_state(&state).await?;
2052 }
2053 }
2054 }
2055}
2056
2057#[cfg(test)]
2058mod test {
2059 use super::*;
2060
2061 use bitcoin::secp256k1::Secp256k1;
2062
2063 use ark::VtxoPolicy;
2064 use ark::tree::signed::{HashlockVersion, UnlockPreimage};
2065 use ark::vtxo::policy::MAX_BLOCK_DELTA;
2066
2067 fn pubkey() -> bitcoin::secp256k1::PublicKey {
2068 let secp = Secp256k1::new();
2069 Keypair::new(&secp, &mut rand::thread_rng()).public_key()
2070 }
2071
2072 fn nonces() -> Vec<Vec<SecretNonce>> {
2073 let secp = Secp256k1::new();
2074 let key = Keypair::new(&secp, &mut rand::thread_rng());
2075 vec![vec![musig::nonce_pair(&key).0, musig::nonce_pair(&key).0]]
2079 }
2080
2081 #[test]
2082 fn accepts_hash_locked_leaves_of_both_versions() {
2083 let secp = Secp256k1::new();
2084 let mut rng = rand::thread_rng();
2085 let user_key = Keypair::new(&secp, &mut rng);
2086 let user_cosign_key = Keypair::new(&secp, &mut rng);
2087 let server_key = Keypair::new(&secp, &mut rng);
2088 let server_cosign_key = Keypair::new(&secp, &mut rng);
2089
2090 let preimage: UnlockPreimage = rand::random();
2091 let unlock_hash = UnlockHash::hash(&preimage);
2092
2093 let outputs = (0..2u64).map(|i| VtxoRequest {
2094 amount: Amount::from_sat(10_000 + i),
2095 policy: VtxoPolicy::new_pubkey(user_key.public_key()),
2096 }).collect::<Vec<_>>();
2097
2098 for version in [HashlockVersion::V0, HashlockVersion::V1] {
2102 let (tree, funding_tx) = ark::test_util::build_signed_tree(
2103 version, outputs.iter().cloned(),
2104 &user_cosign_key, &server_key, &server_cosign_key, unlock_hash,
2105 );
2106 for vtxo in tree.into_cached_tree().output_vtxos() {
2107 check_vtxo_fails_hash_lock(&funding_tx, &vtxo).unwrap_or_else(|e| panic!(
2108 "locked {:?} leaf vtxo should be accepted: {:#}", version, e,
2109 ));
2110 }
2111 }
2112 }
2113
2114 #[test]
2115 fn vtxo_tree_params_accepts_honest_and_rejects_hostile() {
2116 let secp = Secp256k1::new();
2117 let mut rng = rand::thread_rng();
2118 let server = Keypair::new(&secp, &mut rng).public_key();
2119 let other = Keypair::new(&secp, &mut rng).public_key();
2120
2121 let exit_delta: BlockDelta = 144;
2122 let min_expiry: BlockHeight = 100_000;
2123
2124 validate_vtxo_tree_params(server, exit_delta, min_expiry, server, exit_delta, min_expiry)
2126 .expect("expiry exactly at the minimum should validate");
2127 validate_vtxo_tree_params(
2128 server, exit_delta, min_expiry + 5_000, server, exit_delta, min_expiry,
2129 ).expect("expiry above the minimum should validate");
2130
2131 assert!(validate_vtxo_tree_params(
2133 other, exit_delta, min_expiry, server, exit_delta, min_expiry,
2134 ).is_err(), "wrong server pubkey must be rejected");
2135
2136 assert!(validate_vtxo_tree_params(
2138 server, MAX_BLOCK_DELTA, min_expiry, server, exit_delta, min_expiry,
2139 ).is_err(), "inflated exit delta must be rejected");
2140
2141 assert!(validate_vtxo_tree_params(
2144 server, exit_delta, min_expiry - 1, server, exit_delta, min_expiry,
2145 ).is_err(), "expiry one block below the minimum must be rejected");
2146 }
2147
2148 #[test]
2149 fn stash_and_take() {
2150 let store = RoundSecretNonces::new();
2151 let k = pubkey();
2152 store.stash(k, nonces());
2153
2154 assert!(store.take(&k).is_some());
2155 }
2156
2157 #[test]
2158 fn cannot_take_twice() {
2159 let store = RoundSecretNonces::new();
2160 let k = pubkey();
2161 store.stash(k, nonces());
2162
2163 assert!(store.take(&k).is_some());
2164 assert!(store.take(&k).is_none());
2165 }
2166
2167 #[test]
2168 fn cannot_take_after_forget() {
2169 let store = RoundSecretNonces::new();
2170 let k = pubkey();
2171 store.stash(k, nonces());
2172 store.forget(&k);
2173
2174 assert!(store.take(&k).is_none());
2175 }
2176
2177 #[test]
2178 fn stash_overrides_stash() {
2179 let secp = Secp256k1::new();
2180 let key = Keypair::new(&secp, &mut rand::thread_rng());
2181 let nonces_1 = vec![vec![musig::nonce_pair(&key).0]];
2182 let nonces_2 = vec![];
2183
2184 let store = RoundSecretNonces::new();
2185 store.stash(key.public_key(), nonces_1);
2186 store.stash(key.public_key(), nonces_2);
2187
2188 let taken = store.take(&key.public_key()).expect("nonces present");
2189 assert_eq!(taken.len(), 0);
2190 }
2191}