diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 4be14e20..e9212551 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -6,7 +6,6 @@ use futures::{ future::{select, Either}, pin_mut, }; -use rand::{CryptoRng, RngCore}; use std::sync::Arc; use tracing::info; use uuid::Uuid; @@ -32,10 +31,6 @@ use crate::{ ExpiredTimelocks, }; -trait Rng: RngCore + CryptoRng + Send {} - -impl Rng for T where T: RngCore + CryptoRng + Send {} - pub struct AliceActor { event_loop_handle: EventLoopHandle, bitcoin_wallet: Arc, diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 7374c2aa..93a62556 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -8,37 +8,253 @@ use uuid::Uuid; use crate::{ bitcoin, + config::Config, database::{Database, Swap}, monero, protocol::bob::{self, event_loop::EventLoopHandle, state::*}, ExpiredTimelocks, SwapAmounts, }; -// TODO(Franck): Make this a method on a struct -#[allow(clippy::too_many_arguments)] -pub async fn swap( - state: BobState, - event_loop_handle: EventLoopHandle, - db: Database, - bitcoin_wallet: Arc, - monero_wallet: Arc, - rng: R, - swap_id: Uuid, -) -> Result +pub struct BobActor where R: RngCore + CryptoRng + Send, { - run_until( - state, - is_complete, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .await + event_loop_handle: EventLoopHandle, + bitcoin_wallet: Arc, + monero_wallet: Arc, + db: Database, + config: Config, + swap_id: Uuid, + rng: R, +} + +impl BobActor +where + R: RngCore + CryptoRng + Send, +{ + pub fn new( + event_loop_handle: EventLoopHandle, + bitcoin_wallet: Arc, + monero_wallet: Arc, + db: Database, + config: Config, + swap_id: Uuid, + rng: R, + ) -> Self { + Self { + event_loop_handle, + bitcoin_wallet, + monero_wallet, + db, + config, + swap_id, + rng, + } + } + + pub async fn swap(self, start_state: BobState) -> Result { + self.run_until(start_state, is_complete).await + } + + // State machine driver for swap execution + #[allow(clippy::too_many_arguments)] + #[async_recursion] + pub async fn run_until( + mut self, + state: BobState, + is_target_state: fn(&BobState) -> bool, + ) -> Result { + let BobActor { + mut event_loop_handle, + bitcoin_wallet, + monero_wallet, + db, + config, + swap_id, + mut rng, + } = self; + info!("Current state: {}", state); + if is_target_state(&state) { + Ok(state) + } else { + match state { + BobState::Started { state0, amounts } => { + event_loop_handle.dial().await?; + + let state2 = negotiate( + state0, + amounts, + &mut event_loop_handle, + &mut rng, + bitcoin_wallet.clone(), + ) + .await?; + + let state = BobState::Negotiated(state2); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + self.run_until(state, is_target_state).await + } + BobState::Negotiated(state2) => { + // Do not lock Bitcoin if not connected to Alice. + event_loop_handle.dial().await?; + // Alice and Bob have exchanged info + let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; + + let state = BobState::BtcLocked(state3); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + self.run_until(state, is_target_state).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 msg2_watcher = event_loop_handle.recv_message2(); + let cancel_timelock_expires = + state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); + + select! { + msg2 = msg2_watcher => { + + let xmr_lock_watcher = state3.clone() + .watch_for_lock_xmr(monero_wallet.as_ref(), msg2?); + let cancel_timelock_expires = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); + + select! { + state4 = xmr_lock_watcher => { + BobState::XmrLocked(state4?) + }, + _ = cancel_timelock_expires => { + let state4 = state3.state4(); + BobState::CancelTimelockExpired(state4) + } + } + + }, + _ = cancel_timelock_expires => { + let state4 = state3.state4(); + BobState::CancelTimelockExpired(state4) + } + } + } else { + let state4 = state3.state4(); + BobState::CancelTimelockExpired(state4) + }; + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + self.run_until(state, is_target_state).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(); + // TODO(Franck): Refund if message cannot be sent. + let enc_sig_sent_watcher = + event_loop_handle.send_message3(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?; + self.run_until(state, is_target_state).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?; + self.run_until(state, is_target_state).await + } + BobState::BtcRedeemed(state) => { + // Bob redeems XMR using revealed s_a + state.claim_xmr(monero_wallet.as_ref()).await?; + + let state = BobState::XmrRedeemed; + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + self.run_until(state, is_target_state).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?; + + self.run_until(state, is_target_state).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, + }; + + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + self.run_until(state, is_target_state).await + } + BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)), + BobState::BtcPunished => Ok(BobState::BtcPunished), + BobState::SafelyAborted => Ok(BobState::SafelyAborted), + BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), + } + } + } } pub fn is_complete(state: &BobState) -> bool { @@ -63,283 +279,6 @@ pub fn is_encsig_sent(state: &BobState) -> bool { matches!(state, BobState::EncSigSent(..)) } -// State machine driver for swap execution -#[allow(clippy::too_many_arguments)] -#[async_recursion] -pub async fn run_until( - state: BobState, - is_target_state: fn(&BobState) -> bool, - mut event_loop_handle: EventLoopHandle, - db: Database, - bitcoin_wallet: Arc, - monero_wallet: Arc, - mut rng: R, - swap_id: Uuid, -) -> Result -where - R: RngCore + CryptoRng + Send, -{ - info!("Current state: {}", state); - if is_target_state(&state) { - Ok(state) - } else { - match state { - BobState::Started { state0, amounts } => { - event_loop_handle.dial().await?; - - let state2 = negotiate( - state0, - amounts, - &mut event_loop_handle, - &mut rng, - bitcoin_wallet.clone(), - ) - .await?; - - let state = BobState::Negotiated(state2); - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .await - } - BobState::Negotiated(state2) => { - // Do not lock Bitcoin if not connected to Alice. - event_loop_handle.dial().await?; - // Alice and Bob have exchanged info - let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).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( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .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 msg2_watcher = event_loop_handle.recv_message2(); - let cancel_timelock_expires = - state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - select! { - msg2 = msg2_watcher => { - - let xmr_lock_watcher = state3.clone() - .watch_for_lock_xmr(monero_wallet.as_ref(), msg2?); - let cancel_timelock_expires = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - select! { - state4 = xmr_lock_watcher => { - BobState::XmrLocked(state4?) - }, - _ = cancel_timelock_expires => { - let state4 = state3.state4(); - BobState::CancelTimelockExpired(state4) - } - } - - }, - _ = cancel_timelock_expires => { - let state4 = state3.state4(); - BobState::CancelTimelockExpired(state4) - } - } - } else { - let state4 = state3.state4(); - BobState::CancelTimelockExpired(state4) - }; - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .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(); - // TODO(Franck): Refund if message cannot be sent. - let enc_sig_sent_watcher = event_loop_handle.send_message3(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( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .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( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet.clone(), - monero_wallet, - rng, - swap_id, - ) - .await - } - BobState::BtcRedeemed(state) => { - // Bob redeems XMR using revealed s_a - state.claim_xmr(monero_wallet.as_ref()).await?; - - let state = BobState::XmrRedeemed; - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .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( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .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, - }; - - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .await - } - BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)), - BobState::BtcPunished => Ok(BobState::BtcPunished), - BobState::SafelyAborted => Ok(BobState::SafelyAborted), - BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), - } - } -} - pub async fn negotiate( state0: crate::protocol::bob::state::State0, amounts: SwapAmounts, diff --git a/swap/tests/happy_path.rs b/swap/tests/happy_path.rs index eb8d6715..1d8862fb 100644 --- a/swap/tests/happy_path.rs +++ b/swap/tests/happy_path.rs @@ -1,5 +1,3 @@ -use crate::testutils::Test; -use swap::{bitcoin, monero}; use tokio::join; pub mod testutils; @@ -13,5 +11,6 @@ async fn happy_path() { alice.assert_btc_redeemed(); bob.assert_btc_redeemed(); - }).await; + }) + .await; }