diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index 680c49a5..91242e7d 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -29,6 +29,7 @@ use xmr_btc::{ bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction}, config::Config, monero::CreateWalletForOutput, + Epoch, }; trait Rng: RngCore + CryptoRng + Send {} @@ -75,7 +76,7 @@ pub enum AliceState { state3: State3, }, XmrRefunded, - WaitingToCancel { + Cancelling { state3: State3, }, Punished, @@ -89,7 +90,7 @@ impl fmt::Display for AliceState { AliceState::Negotiated { .. } => write!(f, "negotiated"), AliceState::BtcLocked { .. } => write!(f, "btc_locked"), AliceState::XmrLocked { .. } => write!(f, "xmr_locked"), - AliceState::EncSignLearned { .. } => write!(f, "encsig_sent"), + AliceState::EncSignLearned { .. } => write!(f, "encsig_learnt"), AliceState::BtcRedeemed => write!(f, "btc_redeemed"), AliceState::BtcCancelled { .. } => write!(f, "btc_cancelled"), AliceState::BtcRefunded { .. } => write!(f, "btc_refunded"), @@ -97,7 +98,7 @@ impl fmt::Display for AliceState { AliceState::SafelyAborted => write!(f, "safely_aborted"), AliceState::BtcPunishable { .. } => write!(f, "btc_punishable"), AliceState::XmrRefunded => write!(f, "xmr_refunded"), - AliceState::WaitingToCancel { .. } => write!(f, "waiting_to_cancel"), + AliceState::Cancelling { .. } => write!(f, "cancelling"), } } } @@ -218,31 +219,46 @@ pub async fn run_until( .await } AliceState::XmrLocked { state3 } => { - // Our Monero is locked, we need to go through the cancellation process if this - // step fails - match wait_for_bitcoin_encrypted_signature( - &mut event_loop_handle, - config.monero_max_finality_time, - ) - .await - { - Ok(encrypted_signature) => { - run_until( - AliceState::EncSignLearned { - state3, - encrypted_signature, - }, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - config, - ) - .await + // todo: match statement and wait for t1 can probably expressed more cleanly + match state3.current_epoch(bitcoin_wallet.as_ref()).await? { + Epoch::T0 => { + let wait_for_enc_sig = wait_for_bitcoin_encrypted_signature( + &mut event_loop_handle, + config.monero_max_finality_time, + ); + let t1_timeout = state3.wait_for_t1(bitcoin_wallet.as_ref()); + + tokio::select! { + _ = t1_timeout => { + run_until( + AliceState::Cancelling { state3 }, + is_target_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + config, + ) + .await + } + enc_sig = wait_for_enc_sig => { + run_until( + AliceState::EncSignLearned { + state3, + encrypted_signature: enc_sig?, + }, + is_target_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + config, + ) + .await + } + } } - Err(_) => { + _ => { run_until( - AliceState::WaitingToCancel { state3 }, + AliceState::Cancelling { state3 }, is_target_state, event_loop_handle, bitcoin_wallet, @@ -253,6 +269,7 @@ pub async fn run_until( } } } + AliceState::EncSignLearned { state3, encrypted_signature, @@ -268,7 +285,7 @@ pub async fn run_until( Ok(tx) => tx, Err(_) => { return run_until( - AliceState::WaitingToCancel { state3 }, + AliceState::Cancelling { state3 }, is_target_state, event_loop_handle, bitcoin_wallet, @@ -299,7 +316,7 @@ pub async fn run_until( ) .await } - AliceState::WaitingToCancel { state3 } => { + AliceState::Cancelling { state3 } => { let tx_cancel = publish_cancel_transaction( state3.tx_lock.clone(), state3.a.clone(), diff --git a/swap/src/bob/swap.rs b/swap/src/bob/swap.rs index 5eb5dc57..8dd5a797 100644 --- a/swap/src/bob/swap.rs +++ b/swap/src/bob/swap.rs @@ -10,7 +10,10 @@ use rand::{CryptoRng, RngCore}; use std::{fmt, sync::Arc}; use tracing::info; use uuid::Uuid; -use xmr_btc::bob::{self, Epoch}; +use xmr_btc::{ + bob::{self}, + Epoch, +}; // The same data structure is used for swap execution and recovery. // This allows for a seamless transition from a failed swap to recovery. @@ -27,7 +30,7 @@ pub enum BobState { EncSigSent(bob::State4, PeerId), BtcRedeemed(bob::State5), Cancelled(bob::State4), - BtcRefunded, + BtcRefunded(bob::State4), XmrRedeemed, Punished, SafelyAborted, @@ -41,9 +44,9 @@ impl fmt::Display for BobState { BobState::BtcLocked(..) => write!(f, "btc_locked"), BobState::XmrLocked(..) => write!(f, "xmr_locked"), BobState::EncSigSent(..) => write!(f, "encsig_sent"), - BobState::BtcRedeemed(_) => write!(f, "btc_redeemed"), - BobState::Cancelled(_) => write!(f, "cancelled"), - BobState::BtcRefunded => write!(f, "btc_refunded"), + BobState::BtcRedeemed(..) => write!(f, "btc_redeemed"), + BobState::Cancelled(..) => write!(f, "cancelled"), + BobState::BtcRefunded(..) => write!(f, "btc_refunded"), BobState::XmrRedeemed => write!(f, "xmr_redeemed"), BobState::Punished => write!(f, "punished"), BobState::SafelyAborted => write!(f, "safely_aborted"), @@ -79,7 +82,7 @@ where pub fn is_complete(state: &BobState) -> bool { matches!( state, - BobState::BtcRefunded + BobState::BtcRefunded(..) | BobState::XmrRedeemed | BobState::Punished | BobState::SafelyAborted @@ -267,9 +270,9 @@ where Epoch::T1 => { state.refund_btc(bitcoin_wallet.as_ref()).await?; run_until( - BobState::BtcRefunded, + BobState::BtcRefunded(state), is_target_state, - swarm, + event_loop_handle, db, bitcoin_wallet, monero_wallet, @@ -284,7 +287,7 @@ where run_until( BobState::Punished, is_target_state, - swarm, + event_loop_handle, db, bitcoin_wallet, monero_wallet, @@ -295,7 +298,7 @@ where } } } - BobState::BtcRefunded => Ok(BobState::BtcRefunded), + BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)), BobState::Punished => Ok(BobState::Punished), BobState::SafelyAborted => Ok(BobState::SafelyAborted), BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 0fe3441d..9fa7aa09 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -269,7 +269,7 @@ async fn both_refund() { let ( alice_state, - mut alice_swarm, + mut alice_swarm_driver, alice_swarm_handle, alice_btc_wallet, alice_xmr_wallet, @@ -282,6 +282,7 @@ async fn both_refund() { xmr_to_swap, alice_xmr_starting_balance, alice_multiaddr.clone(), + Config::regtest(), ) .await; @@ -295,6 +296,7 @@ async fn both_refund() { bob_btc_starting_balance, xmr_to_swap, bob_xmr_starting_balance, + Config::regtest(), ) .await; @@ -310,7 +312,7 @@ async fn both_refund() { tokio::spawn(async move { bob_swarm_driver.run().await }); - let alice_fut = alice::swap::run_until( + let alice_xmr_locked_fut = alice::swap::run_until( alice_state, alice::swap::is_xmr_locked, alice_swarm_handle, @@ -319,11 +321,62 @@ async fn both_refund() { Config::regtest(), ); - tokio::spawn(async move { alice_swarm.run().await }); + tokio::spawn(async move { alice_swarm_driver.run().await }); - let ((_alice_state, _), bob_state) = try_join(alice_fut, bob_fut).await.unwrap(); + // Wait until alice has locked xmr and bob has locked btc + let (bob_state, (alice_state, alice_swarm_handle)) = + try_join(bob_fut, alice_xmr_locked_fut).await.unwrap(); - assert!(matches!(bob_state, BobState::BtcRefunded)); + let bob_state4 = if let BobState::BtcRefunded(state4) = bob_state { + state4 + } else { + panic!("Bob in unexpected state"); + }; + + let (alice_state, _) = alice::swap::swap( + alice_state, + alice_swarm_handle, + alice_btc_wallet.clone(), + alice_xmr_wallet.clone(), + Config::regtest(), + ) + .await + .unwrap(); + + assert!(matches!(alice_state, AliceState::XmrRefunded)); + + let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap(); + let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap(); + + // lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal + // to TX_FEE + let lock_tx_bitcoin_fee = bob_btc_wallet + .transaction_fee(bob_state4.tx_lock_id()) + .await + .unwrap(); + + assert_eq!(btc_alice_final, alice_btc_starting_balance); + + // Alice or Bob could publish TxCancel. This means Bob could pay tx fees for + // TxCancel and TxRefund or only TxRefund + let btc_bob_final_alice_submitted_cancel = btc_bob_final + == bob_btc_starting_balance + - lock_tx_bitcoin_fee + - bitcoin::Amount::from_sat(bitcoin::TX_FEE); + + let btc_bob_final_bob_submitted_cancel = btc_bob_final + == bob_btc_starting_balance + - lock_tx_bitcoin_fee + - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE); + assert!(btc_bob_final_alice_submitted_cancel || btc_bob_final_bob_submitted_cancel); + + alice_xmr_wallet.as_ref().0.refresh().await.unwrap(); + let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap(); + assert_eq!(xmr_alice_final, xmr_to_swap); + + bob_xmr_wallet.as_ref().0.refresh().await.unwrap(); + let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap(); + assert_eq!(xmr_bob_final, bob_xmr_starting_balance); } #[allow(clippy::too_many_arguments)] @@ -383,7 +436,6 @@ async fn init_alice( punish_address, ); - // let msg0 = AliceToBob::Message0(self.state.next_message(&mut OsRng)); ( AliceState::Started { amounts, diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 9a1ea914..26f157d3 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -4,6 +4,7 @@ use crate::{ bob, monero, monero::{CreateWalletForOutput, Transfer}, transport::{ReceiveMessage, SendMessage}, + Epoch, }; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -26,8 +27,8 @@ use std::{ }; use tokio::{sync::Mutex, time::timeout}; use tracing::{error, info}; - pub mod message; +use crate::bitcoin::{BlockHeight, TransactionBlockHeight}; pub use message::{Message, Message0, Message1, Message2}; #[derive(Debug)] @@ -678,6 +679,37 @@ impl State3 { tx_cancel_sig_bob: self.tx_cancel_sig_bob, }) } + + pub async fn wait_for_t1(&self, bitcoin_wallet: &W) -> Result<()> + where + W: WatchForRawTransaction + TransactionBlockHeight + BlockHeight, + { + let tx_id = self.tx_lock.txid(); + let tx_lock_height = bitcoin_wallet.transaction_block_height(tx_id).await; + + let t1_timeout = + poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + self.refund_timelock); + t1_timeout.await; + Ok(()) + } + + pub async fn current_epoch(&self, bitcoin_wallet: &W) -> Result + where + W: WatchForRawTransaction + TransactionBlockHeight + BlockHeight, + { + let current_block_height = bitcoin_wallet.block_height().await; + let t0 = bitcoin_wallet + .transaction_block_height(self.tx_lock.txid()) + .await; + let t1 = t0 + self.refund_timelock; + let t2 = t1 + self.punish_timelock; + + match (current_block_height < t1, current_block_height < t2) { + (true, _) => Ok(Epoch::T0), + (false, true) => Ok(Epoch::T1), + (false, false) => Ok(Epoch::T2), + } + } } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index f0a84b14..9927be7f 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -7,6 +7,7 @@ use crate::{ monero, serde::monero_private_key, transport::{ReceiveMessage, SendMessage}, + Epoch, }; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -708,7 +709,6 @@ impl State4 { let tx_cancel = bitcoin::TxCancel::new(&self.tx_lock, self.refund_timelock, self.A, self.b.public()); - // todo: check if this is correct let sig_a = self.tx_cancel_sig_a.clone(); let sig_b = self.b.sign(tx_cancel.digest()); @@ -732,7 +732,6 @@ impl State4 { let tx_cancel = bitcoin::TxCancel::new(&self.tx_lock, self.refund_timelock, self.A, self.b.public()); - // todo: check if this is correct let sig_a = self.tx_cancel_sig_a.clone(); let sig_b = self.b.sign(tx_cancel.digest()); @@ -866,13 +865,6 @@ impl State4 { } } -#[derive(Debug, Clone, Copy)] -pub enum Epoch { - T0, - T1, - T2, -} - #[derive(Debug, Clone, Deserialize, Serialize)] pub struct State5 { A: bitcoin::PublicKey, diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 7d200891..0c9e8728 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -14,6 +14,13 @@ #![forbid(unsafe_code)] #![allow(non_snake_case)] +#[derive(Debug, Clone, Copy)] +pub enum Epoch { + T0, + T1, + T2, +} + #[macro_use] mod utils {