diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index e54bc988..d1d0bc43 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -1,12 +1,12 @@ +use crate::bitcoin; use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, TX_FEE, }; use ::bitcoin::util::bip143::SigHashCache; -use ::bitcoin::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid}; +use ::bitcoin::{OutPoint, Script, SigHash, SigHashType, TxIn, TxOut, Txid}; use anyhow::Result; -use bitcoin::Script; use ecdsa_fun::Signature; use miniscript::{Descriptor, DescriptorTrait}; use serde::{Deserialize, Serialize}; @@ -149,7 +149,39 @@ impl TxCancel { OutPoint::new(self.inner.txid(), 0) } - pub fn add_signatures( + pub fn complete_as_alice( + self, + a: bitcoin::SecretKey, + B: bitcoin::PublicKey, + tx_cancel_sig_B: bitcoin::Signature, + ) -> Result { + let sig_a = a.sign(self.digest()); + let sig_b = tx_cancel_sig_B; + + let tx_cancel = self + .add_signatures((a.public(), sig_a), (B, sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + Ok(tx_cancel) + } + + pub fn complete_as_bob( + self, + A: bitcoin::PublicKey, + b: bitcoin::SecretKey, + tx_cancel_sig_A: bitcoin::Signature, + ) -> Result { + let sig_a = tx_cancel_sig_A; + let sig_b = b.sign(self.digest()); + + let tx_cancel = self + .add_signatures((A, sig_a), (b.public(), sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + Ok(tx_cancel) + } + + fn add_signatures( self, (A, sig_a): (PublicKey, Signature), (B, sig_b): (PublicKey, Signature), diff --git a/swap/src/bitcoin/refund.rs b/swap/src/bitcoin/refund.rs index 3d282c99..34057c79 100644 --- a/swap/src/bitcoin/refund.rs +++ b/swap/src/bitcoin/refund.rs @@ -3,10 +3,10 @@ use crate::bitcoin::{ verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs, Transaction, TxCancel, }; +use crate::{bitcoin, monero}; use ::bitcoin::util::bip143::SigHashCache; -use ::bitcoin::{SigHash, SigHashType, Txid}; +use ::bitcoin::{Script, SigHash, SigHashType, Txid}; use anyhow::{bail, Context, Result}; -use bitcoin::Script; use ecdsa_fun::Signature; use miniscript::{Descriptor, DescriptorTrait}; use std::collections::HashMap; @@ -77,7 +77,31 @@ impl TxRefund { Ok(tx_refund) } - pub fn extract_signature_by_key( + pub fn extract_monero_private_key( + &self, + published_refund_tx: bitcoin::Transaction, + s_a: monero::Scalar, + a: bitcoin::SecretKey, + S_b_bitcoin: bitcoin::PublicKey, + ) -> Result { + let s_a = monero::PrivateKey { scalar: s_a }; + + let tx_refund_sig = self + .extract_signature_by_key(published_refund_tx, a.public()) + .context("Failed to extract signature from Bitcoin refund tx")?; + let tx_refund_encsig = a.encsign(S_b_bitcoin, self.digest()); + + let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig) + .context("Failed to recover Monero secret key from Bitcoin signature")?; + + let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); + + let spend_key = s_a + s_b; + + Ok(spend_key) + } + + fn extract_signature_by_key( &self, candidate_transaction: Transaction, B: PublicKey, diff --git a/swap/src/database/bob.rs b/swap/src/database/bob.rs index 943957dd..322e21ba 100644 --- a/swap/src/database/bob.rs +++ b/swap/src/database/bob.rs @@ -29,8 +29,8 @@ pub enum Bob { state4: bob::State4, }, BtcRedeemed(bob::State5), - CancelTimelockExpired(bob::State4), - BtcCancelled(bob::State4), + CancelTimelockExpired(bob::State6), + BtcCancelled(bob::State6), Done(BobEndState), } @@ -38,7 +38,7 @@ pub enum Bob { pub enum BobEndState { SafelyAborted, XmrRedeemed { tx_lock_id: bitcoin::Txid }, - BtcRefunded(Box), + BtcRefunded(Box), BtcPunished { tx_lock_id: bitcoin::Txid }, } @@ -60,9 +60,9 @@ impl From for Bob { BobState::XmrLocked(state4) => Bob::XmrLocked { state4 }, BobState::EncSigSent(state4) => Bob::EncSigSent { state4 }, BobState::BtcRedeemed(state5) => Bob::BtcRedeemed(state5), - BobState::CancelTimelockExpired(state4) => Bob::CancelTimelockExpired(state4), - BobState::BtcCancelled(state4) => Bob::BtcCancelled(state4), - BobState::BtcRefunded(state4) => Bob::Done(BobEndState::BtcRefunded(Box::new(state4))), + BobState::CancelTimelockExpired(state6) => Bob::CancelTimelockExpired(state6), + BobState::BtcCancelled(state6) => Bob::BtcCancelled(state6), + BobState::BtcRefunded(state6) => Bob::Done(BobEndState::BtcRefunded(Box::new(state6))), BobState::XmrRedeemed { tx_lock_id } => { Bob::Done(BobEndState::XmrRedeemed { tx_lock_id }) } @@ -92,12 +92,12 @@ impl From for BobState { Bob::XmrLocked { state4 } => BobState::XmrLocked(state4), Bob::EncSigSent { state4 } => BobState::EncSigSent(state4), Bob::BtcRedeemed(state5) => BobState::BtcRedeemed(state5), - Bob::CancelTimelockExpired(state4) => BobState::CancelTimelockExpired(state4), - Bob::BtcCancelled(state4) => BobState::BtcCancelled(state4), + Bob::CancelTimelockExpired(state6) => BobState::CancelTimelockExpired(state6), + Bob::BtcCancelled(state6) => BobState::BtcCancelled(state6), Bob::Done(end_state) => match end_state { BobEndState::SafelyAborted => BobState::SafelyAborted, BobEndState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, - BobEndState::BtcRefunded(state4) => BobState::BtcRefunded(*state4), + BobEndState::BtcRefunded(state6) => BobState::BtcRefunded(*state6), BobEndState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id }, }, } diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 77b7dae9..407f7aa2 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -182,7 +182,7 @@ impl fmt::Display for TxHash { } #[derive(Debug, Clone, Copy, thiserror::Error)] -#[error("transaction does not pay enough: expected {expected}, got {actual}")] +#[error("expected {expected}, got {actual}")] pub struct InsufficientFunds { pub expected: Amount, pub actual: Amount, diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index 764235c9..db9c7c57 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -120,12 +120,13 @@ impl Wallet { Ok(()) } - pub async fn transfer( - &self, - public_spend_key: PublicKey, - public_view_key: PublicViewKey, - amount: Amount, - ) -> Result { + pub async fn transfer(&self, request: TransferRequest) -> Result { + let TransferRequest { + public_spend_key, + public_view_key, + amount, + } = request; + let destination_address = Address::standard(self.network, public_spend_key, public_view_key.into()); @@ -149,14 +150,15 @@ impl Wallet { )) } - pub async fn watch_for_transfer( - &self, - public_spend_key: PublicKey, - public_view_key: PublicViewKey, - transfer_proof: TransferProof, - expected: Amount, - conf_target: u32, - ) -> Result<(), InsufficientFunds> { + pub async fn watch_for_transfer(&self, request: WatchRequest) -> Result<()> { + let WatchRequest { + conf_target, + public_view_key, + public_spend_key, + transfer_proof, + expected, + } = request; + let txid = transfer_proof.tx_hash(); tracing::info!(%txid, "Waiting for {} confirmation{} of Monero transaction", conf_target, if conf_target > 1 { "s" } else { "" }); @@ -222,6 +224,22 @@ impl Wallet { } } +#[derive(Debug)] +pub struct TransferRequest { + pub public_spend_key: PublicKey, + pub public_view_key: PublicViewKey, + pub amount: Amount, +} + +#[derive(Debug)] +pub struct WatchRequest { + pub public_spend_key: PublicKey, + pub public_view_key: PublicViewKey, + pub transfer_proof: TransferProof, + pub conf_target: u32, + pub expected: Amount, +} + async fn wait_for_confirmations( txid: String, fetch_tx: impl Fn(String) -> Fut, diff --git a/swap/src/protocol/alice.rs b/swap/src/protocol/alice.rs index 351cef71..28ef4bce 100644 --- a/swap/src/protocol/alice.rs +++ b/swap/src/protocol/alice.rs @@ -19,7 +19,6 @@ mod encrypted_signature; pub mod event_loop; mod execution_setup; pub mod state; -mod steps; pub mod swap; mod transfer_proof; diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index 34b196e5..c04dbdb8 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -42,12 +42,6 @@ pub struct EventLoop { swap_sender: mpsc::Sender, } -#[derive(Debug)] -pub struct EventLoopHandle { - recv_encrypted_signature: Option>, - send_transfer_proof: Option>, -} - impl EventLoop where LR: LatestRate, @@ -310,22 +304,30 @@ impl LatestRate for kraken::RateUpdateStream { } } +#[derive(Debug)] +pub struct EventLoopHandle { + recv_encrypted_signature: Option>, + send_transfer_proof: Option>, +} + impl EventLoopHandle { - pub async fn recv_encrypted_signature(&mut self) -> Result { + pub async fn recv_encrypted_signature(&mut self) -> Result { let signature = self .recv_encrypted_signature .take() .context("Encrypted signature was already received")? - .await?; + .await? + .tx_redeem_encsig; Ok(signature) } - pub async fn send_transfer_proof(&mut self, msg: TransferProof) -> Result<()> { + + pub async fn send_transfer_proof(&mut self, msg: monero::TransferProof) -> Result<()> { if self .send_transfer_proof .take() .context("Transfer proof was already sent")? - .send(msg) + .send(TransferProof { tx_lock_proof: msg }) .is_err() { bail!("Failed to send transfer proof, receiver no longer listening?") diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index e961a13b..bc443f03 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -2,6 +2,7 @@ use crate::bitcoin::{ current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund, }; use crate::env::Config; +use crate::monero::wallet::TransferRequest; use crate::protocol::alice::{Message1, Message3}; use crate::protocol::bob::{Message0, Message2, Message4}; use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; @@ -343,6 +344,19 @@ impl State3 { )) } + pub fn lock_xmr_transfer_request(&self) -> TransferRequest { + let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a }); + + let public_spend_key = S_a + self.S_b_monero; + let public_view_key = self.v.public(); + + TransferRequest { + public_spend_key, + public_view_key, + amount: self.xmr, + } + } + pub fn tx_cancel(&self) -> TxCancel { TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B) } diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs deleted file mode 100644 index eec8a86b..00000000 --- a/swap/src/protocol/alice/steps.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::bitcoin::{ - CancelTimelock, EncryptedSignature, PunishTimelock, TxCancel, TxLock, TxRefund, -}; -use crate::protocol::alice; -use crate::protocol::alice::event_loop::EventLoopHandle; -use crate::protocol::alice::TransferProof; -use crate::{bitcoin, monero}; -use anyhow::{bail, Context, Result}; -use futures::pin_mut; - -pub async fn lock_xmr( - state3: alice::State3, - event_loop_handle: &mut EventLoopHandle, - monero_wallet: &monero::Wallet, -) -> Result<()> { - let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: state3.s_a }); - - let public_spend_key = S_a + state3.S_b_monero; - let public_view_key = state3.v.public(); - - let transfer_proof = monero_wallet - .transfer(public_spend_key, public_view_key, state3.xmr) - .await?; - - // TODO(Franck): Wait for Monero to be confirmed once - // Waiting for XMR confirmations should not be done in here, but in a separate - // state! We have to record that Alice has already sent the transaction. - // Otherwise Alice might publish the lock tx twice! - - event_loop_handle - .send_transfer_proof(TransferProof { - tx_lock_proof: transfer_proof, - }) - .await?; - - Ok(()) -} - -pub async fn wait_for_bitcoin_encrypted_signature( - event_loop_handle: &mut EventLoopHandle, -) -> Result { - let msg3 = event_loop_handle - .recv_encrypted_signature() - .await - .context("Failed to receive Bitcoin encrypted signature from Bob")?; - - tracing::debug!("Message 3 received, returning it"); - - Ok(msg3.tx_redeem_encsig) -} - -pub async fn publish_cancel_transaction( - tx_lock: TxLock, - a: bitcoin::SecretKey, - B: bitcoin::PublicKey, - cancel_timelock: CancelTimelock, - tx_cancel_sig_bob: bitcoin::Signature, - bitcoin_wallet: &bitcoin::Wallet, -) -> Result<()> { - bitcoin_wallet - .watch_until_status(&tx_lock, |status| status.is_confirmed_with(cancel_timelock)) - .await?; - - let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); - - // If Bob hasn't yet broadcasted the tx cancel, we do it - if bitcoin_wallet - .get_raw_transaction(tx_cancel.txid()) - .await - .is_err() - { - // TODO(Franck): Maybe the cancel transaction is already mined, in this case, - // the broadcast will error out. - - let sig_a = a.sign(tx_cancel.digest()); - let sig_b = tx_cancel_sig_bob.clone(); - - let tx_cancel = tx_cancel - .add_signatures((a.public(), sig_a), (B, sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel"); - - // TODO(Franck): Error handling is delicate, why can't we broadcast? - let (..) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?; - - // TODO(Franck): Wait until transaction is mined and returned mined - // block height - } - - Ok(()) -} - -pub async fn wait_for_bitcoin_refund( - tx_cancel: &TxCancel, - tx_refund: &TxRefund, - punish_timelock: PunishTimelock, - bitcoin_wallet: &bitcoin::Wallet, -) -> Result> { - let refund_tx_id = tx_refund.txid(); - let seen_refund_tx = - bitcoin_wallet.watch_until_status(tx_refund, |status| status.has_been_seen()); - - let punish_timelock_expired = bitcoin_wallet.watch_until_status(tx_cancel, |status| { - status.is_confirmed_with(punish_timelock) - }); - - pin_mut!(punish_timelock_expired); - pin_mut!(seen_refund_tx); - - tokio::select! { - seen_refund = seen_refund_tx => { - match seen_refund { - Ok(()) => { - let published_refund_tx = bitcoin_wallet.get_raw_transaction(refund_tx_id).await?; - - Ok(Some(published_refund_tx)) - } - Err(e) => { - bail!(e.context("Failed to monitor refund transaction")) - } - } - } - _ = punish_timelock_expired => { - Ok(None) - } - } -} - -pub fn extract_monero_private_key( - published_refund_tx: bitcoin::Transaction, - tx_refund: &TxRefund, - s_a: monero::Scalar, - a: bitcoin::SecretKey, - S_b_bitcoin: bitcoin::PublicKey, -) -> Result { - let s_a = monero::PrivateKey { scalar: s_a }; - - let tx_refund_sig = tx_refund - .extract_signature_by_key(published_refund_tx, a.public()) - .context("Failed to extract signature from Bitcoin refund tx")?; - let tx_refund_encsig = a.encsign(S_b_bitcoin, tx_refund.digest()); - - let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig) - .context("Failed to recover Monero secret key from Bitcoin signature")?; - let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); - - let spend_key = s_a + s_b; - - Ok(spend_key) -} diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index be8c5306..baa734d7 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -6,18 +6,13 @@ use crate::env::Config; use crate::monero_ext::ScalarExt; use crate::protocol::alice; use crate::protocol::alice::event_loop::EventLoopHandle; -use crate::protocol::alice::steps::{ - extract_monero_private_key, lock_xmr, publish_cancel_transaction, - wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, -}; use crate::protocol::alice::AliceState; use crate::{bitcoin, database, monero}; use anyhow::{bail, Context, Result}; use async_recursion::async_recursion; -use futures::future::{select, Either}; -use futures::pin_mut; use rand::{CryptoRng, RngCore}; use std::sync::Arc; +use tokio::select; use tokio::time::timeout; use tracing::{error, info}; use uuid::Uuid; @@ -73,382 +68,275 @@ async fn run_until_internal( ) -> Result { info!("Current state: {}", state); if is_target_state(&state) { - Ok(state) - } else { - match state { - AliceState::Started { state3 } => { - timeout( - env_config.bob_time_to_act, - bitcoin_wallet - .watch_until_status(&state3.tx_lock, |status| status.has_been_seen()), - ) - .await - .context("Failed to find lock Bitcoin tx")??; + return Ok(state); + } - bitcoin_wallet - .watch_until_status(&state3.tx_lock, |status| { - status.is_confirmed_with(env_config.bitcoin_finality_confirmations) - }) - .await?; + let new_state = match state { + AliceState::Started { state3 } => { + timeout( + env_config.bob_time_to_act, + bitcoin_wallet.watch_until_status(&state3.tx_lock, |status| status.has_been_seen()), + ) + .await + .context("Failed to find lock Bitcoin tx")??; - let state = AliceState::BtcLocked { state3 }; + bitcoin_wallet + .watch_until_status(&state3.tx_lock, |status| { + status.is_confirmed_with(env_config.bitcoin_finality_confirmations) + }) + .await?; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - AliceState::BtcLocked { state3 } => { - // Record the current monero wallet block height so we don't have to scan from - // block 0 for scenarios where we create a refund wallet. - let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; + AliceState::BtcLocked { state3 } + } + AliceState::BtcLocked { state3 } => { + // Record the current monero wallet block height so we don't have to scan from + // block 0 for scenarios where we create a refund wallet. + let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; - lock_xmr(*state3.clone(), &mut event_loop_handle, &monero_wallet).await?; + let transfer_proof = monero_wallet + .transfer(state3.lock_xmr_transfer_request()) + .await?; - let state = AliceState::XmrLocked { - state3, - monero_wallet_restore_blockheight, - }; + // TODO(Franck): Wait for Monero to be confirmed once + // Waiting for XMR confirmations should not be done in here, but in a separate + // state! We have to record that Alice has already sent the transaction. + // Otherwise Alice might publish the lock tx twice! + + event_loop_handle + .send_transfer_proof(transfer_proof) + .await?; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } AliceState::XmrLocked { state3, monero_wallet_restore_blockheight, - } => { - let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { - ExpiredTimelocks::None => { - let wait_for_enc_sig = - wait_for_bitcoin_encrypted_signature(&mut event_loop_handle); - let state3_clone = state3.clone(); - let cancel_timelock_expires = state3_clone - .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - pin_mut!(wait_for_enc_sig); - pin_mut!(cancel_timelock_expires); - - match select(cancel_timelock_expires, wait_for_enc_sig).await { - Either::Left(_) => AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, - }, - Either::Right((enc_sig, _)) => AliceState::EncSigLearned { - state3, - encrypted_signature: Box::new(enc_sig?), - monero_wallet_restore_blockheight, - }, + } + } + AliceState::XmrLocked { + state3, + monero_wallet_restore_blockheight, + } => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { + ExpiredTimelocks::None => { + select! { + _ = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { + AliceState::CancelTimelockExpired { + state3, + monero_wallet_restore_blockheight, } } - _ => AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, - }, - }; + enc_sig = event_loop_handle.recv_encrypted_signature() => { + tracing::info!("Received encrypted signature"); - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet.clone(), - monero_wallet, - env_config, - swap_id, - db, - ) - .await + AliceState::EncSigLearned { + state3, + encrypted_signature: Box::new(enc_sig?), + monero_wallet_restore_blockheight, + } + } + } } - AliceState::EncSigLearned { + _ => AliceState::CancelTimelockExpired { state3, - encrypted_signature, monero_wallet_restore_blockheight, - } => { - let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { - ExpiredTimelocks::None => { - match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete( - *encrypted_signature, - state3.a.clone(), - state3.s_a.to_secpfun_scalar(), - state3.B, - ) { - Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { - Ok((_, finality)) => match finality.await { - Ok(_) => AliceState::BtcRedeemed, - Err(e) => { - bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e) - } - }, - Err(e) => { - error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e); - state3 - .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) - .await?; - - AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, - } - } - }, + }, + }, + AliceState::EncSigLearned { + state3, + encrypted_signature, + monero_wallet_restore_blockheight, + } => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { + ExpiredTimelocks::None => { + match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete( + *encrypted_signature, + state3.a.clone(), + state3.s_a.to_secpfun_scalar(), + state3.B, + ) { + Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { + Ok((_, finality)) => match finality.await { + Ok(_) => AliceState::BtcRedeemed, Err(e) => { - error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", e); - state3 - .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) - .await?; + bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e) + } + }, + Err(e) => { + error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e); + state3 + .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) + .await?; - AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, - } + AliceState::CancelTimelockExpired { + state3, + monero_wallet_restore_blockheight, } } - } - _ => AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, }, - }; + Err(e) => { + error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", e); + state3 + .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) + .await?; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - env_config, - swap_id, - db, - ) - .await + AliceState::CancelTimelockExpired { + state3, + monero_wallet_restore_blockheight, + } + } + } } - AliceState::CancelTimelockExpired { + _ => AliceState::CancelTimelockExpired { state3, monero_wallet_restore_blockheight, - } => { - publish_cancel_transaction( - state3.tx_lock.clone(), - state3.a.clone(), - state3.B, - state3.cancel_timelock, - state3.tx_cancel_sig_bob.clone(), - &bitcoin_wallet, - ) - .await?; + }, + }, + AliceState::CancelTimelockExpired { + state3, + monero_wallet_restore_blockheight, + } => { + let tx_cancel = state3.tx_cancel(); - let state = AliceState::BtcCancelled { - state3, - monero_wallet_restore_blockheight, - }; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - env_config, - swap_id, - db, - ) + // If Bob hasn't yet broadcasted the tx cancel, we do it + if bitcoin_wallet + .get_raw_transaction(tx_cancel.txid()) .await + .is_err() + { + let transaction = tx_cancel + .complete_as_alice(state3.a.clone(), state3.B, state3.tx_cancel_sig_bob.clone()) + .context("Failed to complete Bitcoin cancel transaction")?; + + if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await { + tracing::debug!( + "Assuming transaction is already broadcasted because: {:#}", + e + ) + } + + // TODO(Franck): Wait until transaction is mined and + // returned mined block height } + AliceState::BtcCancelled { state3, monero_wallet_restore_blockheight, - } => { - let published_refund_tx = wait_for_bitcoin_refund( - &state3.tx_cancel(), - &state3.tx_refund(), - state3.punish_timelock, - &bitcoin_wallet, - ) + } + } + AliceState::BtcCancelled { + state3, + monero_wallet_restore_blockheight, + } => { + let tx_refund = state3.tx_refund(); + let tx_cancel = state3.tx_cancel(); + + let seen_refund_tx = + bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen()); + + let punish_timelock_expired = bitcoin_wallet.watch_until_status(&tx_cancel, |status| { + status.is_confirmed_with(state3.punish_timelock) + }); + + select! { + seen_refund = seen_refund_tx => { + seen_refund.context("Failed to monitor refund transaction")?; + let published_refund_tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + + let spend_key = tx_refund.extract_monero_private_key( + published_refund_tx, + state3.s_a, + state3.a.clone(), + state3.S_b_bitcoin, + )?; + + AliceState::BtcRefunded { + spend_key, + state3, + monero_wallet_restore_blockheight, + } + } + _ = punish_timelock_expired => { + AliceState::BtcPunishable { + state3, + monero_wallet_restore_blockheight, + } + } + } + } + AliceState::BtcRefunded { + spend_key, + state3, + monero_wallet_restore_blockheight, + } => { + let view_key = state3.v; + + monero_wallet + .create_from(spend_key, view_key, monero_wallet_restore_blockheight) .await?; - // TODO(Franck): Review error handling - match published_refund_tx { - None => { - let state = AliceState::BtcPunishable { - state3, - monero_wallet_restore_blockheight, - }; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet.clone(), - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - Some(published_refund_tx) => { - let spend_key = extract_monero_private_key( - published_refund_tx, - &state3.tx_refund(), - state3.s_a, - state3.a.clone(), - state3.S_b_bitcoin, - )?; - - let state = AliceState::BtcRefunded { - spend_key, - state3, - monero_wallet_restore_blockheight, - }; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet.clone(), - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - } - } - AliceState::BtcRefunded { - spend_key, - state3, - monero_wallet_restore_blockheight, - } => { - let view_key = state3.v; - - monero_wallet - .create_from(spend_key, view_key, monero_wallet_restore_blockheight) - .await?; - - let state = AliceState::XmrRefunded; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - Ok(state) - } - AliceState::BtcPunishable { - state3, - monero_wallet_restore_blockheight, - } => { - let signed_tx_punish = state3.tx_punish().complete( - state3.tx_punish_sig_bob.clone(), - state3.a.clone(), - state3.B, - )?; - - let punish_tx_finalised = async { - let (txid, finality) = - bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; - - finality.await?; - - Result::<_, anyhow::Error>::Ok(txid) - }; - - let tx_refund = state3.tx_refund(); - let refund_tx_seen = - bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen()); - - pin_mut!(punish_tx_finalised); - pin_mut!(refund_tx_seen); - - match select(refund_tx_seen, punish_tx_finalised).await { - Either::Left((Ok(()), _)) => { - let published_refund_tx = - bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; - - let spend_key = extract_monero_private_key( - published_refund_tx, - &tx_refund, - state3.s_a, - state3.a.clone(), - state3.S_b_bitcoin, - )?; - let state = AliceState::BtcRefunded { - spend_key, - state3, - monero_wallet_restore_blockheight, - }; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet.clone(), - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - Either::Left((Err(e), _)) => { - bail!(e.context("Failed to monitor refund transaction")) - } - Either::Right(_) => { - let state = AliceState::BtcPunished; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet.clone(), - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - } - } - AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), - AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), - AliceState::BtcPunished => Ok(AliceState::BtcPunished), - AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), + AliceState::XmrRefunded } - } + AliceState::BtcPunishable { + state3, + monero_wallet_restore_blockheight, + } => { + let signed_tx_punish = state3.tx_punish().complete( + state3.tx_punish_sig_bob.clone(), + state3.a.clone(), + state3.B, + )?; + + let punish_tx_finalised = async { + let (txid, finality) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; + + finality.await?; + + Result::<_, anyhow::Error>::Ok(txid) + }; + + let tx_refund = state3.tx_refund(); + let refund_tx_seen = + bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen()); + + select! { + result = refund_tx_seen => { + result.context("Failed to monitor refund transaction")?; + + let published_refund_tx = + bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + + let spend_key = tx_refund.extract_monero_private_key( + published_refund_tx, + state3.s_a, + state3.a.clone(), + state3.S_b_bitcoin, + )?; + AliceState::BtcRefunded { + spend_key, + state3, + monero_wallet_restore_blockheight, + } + } + _ = punish_tx_finalised => { + AliceState::BtcPunished + } + } + } + AliceState::XmrRefunded => AliceState::XmrRefunded, + AliceState::BtcRedeemed => AliceState::BtcRedeemed, + AliceState::BtcPunished => AliceState::BtcPunished, + AliceState::SafelyAborted => AliceState::SafelyAborted, + }; + + let db_state = (&new_state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + run_until_internal( + new_state, + is_target_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + env_config, + swap_id, + db, + ) + .await } diff --git a/swap/src/protocol/bob/cancel.rs b/swap/src/protocol/bob/cancel.rs index 90209383..ea9f2d9d 100644 --- a/swap/src/protocol/bob/cancel.rs +++ b/swap/src/protocol/bob/cancel.rs @@ -20,12 +20,12 @@ pub async fn cancel( db: Database, force: bool, ) -> Result> { - let state4 = match state { + let state6 = match state { BobState::BtcLocked(state3) => state3.cancel(), BobState::XmrLockProofReceived { state, .. } => state.cancel(), - BobState::XmrLocked(state4) => state4, - BobState::EncSigSent(state4) => state4, - BobState::CancelTimelockExpired(state4) => state4, + BobState::XmrLocked(state4) => state4.cancel(), + BobState::EncSigSent(state4) => state4.cancel(), + BobState::CancelTimelockExpired(state6) => state6, _ => bail!( "Cannot cancel swap {} because it is in state {} which is not refundable.", swap_id, @@ -34,16 +34,16 @@ pub async fn cancel( }; if !force { - if let ExpiredTimelocks::None = state4.expired_timelock(bitcoin_wallet.as_ref()).await? { + if let ExpiredTimelocks::None = state6.expired_timelock(bitcoin_wallet.as_ref()).await? { return Ok(Err(Error::CancelTimelockNotExpiredYet)); } - if state4 + if state6 .check_for_tx_cancel(bitcoin_wallet.as_ref()) .await .is_ok() { - let state = BobState::BtcCancelled(state4); + let state = BobState::BtcCancelled(state6); let db_state = state.into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; @@ -51,9 +51,9 @@ pub async fn cancel( } } - let txid = state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; + let txid = state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; - let state = BobState::BtcCancelled(state4); + let state = BobState::BtcCancelled(state6); let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; diff --git a/swap/src/protocol/bob/refund.rs b/swap/src/protocol/bob/refund.rs index b1390045..4fa96778 100644 --- a/swap/src/protocol/bob/refund.rs +++ b/swap/src/protocol/bob/refund.rs @@ -16,14 +16,14 @@ pub async fn refund( db: Database, force: bool, ) -> Result> { - let state4 = if force { + let state6 = if force { match state { BobState::BtcLocked(state3) => state3.cancel(), BobState::XmrLockProofReceived { state, .. } => state.cancel(), - BobState::XmrLocked(state4) => state4, - BobState::EncSigSent(state4) => state4, - BobState::CancelTimelockExpired(state4) => state4, - BobState::BtcCancelled(state4) => state4, + BobState::XmrLocked(state4) => state4.cancel(), + BobState::EncSigSent(state4) => state4.cancel(), + BobState::CancelTimelockExpired(state6) => state6, + BobState::BtcCancelled(state6) => state6, _ => bail!( "Cannot refund swap {} because it is in state {} which is not refundable.", swap_id, @@ -32,16 +32,16 @@ pub async fn refund( } } else { match state { - BobState::BtcCancelled(state4) => state4, + BobState::BtcCancelled(state6) => state6, _ => { return Ok(Err(SwapNotCancelledYet(swap_id))); } } }; - state4.refund_btc(bitcoin_wallet.as_ref()).await?; + state6.refund_btc(bitcoin_wallet.as_ref()).await?; - let state = BobState::BtcRefunded(state4); + let state = BobState::BtcRefunded(state6); let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 7d288a2e..28aa05a0 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -3,12 +3,13 @@ use crate::bitcoin::{ TxLock, Txid, }; use crate::monero; -use crate::monero::{monero_private_key, InsufficientFunds, TransferProof}; +use crate::monero::wallet::WatchRequest; +use crate::monero::{monero_private_key, TransferProof}; use crate::monero_ext::ScalarExt; use crate::protocol::alice::{Message1, Message3}; use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4}; use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, bail, Context, Result}; use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; use ecdsa_fun::nonce::Deterministic; use ecdsa_fun::Signature; @@ -34,9 +35,9 @@ pub enum BobState { XmrLocked(State4), EncSigSent(State4), BtcRedeemed(State5), - CancelTimelockExpired(State4), - BtcCancelled(State4), - BtcRefunded(State4), + CancelTimelockExpired(State6), + BtcCancelled(State6), + BtcRefunded(State6), XmrRedeemed { tx_lock_id: bitcoin::Txid, }, @@ -305,30 +306,22 @@ pub struct State3 { } impl State3 { - pub async fn watch_for_lock_xmr( - self, - xmr_wallet: &monero::Wallet, - transfer_proof: TransferProof, - monero_wallet_restore_blockheight: BlockHeight, - ) -> Result> { + pub fn lock_xmr_watch_request(&self, transfer_proof: TransferProof) -> WatchRequest { let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b)); let S = self.S_a_monero + S_b_monero; - if let Err(e) = xmr_wallet - .watch_for_transfer( - S, - self.v.public(), - transfer_proof, - self.xmr, - self.min_monero_confirmations, - ) - .await - { - return Ok(Err(e)); + WatchRequest { + public_spend_key: S, + public_view_key: self.v.public(), + transfer_proof, + conf_target: self.min_monero_confirmations, + expected: self.xmr, } + } - Ok(Ok(State4 { + pub fn xmr_locked(self, monero_wallet_restore_blockheight: BlockHeight) -> State4 { + State4 { A: self.A, b: self.b, s_b: self.s_b, @@ -342,7 +335,7 @@ impl State3 { tx_cancel_sig_a: self.tx_cancel_sig_a, tx_refund_encsig: self.tx_refund_encsig, monero_wallet_restore_blockheight, - })) + } } pub async fn wait_for_cancel_timelock_to_expire( @@ -357,23 +350,17 @@ impl State3 { Ok(()) } - pub fn cancel(&self) -> State4 { - State4 { + pub fn cancel(&self) -> State6 { + State6 { A: self.A, b: self.b.clone(), s_b: self.s_b, - S_a_bitcoin: self.S_a_bitcoin, - v: self.v, cancel_timelock: self.cancel_timelock, punish_timelock: self.punish_timelock, refund_address: self.refund_address.clone(), - redeem_address: self.redeem_address.clone(), tx_lock: self.tx_lock.clone(), tx_cancel_sig_a: self.tx_cancel_sig_a.clone(), tx_refund_encsig: self.tx_refund_encsig.clone(), - // For cancel scenarios the monero wallet rescan blockchain height is irrelevant for - // Bob, because Bob's cancel can only lead to refunding on Bitcoin - monero_wallet_restore_blockheight: BlockHeight { height: 0 }, } } @@ -428,37 +415,6 @@ impl State4 { self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()) } - pub async fn check_for_tx_cancel( - &self, - bitcoin_wallet: &bitcoin::Wallet, - ) -> Result { - let tx_cancel = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); - - let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?; - - Ok(tx) - } - - pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { - let tx_cancel = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); - - let sig_a = self.tx_cancel_sig_a.clone(); - let sig_b = self.b.sign(tx_cancel.digest()); - - let tx_cancel = tx_cancel - .add_signatures((self.A, sig_a), (self.b.public(), sig_b)) - .expect( - "sig_{a,b} to be valid signatures for - tx_cancel", - ); - - let (tx_id, _) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?; - - Ok(tx_id) - } - pub async fn watch_for_redeem_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); @@ -513,29 +469,18 @@ impl State4 { )) } - pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> { - let tx_cancel = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address); - - let adaptor = Adaptor::, Deterministic>::default(); - - let sig_b = self.b.sign(tx_refund.digest()); - let sig_a = - adaptor.decrypt_signature(&self.s_b.to_secpfun_scalar(), self.tx_refund_encsig.clone()); - - let signed_tx_refund = - tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?; - - let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; - - finality.await?; - - Ok(()) - } - - pub fn tx_lock_id(&self) -> bitcoin::Txid { - self.tx_lock.txid() + pub fn cancel(self) -> State6 { + State6 { + A: self.A, + b: self.b, + s_b: self.s_b, + cancel_timelock: self.cancel_timelock, + punish_timelock: self.punish_timelock, + refund_address: self.refund_address, + tx_lock: self.tx_lock, + tx_cancel_sig_a: self.tx_cancel_sig_a, + tx_refund_encsig: self.tx_refund_encsig, + } } } @@ -567,3 +512,83 @@ impl State5 { self.tx_lock.txid() } } + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct State6 { + A: bitcoin::PublicKey, + b: bitcoin::SecretKey, + s_b: monero::Scalar, + cancel_timelock: CancelTimelock, + punish_timelock: PunishTimelock, + refund_address: bitcoin::Address, + tx_lock: bitcoin::TxLock, + tx_cancel_sig_a: Signature, + tx_refund_encsig: bitcoin::EncryptedSignature, +} + +impl State6 { + pub async fn expired_timelock( + &self, + bitcoin_wallet: &bitcoin::Wallet, + ) -> Result { + let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + + let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?; + let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?; + + Ok(current_epoch( + self.cancel_timelock, + self.punish_timelock, + tx_lock_status, + tx_cancel_status, + )) + } + + pub async fn check_for_tx_cancel( + &self, + bitcoin_wallet: &bitcoin::Wallet, + ) -> Result { + let tx_cancel = + bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + + let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?; + + Ok(tx) + } + + pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { + let transaction = + bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()) + .complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone()) + .context("Failed to complete Bitcoin cancel transaction")?; + + let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?; + + Ok(tx_id) + } + + pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> { + let tx_cancel = + bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address); + + let adaptor = Adaptor::, Deterministic>::default(); + + let sig_b = self.b.sign(tx_refund.digest()); + let sig_a = + adaptor.decrypt_signature(&self.s_b.to_secpfun_scalar(), self.tx_refund_encsig.clone()); + + let signed_tx_refund = + tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?; + + let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; + + finality.await?; + + Ok(()) + } + + pub fn tx_lock_id(&self) -> bitcoin::Txid { + self.tx_lock.txid() + } +} diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index c2ecc890..f13e5ffc 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,7 +1,6 @@ use crate::bitcoin::ExpiredTimelocks; use crate::database::{Database, Swap}; use crate::env::Config; -use crate::monero::InsufficientFunds; use crate::protocol::bob; use crate::protocol::bob::event_loop::EventLoopHandle; use crate::protocol::bob::state::*; @@ -11,7 +10,7 @@ use async_recursion::async_recursion; use rand::rngs::OsRng; use std::sync::Arc; use tokio::select; -use tracing::{trace, warn}; +use tracing::trace; use uuid::Uuid; pub fn is_complete(state: &BobState) -> bool { @@ -63,348 +62,205 @@ async fn run_until_internal( ) -> Result { trace!("Current state: {}", state); if is_target_state(&state) { - Ok(state) - } else { - match state { - BobState::Started { btc_amount } => { - let bitcoin_refund_address = bitcoin_wallet.new_address().await?; - - event_loop_handle.dial().await?; - - let state2 = request_price_and_setup( - btc_amount, - &mut event_loop_handle, - env_config, - bitcoin_refund_address, - ) - .await?; - - let state = BobState::ExecutionSetupDone(state2); - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::ExecutionSetupDone(state2) => { - // Do not lock Bitcoin if not connected to Alice. - event_loop_handle.dial().await?; - // Alice and Bob have exchanged info - let (state3, tx_lock) = state2.lock_btc().await?; - let signed_tx = bitcoin_wallet - .sign_and_finalize(tx_lock.clone().into()) - .await - .context("Failed to sign Bitcoin lock transaction")?; - let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; - - let state = BobState::BtcLocked(state3); - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - // Bob has locked Btc - // Watch for Alice to Lock Xmr or for cancel timelock to elapse - BobState::BtcLocked(state3) => { - let state = if let ExpiredTimelocks::None = - state3.current_epoch(bitcoin_wallet.as_ref()).await? - { - event_loop_handle.dial().await?; - - let transfer_proof_watcher = event_loop_handle.recv_transfer_proof(); - let cancel_timelock_expires = - state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - // Record the current monero wallet block height so we don't have to scan from - // block 0 once we create the redeem wallet. - let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; - - tracing::info!("Waiting for Alice to lock Monero"); - - select! { - transfer_proof = transfer_proof_watcher => { - let transfer_proof = transfer_proof?.tx_lock_proof; - - tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero"); - - BobState::XmrLockProofReceived { - state: state3, - lock_transfer_proof: transfer_proof, - monero_wallet_restore_blockheight - } - }, - _ = cancel_timelock_expires => { - tracing::info!("Alice took too long to lock Monero, cancelling the swap"); - - let state4 = state3.cancel(); - BobState::CancelTimelockExpired(state4) - } - } - } else { - let state4 = state3.cancel(); - BobState::CancelTimelockExpired(state4) - }; - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::XmrLockProofReceived { - state, - lock_transfer_proof, - monero_wallet_restore_blockheight, - } => { - let state = if let ExpiredTimelocks::None = - state.current_epoch(bitcoin_wallet.as_ref()).await? - { - event_loop_handle.dial().await?; - - let xmr_lock_watcher = state.clone().watch_for_lock_xmr( - monero_wallet.as_ref(), - lock_transfer_proof, - monero_wallet_restore_blockheight, - ); - let cancel_timelock_expires = - state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - select! { - state4 = xmr_lock_watcher => { - match state4? { - Ok(state4) => BobState::XmrLocked(state4), - Err(InsufficientFunds {..}) => { - warn!("The other party has locked insufficient Monero funds! Waiting for refund..."); - state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()).await?; - let state4 = state.cancel(); - BobState::CancelTimelockExpired(state4) - }, - } - }, - _ = cancel_timelock_expires => { - let state4 = state.cancel(); - BobState::CancelTimelockExpired(state4) - } - } - } else { - let state4 = state.cancel(); - BobState::CancelTimelockExpired(state4) - }; - - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::XmrLocked(state) => { - let state = if let ExpiredTimelocks::None = - state.expired_timelock(bitcoin_wallet.as_ref()).await? - { - event_loop_handle.dial().await?; - // Alice has locked Xmr - // Bob sends Alice his key - let tx_redeem_encsig = state.tx_redeem_encsig(); - - let state4_clone = state.clone(); - - let enc_sig_sent_watcher = - event_loop_handle.send_encrypted_signature(tx_redeem_encsig); - let bitcoin_wallet = bitcoin_wallet.clone(); - let cancel_timelock_expires = - state4_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - select! { - _ = enc_sig_sent_watcher => { - BobState::EncSigSent(state) - }, - _ = cancel_timelock_expires => { - BobState::CancelTimelockExpired(state) - } - } - } else { - BobState::CancelTimelockExpired(state) - }; - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::EncSigSent(state) => { - let state = if let ExpiredTimelocks::None = - state.expired_timelock(bitcoin_wallet.as_ref()).await? - { - let state_clone = state.clone(); - let redeem_watcher = state_clone.watch_for_redeem_btc(bitcoin_wallet.as_ref()); - let cancel_timelock_expires = - state_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - select! { - state5 = redeem_watcher => { - BobState::BtcRedeemed(state5?) - }, - _ = cancel_timelock_expires => { - BobState::CancelTimelockExpired(state) - } - } - } else { - BobState::CancelTimelockExpired(state) - }; - - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet.clone(), - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::BtcRedeemed(state) => { - // Bob redeems XMR using revealed s_a - state.claim_xmr(monero_wallet.as_ref()).await?; - - // Ensure that the generated wallet is synced so we have a proper balance - monero_wallet.refresh().await?; - // Sweep (transfer all funds) to the given address - let tx_hashes = monero_wallet.sweep_all(receive_monero_address).await?; - - for tx_hash in tx_hashes { - tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0); - } - - let state = BobState::XmrRedeemed { - tx_lock_id: state.tx_lock_id(), - }; - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::CancelTimelockExpired(state4) => { - if state4 - .check_for_tx_cancel(bitcoin_wallet.as_ref()) - .await - .is_err() - { - state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; - } - - let state = BobState::BtcCancelled(state4); - db.insert_latest_state(swap_id, Swap::Bob(state.clone().into())) - .await?; - - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::BtcCancelled(state) => { - // Bob has cancelled the swap - let state = match state.expired_timelock(bitcoin_wallet.as_ref()).await? { - ExpiredTimelocks::None => { - bail!("Internal error: canceled state reached before cancel timelock was expired"); - } - ExpiredTimelocks::Cancel => { - state.refund_btc(bitcoin_wallet.as_ref()).await?; - BobState::BtcRefunded(state) - } - ExpiredTimelocks::Punish => BobState::BtcPunished { - tx_lock_id: state.tx_lock_id(), - }, - }; - - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)), - BobState::BtcPunished { tx_lock_id } => Ok(BobState::BtcPunished { tx_lock_id }), - BobState::SafelyAborted => Ok(BobState::SafelyAborted), - BobState::XmrRedeemed { tx_lock_id } => Ok(BobState::XmrRedeemed { tx_lock_id }), - } + return Ok(state); } + + let new_state = match state { + BobState::Started { btc_amount } => { + let bitcoin_refund_address = bitcoin_wallet.new_address().await?; + + event_loop_handle.dial().await?; + + let state2 = request_price_and_setup( + btc_amount, + &mut event_loop_handle, + env_config, + bitcoin_refund_address, + ) + .await?; + + BobState::ExecutionSetupDone(state2) + } + BobState::ExecutionSetupDone(state2) => { + // Do not lock Bitcoin if not connected to Alice. + event_loop_handle.dial().await?; + // Alice and Bob have exchanged info + let (state3, tx_lock) = state2.lock_btc().await?; + let signed_tx = bitcoin_wallet + .sign_and_finalize(tx_lock.clone().into()) + .await + .context("Failed to sign Bitcoin lock transaction")?; + let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; + + BobState::BtcLocked(state3) + } + // Bob has locked Btc + // Watch for Alice to Lock Xmr or for cancel timelock to elapse + BobState::BtcLocked(state3) => { + if let ExpiredTimelocks::None = state3.current_epoch(bitcoin_wallet.as_ref()).await? { + event_loop_handle.dial().await?; + + let transfer_proof_watcher = event_loop_handle.recv_transfer_proof(); + let cancel_timelock_expires = + state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); + + // Record the current monero wallet block height so we don't have to scan from + // block 0 once we create the redeem wallet. + let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; + + tracing::info!("Waiting for Alice to lock Monero"); + + select! { + transfer_proof = transfer_proof_watcher => { + let transfer_proof = transfer_proof?.tx_lock_proof; + + tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero"); + + BobState::XmrLockProofReceived { + state: state3, + lock_transfer_proof: transfer_proof, + monero_wallet_restore_blockheight + } + }, + _ = cancel_timelock_expires => { + tracing::info!("Alice took too long to lock Monero, cancelling the swap"); + + let state4 = state3.cancel(); + BobState::CancelTimelockExpired(state4) + } + } + } else { + let state4 = state3.cancel(); + BobState::CancelTimelockExpired(state4) + } + } + BobState::XmrLockProofReceived { + state, + lock_transfer_proof, + monero_wallet_restore_blockheight, + } => { + if let ExpiredTimelocks::None = state.current_epoch(bitcoin_wallet.as_ref()).await? { + event_loop_handle.dial().await?; + + let watch_request = state.lock_xmr_watch_request(lock_transfer_proof); + + select! { + received_xmr = monero_wallet.watch_for_transfer(watch_request) => { + match received_xmr { + Ok(()) => BobState::XmrLocked(state.xmr_locked(monero_wallet_restore_blockheight)), + Err(e) => { + tracing::warn!("Waiting for refund because insufficient Monero have been locked! {}", e); + state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()).await?; + + BobState::CancelTimelockExpired(state.cancel()) + }, + } + } + _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { + BobState::CancelTimelockExpired(state.cancel()) + } + } + } else { + BobState::CancelTimelockExpired(state.cancel()) + } + } + BobState::XmrLocked(state) => { + if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet.as_ref()).await? { + event_loop_handle.dial().await?; + // Alice has locked Xmr + // Bob sends Alice his key + + select! { + _ = event_loop_handle.send_encrypted_signature(state.tx_redeem_encsig()) => { + BobState::EncSigSent(state) + }, + _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { + BobState::CancelTimelockExpired(state.cancel()) + } + } + } else { + BobState::CancelTimelockExpired(state.cancel()) + } + } + BobState::EncSigSent(state) => { + if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet.as_ref()).await? { + select! { + state5 = state.watch_for_redeem_btc(bitcoin_wallet.as_ref()) => { + BobState::BtcRedeemed(state5?) + }, + _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { + BobState::CancelTimelockExpired(state.cancel()) + } + } + } else { + BobState::CancelTimelockExpired(state.cancel()) + } + } + BobState::BtcRedeemed(state) => { + // Bob redeems XMR using revealed s_a + state.claim_xmr(monero_wallet.as_ref()).await?; + + // Ensure that the generated wallet is synced so we have a proper balance + monero_wallet.refresh().await?; + // Sweep (transfer all funds) to the given address + let tx_hashes = monero_wallet.sweep_all(receive_monero_address).await?; + + for tx_hash in tx_hashes { + tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0); + } + + BobState::XmrRedeemed { + tx_lock_id: state.tx_lock_id(), + } + } + BobState::CancelTimelockExpired(state4) => { + if state4 + .check_for_tx_cancel(bitcoin_wallet.as_ref()) + .await + .is_err() + { + state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; + } + + BobState::BtcCancelled(state4) + } + BobState::BtcCancelled(state) => { + // Bob has cancelled the swap + match state.expired_timelock(bitcoin_wallet.as_ref()).await? { + ExpiredTimelocks::None => { + bail!( + "Internal error: canceled state reached before cancel timelock was expired" + ); + } + ExpiredTimelocks::Cancel => { + state.refund_btc(bitcoin_wallet.as_ref()).await?; + BobState::BtcRefunded(state) + } + ExpiredTimelocks::Punish => BobState::BtcPunished { + tx_lock_id: state.tx_lock_id(), + }, + } + } + BobState::BtcRefunded(state4) => BobState::BtcRefunded(state4), + BobState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id }, + BobState::SafelyAborted => BobState::SafelyAborted, + BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, + }; + + let db_state = new_state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + run_until_internal( + new_state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await } pub async fn request_price_and_setup(