diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index 2fd9896d..e1bbe21a 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -112,12 +112,26 @@ impl From<&AliceState> for state::Alice { match alice_state { AliceState::Started { amounts, - state0: State0 { a, s_a, v_a, .. }, + state0: + State0 { + a, + s_a, + v_a, + refund_timelock, + punish_timelock, + redeem_address, + punish_address, + .. + }, } => Alice::Started { amounts: *amounts, a: a.clone(), s_a: *s_a, v_a: *v_a, + refund_timelock: *refund_timelock, + punish_timelock: *punish_timelock, + redeem_address: redeem_address.clone(), + punish_address: punish_address.clone(), }, AliceState::Negotiated { state3, .. } => Alice::Negotiated(state3.clone()), AliceState::BtcLocked { state3, .. } => Alice::BtcLocked(state3.clone()), @@ -150,7 +164,29 @@ impl TryFrom for AliceState { use AliceState::*; if let Swap::Alice(state) = db_state { let alice_state = match state { - Alice::Started { .. } => todo!(), + Alice::Started { + amounts, + a, + s_a, + v_a, + refund_timelock, + punish_timelock, + redeem_address, + punish_address, + } => Started { + amounts, + state0: State0 { + a, + s_a, + v_a, + btc: amounts.btc, + xmr: amounts.xmr, + refund_timelock, + punish_timelock, + redeem_address, + punish_address, + }, + }, Alice::Negotiated(state3) => Negotiated { channel: None, amounts: SwapAmounts { @@ -243,6 +279,29 @@ pub async fn swap( .await } +pub async fn recover( + event_loop_handle: EventLoopHandle, + bitcoin_wallet: Arc, + monero_wallet: Arc, + config: Config, + swap_id: Uuid, + db: Database, +) -> Result { + let db_swap = db.get_state(swap_id)?; + let start_state = AliceState::try_from(db_swap)?; + let (state, _) = swap( + start_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + config, + swap_id, + db, + ) + .await?; + Ok(state) +} + pub fn is_complete(state: &AliceState) -> bool { matches!( state, @@ -279,6 +338,7 @@ pub async fn run_until( config: Config, swap_id: Uuid, db: Database, + // TODO: Remove EventLoopHandle! ) -> Result<(AliceState, EventLoopHandle)> { info!("Current state:{}", state); if is_target_state(&state) { diff --git a/swap/src/state.rs b/swap/src/state.rs index 5d62b548..601505f6 100644 --- a/swap/src/state.rs +++ b/swap/src/state.rs @@ -18,9 +18,15 @@ pub enum Swap { pub enum Alice { Started { amounts: SwapAmounts, + // TODO: This should not be saved, instead always derive it from a seed (and that seed file + // is the only thing that has to be kept secure) a: crate::bitcoin::SecretKey, s_a: cross_curve_dleq::Scalar, v_a: monero::PrivateViewKey, + refund_timelock: u32, + punish_timelock: u32, + redeem_address: ::bitcoin::Address, + punish_address: ::bitcoin::Address, }, Negotiated(alice::State3), BtcLocked(alice::State3), diff --git a/swap/tests/alice_safe_restart.rs b/swap/tests/alice_safe_restart.rs index fbf2a618..571629c9 100644 --- a/swap/tests/alice_safe_restart.rs +++ b/swap/tests/alice_safe_restart.rs @@ -11,7 +11,6 @@ use xmr_btc::config::Config; pub mod testutils; use crate::testutils::{init_alice, init_bob}; -use std::convert::TryFrom; use testutils::init_tracing; #[tokio::test] @@ -32,7 +31,6 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { let bob_btc_starting_balance = btc_to_swap * 10; let bob_xmr_starting_balance = xmr_btc::monero::Amount::ZERO; - let alice_btc_starting_balance = bitcoin::Amount::ZERO; let alice_xmr_starting_balance = xmr_to_swap * 10; let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9877" @@ -42,7 +40,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { let config = Config::regtest(); let ( - alice_state, + start_state, mut alice_event_loop, alice_event_loop_handle, alice_btc_wallet, @@ -51,7 +49,6 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { &bitcoind, &monero, btc_to_swap, - alice_btc_starting_balance, xmr_to_swap, alice_xmr_starting_balance, alice_multiaddr.clone(), @@ -61,7 +58,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) = init_bob( - alice_multiaddr, + alice_multiaddr.clone(), &bitcoind, &monero, btc_to_swap, @@ -94,8 +91,8 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { let alice_swap_id = Uuid::new_v4(); - let (alice_state, alice_event_loop_handle) = alice::swap::run_until( - alice_state, + let (alice_state, _) = alice::swap::run_until( + start_state, alice::swap::is_encsig_learned, alice_event_loop_handle, alice_btc_wallet.clone(), @@ -110,19 +107,29 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { assert!(matches!(alice_state, AliceState::EncSignLearned {..})); let alice_db = Database::open(alice_db_datadir.path()).unwrap(); - let alice_state = alice_db.get_state(alice_swap_id).unwrap(); + let state_before_restart = alice_db.get_state(alice_swap_id).unwrap(); - if let swap::state::Swap::Alice(state) = alice_state.clone() { + if let swap::state::Swap::Alice(state) = state_before_restart.clone() { assert!(matches!(state, swap::state::Alice::EncSignLearned {..})); } - let (alice_state, _) = alice::swap::swap( - AliceState::try_from(alice_state).unwrap(), - alice_event_loop_handle, - alice_btc_wallet.clone(), - alice_xmr_wallet.clone(), + let (_, mut event_loop_after_restart, event_loop_handle_after_restart) = + testutils::init_alice_eventloop( + btc_to_swap, + xmr_to_swap, + alice_btc_wallet.clone(), + alice_multiaddr, + config, + ) + .await; + let _alice_swarm_fut = tokio::spawn(async move { event_loop_after_restart.run().await }); + + let alice_state = alice::swap::recover( + event_loop_handle_after_restart, + alice_btc_wallet, + alice_xmr_wallet, Config::regtest(), - Uuid::new_v4(), + alice_swap_id, alice_db, ) .await @@ -130,198 +137,3 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { assert!(matches!(alice_state, AliceState::BtcRedeemed {..})); } -// #[tokio::test] -// async fn given_alice_restarts_after_xmr_is_locked_refund_swap() { -// setup_tracing(); -// -// let config = Config::regtest(); -// -// let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876" -// .parse() -// .expect("failed to parse Alice's address"); -// -// let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); -// let init_btc_alice = bitcoin::Amount::ZERO; -// let init_btc_bob = btc_to_swap * 10; -// -// let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000); -// let init_xmr_alice = xmr_to_swap * 10; -// let init_xmr_bob = monero::Amount::ZERO; -// -// let cli = Cli::default(); -// let (alice_btc_wallet, alice_xmr_wallet, bob_btc_wallet, bob_xmr_wallet, -// _containers) = setup_wallets( -// &cli, -// init_btc_alice, -// init_xmr_alice, -// init_btc_bob, -// init_xmr_bob, -// config -// ) -// .await; -// -// let alice_btc_wallet = Arc::new(alice_btc_wallet); -// let alice_xmr_wallet = Arc::new(alice_xmr_wallet); -// let bob_btc_wallet = Arc::new(bob_btc_wallet); -// let bob_xmr_wallet = Arc::new(bob_xmr_wallet); -// -// let amounts = SwapAmounts { -// btc: btc_to_swap, -// xmr: xmr_to_swap, -// }; -// -// let alice_db_dir = TempDir::new().unwrap(); -// let alice_swap_fut = async { -// let rng = &mut OsRng; -// let (alice_start_state, state0) = { -// let a = bitcoin::SecretKey::new_random(rng); -// let s_a = cross_curve_dleq::Scalar::random(rng); -// let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng); -// let redeem_address = -// alice_btc_wallet.as_ref().new_address().await.unwrap(); let -// punish_address = redeem_address.clone(); let state0 = -// xmr_btc::alice::State0::new( a, -// s_a, -// v_a, -// amounts.btc, -// amounts.xmr, -// config.bitcoin_refund_timelock, -// config.bitcoin_punish_timelock, -// redeem_address, -// punish_address, -// ); -// -// ( -// AliceState::Started { -// amounts, -// state0: state0.clone(), -// }, -// state0, -// ) -// }; -// let alice_behaviour = alice::Behaviour::new(state0.clone()); -// let alice_transport = build(alice_behaviour.identity()).unwrap(); -// let (mut alice_event_loop_1, alice_event_loop_handle) = -// alice::event_loop::EventLoop::new( alice_transport, -// alice_behaviour, -// alice_multiaddr.clone(), -// ) -// .unwrap(); -// -// let _alice_event_loop_1 = tokio::spawn(async move { -// alice_event_loop_1.run().await }); -// -// let config = xmr_btc::config::Config::regtest(); -// let swap_id = Uuid::new_v4(); -// -// let db = Database::open(alice_db_dir.path()).unwrap(); -// -// // Alice reaches encsig_learned -// alice::swap::run_until( -// alice_start_state, -// |state| matches!(state, AliceState::XmrLocked { .. }), -// alice_event_loop_handle, -// alice_btc_wallet.clone(), -// alice_xmr_wallet.clone(), -// config, -// swap_id, -// db, -// ) -// .await -// .unwrap(); -// -// let db = Database::open(alice_db_dir.path()).unwrap(); -// -// let alice_behaviour = alice::Behaviour::new(state0); -// let alice_transport = build(alice_behaviour.identity()).unwrap(); -// let (mut alice_event_loop_2, alice_event_loop_handle) = -// alice::event_loop::EventLoop::new( alice_transport, -// alice_behaviour, -// alice_multiaddr.clone(), -// ) -// .unwrap(); -// -// let _alice_event_loop_2 = tokio::spawn(async move { -// alice_event_loop_2.run().await }); -// -// // Load the latest state from the db -// let latest_state = db.get_state(swap_id).unwrap(); -// let latest_state = latest_state.try_into().unwrap(); -// -// // Finish the swap -// alice::swap::swap( -// latest_state, -// alice_event_loop_handle, -// alice_btc_wallet.clone(), -// alice_xmr_wallet.clone(), -// config, -// swap_id, -// db, -// ) -// .await -// }; -// -// let (bob_swap, bob_event_loop) = { -// let rng = &mut OsRng; -// let bob_db_dir = tempdir().unwrap(); -// let bob_db = Database::open(bob_db_dir.path()).unwrap(); -// let bob_behaviour = bob::Behaviour::default(); -// let bob_transport = build(bob_behaviour.identity()).unwrap(); -// -// let refund_address = bob_btc_wallet.new_address().await.unwrap(); -// let state0 = xmr_btc::bob::State0::new( -// rng, -// btc_to_swap, -// xmr_to_swap, -// config.bitcoin_refund_timelock, -// config.bitcoin_punish_timelock, -// refund_address, -// ); -// let bob_state = BobState::Started { -// state0, -// amounts, -// addr: alice_multiaddr.clone(), -// }; -// let (bob_event_loop, bob_event_loop_handle) = -// bob::event_loop::EventLoop::new(bob_transport, -// bob_behaviour).unwrap(); -// -// ( -// bob::swap::swap( -// bob_state, -// bob_event_loop_handle, -// bob_db, -// bob_btc_wallet.clone(), -// bob_xmr_wallet.clone(), -// OsRng, -// Uuid::new_v4(), -// ), -// bob_event_loop, -// ) -// }; -// -// let _bob_event_loop = tokio::spawn(async move { -// bob_event_loop.run().await }); -// -// try_join(alice_swap_fut, bob_swap).await.unwrap(); -// -// let btc_alice_final = alice_btc_wallet.balance().await.unwrap(); -// let xmr_alice_final = alice_xmr_wallet.get_balance().await.unwrap(); -// -// let btc_bob_final = bob_btc_wallet.balance().await.unwrap(); -// bob_xmr_wallet.0.refresh().await.unwrap(); -// let xmr_bob_final = bob_xmr_wallet.get_balance().await.unwrap(); -// -// // Alice's BTC balance did not change -// assert_eq!(btc_alice_final, init_btc_alice); -// // Bob wasted some BTC fees -// assert_eq!( -// btc_bob_final, -// init_btc_bob - bitcoin::Amount::from_sat(bitcoin::TX_FEE) -// ); -// -// // Alice wasted some XMR fees -// assert_eq!(init_xmr_alice - xmr_alice_final, monero::Amount::ZERO); -// // Bob's ZMR balance did not change -// assert_eq!(xmr_bob_final, init_xmr_bob); -// } diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs deleted file mode 100644 index 7e538bcc..00000000 --- a/swap/tests/e2e.rs +++ /dev/null @@ -1,395 +0,0 @@ -use crate::testutils::{init_alice, init_bob}; -use bitcoin_harness::Bitcoind; -use futures::future::try_join; -use libp2p::Multiaddr; -use monero_harness::Monero; -use rand::rngs::OsRng; -use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState, storage::Database}; -use tempfile::tempdir; -use testcontainers::clients::Cli; -use testutils::init_tracing; -use uuid::Uuid; -use xmr_btc::{bitcoin, config::Config}; - -pub mod testutils; - -/// Run the following tests with RUST_MIN_STACK=10000000 - -#[tokio::test] -async fn happy_path() { - let _guard = init_tracing(); - - let cli = Cli::default(); - let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); - let _ = bitcoind.init(5).await; - let (monero, _container) = - Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) - .await - .unwrap(); - - let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); - let btc_alice = bitcoin::Amount::ZERO; - let btc_bob = btc_to_swap * 10; - - // this xmr value matches the logic of alice::calculate_amounts i.e. btc * - // 10_000 * 100 - let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); - let xmr_alice = xmr_to_swap * 10; - let xmr_bob = xmr_btc::monero::Amount::ZERO; - - // todo: This should not be hardcoded - let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876" - .parse() - .expect("failed to parse Alice's address"); - - let config = Config::regtest(); - - let ( - alice_state, - mut alice_event_loop, - alice_event_loop_handle, - alice_btc_wallet, - alice_xmr_wallet, - ) = init_alice( - &bitcoind, - &monero, - btc_to_swap, - btc_alice, - xmr_to_swap, - xmr_alice, - alice_multiaddr.clone(), - config, - ) - .await; - - let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) = - init_bob( - alice_multiaddr, - &bitcoind, - &monero, - btc_to_swap, - btc_bob, - xmr_to_swap, - xmr_bob, - config, - ) - .await; - - let alice_db_datadir = tempdir().unwrap(); - let alice_db = Database::open(alice_db_datadir.path()).unwrap(); - - let alice_swap_fut = alice::swap::swap( - alice_state, - alice_event_loop_handle, - alice_btc_wallet.clone(), - alice_xmr_wallet.clone(), - config, - Uuid::new_v4(), - alice_db, - ); - - let _alice_swarm_fut = tokio::spawn(async move { alice_event_loop.run().await }); - - let bob_swap_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 _bob_swarm_fut = tokio::spawn(async move { bob_event_loop.run().await }); - - try_join(alice_swap_fut, bob_swap_fut).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(); - - let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap(); - - bob_xmr_wallet.as_ref().0.refresh().await.unwrap(); - let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap(); - - assert_eq!( - btc_alice_final, - btc_alice + btc_to_swap - bitcoin::Amount::from_sat(bitcoin::TX_FEE) - ); - assert!(btc_bob_final <= btc_bob - btc_to_swap); - - assert!(xmr_alice_final <= xmr_alice - xmr_to_swap); - assert_eq!(xmr_bob_final, xmr_bob + xmr_to_swap); -} - -/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice -/// the encsig and fail to refund or redeem. Alice punishes. -#[tokio::test] -async fn alice_punishes_if_bob_never_acts_after_fund() { - let _guard = init_tracing(); - - let cli = Cli::default(); - let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); - let _ = bitcoind.init(5).await; - let (monero, _container) = - Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) - .await - .unwrap(); - - let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); - let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); - - let bob_btc_starting_balance = btc_to_swap * 10; - let bob_xmr_starting_balance = xmr_btc::monero::Amount::ZERO; - - let alice_btc_starting_balance = bitcoin::Amount::ZERO; - let alice_xmr_starting_balance = xmr_to_swap * 10; - - // todo: This should not be hardcoded - let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9877" - .parse() - .expect("failed to parse Alice's address"); - - let config = Config::regtest(); - - let ( - alice_state, - mut alice_event_loop, - alice_event_loop_handle, - alice_btc_wallet, - alice_xmr_wallet, - ) = init_alice( - &bitcoind, - &monero, - btc_to_swap, - alice_btc_starting_balance, - xmr_to_swap, - alice_xmr_starting_balance, - alice_multiaddr.clone(), - config, - ) - .await; - - let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) = - init_bob( - alice_multiaddr, - &bitcoind, - &monero, - btc_to_swap, - bob_btc_starting_balance, - xmr_to_swap, - bob_xmr_starting_balance, - config, - ) - .await; - - let bob_btc_locked_fut = bob::swap::run_until( - bob_state, - bob::swap::is_btc_locked, - bob_event_loop_handle, - bob_db, - bob_btc_wallet.clone(), - bob_xmr_wallet.clone(), - OsRng, - Uuid::new_v4(), - ); - - let _bob_swarm_fut = tokio::spawn(async move { bob_event_loop.run().await }); - - let alice_db_datadir = tempdir().unwrap(); - let alice_db = Database::open(alice_db_datadir.path()).unwrap(); - - let alice_fut = alice::swap::swap( - alice_state, - alice_event_loop_handle, - alice_btc_wallet.clone(), - alice_xmr_wallet.clone(), - Config::regtest(), - Uuid::new_v4(), - alice_db, - ); - - let _alice_swarm_fut = tokio::spawn(async move { alice_event_loop.run().await }); - - // Wait until alice has locked xmr and bob has locked btc - let ((alice_state, _), bob_state) = try_join(alice_fut, bob_btc_locked_fut).await.unwrap(); - - assert!(matches!(alice_state, AliceState::Punished)); - let bob_state3 = if let BobState::BtcLocked(state3, ..) = bob_state { - state3 - } else { - panic!("Bob in unexpected state"); - }; - - 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_state3.tx_lock_id()) - .await - .unwrap(); - - assert_eq!( - btc_alice_final, - alice_btc_starting_balance + btc_to_swap - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) - ); - - assert_eq!( - btc_bob_final, - bob_btc_starting_balance - btc_to_swap - lock_tx_bitcoin_fee - ); -} - -// Bob locks btc and Alice locks xmr. Alice fails to act so Bob refunds. Alice -// then also refunds. -#[tokio::test] -async fn both_refund() { - use tracing_subscriber::util::SubscriberInitExt as _; - let _guard = tracing_subscriber::fmt() - .with_env_filter("swap=info,xmr_btc=info") - .with_ansi(false) - .set_default(); - - let cli = Cli::default(); - let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); - let _ = bitcoind.init(5).await; - let (monero, _container) = - Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) - .await - .unwrap(); - - let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); - let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); - - let bob_btc_starting_balance = btc_to_swap * 10; - let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0); - - let alice_btc_starting_balance = bitcoin::Amount::ZERO; - let alice_xmr_starting_balance = xmr_to_swap * 10; - - // todo: This should not be hardcoded - let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9879" - .parse() - .expect("failed to parse Alice's address"); - - let ( - alice_state, - mut alice_swarm_driver, - alice_swarm_handle, - alice_btc_wallet, - alice_xmr_wallet, - ) = init_alice( - &bitcoind, - &monero, - btc_to_swap, - alice_btc_starting_balance, - xmr_to_swap, - alice_xmr_starting_balance, - alice_multiaddr.clone(), - Config::regtest(), - ) - .await; - - let (bob_state, bob_swarm_driver, bob_swarm_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) = - init_bob( - alice_multiaddr, - &bitcoind, - &monero, - btc_to_swap, - bob_btc_starting_balance, - xmr_to_swap, - bob_xmr_starting_balance, - Config::regtest(), - ) - .await; - - let bob_fut = bob::swap::swap( - bob_state, - bob_swarm_handle, - bob_db, - bob_btc_wallet.clone(), - bob_xmr_wallet.clone(), - OsRng, - Uuid::new_v4(), - ); - - tokio::spawn(async move { bob_swarm_driver.run().await }); - - let alice_swap_id = Uuid::new_v4(); - let alice_db_datadir = tempdir().unwrap(); - let alice_db = Database::open(alice_db_datadir.path()).unwrap(); - - let alice_xmr_locked_fut = alice::swap::run_until( - alice_state, - alice::swap::is_xmr_locked, - alice_swarm_handle, - alice_btc_wallet.clone(), - alice_xmr_wallet.clone(), - Config::regtest(), - alice_swap_id, - alice_db, - ); - - tokio::spawn(async move { alice_swarm_driver.run().await }); - - // 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(); - - let bob_state4 = if let BobState::BtcRefunded(state4) = bob_state { - state4 - } else { - panic!("Bob in unexpected state"); - }; - - let alice_db = Database::open(alice_db_datadir.path()).unwrap(); - - let (alice_state, _) = alice::swap::swap( - alice_state, - alice_swarm_handle, - alice_btc_wallet.clone(), - alice_xmr_wallet.clone(), - Config::regtest(), - alice_swap_id, - alice_db, - ) - .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); -} diff --git a/swap/tests/happy_path.rs b/swap/tests/happy_path.rs new file mode 100644 index 00000000..bb19212b --- /dev/null +++ b/swap/tests/happy_path.rs @@ -0,0 +1,122 @@ +use crate::testutils::{init_alice, init_bob}; +use bitcoin_harness::Bitcoind; +use futures::future::try_join; +use libp2p::Multiaddr; +use monero_harness::Monero; +use rand::rngs::OsRng; +use swap::{alice, bob, storage::Database}; +use tempfile::tempdir; +use testcontainers::clients::Cli; +use testutils::init_tracing; +use uuid::Uuid; +use xmr_btc::{bitcoin, config::Config}; + +pub mod testutils; + +/// Run the following tests with RUST_MIN_STACK=10000000 + +#[tokio::test] +async fn happy_path() { + let _guard = init_tracing(); + + let cli = Cli::default(); + let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); + let _ = bitcoind.init(5).await; + let (monero, _container) = + Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) + .await + .unwrap(); + + let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); + let btc_alice = bitcoin::Amount::ZERO; + let btc_bob = btc_to_swap * 10; + + // this xmr value matches the logic of alice::calculate_amounts i.e. btc * + // 10_000 * 100 + let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); + let xmr_alice = xmr_to_swap * 10; + let xmr_bob = xmr_btc::monero::Amount::ZERO; + + // todo: This should not be hardcoded + let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876" + .parse() + .expect("failed to parse Alice's address"); + + let config = Config::regtest(); + + let ( + alice_state, + mut alice_event_loop, + alice_event_loop_handle, + alice_btc_wallet, + alice_xmr_wallet, + ) = init_alice( + &bitcoind, + &monero, + btc_to_swap, + xmr_to_swap, + xmr_alice, + alice_multiaddr.clone(), + config, + ) + .await; + + let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) = + init_bob( + alice_multiaddr, + &bitcoind, + &monero, + btc_to_swap, + btc_bob, + xmr_to_swap, + xmr_bob, + config, + ) + .await; + + let alice_db_datadir = tempdir().unwrap(); + let alice_db = Database::open(alice_db_datadir.path()).unwrap(); + + let alice_swap_fut = alice::swap::swap( + alice_state, + alice_event_loop_handle, + alice_btc_wallet.clone(), + alice_xmr_wallet.clone(), + config, + Uuid::new_v4(), + alice_db, + ); + + let _alice_swarm_fut = tokio::spawn(async move { alice_event_loop.run().await }); + + let bob_swap_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 _bob_swarm_fut = tokio::spawn(async move { bob_event_loop.run().await }); + + try_join(alice_swap_fut, bob_swap_fut).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(); + + let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap(); + + bob_xmr_wallet.as_ref().0.refresh().await.unwrap(); + let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap(); + + assert_eq!( + btc_alice_final, + btc_alice + btc_to_swap - bitcoin::Amount::from_sat(bitcoin::TX_FEE) + ); + assert!(btc_bob_final <= btc_bob - btc_to_swap); + + assert!(xmr_alice_final <= xmr_alice - xmr_to_swap); + assert_eq!(xmr_bob_final, xmr_bob + xmr_to_swap); +} diff --git a/swap/tests/punish.rs b/swap/tests/punish.rs new file mode 100644 index 00000000..1db6a360 --- /dev/null +++ b/swap/tests/punish.rs @@ -0,0 +1,133 @@ +use crate::testutils::{init_alice, init_bob}; +use bitcoin_harness::Bitcoind; +use futures::future::try_join; +use libp2p::Multiaddr; +use monero_harness::Monero; +use rand::rngs::OsRng; +use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState, storage::Database}; +use tempfile::tempdir; +use testcontainers::clients::Cli; +use testutils::init_tracing; +use uuid::Uuid; +use xmr_btc::{bitcoin, config::Config}; + +pub mod testutils; + +/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice +/// the encsig and fail to refund or redeem. Alice punishes. +#[tokio::test] +async fn alice_punishes_if_bob_never_acts_after_fund() { + let _guard = init_tracing(); + + let cli = Cli::default(); + let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); + let _ = bitcoind.init(5).await; + let (monero, _container) = + Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) + .await + .unwrap(); + + let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); + let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); + + let bob_btc_starting_balance = btc_to_swap * 10; + let bob_xmr_starting_balance = xmr_btc::monero::Amount::ZERO; + + let alice_btc_starting_balance = bitcoin::Amount::ZERO; + let alice_xmr_starting_balance = xmr_to_swap * 10; + + // todo: This should not be hardcoded + let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9877" + .parse() + .expect("failed to parse Alice's address"); + + let config = Config::regtest(); + + let ( + alice_state, + mut alice_event_loop, + alice_event_loop_handle, + alice_btc_wallet, + alice_xmr_wallet, + ) = init_alice( + &bitcoind, + &monero, + btc_to_swap, + xmr_to_swap, + alice_xmr_starting_balance, + alice_multiaddr.clone(), + config, + ) + .await; + + let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) = + init_bob( + alice_multiaddr, + &bitcoind, + &monero, + btc_to_swap, + bob_btc_starting_balance, + xmr_to_swap, + bob_xmr_starting_balance, + config, + ) + .await; + + let bob_btc_locked_fut = bob::swap::run_until( + bob_state, + bob::swap::is_btc_locked, + bob_event_loop_handle, + bob_db, + bob_btc_wallet.clone(), + bob_xmr_wallet.clone(), + OsRng, + Uuid::new_v4(), + ); + + let _bob_swarm_fut = tokio::spawn(async move { bob_event_loop.run().await }); + + let alice_db_datadir = tempdir().unwrap(); + let alice_db = Database::open(alice_db_datadir.path()).unwrap(); + + let alice_fut = alice::swap::swap( + alice_state, + alice_event_loop_handle, + alice_btc_wallet.clone(), + alice_xmr_wallet.clone(), + Config::regtest(), + Uuid::new_v4(), + alice_db, + ); + + let _alice_swarm_fut = tokio::spawn(async move { alice_event_loop.run().await }); + + // Wait until alice has locked xmr and bob has locked btc + let ((alice_state, _), bob_state) = try_join(alice_fut, bob_btc_locked_fut).await.unwrap(); + + assert!(matches!(alice_state, AliceState::Punished)); + let bob_state3 = if let BobState::BtcLocked(state3, ..) = bob_state { + state3 + } else { + panic!("Bob in unexpected state"); + }; + + 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_state3.tx_lock_id()) + .await + .unwrap(); + + assert_eq!( + btc_alice_final, + alice_btc_starting_balance + btc_to_swap - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) + ); + + assert_eq!( + btc_bob_final, + bob_btc_starting_balance - btc_to_swap - lock_tx_bitcoin_fee + ); +} diff --git a/swap/tests/refund.rs b/swap/tests/refund.rs new file mode 100644 index 00000000..ce99f36e --- /dev/null +++ b/swap/tests/refund.rs @@ -0,0 +1,167 @@ +use crate::testutils::{init_alice, init_bob}; +use bitcoin_harness::Bitcoind; +use futures::future::try_join; +use libp2p::Multiaddr; +use monero_harness::Monero; +use rand::rngs::OsRng; +use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState, storage::Database}; +use tempfile::tempdir; +use testcontainers::clients::Cli; +use testutils::init_tracing; +use uuid::Uuid; +use xmr_btc::{bitcoin, config::Config}; + +pub mod testutils; + +// Bob locks btc and Alice locks xmr. Alice fails to act so Bob refunds. Alice +// then also refunds. +#[tokio::test] +async fn both_refund() { + let _guard = init_tracing(); + + use tracing_subscriber::util::SubscriberInitExt as _; + let _guard = tracing_subscriber::fmt() + .with_env_filter("swap=info,xmr_btc=info") + .with_ansi(false) + .set_default(); + + let cli = Cli::default(); + let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); + let _ = bitcoind.init(5).await; + let (monero, _container) = + Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) + .await + .unwrap(); + + let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); + let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); + + let bob_btc_starting_balance = btc_to_swap * 10; + let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0); + + let alice_btc_starting_balance = bitcoin::Amount::ZERO; + let alice_xmr_starting_balance = xmr_to_swap * 10; + + // todo: This should not be hardcoded + let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9879" + .parse() + .expect("failed to parse Alice's address"); + + let ( + alice_state, + mut alice_swarm_driver, + alice_swarm_handle, + 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(), + ) + .await; + + let (bob_state, bob_swarm_driver, bob_swarm_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) = + init_bob( + alice_multiaddr, + &bitcoind, + &monero, + btc_to_swap, + bob_btc_starting_balance, + xmr_to_swap, + bob_xmr_starting_balance, + Config::regtest(), + ) + .await; + + let bob_fut = bob::swap::swap( + bob_state, + bob_swarm_handle, + bob_db, + bob_btc_wallet.clone(), + bob_xmr_wallet.clone(), + OsRng, + Uuid::new_v4(), + ); + + tokio::spawn(async move { bob_swarm_driver.run().await }); + + let alice_swap_id = Uuid::new_v4(); + let alice_db_datadir = tempdir().unwrap(); + let alice_db = Database::open(alice_db_datadir.path()).unwrap(); + + let alice_xmr_locked_fut = alice::swap::run_until( + alice_state, + alice::swap::is_xmr_locked, + alice_swarm_handle, + alice_btc_wallet.clone(), + alice_xmr_wallet.clone(), + Config::regtest(), + alice_swap_id, + alice_db, + ); + + tokio::spawn(async move { alice_swarm_driver.run().await }); + + // 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(); + + let bob_state4 = if let BobState::BtcRefunded(state4) = bob_state { + state4 + } else { + panic!("Bob in unexpected state"); + }; + + let alice_db = Database::open(alice_db_datadir.path()).unwrap(); + + let (alice_state, _) = alice::swap::swap( + alice_state, + alice_swarm_handle, + alice_btc_wallet.clone(), + alice_xmr_wallet.clone(), + Config::regtest(), + alice_swap_id, + alice_db, + ) + .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); +} diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 0fe62024..24850c99 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -89,23 +89,60 @@ pub async fn init_bob( ) } -#[allow(clippy::too_many_arguments)] -pub async fn init_alice( - bitcoind: &Bitcoind<'_>, - monero: &Monero, +pub async fn init_alice_eventloop( btc_to_swap: bitcoin::Amount, - _btc_starting_balance: bitcoin::Amount, - xmr_to_swap: xmr_btc::monero::Amount, - xmr_starting_balance: xmr_btc::monero::Amount, + xmr_to_swap: monero::Amount, + alice_btc_wallet: Arc, listen: Multiaddr, config: Config, ) -> ( AliceState, alice::event_loop::EventLoop, alice::event_loop::EventLoopHandle, - Arc, - Arc, ) { + let rng = &mut OsRng; + + let amounts = SwapAmounts { + btc: btc_to_swap, + xmr: xmr_to_swap, + }; + + let a = crate::bitcoin::SecretKey::new_random(rng); + let s_a = cross_curve_dleq::Scalar::random(rng); + let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng); + let redeem_address = alice_btc_wallet.as_ref().new_address().await.unwrap(); + let punish_address = redeem_address.clone(); + let state0 = State0::new( + a, + s_a, + v_a, + amounts.btc, + amounts.xmr, + config.bitcoin_refund_timelock, + config.bitcoin_punish_timelock, + redeem_address, + punish_address, + ); + let start_state = AliceState::Started { + amounts, + state0: state0.clone(), + }; + + let alice_behaviour = alice::Behaviour::new(state0); + let alice_transport = build(alice_behaviour.identity()).unwrap(); + + let (swarm_driver, handle) = + alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen).unwrap(); + + (start_state, swarm_driver, handle) +} + +pub async fn init_alice_wallets( + bitcoind: &Bitcoind<'_>, + monero: &Monero, + xmr_starting_balance: xmr_btc::monero::Amount, + config: Config, +) -> (Arc, Arc) { monero .init(vec![("alice", xmr_starting_balance.as_piconero())]) .await @@ -121,46 +158,38 @@ pub async fn init_alice( .unwrap(), ); - let amounts = SwapAmounts { - btc: btc_to_swap, - xmr: xmr_to_swap, - }; + (alice_xmr_wallet, alice_btc_wallet) +} - let rng = &mut OsRng; - let (alice_state, alice_behaviour) = { - let a = crate::bitcoin::SecretKey::new_random(rng); - let s_a = cross_curve_dleq::Scalar::random(rng); - let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng); - let redeem_address = alice_btc_wallet.as_ref().new_address().await.unwrap(); - let punish_address = redeem_address.clone(); - let state0 = State0::new( - a, - s_a, - v_a, - amounts.btc, - amounts.xmr, - config.bitcoin_refund_timelock, - config.bitcoin_punish_timelock, - redeem_address, - punish_address, - ); - - ( - AliceState::Started { - amounts, - state0: state0.clone(), - }, - alice::Behaviour::new(state0), - ) - }; - - let alice_transport = build(alice_behaviour.identity()).unwrap(); - - let (swarm_driver, handle) = - alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen).unwrap(); +#[allow(clippy::too_many_arguments)] +pub async fn init_alice( + bitcoind: &Bitcoind<'_>, + monero: &Monero, + btc_to_swap: bitcoin::Amount, + xmr_to_swap: monero::Amount, + xmr_starting_balance: xmr_btc::monero::Amount, + listen: Multiaddr, + config: Config, +) -> ( + AliceState, + alice::event_loop::EventLoop, + alice::event_loop::EventLoopHandle, + Arc, + Arc, +) { + let (alice_xmr_wallet, alice_btc_wallet) = + init_alice_wallets(bitcoind, monero, xmr_starting_balance, config).await; + let (alice_start_state, swarm_driver, handle) = init_alice_eventloop( + btc_to_swap, + xmr_to_swap, + alice_btc_wallet.clone(), + listen, + config, + ) + .await; ( - alice_state, + alice_start_state, swarm_driver, handle, alice_btc_wallet, @@ -168,63 +197,6 @@ pub async fn init_alice( ) } -/// Returns Alice's and Bob's wallets, in this order -pub async fn setup_wallets( - cli: &Cli, - _init_btc_alice: bitcoin::Amount, - init_xmr_alice: xmr_btc::monero::Amount, - init_btc_bob: bitcoin::Amount, - init_xmr_bob: xmr_btc::monero::Amount, - config: Config, -) -> ( - bitcoin::Wallet, - monero::Wallet, - bitcoin::Wallet, - monero::Wallet, - Containers<'_>, -) { - let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); - let _ = bitcoind.init(5).await; - - let alice_btc_wallet = - swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone(), config.bitcoin_network) - .await - .unwrap(); - let bob_btc_wallet = - swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone(), config.bitcoin_network) - .await - .unwrap(); - bitcoind - .mint( - bob_btc_wallet.inner.new_address().await.unwrap(), - init_btc_bob, - ) - .await - .unwrap(); - - let (monero, monerods) = Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) - .await - .unwrap(); - monero - .init(vec![ - ("alice", init_xmr_alice.as_piconero()), - ("bob", init_xmr_bob.as_piconero()), - ]) - .await - .unwrap(); - - let alice_xmr_wallet = swap::monero::Wallet(monero.wallet("alice").unwrap().client()); - let bob_xmr_wallet = swap::monero::Wallet(monero.wallet("bob").unwrap().client()); - - ( - alice_btc_wallet, - alice_xmr_wallet, - bob_btc_wallet, - bob_xmr_wallet, - Containers { bitcoind, monerods }, - ) -} - // This is just to keep the containers alive #[allow(dead_code)] pub struct Containers<'a> { diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 399c89f0..74557f5d 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -439,12 +439,12 @@ pub struct State0 { pub s_a: cross_curve_dleq::Scalar, pub v_a: monero::PrivateViewKey, #[serde(with = "::bitcoin::util::amount::serde::as_sat")] - btc: bitcoin::Amount, - xmr: monero::Amount, - refund_timelock: u32, - punish_timelock: u32, - redeem_address: bitcoin::Address, - punish_address: bitcoin::Address, + pub btc: bitcoin::Amount, + pub xmr: monero::Amount, + pub refund_timelock: u32, + pub punish_timelock: u32, + pub redeem_address: bitcoin::Address, + pub punish_address: bitcoin::Address, } impl State0 {