diff --git a/swap/src/main.rs b/swap/src/main.rs index c924f54a..8e692839 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -300,15 +300,17 @@ async fn alice_swap( let (mut event_loop, handle) = alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen_addr)?; - let swap = alice::swap::swap( + let swap = alice::Swap { state, - handle, - bitcoin_wallet.clone(), - monero_wallet.clone(), + event_loop_handle: handle, + bitcoin_wallet, + monero_wallet, config, swap_id, db, - ); + }; + + let swap = alice::swap::run(swap); tokio::spawn(async move { event_loop.run().await }); swap.await @@ -331,15 +333,16 @@ async fn bob_swap( let (event_loop, handle) = bob::event_loop::EventLoop::new(bob_transport, bob_behaviour, alice_peer_id, alice_addr)?; - let swap = bob::swap::swap( + let swap = bob::Swap { state, - handle, + event_loop_handle: handle, db, - bitcoin_wallet.clone(), - monero_wallet.clone(), - OsRng, + bitcoin_wallet, + monero_wallet, swap_id, - ); + }; + + let swap = bob::swap::run(swap); tokio::spawn(event_loop.run()); swap.await diff --git a/swap/src/protocol/alice.rs b/swap/src/protocol/alice.rs index 6ce5fd95..65419783 100644 --- a/swap/src/protocol/alice.rs +++ b/swap/src/protocol/alice.rs @@ -9,6 +9,7 @@ use libp2p::{ use tracing::{debug, info}; use crate::{ + bitcoin, monero, network::{ peer_tracker::{self, PeerTracker}, request_response::AliceToBob, @@ -26,8 +27,11 @@ pub use self::{ message1::Message1, message2::Message2, state::*, - swap::{run_until, swap}, + swap::{run, run_until}, }; +use crate::{config::Config, database::Database}; +use std::sync::Arc; +use uuid::Uuid; mod amounts; pub mod event_loop; @@ -39,6 +43,16 @@ pub mod state; mod steps; pub mod swap; +pub struct Swap { + pub state: AliceState, + pub event_loop_handle: EventLoopHandle, + pub bitcoin_wallet: Arc, + pub monero_wallet: Arc, + pub config: Config, + pub swap_id: Uuid, + pub db: Database, +} + pub type Swarm = libp2p::Swarm; pub fn new_swarm( diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 3aec20fb..f6dcb3e0 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -15,18 +15,23 @@ use crate::{ bitcoin, bitcoin::{TransactionBlockHeight, WatchForRawTransaction}, config::Config, - database::{Database, Swap}, + database, + database::Database, monero, monero::CreateWalletForOutput, - protocol::alice::{ - event_loop::EventLoopHandle, - steps::{ - build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, - extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction, - publish_bitcoin_redeem_transaction, publish_cancel_transaction, - wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin, + protocol::{ + alice, + alice::{ + event_loop::EventLoopHandle, + steps::{ + build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, + extract_monero_private_key, lock_xmr, negotiate, + publish_bitcoin_punish_transaction, publish_bitcoin_redeem_transaction, + publish_cancel_transaction, wait_for_bitcoin_encrypted_signature, + wait_for_bitcoin_refund, wait_for_locked_bitcoin, + }, + AliceState, }, - AliceState, }, ExpiredTimelocks, }; @@ -35,28 +40,6 @@ trait Rng: RngCore + CryptoRng + Send {} impl Rng for T where T: RngCore + CryptoRng + Send {} -pub async fn swap( - state: AliceState, - event_loop_handle: EventLoopHandle, - bitcoin_wallet: Arc, - monero_wallet: Arc, - config: Config, - swap_id: Uuid, - db: Database, -) -> Result { - run_until( - state, - is_complete, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - config, - swap_id, - db, - ) - .await -} - pub fn is_complete(state: &AliceState) -> bool { matches!( state, @@ -81,10 +64,31 @@ pub fn is_encsig_learned(state: &AliceState) -> bool { ) } +pub async fn run(swap: alice::Swap) -> Result { + run_until(swap, is_complete).await +} + +pub async fn run_until( + swap: alice::Swap, + is_target_state: fn(&AliceState) -> bool, +) -> Result { + do_run_until( + swap.state, + is_target_state, + swap.event_loop_handle, + swap.bitcoin_wallet, + swap.monero_wallet, + swap.config, + swap.swap_id, + swap.db, + ) + .await +} + // State machine driver for swap execution #[async_recursion] #[allow(clippy::too_many_arguments)] -pub async fn run_until( +async fn do_run_until( state: AliceState, is_target_state: fn(&AliceState) -> bool, mut event_loop_handle: EventLoopHandle, @@ -110,9 +114,9 @@ pub async fn run_until( }; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -153,9 +157,9 @@ pub async fn run_until( }; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -194,9 +198,9 @@ pub async fn run_until( }; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -232,9 +236,9 @@ pub async fn run_until( }; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -270,9 +274,9 @@ pub async fn run_until( let state = AliceState::CancelTimelockExpired { state3 }; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - return run_until( + return do_run_until( state, is_target_state, event_loop_handle, @@ -298,9 +302,9 @@ pub async fn run_until( let state = AliceState::BtcRedeemed; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -325,9 +329,9 @@ pub async fn run_until( let state = AliceState::BtcCancelled { state3, tx_cancel }; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -358,10 +362,10 @@ pub async fn run_until( None => { let state = AliceState::BtcPunishable { tx_refund, state3 }; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -384,9 +388,9 @@ pub async fn run_until( let state = AliceState::BtcRefunded { spend_key, state3 }; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -409,7 +413,7 @@ pub async fn run_until( let state = AliceState::XmrRefunded; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; Ok(state) } @@ -439,9 +443,9 @@ pub async fn run_until( Either::Left(_) => { let state = AliceState::BtcPunished; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -463,9 +467,9 @@ pub async fn run_until( )?; let state = AliceState::BtcRefunded { spend_key, state3 }; let db_state = (&state).into(); - db.insert_latest_state(swap_id, Swap::Alice(db_state)) + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index ab9bbfc6..5558d616 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -8,7 +8,9 @@ use libp2p::{ use tracing::{debug, info}; use crate::{ + bitcoin, bitcoin::EncryptedSignature, + monero, network::{ peer_tracker::{self, PeerTracker}, transport::SwapTransport, @@ -26,8 +28,11 @@ pub use self::{ message2::Message2, message3::Message3, state::*, - swap::{run_until, swap}, + swap::{run, run_until}, }; +use crate::database::Database; +use std::sync::Arc; +use uuid::Uuid; mod amounts; pub mod event_loop; @@ -38,6 +43,15 @@ mod message3; pub mod state; pub mod swap; +pub struct Swap { + pub state: BobState, + pub event_loop_handle: bob::EventLoopHandle, + pub db: Database, + pub bitcoin_wallet: Arc, + pub monero_wallet: Arc, + pub swap_id: Uuid, +} + pub type Swarm = libp2p::Swarm; pub fn new_swarm(transport: SwapTransport, behaviour: Behaviour) -> Result { diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 9046e5a6..b01a129b 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -13,33 +13,7 @@ use crate::{ 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 -where - R: RngCore + CryptoRng + Send, -{ - run_until( - state, - is_complete, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .await -} +use ecdsa_fun::fun::rand_core::OsRng; pub fn is_complete(state: &BobState) -> bool { matches!( @@ -63,10 +37,32 @@ pub fn is_encsig_sent(state: &BobState) -> bool { matches!(state, BobState::EncSigSent(..)) } +#[allow(clippy::too_many_arguments)] +pub async fn run(swap: bob::Swap) -> Result { + run_until(swap, is_complete).await +} + +pub async fn run_until( + swap: bob::Swap, + is_target_state: fn(&BobState) -> bool, +) -> Result { + do_run_until( + swap.state, + is_target_state, + swap.event_loop_handle, + swap.db, + swap.bitcoin_wallet, + swap.monero_wallet, + OsRng, + swap.swap_id, + ) + .await +} + // State machine driver for swap execution #[allow(clippy::too_many_arguments)] #[async_recursion] -pub async fn run_until( +async fn do_run_until( state: BobState, is_target_state: fn(&BobState) -> bool, mut event_loop_handle: EventLoopHandle, @@ -99,7 +95,7 @@ where let state = BobState::Negotiated(state2); let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -120,7 +116,7 @@ where let state = BobState::BtcLocked(state3); let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -185,7 +181,7 @@ where }; let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -226,7 +222,7 @@ where }; let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -261,7 +257,7 @@ where let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -282,7 +278,7 @@ where }; let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -307,7 +303,7 @@ where db.insert_latest_state(swap_id, Swap::Bob(state.clone().into())) .await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, @@ -336,7 +332,7 @@ where let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until( + do_run_until( state, is_target_state, event_loop_handle, diff --git a/swap/tests/happy_path.rs b/swap/tests/happy_path.rs index 18d31198..054a9d08 100644 --- a/swap/tests/happy_path.rs +++ b/swap/tests/happy_path.rs @@ -1,4 +1,3 @@ -use rand::rngs::OsRng; use swap::protocol::{alice, bob}; use tokio::join; @@ -8,33 +7,17 @@ pub mod testutils; #[tokio::test] async fn happy_path() { - testutils::test(|alice_harness, bob_harness| async move { - let alice = alice_harness.new_alice().await; - let bob = bob_harness.new_bob().await; + testutils::init(|test| async move { + let alice_swap = test.new_swap_as_alice().await; + let bob_swap = test.new_swap_as_bob().await; - let alice_swap = alice::swap( - alice.state, - alice.event_loop_handle, - alice.bitcoin_wallet.clone(), - alice.monero_wallet.clone(), - alice.config, - alice.swap_id, - alice.db, - ); + let alice = alice::run(alice_swap); - let bob_swap = bob::swap( - bob.state, - bob.event_loop_handle, - bob.db, - bob.bitcoin_wallet.clone(), - bob.monero_wallet.clone(), - OsRng, - bob.swap_id, - ); - let (alice_state, bob_state) = join!(alice_swap, bob_swap); + let bob = bob::run(bob_swap); + let (alice_state, bob_state) = join!(alice, bob); - alice_harness.assert_redeemed(alice_state.unwrap()).await; - bob_harness.assert_redeemed(bob_state.unwrap()).await; + test.assert_alice_redeemed(alice_state.unwrap()).await; + test.assert_bob_redeemed(bob_state.unwrap()).await; }) .await; } diff --git a/swap/tests/happy_path_restart_alice.rs b/swap/tests/happy_path_restart_alice.rs index 1e49fdf3..8f84129b 100644 --- a/swap/tests/happy_path_restart_alice.rs +++ b/swap/tests/happy_path_restart_alice.rs @@ -1,58 +1,30 @@ -use rand::rngs::OsRng; use swap::protocol::{alice, alice::AliceState, bob}; pub mod testutils; #[tokio::test] async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { - testutils::test(|alice_harness, bob_harness| async move { - let alice = alice_harness.new_alice().await; - let bob = bob_harness.new_bob().await; - - let bob_swap = bob::swap( - bob.state, - bob.event_loop_handle, - bob.db, - bob.bitcoin_wallet.clone(), - bob.monero_wallet.clone(), - OsRng, - bob.swap_id, - ); - let bob_swap_handle = tokio::spawn(bob_swap); - - let alice_state = alice::run_until( - alice.state, - alice::swap::is_encsig_learned, - alice.event_loop_handle, - alice.bitcoin_wallet.clone(), - alice.monero_wallet.clone(), - alice.config, - alice.swap_id, - alice.db, - ) - .await - .unwrap(); + testutils::init(|test| async move { + let alice_swap = test.new_swap_as_alice().await; + let bob_swap = test.new_swap_as_bob().await; + + let bob = bob::run(bob_swap); + let bob_handle = tokio::spawn(bob); + + let alice_state = alice::run_until(alice_swap, alice::swap::is_encsig_learned) + .await + .unwrap(); assert!(matches!(alice_state, AliceState::EncSigLearned {..})); - let alice = alice_harness.recover_alice_from_db().await; - assert!(matches!(alice.state, AliceState::EncSigLearned {..})); - - let alice_state = alice::swap( - alice.state, - alice.event_loop_handle, - alice.bitcoin_wallet.clone(), - alice.monero_wallet.clone(), - alice.config, - alice.swap_id, - alice.db, - ) - .await - .unwrap(); - - alice_harness.assert_redeemed(alice_state).await; - - let bob_state = bob_swap_handle.await.unwrap(); - bob_harness.assert_redeemed(bob_state.unwrap()).await + let alice_swap = test.recover_alice_from_db().await; + assert!(matches!(alice_swap.state, AliceState::EncSigLearned {..})); + + let alice_state = alice::run(alice_swap).await.unwrap(); + + test.assert_alice_redeemed(alice_state).await; + + let bob_state = bob_handle.await.unwrap(); + test.assert_bob_redeemed(bob_state.unwrap()).await }) .await; } diff --git a/swap/tests/happy_path_restart_bob_after_comm.rs b/swap/tests/happy_path_restart_bob_after_comm.rs index 7c1618b0..d49c939f 100644 --- a/swap/tests/happy_path_restart_bob_after_comm.rs +++ b/swap/tests/happy_path_restart_bob_after_comm.rs @@ -1,59 +1,31 @@ -use rand::rngs::OsRng; use swap::protocol::{alice, bob, bob::BobState}; pub mod testutils; #[tokio::test] async fn given_bob_restarts_after_encsig_is_sent_resume_swap() { - testutils::test(|alice_harness, bob_harness| async move { - let alice = alice_harness.new_alice().await; - let bob = bob_harness.new_bob().await; - - let alice_swap = alice::swap( - alice.state, - alice.event_loop_handle, - alice.bitcoin_wallet.clone(), - alice.monero_wallet.clone(), - alice.config, - alice.swap_id, - alice.db, - ); - let alice_swap_handle = tokio::spawn(alice_swap); - - let bob_state = bob::run_until( - bob.state, - bob::swap::is_encsig_sent, - bob.event_loop_handle, - bob.db, - bob.bitcoin_wallet.clone(), - bob.monero_wallet.clone(), - OsRng, - bob.swap_id, - ) - .await - .unwrap(); + testutils::init(|test| async move { + let alice_swap = test.new_swap_as_alice().await; + let bob_swap = test.new_swap_as_bob().await; + + let alice = alice::run(alice_swap); + let alice_handle = tokio::spawn(alice); + + let bob_state = bob::run_until(bob_swap, bob::swap::is_encsig_sent) + .await + .unwrap(); assert!(matches!(bob_state, BobState::EncSigSent {..})); - let bob = bob_harness.recover_bob_from_db().await; - assert!(matches!(bob.state, BobState::EncSigSent {..})); - - let bob_state = bob::swap( - bob.state, - bob.event_loop_handle, - bob.db, - bob.bitcoin_wallet.clone(), - bob.monero_wallet.clone(), - OsRng, - bob.swap_id, - ) - .await - .unwrap(); - - bob_harness.assert_redeemed(bob_state).await; - - let alice_state = alice_swap_handle.await.unwrap(); - alice_harness.assert_redeemed(alice_state.unwrap()).await; + let bob_swap = test.recover_bob_from_db().await; + assert!(matches!(bob_swap.state, BobState::EncSigSent {..})); + + let bob_state = bob::run(bob_swap).await.unwrap(); + + test.assert_bob_redeemed(bob_state).await; + + let alice_state = alice_handle.await.unwrap(); + test.assert_alice_redeemed(alice_state.unwrap()).await; }) .await; } diff --git a/swap/tests/happy_path_restart_bob_before_comm.rs b/swap/tests/happy_path_restart_bob_before_comm.rs index c206e2fd..1fce0e5c 100644 --- a/swap/tests/happy_path_restart_bob_before_comm.rs +++ b/swap/tests/happy_path_restart_bob_before_comm.rs @@ -1,59 +1,32 @@ -use rand::rngs::OsRng; -use swap::protocol::{alice, bob, bob::BobState}; +use swap::protocol::{ + alice, bob, + bob::{swap::is_xmr_locked, BobState}, +}; pub mod testutils; #[tokio::test] async fn given_bob_restarts_after_xmr_is_locked_resume_swap() { - testutils::test(|alice_harness, bob_harness| async move { - let alice = alice_harness.new_alice().await; - let bob = bob_harness.new_bob().await; - - let alice_swap = alice::swap( - alice.state, - alice.event_loop_handle, - alice.bitcoin_wallet.clone(), - alice.monero_wallet.clone(), - alice.config, - alice.swap_id, - alice.db, - ); - let alice_swap_handle = tokio::spawn(alice_swap); - - let bob_state = bob::run_until( - bob.state, - bob::swap::is_xmr_locked, - bob.event_loop_handle, - bob.db, - bob.bitcoin_wallet.clone(), - bob.monero_wallet.clone(), - OsRng, - bob.swap_id, - ) - .await - .unwrap(); + testutils::init(|test| async move { + let alice_swap = test.new_swap_as_alice().await; + let bob_swap = test.new_swap_as_bob().await; + + let alice_handle = alice::run(alice_swap); + let alice_swap_handle = tokio::spawn(alice_handle); + + let bob_state = bob::run_until(bob_swap, is_xmr_locked).await.unwrap(); assert!(matches!(bob_state, BobState::XmrLocked {..})); - let bob = bob_harness.recover_bob_from_db().await; - assert!(matches!(bob.state, BobState::XmrLocked {..})); + let bob_swap = test.recover_bob_from_db().await; + assert!(matches!(bob_swap.state, BobState::XmrLocked {..})); - let bob_state = bob::swap( - bob.state, - bob.event_loop_handle, - bob.db, - bob.bitcoin_wallet.clone(), - bob.monero_wallet.clone(), - OsRng, - bob.swap_id, - ) - .await - .unwrap(); + let bob_state = bob::run(bob_swap).await.unwrap(); - bob_harness.assert_redeemed(bob_state).await; + test.assert_bob_redeemed(bob_state).await; let alice_state = alice_swap_handle.await.unwrap(); - alice_harness.assert_redeemed(alice_state.unwrap()).await; + test.assert_alice_redeemed(alice_state.unwrap()).await; }) .await; } diff --git a/swap/tests/punish.rs b/swap/tests/punish.rs index e17de0ff..16515569 100644 --- a/swap/tests/punish.rs +++ b/swap/tests/punish.rs @@ -1,5 +1,7 @@ -use rand::rngs::OsRng; -use swap::protocol::{alice, bob, bob::BobState}; +use swap::protocol::{ + alice, bob, + bob::{swap::is_btc_locked, BobState}, +}; pub mod testutils; @@ -7,57 +9,28 @@ pub mod testutils; /// the encsig and fail to refund or redeem. Alice punishes. #[tokio::test] async fn alice_punishes_if_bob_never_acts_after_fund() { - testutils::test(|alice_harness, bob_harness| async move { - let alice = alice_harness.new_alice().await; - let bob = bob_harness.new_bob().await; - - let alice_swap = alice::swap( - alice.state, - alice.event_loop_handle, - alice.bitcoin_wallet.clone(), - alice.monero_wallet.clone(), - alice.config, - alice.swap_id, - alice.db, - ); - let alice_swap_handle = tokio::spawn(alice_swap); - - let bob_state = bob::run_until( - bob.state, - bob::swap::is_btc_locked, - bob.event_loop_handle, - bob.db, - bob.bitcoin_wallet.clone(), - bob.monero_wallet.clone(), - OsRng, - bob.swap_id, - ) - .await - .unwrap(); + testutils::init(|test| async move { + let alice_swap = test.new_swap_as_alice().await; + let bob_swap = test.new_swap_as_bob().await; + + let alice = alice::run(alice_swap); + let alice_handle = tokio::spawn(alice); + + let bob_state = bob::run_until(bob_swap, is_btc_locked).await.unwrap(); assert!(matches!(bob_state, BobState::BtcLocked {..})); - let alice_state = alice_swap_handle.await.unwrap(); - alice_harness.assert_punished(alice_state.unwrap()).await; + let alice_state = alice_handle.await.unwrap(); + test.assert_alice_punished(alice_state.unwrap()).await; // Restart Bob after Alice punished to ensure Bob transitions to // punished and does not run indefinitely - let bob = bob_harness.recover_bob_from_db().await; - assert!(matches!(bob.state, BobState::BtcLocked {..})); - - let bob_state = bob::swap( - bob.state, - bob.event_loop_handle, - bob.db, - bob.bitcoin_wallet.clone(), - bob.monero_wallet.clone(), - OsRng, - bob.swap_id, - ) - .await - .unwrap(); - - bob_harness.assert_punished(bob_state).await; + let bob_swap = test.recover_bob_from_db().await; + assert!(matches!(bob_swap.state, BobState::BtcLocked {..})); + + let bob_state = bob::run(bob_swap).await.unwrap(); + + test.assert_bob_punished(bob_state).await; }) .await; } diff --git a/swap/tests/refund_restart_alice.rs b/swap/tests/refund_restart_alice.rs index 2a1d338c..e5e91ae8 100644 --- a/swap/tests/refund_restart_alice.rs +++ b/swap/tests/refund_restart_alice.rs @@ -1,4 +1,3 @@ -use rand::rngs::OsRng; use swap::protocol::{alice, alice::AliceState, bob}; pub mod testutils; @@ -7,60 +6,33 @@ pub mod testutils; /// then also refunds. #[tokio::test] async fn given_alice_restarts_after_xmr_is_locked_abort_swap() { - testutils::test(|alice_harness, bob_harness| async move { - let alice = alice_harness.new_alice().await; - let bob = bob_harness.new_bob().await; + testutils::init(|test| async move { + let alice_swap = test.new_swap_as_alice().await; + let bob_swap = test.new_swap_as_bob().await; - let bob_swap = bob::swap( - bob.state, - bob.event_loop_handle, - bob.db, - bob.bitcoin_wallet.clone(), - bob.monero_wallet.clone(), - OsRng, - bob.swap_id, - ); - let bob_swap_handle = tokio::spawn(bob_swap); + let bob = bob::run(bob_swap); + let bob_handle = tokio::spawn(bob); - let alice_state = alice::run_until( - alice.state, - alice::swap::is_xmr_locked, - alice.event_loop_handle, - alice.bitcoin_wallet.clone(), - alice.monero_wallet.clone(), - alice.config, - alice.swap_id, - alice.db, - ) - .await - .unwrap(); + let alice_state = alice::run_until(alice_swap, alice::swap::is_xmr_locked) + .await + .unwrap(); assert!(matches!(alice_state, AliceState::XmrLocked {..})); // Alice does not act, Bob refunds - let bob_state = bob_swap_handle.await.unwrap(); + let bob_state = bob_handle.await.unwrap(); // Once bob has finished Alice is restarted and refunds as well - let alice = alice_harness.recover_alice_from_db().await; - assert!(matches!(alice.state, AliceState::XmrLocked {..})); + let alice_swap = test.recover_alice_from_db().await; + assert!(matches!(alice_swap.state, AliceState::XmrLocked {..})); - let alice_state = alice::swap( - alice.state, - alice.event_loop_handle, - alice.bitcoin_wallet.clone(), - alice.monero_wallet.clone(), - alice.config, - alice.swap_id, - alice.db, - ) - .await - .unwrap(); + let alice_state = alice::run(alice_swap).await.unwrap(); // TODO: The test passes like this, but the assertion should be done after Bob // refunded, not at the end because this can cause side-effects! // We have to properly wait for the refund tx's finality inside the assertion, // which requires storing the refund_tx_id in the the state! - bob_harness.assert_refunded(bob_state.unwrap()).await; - alice_harness.assert_refunded(alice_state).await; + test.assert_bob_refunded(bob_state.unwrap()).await; + test.assert_alice_refunded(alice_state).await; }) .await; } diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 30f69b7b..ef935769 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -22,9 +22,285 @@ use tracing_core::dispatcher::DefaultGuard; use tracing_log::LogTracer; use uuid::Uuid; -pub async fn test(testfn: T) +pub struct Test { + swap_amounts: SwapAmounts, + + alice_swap_factory: AliceSwapFactory, + bob_swap_factory: BobSwapFactory, +} + +impl Test { + pub async fn new_swap_as_alice(&self) -> alice::Swap { + let (swap, mut event_loop) = self + .alice_swap_factory + .new_swap_as_alice(self.swap_amounts) + .await; + + tokio::spawn(async move { event_loop.run().await }); + + swap + } + + pub async fn new_swap_as_bob(&self) -> bob::Swap { + let (swap, event_loop) = self + .bob_swap_factory + .new_swap_as_bob(self.swap_amounts) + .await; + + tokio::spawn(async move { event_loop.run().await }); + + swap + } + + pub async fn recover_alice_from_db(&self) -> alice::Swap { + let (swap, mut event_loop) = self.alice_swap_factory.recover_alice_from_db().await; + + tokio::spawn(async move { event_loop.run().await }); + + swap + } + + pub async fn recover_bob_from_db(&self) -> bob::Swap { + let (swap, event_loop) = self.bob_swap_factory.recover_bob_from_db().await; + + tokio::spawn(async move { event_loop.run().await }); + + swap + } + + pub async fn assert_alice_redeemed(&self, state: AliceState) { + assert!(matches!(state, AliceState::BtcRedeemed)); + + let btc_balance_after_swap = self + .alice_swap_factory + .bitcoin_wallet + .as_ref() + .balance() + .await + .unwrap(); + assert_eq!( + btc_balance_after_swap, + self.alice_swap_factory.starting_balances.btc + self.swap_amounts.btc + - bitcoin::Amount::from_sat(bitcoin::TX_FEE) + ); + + let xmr_balance_after_swap = self + .alice_swap_factory + .monero_wallet + .as_ref() + .get_balance() + .await + .unwrap(); + assert!( + xmr_balance_after_swap + <= self.alice_swap_factory.starting_balances.xmr - self.swap_amounts.xmr + ); + } + + pub async fn assert_alice_refunded(&self, state: AliceState) { + assert!(matches!(state, AliceState::XmrRefunded)); + + let btc_balance_after_swap = self + .alice_swap_factory + .bitcoin_wallet + .as_ref() + .balance() + .await + .unwrap(); + assert_eq!( + btc_balance_after_swap, + self.alice_swap_factory.starting_balances.btc + ); + + // Ensure that Alice's balance is refreshed as we use a newly created wallet + self.alice_swap_factory + .monero_wallet + .as_ref() + .inner + .refresh() + .await + .unwrap(); + let xmr_balance_after_swap = self + .alice_swap_factory + .monero_wallet + .as_ref() + .get_balance() + .await + .unwrap(); + assert_eq!(xmr_balance_after_swap, self.swap_amounts.xmr); + } + + pub async fn assert_alice_punished(&self, state: AliceState) { + assert!(matches!(state, AliceState::BtcPunished)); + + let btc_balance_after_swap = self + .alice_swap_factory + .bitcoin_wallet + .as_ref() + .balance() + .await + .unwrap(); + assert_eq!( + btc_balance_after_swap, + self.alice_swap_factory.starting_balances.btc + self.swap_amounts.btc + - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) + ); + + let xmr_balance_after_swap = self + .alice_swap_factory + .monero_wallet + .as_ref() + .get_balance() + .await + .unwrap(); + assert!( + xmr_balance_after_swap + <= self.alice_swap_factory.starting_balances.xmr - self.swap_amounts.xmr + ); + } + + pub async fn assert_bob_redeemed(&self, state: BobState) { + let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state { + tx_lock_id + } else { + panic!("Bob in unexpected state"); + }; + + let lock_tx_bitcoin_fee = self + .bob_swap_factory + .bitcoin_wallet + .transaction_fee(lock_tx_id) + .await + .unwrap(); + + let btc_balance_after_swap = self + .bob_swap_factory + .bitcoin_wallet + .as_ref() + .balance() + .await + .unwrap(); + assert_eq!( + btc_balance_after_swap, + self.bob_swap_factory.starting_balances.btc + - self.swap_amounts.btc + - lock_tx_bitcoin_fee + ); + + // Ensure that Bob's balance is refreshed as we use a newly created wallet + self.bob_swap_factory + .monero_wallet + .as_ref() + .inner + .refresh() + .await + .unwrap(); + let xmr_balance_after_swap = self + .bob_swap_factory + .monero_wallet + .as_ref() + .get_balance() + .await + .unwrap(); + assert_eq!( + xmr_balance_after_swap, + self.bob_swap_factory.starting_balances.xmr + self.swap_amounts.xmr + ); + } + + pub async fn assert_bob_refunded(&self, state: BobState) { + let lock_tx_id = if let BobState::BtcRefunded(state4) = state { + state4.tx_lock_id() + } else { + panic!("Bob in unexpected state"); + }; + let lock_tx_bitcoin_fee = self + .bob_swap_factory + .bitcoin_wallet + .transaction_fee(lock_tx_id) + .await + .unwrap(); + + let btc_balance_after_swap = self + .bob_swap_factory + .bitcoin_wallet + .as_ref() + .balance() + .await + .unwrap(); + + let alice_submitted_cancel = btc_balance_after_swap + == self.bob_swap_factory.starting_balances.btc + - lock_tx_bitcoin_fee + - bitcoin::Amount::from_sat(bitcoin::TX_FEE); + + let bob_submitted_cancel = btc_balance_after_swap + == self.bob_swap_factory.starting_balances.btc + - lock_tx_bitcoin_fee + - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE); + + // The cancel tx can be submitted by both Alice and Bob. + // Since we cannot be sure who submitted it we have to assert accordingly + assert!(alice_submitted_cancel || bob_submitted_cancel); + + let xmr_balance_after_swap = self + .bob_swap_factory + .monero_wallet + .as_ref() + .get_balance() + .await + .unwrap(); + assert_eq!( + xmr_balance_after_swap, + self.bob_swap_factory.starting_balances.xmr + ); + } + + pub async fn assert_bob_punished(&self, state: BobState) { + let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state { + tx_lock_id + } else { + panic!("Bob in unexpected state"); + }; + + let lock_tx_bitcoin_fee = self + .bob_swap_factory + .bitcoin_wallet + .transaction_fee(lock_tx_id) + .await + .unwrap(); + + let btc_balance_after_swap = self + .bob_swap_factory + .bitcoin_wallet + .as_ref() + .balance() + .await + .unwrap(); + assert_eq!( + btc_balance_after_swap, + self.bob_swap_factory.starting_balances.btc + - self.swap_amounts.btc + - lock_tx_bitcoin_fee + ); + + let xmr_balance_after_swap = self + .bob_swap_factory + .monero_wallet + .as_ref() + .get_balance() + .await + .unwrap(); + assert_eq!( + xmr_balance_after_swap, + self.bob_swap_factory.starting_balances.xmr + ); + } +} + +pub async fn init(testfn: T) where - T: Fn(AliceHarness, BobHarness) -> F, + T: Fn(Test) -> F, F: Future, { let cli = Cli::default(); @@ -44,9 +320,8 @@ where xmr: swap_amounts.xmr * 10, btc: bitcoin::Amount::ZERO, }; - let alice_harness = AliceHarness::new( + let alice_swap_factory = AliceSwapFactory::new( config, - swap_amounts, Uuid::new_v4(), &monero, &containers.bitcoind, @@ -59,32 +334,27 @@ where btc: swap_amounts.btc * 10, }; - let bob_harness = BobHarness::new( + let bob_swap_factory = BobSwapFactory::new( config, - swap_amounts, Uuid::new_v4(), &monero, &containers.bitcoind, bob_starting_balances, - alice_harness.listen_address(), - alice_harness.peer_id(), + alice_swap_factory.listen_address(), + alice_swap_factory.peer_id(), ) .await; - testfn(alice_harness, bob_harness).await -} + let test = Test { + swap_amounts, + alice_swap_factory, + bob_swap_factory, + }; -pub struct Alice { - pub state: AliceState, - pub event_loop_handle: alice::EventLoopHandle, - pub bitcoin_wallet: Arc, - pub monero_wallet: Arc, - pub config: Config, - pub swap_id: Uuid, - pub db: Database, + testfn(test).await } -pub struct AliceHarness { +pub struct AliceSwapFactory { listen_address: Multiaddr, peer_id: PeerId, @@ -92,17 +362,15 @@ pub struct AliceHarness { db_path: PathBuf, swap_id: Uuid, - swap_amounts: SwapAmounts, bitcoin_wallet: Arc, monero_wallet: Arc, config: Config, starting_balances: StartingBalances, } -impl AliceHarness { +impl AliceSwapFactory { async fn new( config: Config, - swap_amounts: SwapAmounts, swap_id: Uuid, monero: &Monero, bitcoind: &Bitcoind<'_>, @@ -132,7 +400,6 @@ impl AliceHarness { listen_address, peer_id, swap_id, - swap_amounts, bitcoin_wallet, monero_wallet, config, @@ -140,39 +407,37 @@ impl AliceHarness { } } - pub async fn new_alice(&self) -> Alice { + pub async fn new_swap_as_alice( + &self, + swap_amounts: SwapAmounts, + ) -> (alice::Swap, alice::EventLoop) { let initial_state = init_alice_state( - self.swap_amounts.btc, - self.swap_amounts.xmr, + swap_amounts.btc, + swap_amounts.xmr, self.bitcoin_wallet.clone(), self.config, ) .await; - let (mut event_loop, event_loop_handle) = + let (event_loop, event_loop_handle) = init_alice_event_loop(self.listen_address.clone(), self.seed); - tokio::spawn(async move { event_loop.run().await }); - let db = Database::open(self.db_path.as_path()).unwrap(); - - Alice { - event_loop_handle, - bitcoin_wallet: self.bitcoin_wallet.clone(), - monero_wallet: self.monero_wallet.clone(), - config: self.config, - db, - state: initial_state, - swap_id: self.swap_id, - } + ( + alice::Swap { + event_loop_handle, + bitcoin_wallet: self.bitcoin_wallet.clone(), + monero_wallet: self.monero_wallet.clone(), + config: self.config, + db, + state: initial_state, + swap_id: self.swap_id, + }, + event_loop, + ) } - pub async fn recover_alice_from_db(&self) -> Alice { - // TODO: "simulated restart" issues: - // - create new wallets instead of reusing (hard because of container - // lifetimes) - // - consider aborting the old event loop (currently just keeps running) - + pub async fn recover_alice_from_db(&self) -> (alice::Swap, alice::EventLoop) { // reopen the existing database let db = Database::open(self.db_path.clone().as_path()).unwrap(); @@ -183,60 +448,21 @@ impl AliceHarness { unreachable!() }; - let (mut event_loop, event_loop_handle) = + let (event_loop, event_loop_handle) = init_alice_event_loop(self.listen_address.clone(), self.seed); - tokio::spawn(async move { event_loop.run().await }); - - Alice { - state: resume_state, - event_loop_handle, - bitcoin_wallet: self.bitcoin_wallet.clone(), - monero_wallet: self.monero_wallet.clone(), - config: self.config, - swap_id: self.swap_id, - db, - } - } - - pub async fn assert_redeemed(&self, state: AliceState) { - assert!(matches!(state, AliceState::BtcRedeemed)); - - let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!( - btc_balance_after_swap, - self.starting_balances.btc + self.swap_amounts.btc - - bitcoin::Amount::from_sat(bitcoin::TX_FEE) - ); - - let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); - assert!(xmr_balance_after_swap <= self.starting_balances.xmr - self.swap_amounts.xmr); - } - - pub async fn assert_refunded(&self, state: AliceState) { - assert!(matches!(state, AliceState::XmrRefunded)); - - let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!(btc_balance_after_swap, self.starting_balances.btc); - - // Ensure that Alice's balance is refreshed as we use a newly created wallet - self.monero_wallet.as_ref().inner.refresh().await.unwrap(); - let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); - assert_eq!(xmr_balance_after_swap, self.swap_amounts.xmr); - } - - pub async fn assert_punished(&self, state: AliceState) { - assert!(matches!(state, AliceState::BtcPunished)); - - let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!( - btc_balance_after_swap, - self.starting_balances.btc + self.swap_amounts.btc - - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) - ); - - let xnr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); - assert!(xnr_balance_after_swap <= self.starting_balances.xmr - self.swap_amounts.xmr); + ( + alice::Swap { + state: resume_state, + event_loop_handle, + bitcoin_wallet: self.bitcoin_wallet.clone(), + monero_wallet: self.monero_wallet.clone(), + config: self.config, + swap_id: self.swap_id, + db, + }, + event_loop, + ) } pub fn peer_id(&self) -> PeerId { @@ -248,20 +474,10 @@ impl AliceHarness { } } -pub struct Bob { - pub state: BobState, - pub event_loop_handle: bob::EventLoopHandle, - pub db: Database, - pub bitcoin_wallet: Arc, - pub monero_wallet: Arc, - pub swap_id: Uuid, -} - -pub struct BobHarness { +pub struct BobSwapFactory { db_path: PathBuf, swap_id: Uuid, - swap_amounts: SwapAmounts, bitcoin_wallet: Arc, monero_wallet: Arc, config: Config, @@ -271,11 +487,10 @@ pub struct BobHarness { alice_connect_peer_id: PeerId, } -impl BobHarness { +impl BobSwapFactory { #[allow(clippy::too_many_arguments)] async fn new( config: Config, - swap_amounts: SwapAmounts, swap_id: Uuid, monero: &Monero, bitcoind: &Bitcoind<'_>, @@ -291,7 +506,6 @@ impl BobHarness { Self { db_path, swap_id, - swap_amounts, bitcoin_wallet, monero_wallet, config, @@ -301,10 +515,10 @@ impl BobHarness { } } - pub async fn new_bob(&self) -> Bob { + pub async fn new_swap_as_bob(&self, swap_amounts: SwapAmounts) -> (bob::Swap, bob::EventLoop) { let initial_state = init_bob_state( - self.swap_amounts.btc, - self.swap_amounts.xmr, + swap_amounts.btc, + swap_amounts.xmr, self.bitcoin_wallet.clone(), self.config, ) @@ -315,26 +529,22 @@ impl BobHarness { self.alice_connect_address.clone(), ); - tokio::spawn(async move { event_loop.run().await }); - let db = Database::open(self.db_path.as_path()).unwrap(); - Bob { - state: initial_state, - event_loop_handle, - db, - bitcoin_wallet: self.bitcoin_wallet.clone(), - monero_wallet: self.monero_wallet.clone(), - swap_id: self.swap_id, - } + ( + bob::Swap { + state: initial_state, + event_loop_handle, + db, + bitcoin_wallet: self.bitcoin_wallet.clone(), + monero_wallet: self.monero_wallet.clone(), + swap_id: self.swap_id, + }, + event_loop, + ) } - pub async fn recover_bob_from_db(&self) -> Bob { - // TODO: "simulated restart" issues: - // - create new wallets instead of reusing (hard because of container - // lifetimes) - // - consider aborting the old event loop (currently just keeps running) - + pub async fn recover_bob_from_db(&self) -> (bob::Swap, bob::EventLoop) { // reopen the existing database let db = Database::open(self.db_path.clone().as_path()).unwrap(); @@ -350,99 +560,17 @@ impl BobHarness { self.alice_connect_address.clone(), ); - tokio::spawn(async move { event_loop.run().await }); - - Bob { - state: resume_state, - event_loop_handle, - db, - bitcoin_wallet: self.bitcoin_wallet.clone(), - monero_wallet: self.monero_wallet.clone(), - swap_id: self.swap_id, - } - } - - pub async fn assert_redeemed(&self, state: BobState) { - let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state { - tx_lock_id - } else { - panic!("Bob in unexpected state"); - }; - - let lock_tx_bitcoin_fee = self - .bitcoin_wallet - .transaction_fee(lock_tx_id) - .await - .unwrap(); - - let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!( - btc_balance_after_swap, - self.starting_balances.btc - self.swap_amounts.btc - lock_tx_bitcoin_fee - ); - - // Ensure that Bob's balance is refreshed as we use a newly created wallet - self.monero_wallet.as_ref().inner.refresh().await.unwrap(); - let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); - assert_eq!( - xmr_balance_after_swap, - self.starting_balances.xmr + self.swap_amounts.xmr - ); - } - - pub async fn assert_refunded(&self, state: BobState) { - let lock_tx_id = if let BobState::BtcRefunded(state4) = state { - state4.tx_lock_id() - } else { - panic!("Bob in unexpected state"); - }; - let lock_tx_bitcoin_fee = self - .bitcoin_wallet - .transaction_fee(lock_tx_id) - .await - .unwrap(); - - let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); - - let alice_submitted_cancel = btc_balance_after_swap - == self.starting_balances.btc - - lock_tx_bitcoin_fee - - bitcoin::Amount::from_sat(bitcoin::TX_FEE); - - let bob_submitted_cancel = btc_balance_after_swap - == self.starting_balances.btc - - lock_tx_bitcoin_fee - - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE); - - // The cancel tx can be submitted by both Alice and Bob. - // Since we cannot be sure who submitted it we have to assert accordingly - assert!(alice_submitted_cancel || bob_submitted_cancel); - - let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); - assert_eq!(xmr_balance_after_swap, self.starting_balances.xmr); - } - - pub async fn assert_punished(&self, state: BobState) { - let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state { - tx_lock_id - } else { - panic!("Bob in unexpected state"); - }; - - let lock_tx_bitcoin_fee = self - .bitcoin_wallet - .transaction_fee(lock_tx_id) - .await - .unwrap(); - - let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!( - btc_balance_after_swap, - self.starting_balances.btc - self.swap_amounts.btc - lock_tx_bitcoin_fee - ); - - let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); - assert_eq!(xmr_balance_after_swap, self.starting_balances.xmr); + ( + bob::Swap { + state: resume_state, + event_loop_handle, + db, + bitcoin_wallet: self.bitcoin_wallet.clone(), + monero_wallet: self.monero_wallet.clone(), + swap_id: self.swap_id, + }, + event_loop, + ) } }