diff --git a/swap/src/lib.rs b/swap/src/lib.rs index f7020277..7644da0a 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -56,6 +56,12 @@ pub struct SwapAmounts { pub xmr: monero::Amount, } +#[derive(Debug, Clone)] +pub struct StartingBalances { + pub xmr: monero::Amount, + pub btc: bitcoin::Amount, +} + // TODO: Display in XMR and BTC (not picos and sats). impl Display for SwapAmounts { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/swap/src/main.rs b/swap/src/main.rs index d9a339bb..4a7cc7ef 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -26,10 +26,10 @@ use swap::{ database::{Database, Swap}, monero, network, network::transport::build, - protocol::{alice, alice::AliceState, bob, bob::BobState}, + protocol::{alice, bob, bob::BobState}, seed::Seed, trace::init_tracing, - SwapAmounts, + StartingBalances, SwapAmounts, }; use tracing::{info, log::LevelFilter}; use uuid::Uuid; @@ -51,8 +51,9 @@ async fn main() -> Result<()> { opt.data_dir ); let data_dir = std::path::Path::new(opt.data_dir.as_str()).to_path_buf(); - let db = - Database::open(data_dir.join("database").as_path()).context("Could not open database")?; + let db_path = data_dir.join("database"); + + let db = Database::open(db_path.as_path()).context("Could not open database")?; let seed = swap::config::seed::Seed::from_file_or_generate(&data_dir) .expect("Could not retrieve/initialize seed") @@ -67,7 +68,12 @@ async fn main() -> Result<()> { send_monero, receive_bitcoin, } => { - let (bitcoin_wallet, monero_wallet) = setup_wallets( + let swap_amounts = SwapAmounts { + xmr: send_monero, + btc: receive_bitcoin, + }; + + let (bitcoin_wallet, monero_wallet, starting_balances) = setup_wallets( bitcoind_url, bitcoin_wallet_name.as_str(), monero_wallet_rpc_url, @@ -75,50 +81,23 @@ async fn main() -> Result<()> { ) .await?; - let amounts = SwapAmounts { - btc: receive_bitcoin, - xmr: send_monero, - }; - - let alice_state = { - let rng = &mut OsRng; - let a = bitcoin::SecretKey::new_random(rng); - let s_a = cross_curve_dleq::Scalar::random(rng); - let v_a = monero::PrivateViewKey::new_random(rng); - let redeem_address = bitcoin_wallet.as_ref().new_address().await?; - let punish_address = redeem_address.clone(); - let state0 = alice::state::State0::new( - a, - s_a, - v_a, - amounts.btc, - amounts.xmr, - config.bitcoin_cancel_timelock, - config.bitcoin_punish_timelock, - redeem_address, - punish_address, - ); - - AliceState::Started { amounts, state0 } - }; - let swap_id = Uuid::new_v4(); - info!( - "Swap sending {} and receiving {} started with ID {}", - send_monero, receive_bitcoin, swap_id - ); - alice_swap( + let alice_factory = alice::AliceSwapFactory::new( + seed, + config, swap_id, - alice_state, - listen_addr, bitcoin_wallet, monero_wallet, - config, - db, - seed, + starting_balances, + db_path, + listen_addr, ) - .await?; + .await; + let (swap, mut event_loop) = alice_factory.new_swap_as_alice(swap_amounts).await?; + + tokio::spawn(async move { event_loop.run().await }); + alice::run(swap).await?; } Command::BuyXmr { alice_peer_id, @@ -129,7 +108,7 @@ async fn main() -> Result<()> { send_bitcoin, receive_monero, } => { - let (bitcoin_wallet, monero_wallet) = setup_wallets( + let (bitcoin_wallet, monero_wallet, _) = setup_wallets( bitcoind_url, bitcoin_wallet_name.as_str(), monero_wallet_rpc_url, @@ -192,30 +171,29 @@ async fn main() -> Result<()> { monero_wallet_rpc_url, listen_addr, }) => { - let db_state = if let Swap::Alice(db_state) = db.get_state(swap_id)? { - db_state - } else { - bail!("Swap {} is not sell xmr.", swap_id) - }; - - let (bitcoin_wallet, monero_wallet) = setup_wallets( + let (bitcoin_wallet, monero_wallet, starting_balances) = setup_wallets( bitcoind_url, bitcoin_wallet_name.as_str(), monero_wallet_rpc_url, config, ) .await?; - alice_swap( + + let alice_factory = alice::AliceSwapFactory::new( + seed, + config, swap_id, - db_state.into(), - listen_addr, bitcoin_wallet, monero_wallet, - config, - db, - seed, + starting_balances, + db_path, + listen_addr, ) - .await?; + .await; + let (swap, mut event_loop) = alice_factory.recover_alice_from_db().await?; + + tokio::spawn(async move { event_loop.run().await }); + alice::run(swap).await?; } Command::Resume(Resume::BuyXmr { swap_id, @@ -231,7 +209,7 @@ async fn main() -> Result<()> { bail!("Swap {} is not buy xmr.", swap_id) }; - let (bitcoin_wallet, monero_wallet) = setup_wallets( + let (bitcoin_wallet, monero_wallet, _) = setup_wallets( bitcoind_url, bitcoin_wallet_name.as_str(), monero_wallet_rpc_url, @@ -260,7 +238,11 @@ async fn setup_wallets( bitcoin_wallet_name: &str, monero_wallet_rpc_url: url::Url, config: Config, -) -> Result<(Arc, Arc)> { +) -> Result<( + Arc, + Arc, + StartingBalances, +)> { let bitcoin_wallet = swap::bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network) .await?; @@ -279,44 +261,12 @@ async fn setup_wallets( ); let monero_wallet = Arc::new(monero_wallet); - Ok((bitcoin_wallet, monero_wallet)) -} -#[allow(clippy::too_many_arguments)] -async fn alice_swap( - swap_id: Uuid, - state: AliceState, - listen_addr: Multiaddr, - bitcoin_wallet: Arc, - monero_wallet: Arc, - config: Config, - db: Database, - seed: Seed, -) -> Result { - let identity = network::Seed::new(seed).derive_libp2p_identity(); - - let peer_id = identity.public().into_peer_id(); - - let alice_behaviour = alice::Behaviour::default(); - info!("Own Peer-ID: {}", peer_id); - let alice_transport = build(identity)?; - - let (mut event_loop, handle) = - alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen_addr, peer_id)?; - - let swap = alice::Swap { - state, - event_loop_handle: handle, - bitcoin_wallet, - monero_wallet, - config, - swap_id, - db, + let starting_balances = StartingBalances { + btc: bitcoin_balance, + xmr: monero_balance, }; - let swap = alice::swap::run(swap); - - tokio::spawn(async move { event_loop.run().await }); - swap.await + Ok((bitcoin_wallet, monero_wallet, starting_balances)) } #[allow(clippy::too_many_arguments)] diff --git a/swap/src/protocol/alice.rs b/swap/src/protocol/alice.rs index a3b12fb5..bf06aa47 100644 --- a/swap/src/protocol/alice.rs +++ b/swap/src/protocol/alice.rs @@ -1,16 +1,18 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. +use anyhow::Result; use libp2p::{request_response::ResponseChannel, NetworkBehaviour, PeerId}; use tracing::{debug, info}; use crate::{ - bitcoin, monero, + bitcoin, database, monero, network::{ peer_tracker::{self, PeerTracker}, request_response::AliceToBob, + Seed as NetworkSeed, }, protocol::bob, - SwapAmounts, + StartingBalances, SwapAmounts, }; pub use self::{ @@ -22,8 +24,10 @@ pub use self::{ state::*, swap::{run, run_until}, }; -use crate::{config::Config, database::Database}; -use std::sync::Arc; +use crate::{config::Config, database::Database, network::transport::build, seed::Seed}; +use libp2p::{core::Multiaddr, identity::Keypair}; +use rand::rngs::OsRng; +use std::{path::PathBuf, sync::Arc}; use uuid::Uuid; mod amounts; @@ -46,6 +50,162 @@ pub struct Swap { pub db: Database, } +pub struct AliceSwapFactory { + listen_address: Multiaddr, + identity: Keypair, + peer_id: PeerId, + + db_path: PathBuf, + swap_id: Uuid, + + pub bitcoin_wallet: Arc, + pub monero_wallet: Arc, + config: Config, + pub starting_balances: StartingBalances, +} + +impl AliceSwapFactory { + #[allow(clippy::too_many_arguments)] + pub async fn new( + seed: Seed, + config: Config, + swap_id: Uuid, + bitcoin_wallet: Arc, + monero_wallet: Arc, + starting_balances: StartingBalances, + db_path: PathBuf, + listen_address: Multiaddr, + ) -> Self { + let network_seed = NetworkSeed::new(seed); + let identity = network_seed.derive_libp2p_identity(); + let peer_id = PeerId::from(identity.public()); + + Self { + listen_address, + identity, + peer_id, + db_path, + swap_id, + bitcoin_wallet, + monero_wallet, + config, + starting_balances, + } + } + + pub async fn new_swap_as_alice(&self, swap_amounts: SwapAmounts) -> Result<(Swap, EventLoop)> { + let initial_state = init_alice_state( + swap_amounts.btc, + swap_amounts.xmr, + self.bitcoin_wallet.clone(), + self.config, + ) + .await?; + + let (event_loop, event_loop_handle) = init_alice_event_loop( + self.listen_address.clone(), + self.identity.clone(), + self.peer_id.clone(), + )?; + + let db = Database::open(self.db_path.as_path())?; + + Ok(( + 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) -> Result<(Swap, EventLoop)> { + // reopen the existing database + let db = Database::open(self.db_path.clone().as_path())?; + + let resume_state = if let database::Swap::Alice(state) = db.get_state(self.swap_id)? { + state.into() + } else { + unreachable!() + }; + + let (event_loop, event_loop_handle) = init_alice_event_loop( + self.listen_address.clone(), + self.identity.clone(), + self.peer_id.clone(), + )?; + + Ok(( + 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 { + self.peer_id.clone() + } + + pub fn listen_address(&self) -> Multiaddr { + self.listen_address.clone() + } +} + +async fn init_alice_state( + btc_to_swap: bitcoin::Amount, + xmr_to_swap: monero::Amount, + alice_btc_wallet: Arc, + config: Config, +) -> Result { + let rng = &mut OsRng; + + let amounts = SwapAmounts { + btc: btc_to_swap, + xmr: xmr_to_swap, + }; + + let a = bitcoin::SecretKey::new_random(rng); + let s_a = cross_curve_dleq::Scalar::random(rng); + let v_a = monero::PrivateViewKey::new_random(rng); + let redeem_address = alice_btc_wallet.as_ref().new_address().await?; + let punish_address = redeem_address.clone(); + let state0 = State0::new( + a, + s_a, + v_a, + amounts.btc, + amounts.xmr, + config.bitcoin_cancel_timelock, + config.bitcoin_punish_timelock, + redeem_address, + punish_address, + ); + + Ok(AliceState::Started { amounts, state0 }) +} + +fn init_alice_event_loop( + listen: Multiaddr, + identity: Keypair, + peer_id: PeerId, +) -> Result<(EventLoop, EventLoopHandle)> { + let alice_behaviour = Behaviour::default(); + let alice_transport = build(identity)?; + EventLoop::new(alice_transport, alice_behaviour, listen, peer_id) +} + pub type Swarm = libp2p::Swarm; #[derive(Debug)] diff --git a/swap/tests/happy_path.rs b/swap/tests/happy_path.rs index 054a9d08..d3385229 100644 --- a/swap/tests/happy_path.rs +++ b/swap/tests/happy_path.rs @@ -7,7 +7,7 @@ pub mod testutils; #[tokio::test] async fn happy_path() { - testutils::init(|test| async move { + testutils::setup_test(|test| async move { let alice_swap = test.new_swap_as_alice().await; let bob_swap = test.new_swap_as_bob().await; diff --git a/swap/tests/happy_path_restart_alice.rs b/swap/tests/happy_path_restart_alice.rs index 8f84129b..6cce546b 100644 --- a/swap/tests/happy_path_restart_alice.rs +++ b/swap/tests/happy_path_restart_alice.rs @@ -4,7 +4,7 @@ pub mod testutils; #[tokio::test] async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { - testutils::init(|test| async move { + testutils::setup_test(|test| async move { let alice_swap = test.new_swap_as_alice().await; let bob_swap = test.new_swap_as_bob().await; diff --git a/swap/tests/happy_path_restart_bob_after_comm.rs b/swap/tests/happy_path_restart_bob_after_comm.rs index d49c939f..194b6349 100644 --- a/swap/tests/happy_path_restart_bob_after_comm.rs +++ b/swap/tests/happy_path_restart_bob_after_comm.rs @@ -4,7 +4,7 @@ pub mod testutils; #[tokio::test] async fn given_bob_restarts_after_encsig_is_sent_resume_swap() { - testutils::init(|test| async move { + testutils::setup_test(|test| async move { let alice_swap = test.new_swap_as_alice().await; let bob_swap = test.new_swap_as_bob().await; diff --git a/swap/tests/happy_path_restart_bob_before_comm.rs b/swap/tests/happy_path_restart_bob_before_comm.rs index 1fce0e5c..aea07c52 100644 --- a/swap/tests/happy_path_restart_bob_before_comm.rs +++ b/swap/tests/happy_path_restart_bob_before_comm.rs @@ -7,7 +7,7 @@ pub mod testutils; #[tokio::test] async fn given_bob_restarts_after_xmr_is_locked_resume_swap() { - testutils::init(|test| async move { + testutils::setup_test(|test| async move { let alice_swap = test.new_swap_as_alice().await; let bob_swap = test.new_swap_as_bob().await; diff --git a/swap/tests/punish.rs b/swap/tests/punish.rs index 16515569..aa4524d4 100644 --- a/swap/tests/punish.rs +++ b/swap/tests/punish.rs @@ -9,7 +9,7 @@ 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::init(|test| async move { + testutils::setup_test(|test| async move { let alice_swap = test.new_swap_as_alice().await; let bob_swap = test.new_swap_as_bob().await; diff --git a/swap/tests/refund_restart_alice.rs b/swap/tests/refund_restart_alice.rs index e5e91ae8..48bc09e0 100644 --- a/swap/tests/refund_restart_alice.rs +++ b/swap/tests/refund_restart_alice.rs @@ -6,7 +6,7 @@ pub mod testutils; /// then also refunds. #[tokio::test] async fn given_alice_restarts_after_xmr_is_locked_abort_swap() { - testutils::init(|test| async move { + testutils::setup_test(|test| async move { let alice_swap = test.new_swap_as_alice().await; let bob_swap = test.new_swap_as_bob().await; diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 9fb22e1c..7bb05f8a 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -12,9 +12,14 @@ use swap::{ database::Database, monero, network, network::transport::build, - protocol::{alice, alice::AliceState, bob, bob::BobState}, + protocol::{ + alice, + alice::{AliceState, AliceSwapFactory}, + bob, + bob::BobState, + }, seed::Seed, - SwapAmounts, + StartingBalances, SwapAmounts, }; use tempfile::tempdir; use testcontainers::{clients::Cli, Container}; @@ -34,7 +39,8 @@ impl Test { let (swap, mut event_loop) = self .alice_swap_factory .new_swap_as_alice(self.swap_amounts) - .await; + .await + .unwrap(); tokio::spawn(async move { event_loop.run().await }); @@ -53,7 +59,11 @@ impl Test { } pub async fn recover_alice_from_db(&self) -> alice::Swap { - let (swap, mut event_loop) = self.alice_swap_factory.recover_alice_from_db().await; + let (swap, mut event_loop) = self + .alice_swap_factory + .recover_alice_from_db() + .await + .unwrap(); tokio::spawn(async move { event_loop.run().await }); @@ -298,7 +308,7 @@ impl Test { } } -pub async fn init(testfn: T) +pub async fn setup_test(testfn: T) where T: Fn(Test) -> F, F: Future, @@ -320,12 +330,31 @@ where xmr: swap_amounts.xmr * 10, btc: bitcoin::Amount::ZERO, }; + + let port = get_port().expect("Failed to find a free port"); + + let listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port) + .parse() + .expect("failed to parse Alice's address"); + + let (alice_bitcoin_wallet, alice_monero_wallet) = init_wallets( + "alice", + &containers.bitcoind, + &monero, + alice_starting_balances.clone(), + config, + ) + .await; + let alice_swap_factory = AliceSwapFactory::new( + Seed::random().unwrap(), config, Uuid::new_v4(), - &monero, - &containers.bitcoind, + alice_bitcoin_wallet, + alice_monero_wallet, alice_starting_balances, + tempdir().unwrap().path().to_path_buf(), + listen_address, ) .await; @@ -334,10 +363,8 @@ where btc: swap_amounts.btc * 10, }; - let bob_seed = Seed::random().unwrap(); - let bob_swap_factory = BobSwapFactory::new( - bob_seed, + Seed::random().unwrap(), config, Uuid::new_v4(), &monero, @@ -357,126 +384,6 @@ where testfn(test).await } -pub struct AliceSwapFactory { - listen_address: Multiaddr, - peer_id: PeerId, - - seed: Seed, - db_path: PathBuf, - swap_id: Uuid, - - bitcoin_wallet: Arc, - monero_wallet: Arc, - config: Config, - starting_balances: StartingBalances, -} - -impl AliceSwapFactory { - async fn new( - config: Config, - swap_id: Uuid, - monero: &Monero, - bitcoind: &Bitcoind<'_>, - starting_balances: StartingBalances, - ) -> Self { - let port = get_port().expect("Failed to find a free port"); - - let listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port) - .parse() - .expect("failed to parse Alice's address"); - - let seed = Seed::random().unwrap(); - - let db_path = tempdir().unwrap().path().to_path_buf(); - - let (bitcoin_wallet, monero_wallet) = - init_wallets("alice", bitcoind, monero, starting_balances.clone(), config).await; - - // TODO: This should be done by changing the production code - let network_seed = network::Seed::new(seed); - let identity = network_seed.derive_libp2p_identity(); - let peer_id = PeerId::from(identity.public()); - - Self { - seed, - db_path, - listen_address, - peer_id, - swap_id, - bitcoin_wallet, - monero_wallet, - config, - starting_balances, - } - } - - pub async fn new_swap_as_alice( - &self, - swap_amounts: SwapAmounts, - ) -> (alice::Swap, alice::EventLoop) { - let initial_state = init_alice_state( - swap_amounts.btc, - swap_amounts.xmr, - self.bitcoin_wallet.clone(), - self.config, - ) - .await; - - let (event_loop, event_loop_handle) = - init_alice_event_loop(self.listen_address.clone(), self.seed); - - let db = Database::open(self.db_path.as_path()).unwrap(); - ( - 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::Swap, alice::EventLoop) { - // reopen the existing database - let db = Database::open(self.db_path.clone().as_path()).unwrap(); - - let resume_state = - if let swap::database::Swap::Alice(state) = db.get_state(self.swap_id).unwrap() { - state.into() - } else { - unreachable!() - }; - - let (event_loop, event_loop_handle) = - init_alice_event_loop(self.listen_address.clone(), self.seed); - - ( - 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 { - self.peer_id.clone() - } - - pub fn listen_address(&self) -> Multiaddr { - self.listen_address.clone() - } -} - pub struct BobSwapFactory { seed: Seed, @@ -583,12 +490,6 @@ impl BobSwapFactory { } } -#[derive(Debug, Clone)] -struct StartingBalances { - pub xmr: monero::Amount, - pub btc: bitcoin::Amount, -} - async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) { let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); let _ = bitcoind.init(5).await; @@ -635,55 +536,6 @@ async fn init_wallets( (btc_wallet, xmr_wallet) } -async fn init_alice_state( - btc_to_swap: bitcoin::Amount, - xmr_to_swap: monero::Amount, - alice_btc_wallet: Arc, - config: Config, -) -> AliceState { - let rng = &mut OsRng; - - let amounts = SwapAmounts { - btc: btc_to_swap, - xmr: xmr_to_swap, - }; - - let a = bitcoin::SecretKey::new_random(rng); - let s_a = cross_curve_dleq::Scalar::random(rng); - let v_a = 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 = alice::State0::new( - a, - s_a, - v_a, - amounts.btc, - amounts.xmr, - config.bitcoin_cancel_timelock, - config.bitcoin_punish_timelock, - redeem_address, - punish_address, - ); - - AliceState::Started { amounts, state0 } -} - -fn init_alice_event_loop( - listen: Multiaddr, - seed: Seed, -) -> ( - alice::event_loop::EventLoop, - alice::event_loop::EventLoopHandle, -) { - let identity = network::Seed::new(seed).derive_libp2p_identity(); - - let peer_id = identity.public().into_peer_id(); - - let alice_behaviour = alice::Behaviour::default(); - let alice_transport = build(identity).unwrap(); - alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen, peer_id).unwrap() -} - async fn init_bob_state( btc_to_swap: bitcoin::Amount, xmr_to_swap: monero::Amount,