diff --git a/swap/tests/refund_restart_alice.rs b/swap/tests/refund_restart_alice.rs index 0f9a062f..2a1d338c 100644 --- a/swap/tests/refund_restart_alice.rs +++ b/swap/tests/refund_restart_alice.rs @@ -1,174 +1,66 @@ -use crate::testutils::{init_alice, init_bob}; -use futures::future::try_join; -use get_port::get_port; -use libp2p::Multiaddr; use rand::rngs::OsRng; -use swap::{ - bitcoin, - config::Config, - database::Database, - monero, - protocol::{alice, alice::AliceState, bob, bob::BobState}, - seed::Seed, -}; -use tempfile::tempdir; -use testcontainers::clients::Cli; -use testutils::init_tracing; -use tokio::select; -use uuid::Uuid; +use swap::protocol::{alice, alice::AliceState, bob}; pub mod testutils; -// Bob locks btc and Alice locks xmr. Alice fails to act so Bob refunds. Alice -// then also refunds. +/// Bob locks btc and Alice locks xmr. Alice fails to act so Bob refunds. Alice +/// then also refunds. #[tokio::test] async fn given_alice_restarts_after_xmr_is_locked_abort_swap() { - let _guard = init_tracing(); + testutils::test(|alice_harness, bob_harness| async move { + let alice = alice_harness.new_alice().await; + let bob = bob_harness.new_bob().await; - let cli = Cli::default(); - let ( - monero, - testutils::Containers { - bitcoind, - monerods: _monerods, - }, - ) = testutils::init_containers(&cli).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 btc_to_swap = bitcoin::Amount::from_sat(1_000_000); - let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000); - - let bob_btc_starting_balance = btc_to_swap * 10; - let bob_xmr_starting_balance = monero::Amount::from_piconero(0); - - let alice_btc_starting_balance = bitcoin::Amount::ZERO; - let alice_xmr_starting_balance = xmr_to_swap * 10; - - let port = get_port().expect("Failed to find a free port"); - let alice_multiaddr: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port) - .parse() - .expect("failed to parse Alice's address"); - - let alice_seed = Seed::random().unwrap(); - let ( - alice_state, - mut alice_event_loop_1, - alice_event_loop_handle_1, - alice_btc_wallet, - alice_xmr_wallet, - _, - ) = init_alice( - &bitcoind, - &monero, - btc_to_swap, - xmr_to_swap, - alice_xmr_starting_balance, - alice_multiaddr.clone(), - Config::regtest(), - alice_seed, - ) - .await; - - let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) = - init_bob( - alice_multiaddr.clone(), - alice_event_loop_1.peer_id(), - &bitcoind, - &monero, - btc_to_swap, - bob_btc_starting_balance, - xmr_to_swap, - Config::regtest(), - ) - .await; - - let bob_fut = bob::swap::swap( - bob_state, - bob_event_loop_handle, - bob_db, - bob_btc_wallet.clone(), - bob_xmr_wallet.clone(), - OsRng, - Uuid::new_v4(), - ); - - let alice_swap_id = Uuid::new_v4(); - let alice_db_datadir = tempdir().unwrap(); - - let alice_xmr_locked_fut = { - let alice_db = Database::open(alice_db_datadir.path()).unwrap(); - alice::swap::run_until( - alice_state, + let alice_state = alice::run_until( + alice.state, alice::swap::is_xmr_locked, - alice_event_loop_handle_1, - alice_btc_wallet.clone(), - alice_xmr_wallet.clone(), - Config::regtest(), - alice_swap_id, - alice_db, - ) - }; - - tokio::spawn(bob_event_loop.run()); - - // We are selecting with alice_event_loop_1 so that we stop polling on it once - // the try_join is finished. - let (bob_state, alice_restart_state) = select! { - res = try_join(bob_fut, alice_xmr_locked_fut) => res.unwrap(), - _ = alice_event_loop_1.run() => panic!("The event loop should never finish") - }; - - let tx_lock_id = if let BobState::BtcRefunded(state4) = bob_state { - state4.tx_lock_id() - } else { - panic!("Bob in unexpected state"); - }; - - let (mut alice_event_loop_2, alice_event_loop_handle_2) = - testutils::init_alice_event_loop(alice_multiaddr, alice_seed); - - let alice_final_state = { - let alice_db = Database::open(alice_db_datadir.path()).unwrap(); - alice::swap::swap( - alice_restart_state, - alice_event_loop_handle_2, - alice_btc_wallet.clone(), - alice_xmr_wallet.clone(), - Config::regtest(), - alice_swap_id, - alice_db, + alice.event_loop_handle, + alice.bitcoin_wallet.clone(), + alice.monero_wallet.clone(), + alice.config, + alice.swap_id, + alice.db, ) .await - .unwrap() - }; - tokio::spawn(async move { alice_event_loop_2.run().await }); + .unwrap(); + assert!(matches!(alice_state, AliceState::XmrLocked {..})); - assert!(matches!(alice_final_state, AliceState::XmrRefunded)); + // Alice does not act, Bob refunds + let bob_state = bob_swap_handle.await.unwrap(); - let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap(); - let btc_bob_final = bob_btc_wallet.as_ref().balance().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 lock_tx_bitcoin_fee = bob_btc_wallet.transaction_fee(tx_lock_id).await.unwrap(); + 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(); - 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().inner.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().inner.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); + // 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; + }) + .await; } diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index e9ffa108..569cce46 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -76,6 +76,18 @@ impl AliceHarness { 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)); @@ -319,11 +331,41 @@ impl BobHarness { ); } + 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, lock_tx_id: ::bitcoin::Txid) { assert!(matches!(state, BobState::BtcPunished)); - // lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal - // to TX_FEE let lock_tx_bitcoin_fee = self .bitcoin_wallet .transaction_fee(lock_tx_id)