diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd8a6c30..4920d97f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: cargo tomlfmt -d -p Cargo.toml cargo tomlfmt -d -p xmr-btc/Cargo.toml cargo tomlfmt -d -p monero-harness/Cargo.toml + cargo tomlfmt -d -p swap/Cargo.toml - name: Check code formatting run: cargo fmt --all -- --check diff --git a/swap/Cargo.toml b/swap/Cargo.toml index fa283dd7..a92615f4 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -32,7 +32,7 @@ structopt = "0.3" tempfile = "3" time = "0.2" tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] } -torut = { version = "0.1", optional = true } +torut = { version = "0.1" } tracing = { version = "0.1", features = ["attributes"] } tracing-core = "0.1" tracing-futures = { version = "0.2", features = ["std-future", "futures-03"] } @@ -48,7 +48,3 @@ port_check = "0.1" spectral = "0.6" tempfile = "3" testcontainers = "0.10" - -[features] -default = [] -tor = ["torut"] diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 4e57c135..036e4dc8 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -28,7 +28,8 @@ use crate::{ network::{ peer_tracker::{self, PeerTracker}, request_response::AliceToBob, - transport, TokioExecutor, + transport::SwapTransport, + TokioExecutor, }, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; @@ -43,7 +44,8 @@ pub async fn swap( bitcoin_wallet: Arc, monero_wallet: Arc, listen: Multiaddr, - local_port: Option, + transport: SwapTransport, + behaviour: Alice, ) -> Result<()> { struct Network { swarm: Arc>, @@ -67,7 +69,6 @@ pub async fn swap( // TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed // to `ConstantBackoff`. - #[async_trait] impl ReceiveBitcoinRedeemEncsig for Network { async fn receive_bitcoin_redeem_encsig(&mut self) -> xmr_btc::bitcoin::EncryptedSignature { @@ -96,7 +97,7 @@ pub async fn swap( } } - let mut swarm = new_swarm(listen, local_port)?; + let mut swarm = new_swarm(listen, transport, behaviour)?; let message0: bob::Message0; let mut state0: Option = None; let mut last_amounts: Option = None; @@ -230,30 +231,11 @@ pub async fn swap( pub type Swarm = libp2p::Swarm; -fn new_swarm(listen: Multiaddr, port: Option) -> Result { +fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> Result { use anyhow::Context as _; - let behaviour = Alice::default(); - - let local_key_pair = behaviour.identity(); let local_peer_id = behaviour.peer_id(); - let transport; - #[cfg(feature = "tor")] - { - transport = match port { - Some(port) => transport::build(local_key_pair, Some((listen.clone(), port)))?, - None => anyhow::bail!("Must supply local port"), - }; - } - #[cfg(not(feature = "tor"))] - { - transport = match port { - None => transport::build(local_key_pair)?, - Some(_) => anyhow::bail!("local port should not be provided for non-tor usage"), - }; - } - let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) .executor(Box::new(TokioExecutor { handle: tokio::runtime::Handle::current(), diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index f9e1493d..2d88ae2c 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -4,7 +4,7 @@ use anyhow::Result; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Transaction}; -use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind}; +use bitcoin_harness::bitcoind_rpc::PsbtBase64; use reqwest::Url; use tokio::time; use xmr_btc::bitcoin::{ @@ -20,8 +20,8 @@ pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600; pub struct Wallet(pub bitcoin_harness::Wallet); impl Wallet { - pub async fn new(name: &str, url: &Url) -> Result { - let wallet = bitcoin_harness::Wallet::new(name, url.clone()).await?; + pub async fn new(name: &str, url: Url) -> Result { + let wallet = bitcoin_harness::Wallet::new(name, url).await?; Ok(Self(wallet)) } @@ -46,22 +46,6 @@ impl Wallet { } } -pub async fn make_wallet( - name: &str, - bitcoind: &Bitcoind<'_>, - fund_amount: Amount, -) -> Result { - let wallet = Wallet::new(name, &bitcoind.node_url).await?; - let buffer = Amount::from_btc(1.0).unwrap(); - let amount = fund_amount + buffer; - - let address = wallet.0.new_address().await.unwrap(); - - bitcoind.mint(address, amount).await.unwrap(); - - Ok(wallet) -} - #[async_trait] impl BuildTxLockPsbt for Wallet { async fn build_tx_lock_psbt( diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 451fc200..2f973b99 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -27,7 +27,8 @@ use crate::{ monero, network::{ peer_tracker::{self, PeerTracker}, - transport, TokioExecutor, + transport::SwapTransport, + TokioExecutor, }, Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; @@ -38,6 +39,7 @@ use xmr_btc::{ monero::CreateWalletForOutput, }; +#[allow(clippy::too_many_arguments)] pub async fn swap( bitcoin_wallet: Arc, monero_wallet: Arc, @@ -45,6 +47,8 @@ pub async fn swap( addr: Multiaddr, mut cmd_tx: Sender, mut rsp_rx: Receiver, + transport: SwapTransport, + behaviour: Bob, ) -> Result<()> { struct Network(Swarm); @@ -80,7 +84,7 @@ pub async fn swap( } } - let mut swarm = new_swarm()?; + let mut swarm = new_swarm(transport, behaviour)?; libp2p::Swarm::dial_addr(&mut swarm, addr)?; let alice = match swarm.next().await { @@ -202,23 +206,9 @@ pub async fn swap( pub type Swarm = libp2p::Swarm; -fn new_swarm() -> Result { - let behaviour = Bob::default(); - - let local_key_pair = behaviour.identity(); +fn new_swarm(transport: SwapTransport, behaviour: Bob) -> Result { let local_peer_id = behaviour.peer_id(); - let transport = { - #[cfg(feature = "tor")] - { - transport::build(local_key_pair, None)? - } - #[cfg(not(feature = "tor"))] - { - transport::build(local_key_pair)? - } - }; - let swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) .executor(Box::new(TokioExecutor { handle: tokio::runtime::Handle::current(), diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 5504d87b..aff4d67c 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -1,19 +1,36 @@ +use libp2p::core::Multiaddr; +use url::Url; + #[derive(structopt::StructOpt, Debug)] -pub struct Options { - /// Run the swap as Alice. - #[structopt(long = "as-alice")] - pub as_alice: bool, +#[structopt(name = "xmr-btc-swap", about = "Trustless XMR BTC swaps")] +pub enum Options { + Alice { + #[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")] + bitcoind_url: Url, - /// Run the swap as Bob and try to swap this many XMR (in piconero). - #[structopt(long = "picos")] - pub piconeros: Option, + #[structopt(default_value = "http://127.0.0.1:18083", long = "monerod")] + monerod_url: Url, - /// Run the swap as Bob and try to swap this many BTC (in satoshi). - #[structopt(long = "sats")] - pub satoshis: Option, + #[structopt(default_value = "/ip4/127.0.0.1/tcp/9876", long = "listen-addr")] + listen_addr: Multiaddr, - /// Alice's onion multitaddr (only required for Bob, Alice will autogenerate - /// one) - #[structopt(long)] - pub alice_address: Option, + #[structopt(long = "tor-port")] + tor_port: Option, + }, + Bob { + #[structopt(long = "sats")] + satoshis: u64, + + #[structopt(long = "alice-addr")] + alice_addr: Multiaddr, + + #[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")] + bitcoind_url: Url, + + #[structopt(default_value = "http://127.0.0.1:18083", long = "monerod")] + monerod_url: Url, + + #[structopt(long = "tor")] + tor: bool, + }, } diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 0d682e77..32743dd3 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -7,7 +7,6 @@ pub mod bob; pub mod monero; pub mod network; pub mod storage; -#[cfg(feature = "tor")] pub mod tor; const REFUND_TIMELOCK: u32 = 10; // Relative timelock, this is number of blocks. TODO: What should it be? diff --git a/swap/src/main.rs b/swap/src/main.rs index c2b0b4b6..2f8fc74c 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -12,105 +12,125 @@ )] #![forbid(unsafe_code)] -use anyhow::{bail, Context, Result}; +use anyhow::Result; use futures::{channel::mpsc, StreamExt}; use libp2p::Multiaddr; use log::LevelFilter; use std::{io, io::Write, process, sync::Arc}; use structopt::StructOpt; +use swap::{ + alice, + alice::Alice, + bitcoin, bob, + bob::Bob, + monero, + network::transport::{build, build_tor, SwapTransport}, + Cmd, Rsp, SwapAmounts, +}; use tracing::info; -use url::Url; mod cli; mod trace; use cli::Options; -use swap::{alice, bitcoin, bob, monero, Cmd, Rsp, SwapAmounts}; // TODO: Add root seed file instead of generating new seed each run. -// TODO: Add a config file with these in it. -// Alice's address and port until we have a config file. -pub const PORT: u16 = 9876; // Arbitrarily chosen. -pub const ADDR: &str = "127.0.0.1"; -pub const BITCOIND_JSON_RPC_URL: &str = "http://127.0.0.1:8332"; -pub const MONERO_WALLET_RPC_PORT: u16 = 18083; - -#[cfg(feature = "tor")] -pub const TOR_PORT: u16 = PORT + 1; - #[tokio::main] async fn main() -> Result<()> { let opt = Options::from_args(); trace::init_tracing(LevelFilter::Debug)?; - #[cfg(feature = "tor")] - let (addr, _ac) = { - let tor_secret_key = torut::onion::TorSecretKeyV3::generate(); - let onion_address = tor_secret_key - .public() - .get_onion_address() - .get_address_without_dot_onion(); - ( - format!("/onion3/{}:{}", onion_address, TOR_PORT), - create_tor_service(tor_secret_key).await?, - ) - }; - #[cfg(not(feature = "tor"))] - let addr = format!("/ip4/{}/tcp/{}", ADDR, PORT); + match opt { + Options::Alice { + bitcoind_url, + monerod_url, + listen_addr, + tor_port, + } => { + info!("running swap node as Alice ..."); - let alice: Multiaddr = addr.parse().expect("failed to parse Alice's address"); + let behaviour = Alice::default(); + let local_key_pair = behaviour.identity(); - if opt.as_alice { - info!("running swap node as Alice ..."); + let (listen_addr, _ac, transport) = match tor_port { + Some(tor_port) => { + let tor_secret_key = torut::onion::TorSecretKeyV3::generate(); + let onion_address = tor_secret_key + .public() + .get_onion_address() + .get_address_without_dot_onion(); + let onion_address_string = format!("/onion3/{}:{}", onion_address, tor_port); + let addr: Multiaddr = onion_address_string.parse()?; + let ac = create_tor_service(tor_secret_key, tor_port).await?; + let transport = build_tor(local_key_pair, Some((addr.clone(), tor_port)))?; + (addr, Some(ac), transport) + } + None => { + let transport = build(local_key_pair)?; + (listen_addr, None, transport) + } + }; - if opt.piconeros.is_some() || opt.satoshis.is_some() { - bail!("Alice cannot set the amount to swap via the cli"); + let bitcoin_wallet = bitcoin::Wallet::new("alice", bitcoind_url) + .await + .expect("failed to create bitcoin wallet"); + let bitcoin_wallet = Arc::new(bitcoin_wallet); + + let monero_wallet = Arc::new(monero::Wallet::new(monerod_url)); + + swap_as_alice( + bitcoin_wallet, + monero_wallet, + listen_addr, + transport, + behaviour, + ) + .await?; } + Options::Bob { + alice_addr, + satoshis, + bitcoind_url, + monerod_url, + tor, + } => { + info!("running swap node as Bob ..."); - let url = Url::parse(BITCOIND_JSON_RPC_URL).expect("failed to parse url"); - let bitcoin_wallet = bitcoin::Wallet::new("alice", &url) - .await - .expect("failed to create bitcoin wallet"); - let bitcoin_wallet = Arc::new(bitcoin_wallet); + let behaviour = Bob::default(); + let local_key_pair = behaviour.identity(); - let monero_wallet = Arc::new(monero::Wallet::localhost(MONERO_WALLET_RPC_PORT)); + let transport = match tor { + true => build_tor(local_key_pair, None)?, + false => build(local_key_pair)?, + }; - swap_as_alice(bitcoin_wallet, monero_wallet, alice.clone()).await?; - } else { - info!("running swap node as Bob ..."); + let bitcoin_wallet = bitcoin::Wallet::new("bob", bitcoind_url) + .await + .expect("failed to create bitcoin wallet"); + let bitcoin_wallet = Arc::new(bitcoin_wallet); - let alice = match opt.alice_address { - Some(addr) => addr, - None => bail!("Address required to dial"), - }; - let alice = multiaddr(&alice)?; + let monero_wallet = Arc::new(monero::Wallet::new(monerod_url)); - let url = Url::parse(BITCOIND_JSON_RPC_URL).expect("failed to parse url"); - let bitcoin_wallet = bitcoin::Wallet::new("bob", &url) - .await - .expect("failed to create bitcoin wallet"); - let bitcoin_wallet = Arc::new(bitcoin_wallet); - - let monero_wallet = Arc::new(monero::Wallet::localhost(MONERO_WALLET_RPC_PORT)); - - match (opt.piconeros, opt.satoshis) { - (Some(_), Some(_)) => bail!("Please supply only a single amount to swap"), - (None, None) => bail!("Please supply an amount to swap"), - (Some(_picos), _) => todo!("support starting with picos"), - (None, Some(sats)) => { - swap_as_bob(bitcoin_wallet, monero_wallet, sats, alice).await?; - } - }; + swap_as_bob( + bitcoin_wallet, + monero_wallet, + satoshis, + alice_addr, + transport, + behaviour, + ) + .await?; + } } Ok(()) } -#[cfg(feature = "tor")] async fn create_tor_service( tor_secret_key: torut::onion::TorSecretKeyV3, + tor_port: u16, ) -> Result { // TODO use configurable ports for tor connection let mut authenticated_connection = swap::tor::UnauthenticatedConnection::default() @@ -119,7 +139,7 @@ async fn create_tor_service( tracing::info!("Tor authenticated."); authenticated_connection - .add_service(TOR_PORT, &tor_secret_key) + .add_service(tor_port, &tor_secret_key) .await?; tracing::info!("Tor service added."); @@ -130,15 +150,10 @@ async fn swap_as_alice( bitcoin_wallet: Arc, monero_wallet: Arc, addr: Multiaddr, + transport: SwapTransport, + behaviour: Alice, ) -> Result<()> { - #[cfg(not(feature = "tor"))] - { - alice::swap(bitcoin_wallet, monero_wallet, addr, None).await - } - #[cfg(feature = "tor")] - { - alice::swap(bitcoin_wallet, monero_wallet, addr, Some(PORT)).await - } + alice::swap(bitcoin_wallet, monero_wallet, addr, transport, behaviour).await } async fn swap_as_bob( @@ -146,6 +161,8 @@ async fn swap_as_bob( monero_wallet: Arc, sats: u64, alice: Multiaddr, + transport: SwapTransport, + behaviour: Bob, ) -> Result<()> { let (cmd_tx, mut cmd_rx) = mpsc::channel(1); let (mut rsp_tx, rsp_rx) = mpsc::channel(1); @@ -156,6 +173,8 @@ async fn swap_as_bob( alice, cmd_tx, rsp_rx, + transport, + behaviour, )); loop { @@ -207,10 +226,3 @@ fn verify(amounts: SwapAmounts) -> Rsp { fn is_yes(s: &str) -> bool { matches!(s, "y" | "Y" | "yes" | "YES" | "Yes") } - -fn multiaddr(s: &str) -> Result { - let addr = s - .parse() - .with_context(|| format!("failed to parse multiaddr: {}", s))?; - Ok(addr) -} diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 0d215738..eb9f9bbc 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -5,6 +5,7 @@ use monero::{Address, Network, PrivateKey}; use monero_harness::rpc::wallet; use std::{str::FromStr, time::Duration}; +use url::Url; pub use xmr_btc::monero::{ Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey, Transfer, TransferProof, TxHash, WatchForTransfer, *, @@ -13,8 +14,8 @@ pub use xmr_btc::monero::{ pub struct Wallet(pub wallet::Client); impl Wallet { - pub fn localhost(port: u16) -> Self { - Self(wallet::Client::localhost(port)) + pub fn new(url: Url) -> Self { + Self(wallet::Client::new(url)) } /// Get the balance of the primary account. diff --git a/swap/src/network/transport.rs b/swap/src/network/transport.rs index 19ce5021..5fa715fa 100644 --- a/swap/src/network/transport.rs +++ b/swap/src/network/transport.rs @@ -18,7 +18,6 @@ use libp2p::{ /// - DNS name resolution /// - authentication via noise /// - multiplexing via yamux or mplex -#[cfg(not(feature = "tor"))] pub fn build(id_keys: identity::Keypair) -> Result { use libp2p::tcp::TokioTcpConfig; @@ -40,14 +39,12 @@ pub fn build(id_keys: identity::Keypair) -> Result { Ok(transport) } - /// Builds a libp2p transport with Tor and with the following features: /// - TCP connection over the Tor network /// - DNS name resolution /// - authentication via noise /// - multiplexing via yamux or mplex -#[cfg(feature = "tor")] -pub fn build( +pub fn build_tor( id_keys: identity::Keypair, address_port_pair: Option<(libp2p::core::Multiaddr, u16)>, ) -> Result { diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 09ba8fb0..cb49ef05 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -5,7 +5,7 @@ mod e2e_test { use libp2p::Multiaddr; use monero_harness::Monero; use std::sync::Arc; - use swap::{alice, bob}; + use swap::{alice, bob, network::transport::build}; use testcontainers::clients::Cli; use tracing_subscriber::util::SubscriberInitExt; @@ -37,12 +37,12 @@ mod e2e_test { let xmr_bob = 0; let alice_btc_wallet = Arc::new( - swap::bitcoin::Wallet::new("alice", &bitcoind.node_url) + swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone()) .await .unwrap(), ); let bob_btc_wallet = Arc::new( - swap::bitcoin::Wallet::new("bob", &bitcoind.node_url) + swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone()) .await .unwrap(), ); @@ -57,15 +57,20 @@ mod e2e_test { let alice_xmr_wallet = Arc::new(swap::monero::Wallet(monero.alice_wallet_rpc_client())); let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.bob_wallet_rpc_client())); + let alice_behaviour = alice::Alice::default(); + let alice_transport = build(alice_behaviour.identity()).unwrap(); let alice_swap = alice::swap( alice_btc_wallet.clone(), alice_xmr_wallet.clone(), alice_multiaddr.clone(), - None, + alice_transport, + alice_behaviour, ); let (cmd_tx, mut _cmd_rx) = mpsc::channel(1); let (mut rsp_tx, rsp_rx) = mpsc::channel(1); + let bob_behaviour = bob::Bob::default(); + let bob_transport = build(bob_behaviour.identity()).unwrap(); let bob_swap = bob::swap( bob_btc_wallet.clone(), bob_xmr_wallet.clone(), @@ -73,6 +78,8 @@ mod e2e_test { alice_multiaddr, cmd_tx, rsp_rx, + bob_transport, + bob_behaviour, ); // automate the verification step by accepting any amounts sent over by Alice