From d4d0bb27372ec86d3d7d4da461900c86fd7aebcf Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 2 Dec 2020 12:15:25 +1100 Subject: [PATCH] Remove generator code --- Cargo.lock | 30 ++- swap/src/alice.rs | 285 +--------------------- swap/src/bin/swap.rs | 135 +---------- swap/src/bob.rs | 243 ++----------------- swap/tests/e2e.rs | 113 +-------- xmr-btc/src/alice.rs | 278 +-------------------- xmr-btc/src/bob.rs | 231 +----------------- xmr-btc/tests/on_chain.rs | 497 +------------------------------------- 8 files changed, 74 insertions(+), 1738 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ce88b3e..4b2ec7ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,17 @@ dependencies = [ "syn 1.0.48", ] +[[package]] +name = "async-recursion" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5444eec77a9ec2bfe4524139e09195862e981400c4358d3b760cae634e4c4ee" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.48", +] + [[package]] name = "async-trait" version = "0.1.41" @@ -534,6 +545,21 @@ dependencies = [ "bitflags", ] +[[package]] +name = "conquer-once" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb45322099323eefa1b48850ce6c148f5b510894c531e038539f6370c887214" +dependencies = [ + "conquer-util", +] + +[[package]] +name = "conquer-util" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e763eef8846b13b380f37dfecda401770b0ca4e56e95170237bd7c25c7db3582" + [[package]] name = "const_fn" version = "0.4.3" @@ -1975,10 +2001,8 @@ name = "monero-harness" version = "0.1.0" dependencies = [ "anyhow", - "curve25519-dalek 2.1.0", "digest_auth", "futures", - "monero", "port_check", "rand 0.7.3", "reqwest", @@ -3339,12 +3363,14 @@ name = "swap" version = "0.1.0" dependencies = [ "anyhow", + "async-recursion", "async-trait", "atty", "backoff", "base64 0.12.3", "bitcoin", "bitcoin-harness", + "conquer-once", "derivative", "ecdsa_fun", "futures", diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 0aa0eedf..d848c403 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -2,39 +2,22 @@ //! Alice holds XMR and wishes receive BTC. use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use crate::{ - bitcoin, - bitcoin::TX_LOCK_MINE_TIMEOUT, - monero, network::{ peer_tracker::{self, PeerTracker}, request_response::AliceToBob, transport::SwapTransport, TokioExecutor, }, - state, - storage::Database, - SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, + SwapAmounts, }; use anyhow::Result; -use async_trait::async_trait; -use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; -use genawaiter::GeneratorState; use libp2p::{ core::{identity::Keypair, Multiaddr}, request_response::ResponseChannel, NetworkBehaviour, PeerId, }; -use rand::rngs::OsRng; -use std::{sync::Arc, time::Duration}; -use tokio::sync::Mutex; -use tracing::{debug, info, warn}; -use uuid::Uuid; -use xmr_btc::{ - alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0}, - bitcoin::BroadcastSignedTransaction, - bob, cross_curve_dleq, - monero::{CreateWalletForOutput, Transfer}, -}; +use tracing::{debug, info}; +use xmr_btc::{alice::State0, bob}; mod amounts; mod execution; @@ -44,240 +27,6 @@ mod message2; mod message3; pub mod swap; -pub async fn swap( - bitcoin_wallet: Arc, - monero_wallet: Arc, - db: Database, - listen: Multiaddr, - transport: SwapTransport, - behaviour: Behaviour, -) -> Result<()> { - struct Network { - swarm: Arc>, - channel: Option>, - } - - impl Network { - pub async fn send_message2(&mut self, proof: monero::TransferProof) { - match self.channel.take() { - None => warn!("Channel not found, did you call this twice?"), - Some(channel) => { - let mut guard = self.swarm.lock().await; - guard.send_message2(channel, alice::Message2 { - tx_lock_proof: proof, - }); - info!("Sent transfer proof"); - } - } - } - } - - // 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) -> bitcoin::EncryptedSignature { - #[derive(Debug)] - struct UnexpectedMessage; - - let encsig = (|| async { - let mut guard = self.swarm.lock().await; - let encsig = match guard.next().await { - OutEvent::Message3(msg) => msg.tx_redeem_encsig, - other => { - warn!("Expected Bob's Bitcoin redeem encsig, got: {:?}", other); - return Err(backoff::Error::Transient(UnexpectedMessage)); - } - }; - - Result::<_, backoff::Error>::Ok(encsig) - }) - .retry(ConstantBackoff::new(Duration::from_secs(1))) - .await - .expect("transient errors to be retried"); - - info!("Received Bitcoin redeem encsig"); - - encsig - } - } - - let mut swarm = new_swarm(listen, transport, behaviour)?; - let message0: bob::Message0; - let mut state0: Option = None; - let mut last_amounts: Option = None; - - // TODO: This loop is a neat idea for local development, as it allows us to keep - // Alice up and let Bob keep trying to connect, request amounts and/or send the - // first message of the handshake, but it comes at the cost of needing to handle - // mutable state, which has already been the source of a bug at one point. This - // is an obvious candidate for refactoring - loop { - match swarm.next().await { - OutEvent::ConnectionEstablished(bob) => { - info!("Connection established with: {}", bob); - } - OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => { - let amounts = calculate_amounts(btc); - last_amounts = Some(amounts); - swarm.send_amounts(channel, amounts); - - let SwapAmounts { btc, xmr } = amounts; - - let redeem_address = bitcoin_wallet.as_ref().new_address().await?; - let punish_address = redeem_address.clone(); - - // TODO: Pass this in using - 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 state = State0::new( - a, - s_a, - v_a, - btc, - xmr, - REFUND_TIMELOCK, - PUNISH_TIMELOCK, - redeem_address, - punish_address, - ); - - info!("Commencing handshake"); - swarm.set_state0(state.clone()); - - state0 = Some(state) - } - OutEvent::Message0(msg) => { - // We don't want Bob to be able to crash us by sending an out of - // order message. Keep looping if Bob has not requested amounts. - if last_amounts.is_some() { - // TODO: We should verify the amounts and notify Bob if they have changed. - message0 = msg; - break; - } - } - other => panic!("Unexpected event: {:?}", other), - }; - } - - let state1 = state0.expect("to be set").receive(message0)?; - - let (state2, channel) = match swarm.next().await { - OutEvent::Message1 { msg, channel } => { - let state2 = state1.receive(msg); - (state2, channel) - } - other => panic!("Unexpected event: {:?}", other), - }; - - let msg = state2.next_message(); - swarm.send_message1(channel, msg); - - let (state3, channel) = match swarm.next().await { - OutEvent::Message2 { msg, channel } => { - let state3 = state2.receive(msg)?; - (state3, channel) - } - other => panic!("Unexpected event: {:?}", other), - }; - - let swap_id = Uuid::new_v4(); - db.insert_latest_state(swap_id, state::Alice::Negotiated(state3.clone()).into()) - .await?; - - info!("Handshake complete, we now have State3 for Alice."); - - let network = Arc::new(Mutex::new(Network { - swarm: Arc::new(Mutex::new(swarm)), - channel: Some(channel), - })); - - let mut action_generator = action_generator( - network.clone(), - bitcoin_wallet.clone(), - state3.clone(), - TX_LOCK_MINE_TIMEOUT, - ); - - loop { - let state = action_generator.async_resume().await; - - tracing::info!("Resumed execution of generator, got: {:?}", state); - - match state { - GeneratorState::Yielded(Action::LockXmr { - amount, - public_spend_key, - public_view_key, - }) => { - db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into()) - .await?; - - let (transfer_proof, _) = monero_wallet - .transfer(public_spend_key, public_view_key, amount) - .await?; - - db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into()) - .await?; - - let mut guard = network.as_ref().lock().await; - guard.send_message2(transfer_proof).await; - info!("Sent transfer proof"); - } - - GeneratorState::Yielded(Action::RedeemBtc(tx)) => { - db.insert_latest_state( - swap_id, - state::Alice::BtcRedeemable { - state: state3.clone(), - redeem_tx: tx.clone(), - } - .into(), - ) - .await?; - - let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; - } - GeneratorState::Yielded(Action::CancelBtc(tx)) => { - let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; - } - GeneratorState::Yielded(Action::PunishBtc(tx)) => { - db.insert_latest_state(swap_id, state::Alice::BtcPunishable(state3.clone()).into()) - .await?; - - let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; - } - GeneratorState::Yielded(Action::CreateMoneroWalletForOutput { - spend_key, - view_key, - }) => { - db.insert_latest_state( - swap_id, - state::Alice::BtcRefunded { - state: state3.clone(), - spend_key, - view_key, - } - .into(), - ) - .await?; - - monero_wallet - .create_and_load_wallet_for_output(spend_key, view_key) - .await?; - } - GeneratorState::Complete(()) => { - db.insert_latest_state(swap_id, state::Alice::SwapComplete.into()) - .await?; - - return Ok(()); - } - } - } -} - pub type Swarm = libp2p::Swarm; pub fn new_swarm( @@ -444,31 +193,3 @@ impl Default for Behaviour { } } } - -fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapAmounts { - // TODO (Franck): This should instead verify that the received amounts matches - // the command line arguments This value corresponds to 100 XMR per BTC - const PICONERO_PER_SAT: u64 = 1_000_000; - - let picos = btc.as_sat() * PICONERO_PER_SAT; - let xmr = monero::Amount::from_piconero(picos); - - SwapAmounts { btc, xmr } -} - -#[cfg(test)] -mod tests { - use super::*; - - const ONE_BTC: u64 = 100_000_000; - const HUNDRED_XMR: u64 = 100_000_000_000_000; - - #[test] - fn one_bitcoin_equals_a_hundred_moneroj() { - let btc = ::bitcoin::Amount::from_sat(ONE_BTC); - let want = monero::Amount::from_piconero(HUNDRED_XMR); - - let SwapAmounts { xmr: got, .. } = calculate_amounts(btc); - assert_eq!(got, want); - } -} diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index c6e3d82d..9f8b7708 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -13,19 +13,17 @@ #![forbid(unsafe_code)] use anyhow::Result; -use futures::{channel::mpsc, StreamExt}; use libp2p::Multiaddr; use prettytable::{row, Table}; -use std::{io, io::Write, process, sync::Arc}; +use std::sync::Arc; use structopt::StructOpt; use swap::{ alice, bitcoin, bob, cli::Options, monero, - network::transport::{build, build_tor, SwapTransport}, + network::transport::{build, build_tor}, recover::recover, storage::Database, - Cmd, Rsp, SwapAmounts, }; use tracing::info; @@ -53,7 +51,7 @@ async fn main() -> Result<()> { let behaviour = alice::Behaviour::default(); let local_key_pair = behaviour.identity(); - let (listen_addr, _ac, transport) = match tor_port { + 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 @@ -75,23 +73,15 @@ async fn main() -> Result<()> { let bitcoin_wallet = bitcoin::Wallet::new("alice", bitcoind_url) .await .expect("failed to create bitcoin wallet"); - let bitcoin_wallet = Arc::new(bitcoin_wallet); + let _bitcoin_wallet = Arc::new(bitcoin_wallet); - let monero_wallet = Arc::new(monero::Wallet::new(monerod_url)); + let _monero_wallet = Arc::new(monero::Wallet::new(monerod_url)); - swap_as_alice( - bitcoin_wallet, - monero_wallet, - db, - listen_addr, - transport, - behaviour, - ) - .await?; + // TODO: Call swap function } Options::Bob { - alice_addr, - satoshis, + alice_addr: _, + satoshis: _, bitcoind_url, monerod_url, tor, @@ -101,7 +91,7 @@ async fn main() -> Result<()> { let behaviour = bob::Behaviour::default(); let local_key_pair = behaviour.identity(); - let transport = match tor { + let _transport = match tor { true => build_tor(local_key_pair, None)?, false => build(local_key_pair)?, }; @@ -109,20 +99,11 @@ async fn main() -> Result<()> { let bitcoin_wallet = bitcoin::Wallet::new("bob", bitcoind_url) .await .expect("failed to create bitcoin wallet"); - let bitcoin_wallet = Arc::new(bitcoin_wallet); + let _bitcoin_wallet = Arc::new(bitcoin_wallet); - let monero_wallet = Arc::new(monero::Wallet::new(monerod_url)); + let _monero_wallet = Arc::new(monero::Wallet::new(monerod_url)); - swap_as_bob( - bitcoin_wallet, - monero_wallet, - db, - satoshis, - alice_addr, - transport, - behaviour, - ) - .await?; + // TODO: Call swap function } Options::History => { let mut table = Table::new(); @@ -171,95 +152,3 @@ async fn create_tor_service( Ok(authenticated_connection) } - -async fn swap_as_alice( - bitcoin_wallet: Arc, - monero_wallet: Arc, - db: Database, - addr: Multiaddr, - transport: SwapTransport, - behaviour: alice::Behaviour, -) -> Result<()> { - alice::swap( - bitcoin_wallet, - monero_wallet, - db, - addr, - transport, - behaviour, - ) - .await -} - -async fn swap_as_bob( - bitcoin_wallet: Arc, - monero_wallet: Arc, - db: Database, - sats: u64, - alice: Multiaddr, - transport: SwapTransport, - behaviour: bob::Behaviour, -) -> Result<()> { - let (cmd_tx, mut cmd_rx) = mpsc::channel(1); - let (mut rsp_tx, rsp_rx) = mpsc::channel(1); - tokio::spawn(bob::swap( - bitcoin_wallet, - monero_wallet, - db, - sats, - alice, - cmd_tx, - rsp_rx, - transport, - behaviour, - )); - - loop { - let read = cmd_rx.next().await; - match read { - Some(cmd) => match cmd { - Cmd::VerifyAmounts(p) => { - let rsp = verify(p); - rsp_tx.try_send(rsp)?; - if rsp == Rsp::Abort { - process::exit(0); - } - } - }, - None => { - info!("Channel closed from other end"); - return Ok(()); - } - } - } -} - -fn verify(amounts: SwapAmounts) -> Rsp { - let mut s = String::new(); - println!("Got rate from Alice for XMR/BTC swap\n"); - println!("{}", amounts); - print!("Would you like to continue with this swap [y/N]: "); - - let _ = io::stdout().flush(); - io::stdin() - .read_line(&mut s) - .expect("Did not enter a correct string"); - - if let Some('\n') = s.chars().next_back() { - s.pop(); - } - if let Some('\r') = s.chars().next_back() { - s.pop(); - } - - if !is_yes(&s) { - println!("No worries, try again later - Alice updates her rate regularly"); - return Rsp::Abort; - } - - Rsp::VerifiedAmounts -} - -fn is_yes(s: &str) -> bool { - matches!(s, "y" | "Y" | "yes" | "YES" | "Yes") -} diff --git a/swap/src/bob.rs b/swap/src/bob.rs index f5602b8a..83fe18c7 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -1,20 +1,22 @@ //! Run an XMR/BTC swap in the role of Bob. //! Bob holds BTC and wishes receive XMR. -use anyhow::Result; - -use async_trait::async_trait; -use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; -use futures::{ - channel::mpsc::{Receiver, Sender}, - FutureExt, StreamExt, +use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; +use crate::{ + network::{ + peer_tracker::{self, PeerTracker}, + transport::SwapTransport, + TokioExecutor, + }, + SwapAmounts, +}; +use anyhow::Result; +use libp2p::{core::identity::Keypair, NetworkBehaviour, PeerId}; +use tracing::{debug, info}; +use xmr_btc::{ + alice, + bitcoin::EncryptedSignature, + bob::{self}, }; -use genawaiter::GeneratorState; -use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; -use rand::rngs::OsRng; -use std::{process, sync::Arc, time::Duration}; -use tokio::sync::Mutex; -use tracing::{debug, info, warn}; -use uuid::Uuid; mod amounts; mod execution; @@ -24,219 +26,6 @@ mod message2; mod message3; pub mod swap; -use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; -use crate::{ - bitcoin::{self, TX_LOCK_MINE_TIMEOUT}, - monero, - network::{ - peer_tracker::{self, PeerTracker}, - transport::SwapTransport, - TokioExecutor, - }, - state, - storage::Database, - Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, -}; -use xmr_btc::{ - alice, - bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock}, - bob::{self, action_generator, ReceiveTransferProof, State0}, - monero::CreateWalletForOutput, -}; - -#[allow(clippy::too_many_arguments)] -pub async fn swap( - bitcoin_wallet: Arc, - monero_wallet: Arc, - db: Database, - btc: u64, - addr: Multiaddr, - mut cmd_tx: Sender, - mut rsp_rx: Receiver, - transport: SwapTransport, - behaviour: Behaviour, -) -> Result<()> { - struct Network(Swarm); - - // TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed - // to `ConstantBackoff`. - - #[async_trait] - impl ReceiveTransferProof for Network { - async fn receive_transfer_proof(&mut self) -> monero::TransferProof { - #[derive(Debug)] - struct UnexpectedMessage; - - let future = self.0.next().shared(); - - let proof = (|| async { - let proof = match future.clone().await { - OutEvent::Message2(msg) => msg.tx_lock_proof, - other => { - warn!("Expected transfer proof, got: {:?}", other); - return Err(backoff::Error::Transient(UnexpectedMessage)); - } - }; - - Result::<_, backoff::Error>::Ok(proof) - }) - .retry(ConstantBackoff::new(Duration::from_secs(1))) - .await - .expect("transient errors to be retried"); - - info!("Received transfer proof"); - - proof - } - } - - let mut swarm = new_swarm(transport, behaviour)?; - - libp2p::Swarm::dial_addr(&mut swarm, addr)?; - let alice = match swarm.next().await { - OutEvent::ConnectionEstablished(alice) => alice, - other => panic!("unexpected event: {:?}", other), - }; - info!("Connection established with: {}", alice); - - swarm.request_amounts(alice.clone(), btc); - - let (btc, xmr) = match swarm.next().await { - OutEvent::Amounts(amounts) => { - info!("Got amounts from Alice: {:?}", amounts); - let cmd = Cmd::VerifyAmounts(amounts); - cmd_tx.try_send(cmd)?; - let response = rsp_rx.next().await; - if response == Some(Rsp::Abort) { - info!("User rejected amounts proposed by Alice, aborting..."); - process::exit(0); - } - - info!("User accepted amounts proposed by Alice"); - (amounts.btc, amounts.xmr) - } - other => panic!("unexpected event: {:?}", other), - }; - - let refund_address = bitcoin_wallet.new_address().await?; - - // TODO: Pass this in using - let rng = &mut OsRng; - let state0 = State0::new( - rng, - btc, - xmr, - REFUND_TIMELOCK, - PUNISH_TIMELOCK, - refund_address, - ); - - info!("Commencing handshake"); - - swarm.send_message0(alice.clone(), state0.next_message(rng)); - let state1 = match swarm.next().await { - OutEvent::Message0(msg) => state0.receive(bitcoin_wallet.as_ref(), msg).await?, - other => panic!("unexpected event: {:?}", other), - }; - - swarm.send_message1(alice.clone(), state1.next_message()); - let state2 = match swarm.next().await { - OutEvent::Message1(msg) => { - state1.receive(msg)? // TODO: Same as above. - } - other => panic!("unexpected event: {:?}", other), - }; - - let swap_id = Uuid::new_v4(); - db.insert_latest_state(swap_id, state::Bob::Handshaken(state2.clone()).into()) - .await?; - - swarm.send_message2(alice.clone(), state2.next_message()); - - info!("Handshake complete"); - - let network = Arc::new(Mutex::new(Network(swarm))); - - let mut action_generator = action_generator( - network.clone(), - monero_wallet.clone(), - bitcoin_wallet.clone(), - state2.clone(), - TX_LOCK_MINE_TIMEOUT, - ); - - loop { - let state = action_generator.async_resume().await; - - info!("Resumed execution of generator, got: {:?}", state); - - // TODO: Protect against transient errors - // TODO: Ignore transaction-already-in-block-chain errors - - match state { - GeneratorState::Yielded(bob::Action::LockBtc(tx_lock)) => { - let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?; - let _ = bitcoin_wallet - .broadcast_signed_transaction(signed_tx_lock) - .await?; - db.insert_latest_state(swap_id, state::Bob::BtcLocked(state2.clone()).into()) - .await?; - } - GeneratorState::Yielded(bob::Action::SendBtcRedeemEncsig(tx_redeem_encsig)) => { - db.insert_latest_state(swap_id, state::Bob::XmrLocked(state2.clone()).into()) - .await?; - - let mut guard = network.as_ref().lock().await; - guard.0.send_message3(alice.clone(), tx_redeem_encsig); - info!("Sent Bitcoin redeem encsig"); - - // FIXME: Having to wait for Alice's response here is a big problem, because - // we're stuck if she doesn't send her response back. I believe this is - // currently necessary, so we may have to rework this and/or how we use libp2p - match guard.0.next().shared().await { - OutEvent::Message3 => { - debug!("Got Message3 empty response"); - } - other => panic!("unexpected event: {:?}", other), - }; - } - GeneratorState::Yielded(bob::Action::CreateXmrWalletForOutput { - spend_key, - view_key, - }) => { - db.insert_latest_state(swap_id, state::Bob::BtcRedeemed(state2.clone()).into()) - .await?; - - monero_wallet - .create_and_load_wallet_for_output(spend_key, view_key) - .await?; - } - GeneratorState::Yielded(bob::Action::CancelBtc(tx_cancel)) => { - db.insert_latest_state(swap_id, state::Bob::BtcRefundable(state2.clone()).into()) - .await?; - - let _ = bitcoin_wallet - .broadcast_signed_transaction(tx_cancel) - .await?; - } - GeneratorState::Yielded(bob::Action::RefundBtc(tx_refund)) => { - db.insert_latest_state(swap_id, state::Bob::BtcRefundable(state2.clone()).into()) - .await?; - - let _ = bitcoin_wallet - .broadcast_signed_transaction(tx_refund) - .await?; - } - GeneratorState::Complete(()) => { - db.insert_latest_state(swap_id, state::Bob::SwapComplete.into()) - .await?; - - return Ok(()); - } - } - } -} - pub type Swarm = libp2p::Swarm; pub fn new_swarm(transport: SwapTransport, behaviour: Behaviour) -> Result { diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 442fba5f..cfa42414 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -1,5 +1,5 @@ use bitcoin_harness::Bitcoind; -use futures::{channel::mpsc, future::try_join}; +use futures::future::try_join; use libp2p::Multiaddr; use monero_harness::Monero; use rand::rngs::OsRng; @@ -13,117 +13,6 @@ use testcontainers::clients::Cli; use uuid::Uuid; use xmr_btc::{bitcoin, cross_curve_dleq}; -#[ignore] -#[tokio::test] -async fn swap() { - 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 alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876" - .parse() - .expect("failed to parse Alice's address"); - - let cli = Cli::default(); - let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); - dbg!(&bitcoind.node_url); - let _ = bitcoind.init(5).await; - - let btc = bitcoin::Amount::from_sat(1_000_000); - let btc_alice = bitcoin::Amount::ZERO; - let btc_bob = btc * 10; - - // this xmr value matches the logic of alice::calculate_amounts i.e. btc * - // 10_000 * 100 - let xmr = 1_000_000_000_000; - let xmr_alice = xmr * 10; - let xmr_bob = 0; - - let alice_btc_wallet = Arc::new( - 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.clone()) - .await - .unwrap(), - ); - bitcoind - .mint(bob_btc_wallet.0.new_address().await.unwrap(), btc_bob) - .await - .unwrap(); - - let (monero, _container) = - Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) - .await - .unwrap(); - monero - .init(vec![("alice", xmr_alice), ("bob", xmr_bob)]) - .await - .unwrap(); - - let alice_xmr_wallet = Arc::new(swap::monero::Wallet( - monero.wallet("alice").unwrap().client(), - )); - let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client())); - - let alice_behaviour = alice::Behaviour::default(); - let alice_transport = build(alice_behaviour.identity()).unwrap(); - - let db = Database::open(std::path::Path::new("../.swap-db/")).unwrap(); - let alice_swap = alice::swap( - alice_btc_wallet.clone(), - alice_xmr_wallet.clone(), - db, - alice_multiaddr.clone(), - alice_transport, - alice_behaviour, - ); - - let db_dir = tempdir().unwrap(); - let db = Database::open(db_dir.path()).unwrap(); - let (cmd_tx, mut _cmd_rx) = mpsc::channel(1); - let (mut rsp_tx, rsp_rx) = mpsc::channel(1); - let bob_behaviour = bob::Behaviour::default(); - let bob_transport = build(bob_behaviour.identity()).unwrap(); - let bob_swap = bob::swap( - bob_btc_wallet.clone(), - bob_xmr_wallet.clone(), - db, - btc.as_sat(), - alice_multiaddr, - cmd_tx, - rsp_rx, - bob_transport, - bob_behaviour, - ); - - // automate the verification step by accepting any amounts sent over by Alice - rsp_tx.try_send(swap::Rsp::VerifiedAmounts).unwrap(); - - try_join(alice_swap, bob_swap).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 - bitcoin::Amount::from_sat(bitcoin::TX_FEE) - ); - assert!(btc_bob_final <= btc_bob - btc); - - assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr); - assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr); -} - #[tokio::test] async fn happy_path_recursive_executor() { use tracing_subscriber::util::SubscriberInitExt as _; diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 477df236..d8dbaafe 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -1,6 +1,6 @@ use crate::{ bitcoin, - bitcoin::{poll_until_block_height_is_gte, BroadcastSignedTransaction, WatchForRawTransaction}, + bitcoin::{BroadcastSignedTransaction, WatchForRawTransaction}, bob, monero, monero::{CreateWalletForOutput, Transfer}, transport::{ReceiveMessage, SendMessage}, @@ -11,21 +11,11 @@ use ecdsa_fun::{ adaptor::{Adaptor, EncryptedSignature}, nonce::Deterministic, }; -use futures::{ - future::{select, Either}, - pin_mut, FutureExt, -}; -use genawaiter::sync::{Gen, GenBoxed}; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::{ - convert::{TryFrom, TryInto}, - sync::Arc, - time::Duration, -}; -use tokio::{sync::Mutex, time::timeout}; -use tracing::{error, info}; +use std::convert::{TryFrom, TryInto}; +use tracing::info; pub mod message; pub use message::{Message, Message0, Message1, Message2}; @@ -54,268 +44,6 @@ pub trait ReceiveBitcoinRedeemEncsig { async fn receive_bitcoin_redeem_encsig(&mut self) -> bitcoin::EncryptedSignature; } -/// Perform the on-chain protocol to swap monero and bitcoin as Alice. -/// -/// This is called post handshake, after all the keys, addresses and most of the -/// signatures have been exchanged. -/// -/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will -/// wait for Bob, the counterparty, to lock up the bitcoin. -pub fn action_generator( - network: Arc>, - bitcoin_client: Arc, - // TODO: Replace this with a new, slimmer struct? - State3 { - a, - B, - s_a, - S_b_monero, - S_b_bitcoin, - v, - xmr, - refund_timelock, - punish_timelock, - refund_address, - redeem_address, - punish_address, - tx_lock, - tx_punish_sig_bob, - tx_cancel_sig_bob, - .. - }: State3, - bitcoin_tx_lock_timeout: u64, -) -> GenBoxed -where - N: ReceiveBitcoinRedeemEncsig + Send + 'static, - B: bitcoin::BlockHeight - + bitcoin::TransactionBlockHeight - + bitcoin::WatchForRawTransaction - + Send - + Sync - + 'static, -{ - #[derive(Debug)] - enum SwapFailed { - BeforeBtcLock(Reason), - AfterXmrLock(Reason), - } - - /// Reason why the swap has failed. - #[derive(Debug)] - enum Reason { - /// Bob was too slow to lock the bitcoin. - InactiveBob, - /// Bob's encrypted signature on the Bitcoin redeem transaction is - /// invalid. - InvalidEncryptedSignature, - /// The refund timelock has been reached. - BtcExpired, - } - - #[derive(Debug)] - enum RefundFailed { - BtcPunishable, - /// Could not find Alice's signature on the refund transaction witness - /// stack. - BtcRefundSignature, - /// Could not recover secret `s_b` from Alice's refund transaction - /// signature. - SecretRecovery, - } - - Gen::new_boxed(|co| async move { - let swap_result: Result<(), SwapFailed> = async { - timeout( - Duration::from_secs(bitcoin_tx_lock_timeout), - bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - ) - .await - .map_err(|_| SwapFailed::BeforeBtcLock(Reason::InactiveBob))?; - - let tx_lock_height = bitcoin_client - .transaction_block_height(tx_lock.txid()) - .await; - let poll_until_btc_has_expired = poll_until_block_height_is_gte( - bitcoin_client.as_ref(), - tx_lock_height + refund_timelock, - ) - .shared(); - pin_mut!(poll_until_btc_has_expired); - - let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { - scalar: s_a.into_ed25519(), - }); - - co.yield_(Action::LockXmr { - amount: xmr, - public_spend_key: S_a + S_b_monero, - public_view_key: v.public(), - }) - .await; - - // TODO: Watch for LockXmr using watch-only wallet. Doing so will prevent Alice - // from cancelling/refunding unnecessarily. - - let tx_redeem_encsig = { - let mut guard = network.as_ref().lock().await; - let tx_redeem_encsig = match select( - guard.receive_bitcoin_redeem_encsig(), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((encsig, _)) => encsig, - Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)), - }; - - tracing::debug!("select returned redeem encsig from message"); - - tx_redeem_encsig - }; - - let (signed_tx_redeem, tx_redeem_txid) = { - let adaptor = Adaptor::>::default(); - - let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); - - bitcoin::verify_encsig( - B.clone(), - s_a.into_secp256k1().into(), - &tx_redeem.digest(), - &tx_redeem_encsig, - ) - .map_err(|_| SwapFailed::AfterXmrLock(Reason::InvalidEncryptedSignature))?; - - let sig_a = a.sign(tx_redeem.digest()); - let sig_b = - adaptor.decrypt_signature(&s_a.into_secp256k1(), tx_redeem_encsig.clone()); - - let tx = tx_redeem - .add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_redeem"); - let txid = tx.txid(); - - (tx, txid) - }; - - co.yield_(Action::RedeemBtc(signed_tx_redeem)).await; - - match select( - bitcoin_client.watch_for_raw_transaction(tx_redeem_txid), - poll_until_btc_has_expired, - ) - .await - { - Either::Left(_) => {} - Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)), - }; - - Ok(()) - } - .await; - - if let Err(ref err) = swap_result { - error!("swap failed: {:?}", err); - } - - if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result { - let refund_result: Result<(), RefundFailed> = async { - let tx_cancel = - bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); - let signed_tx_cancel = { - let sig_a = a.sign(tx_cancel.digest()); - let sig_b = tx_cancel_sig_bob.clone(); - - tx_cancel - .clone() - .add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel") - }; - - co.yield_(Action::CancelBtc(signed_tx_cancel)).await; - - bitcoin_client - .watch_for_raw_transaction(tx_cancel.txid()) - .await; - - let tx_cancel_height = bitcoin_client - .transaction_block_height(tx_cancel.txid()) - .await; - let poll_until_bob_can_be_punished = poll_until_block_height_is_gte( - bitcoin_client.as_ref(), - tx_cancel_height + punish_timelock, - ) - .shared(); - pin_mut!(poll_until_bob_can_be_punished); - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); - let tx_refund_published = match select( - bitcoin_client.watch_for_raw_transaction(tx_refund.txid()), - poll_until_bob_can_be_punished, - ) - .await - { - Either::Left((tx, _)) => tx, - Either::Right(_) => return Err(RefundFailed::BtcPunishable), - }; - - let s_a = monero::PrivateKey { - scalar: s_a.into_ed25519(), - }; - - let tx_refund_sig = tx_refund - .extract_signature_by_key(tx_refund_published, a.public()) - .map_err(|_| RefundFailed::BtcRefundSignature)?; - let tx_refund_encsig = a.encsign(S_b_bitcoin.clone(), tx_refund.digest()); - - let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig) - .map_err(|_| RefundFailed::SecretRecovery)?; - let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); - - co.yield_(Action::CreateMoneroWalletForOutput { - spend_key: s_a + s_b, - view_key: v, - }) - .await; - - Ok(()) - } - .await; - - if let Err(ref err) = refund_result { - error!("refund failed: {:?}", err); - } - - // LIMITATION: When approaching the punish scenario, Bob could theoretically - // wake up in between Alice's publication of tx cancel and beat Alice's punish - // transaction with his refund transaction. Alice would then need to carry on - // with the refund on Monero. Doing so may be too verbose with the current, - // linear approach. A different design may be required - if let Err(RefundFailed::BtcPunishable) = refund_result { - let tx_cancel = - bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); - let tx_punish = - bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock); - let tx_punish_txid = tx_punish.txid(); - let signed_tx_punish = { - let sig_a = a.sign(tx_punish.digest()); - let sig_b = tx_punish_sig_bob; - - tx_punish - .add_signatures(&tx_cancel, (a.public(), sig_a), (B, sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel") - }; - - co.yield_(Action::PunishBtc(signed_tx_punish)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_punish_txid) - .await; - } - } - }) -} - // There are no guarantees that send_message and receive_massage do not block // the flow of execution. Therefore they must be paired between Alice/Bob, one // send to one receive in the correct order. diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 64c45f48..25a940d7 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -1,13 +1,16 @@ use crate::{ alice, bitcoin::{ - self, poll_until_block_height_is_gte, BroadcastSignedTransaction, BuildTxLockPsbt, - SignTxLock, TxCancel, WatchForRawTransaction, + self, poll_until_block_height_is_gte, BlockHeight, BroadcastSignedTransaction, + BuildTxLockPsbt, GetRawTransaction, SignTxLock, TransactionBlockHeight, TxCancel, + WatchForRawTransaction, }, monero, + monero::{CreateWalletForOutput, WatchForTransfer}, serde::monero_private_key, transport::{ReceiveMessage, SendMessage}, }; +use ::bitcoin::{Transaction, Txid}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use ecdsa_fun::{ @@ -15,28 +18,13 @@ use ecdsa_fun::{ nonce::Deterministic, Signature, }; -use futures::{ - future::{select, Either}, - pin_mut, FutureExt, -}; -use genawaiter::sync::{Gen, GenBoxed}; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::{ - convert::{TryFrom, TryInto}, - sync::Arc, - time::Duration, -}; -use tokio::{sync::Mutex, time::timeout}; -use tracing::error; +use std::convert::{TryFrom, TryInto}; pub mod message; -use crate::{ - bitcoin::{BlockHeight, GetRawTransaction, TransactionBlockHeight}, - monero::{CreateWalletForOutput, WatchForTransfer}, -}; -use ::bitcoin::{Transaction, Txid}; + pub use message::{Message, Message0, Message1, Message2, Message3}; #[allow(clippy::large_enum_variant)] @@ -58,211 +46,6 @@ pub trait ReceiveTransferProof { async fn receive_transfer_proof(&mut self) -> monero::TransferProof; } -/// Perform the on-chain protocol to swap monero and bitcoin as Bob. -/// -/// This is called post handshake, after all the keys, addresses and most of the -/// signatures have been exchanged. -/// -/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will -/// wait for Bob, the caller of this function, to lock up the bitcoin. -pub fn action_generator( - network: Arc>, - monero_client: Arc, - bitcoin_client: Arc, - // TODO: Replace this with a new, slimmer struct? - State2 { - A, - b, - s_b, - S_a_monero, - S_a_bitcoin, - v, - xmr, - refund_timelock, - redeem_address, - refund_address, - tx_lock, - tx_cancel_sig_a, - tx_refund_encsig, - .. - }: State2, - bitcoin_tx_lock_timeout: u64, -) -> GenBoxed -where - N: ReceiveTransferProof + Send + 'static, - M: monero::WatchForTransfer + Send + Sync + 'static, - B: bitcoin::BlockHeight - + bitcoin::TransactionBlockHeight - + bitcoin::WatchForRawTransaction - + Send - + Sync - + 'static, -{ - #[derive(Debug)] - enum SwapFailed { - BeforeBtcLock(Reason), - AfterBtcLock(Reason), - AfterBtcRedeem(Reason), - } - - /// Reason why the swap has failed. - #[derive(Debug)] - enum Reason { - /// Bob was too slow to lock the bitcoin. - InactiveBob, - /// The refund timelock has been reached. - BtcExpired, - /// Alice did not lock up enough monero in the shared output. - InsufficientXmr(monero::InsufficientFunds), - /// Could not find Bob's signature on the redeem transaction witness - /// stack. - BtcRedeemSignature, - /// Could not recover secret `s_a` from Bob's redeem transaction - /// signature. - SecretRecovery, - } - - Gen::new_boxed(|co| async move { - let swap_result: Result<(), SwapFailed> = async { - co.yield_(Action::LockBtc(tx_lock.clone())).await; - - timeout( - Duration::from_secs(bitcoin_tx_lock_timeout), - bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - ) - .await - .map(|tx| tx.txid()) - .map_err(|_| SwapFailed::BeforeBtcLock(Reason::InactiveBob))?; - - let tx_lock_height = bitcoin_client - .transaction_block_height(tx_lock.txid()) - .await; - let poll_until_btc_has_expired = poll_until_block_height_is_gte( - bitcoin_client.as_ref(), - tx_lock_height + refund_timelock, - ) - .shared(); - pin_mut!(poll_until_btc_has_expired); - - let transfer_proof = { - let mut guard = network.as_ref().lock().await; - let transfer_proof = match select( - guard.receive_transfer_proof(), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((proof, _)) => proof, - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - }; - - tracing::debug!("select returned transfer proof from message"); - - transfer_proof - }; - - let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar( - s_b.into_ed25519(), - )); - let S = S_a_monero + S_b_monero; - - match select( - monero_client.watch_for_transfer(S, v.public(), transfer_proof, xmr, 0), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((Err(e), _)) => { - return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXmr(e))) - } - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - _ => {} - } - - let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); - let tx_redeem_encsig = b.encsign(S_a_bitcoin.clone(), tx_redeem.digest()); - - co.yield_(Action::SendBtcRedeemEncsig(tx_redeem_encsig.clone())) - .await; - - let tx_redeem_published = match select( - bitcoin_client.watch_for_raw_transaction(tx_redeem.txid()), - poll_until_btc_has_expired, - ) - .await - { - Either::Left((tx, _)) => tx, - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - }; - - let tx_redeem_sig = tx_redeem - .extract_signature_by_key(tx_redeem_published, b.public()) - .map_err(|_| SwapFailed::AfterBtcRedeem(Reason::BtcRedeemSignature))?; - let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig) - .map_err(|_| SwapFailed::AfterBtcRedeem(Reason::SecretRecovery))?; - let s_a = monero::private_key_from_secp256k1_scalar(s_a.into()); - - let s_b = monero::PrivateKey { - scalar: s_b.into_ed25519(), - }; - - co.yield_(Action::CreateXmrWalletForOutput { - spend_key: s_a + s_b, - view_key: v, - }) - .await; - - Ok(()) - } - .await; - - if let Err(ref err) = swap_result { - error!("swap failed: {:?}", err); - } - - if let Err(SwapFailed::AfterBtcLock(_)) = swap_result { - let tx_cancel = - bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public()); - let tx_cancel_txid = tx_cancel.txid(); - let signed_tx_cancel = { - let sig_a = tx_cancel_sig_a.clone(); - let sig_b = b.sign(tx_cancel.digest()); - - tx_cancel - .clone() - .add_signatures(&tx_lock, (A.clone(), sig_a), (b.public(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel") - }; - - co.yield_(Action::CancelBtc(signed_tx_cancel)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_cancel_txid) - .await; - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); - let tx_refund_txid = tx_refund.txid(); - let signed_tx_refund = { - let adaptor = Adaptor::>::default(); - - let sig_a = - adaptor.decrypt_signature(&s_b.into_secp256k1(), tx_refund_encsig.clone()); - let sig_b = b.sign(tx_refund.digest()); - - tx_refund - .add_signatures(&tx_cancel, (A.clone(), sig_a), (b.public(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_refund") - }; - - co.yield_(Action::RefundBtc(signed_tx_refund)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_refund_txid) - .await; - } - }) -} - // There are no guarantees that send_message and receive_massage do not block // the flow of execution. Therefore they must be paired between Alice/Bob, one // send to one receive in the correct order. diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index 459b368e..db454545 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -1,32 +1,11 @@ -pub mod harness; - -use anyhow::Result; use async_trait::async_trait; -use futures::{ - channel::mpsc::{channel, Receiver, Sender}, - future::{select, try_join}, - pin_mut, SinkExt, StreamExt, -}; -use genawaiter::GeneratorState; -use harness::{ - init_bitcoind, init_test, - node::{run_alice_until, run_bob_until}, -}; -use monero_harness::Monero; -use rand::rngs::OsRng; -use std::{convert::TryInto, sync::Arc}; -use testcontainers::clients::Cli; -use tokio::sync::Mutex; -use tracing::info; +use futures::{channel::mpsc::Receiver, StreamExt}; use xmr_btc::{ - alice::{self, ReceiveBitcoinRedeemEncsig}, - bitcoin::{self, BroadcastSignedTransaction, EncryptedSignature, SignTxLock}, - bob::{self, ReceiveTransferProof}, - monero::{CreateWalletForOutput, Transfer, TransferProof}, + alice::ReceiveBitcoinRedeemEncsig, bitcoin::EncryptedSignature, bob::ReceiveTransferProof, + monero::TransferProof, }; -/// Time given to Bob to get the Bitcoin lock transaction included in a block. -const BITCOIN_TX_LOCK_TIMEOUT: u64 = 5; +pub mod harness; type AliceNetwork = Network; type BobNetwork = Network; @@ -38,14 +17,6 @@ struct Network { pub receiver: Receiver, } -impl Network { - pub fn new() -> (Network, Sender) { - let (sender, receiver) = channel(1); - - (Self { receiver }, sender) - } -} - #[async_trait] impl ReceiveTransferProof for BobNetwork { async fn receive_transfer_proof(&mut self) -> TransferProof { @@ -59,463 +30,3 @@ impl ReceiveBitcoinRedeemEncsig for AliceNetwork { self.receiver.next().await.unwrap() } } - -struct AliceBehaviour { - lock_xmr: bool, - redeem_btc: bool, - cancel_btc: bool, - punish_btc: bool, - create_monero_wallet_for_output: bool, -} - -impl Default for AliceBehaviour { - fn default() -> Self { - Self { - lock_xmr: true, - redeem_btc: true, - cancel_btc: true, - punish_btc: true, - create_monero_wallet_for_output: true, - } - } -} - -struct BobBehaviour { - lock_btc: bool, - send_btc_redeem_encsig: bool, - create_monero_wallet_for_output: bool, - cancel_btc: bool, - refund_btc: bool, -} - -impl Default for BobBehaviour { - fn default() -> Self { - Self { - lock_btc: true, - send_btc_redeem_encsig: true, - create_monero_wallet_for_output: true, - cancel_btc: true, - refund_btc: true, - } - } -} - -async fn swap_as_alice( - network: Arc>, - // FIXME: It would be more intuitive to have a single network/transport struct instead of - // splitting into two, but Rust ownership rules make this tedious - mut sender: Sender, - monero_wallet: Arc, - bitcoin_wallet: Arc, - behaviour: AliceBehaviour, - state: alice::State3, -) -> Result<()> { - let mut action_generator = alice::action_generator( - network, - bitcoin_wallet.clone(), - state, - BITCOIN_TX_LOCK_TIMEOUT, - ); - - loop { - let state = action_generator.async_resume().await; - - info!("resumed execution of alice generator, got: {:?}", state); - - match state { - GeneratorState::Yielded(alice::Action::LockXmr { - amount, - public_spend_key, - public_view_key, - }) => { - if behaviour.lock_xmr { - let (transfer_proof, _) = monero_wallet - .transfer(public_spend_key, public_view_key, amount) - .await?; - - sender.send(transfer_proof).await?; - } - } - GeneratorState::Yielded(alice::Action::RedeemBtc(tx)) => { - if behaviour.redeem_btc { - let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; - } - } - GeneratorState::Yielded(alice::Action::CancelBtc(tx)) => { - if behaviour.cancel_btc { - let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; - } - } - GeneratorState::Yielded(alice::Action::PunishBtc(tx)) => { - if behaviour.punish_btc { - let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; - } - } - GeneratorState::Yielded(alice::Action::CreateMoneroWalletForOutput { - spend_key, - view_key, - }) => { - if behaviour.create_monero_wallet_for_output { - monero_wallet - .create_and_load_wallet_for_output(spend_key, view_key) - .await?; - } - } - GeneratorState::Complete(()) => return Ok(()), - } - } -} - -async fn swap_as_bob( - network: Arc>, - mut sender: Sender, - monero_wallet: Arc, - bitcoin_wallet: Arc, - behaviour: BobBehaviour, - state: bob::State2, -) -> Result<()> { - let mut action_generator = bob::action_generator( - network, - monero_wallet.clone(), - bitcoin_wallet.clone(), - state, - BITCOIN_TX_LOCK_TIMEOUT, - ); - - loop { - let state = action_generator.async_resume().await; - - info!("resumed execution of bob generator, got: {:?}", state); - - match state { - GeneratorState::Yielded(bob::Action::LockBtc(tx_lock)) => { - if behaviour.lock_btc { - let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?; - let _ = bitcoin_wallet - .broadcast_signed_transaction(signed_tx_lock) - .await?; - } - } - GeneratorState::Yielded(bob::Action::SendBtcRedeemEncsig(tx_redeem_encsig)) => { - if behaviour.send_btc_redeem_encsig { - sender.send(tx_redeem_encsig).await.unwrap(); - } - } - GeneratorState::Yielded(bob::Action::CreateXmrWalletForOutput { - spend_key, - view_key, - }) => { - if behaviour.create_monero_wallet_for_output { - monero_wallet - .create_and_load_wallet_for_output(spend_key, view_key) - .await?; - } - } - GeneratorState::Yielded(bob::Action::CancelBtc(tx_cancel)) => { - if behaviour.cancel_btc { - let _ = bitcoin_wallet - .broadcast_signed_transaction(tx_cancel) - .await?; - } - } - GeneratorState::Yielded(bob::Action::RefundBtc(tx_refund)) => { - if behaviour.refund_btc { - let _ = bitcoin_wallet - .broadcast_signed_transaction(tx_refund) - .await?; - } - } - GeneratorState::Complete(()) => return Ok(()), - } - } -} - -#[tokio::test] -async fn on_chain_happy_path() { - let cli = Cli::default(); - let (monero, _container) = Monero::new(&cli, Some("ochp".to_string()), vec![ - "alice".to_string(), - "bob".to_string(), - ]) - .await - .unwrap(); - let bitcoind = init_bitcoind(&cli).await; - - let (alice_state0, bob_state0, mut alice_node, mut bob_node, initial_balances, swap_amounts) = - init_test(&monero, &bitcoind, Some(100), Some(100)).await; - - // run the handshake as part of the setup - let (alice_state, bob_state) = try_join( - run_alice_until( - &mut alice_node, - alice_state0.into(), - harness::alice::is_state3, - &mut OsRng, - ), - run_bob_until( - &mut bob_node, - bob_state0.into(), - harness::bob::is_state2, - &mut OsRng, - ), - ) - .await - .unwrap(); - let alice: alice::State3 = alice_state.try_into().unwrap(); - let bob: bob::State2 = bob_state.try_into().unwrap(); - let tx_lock_txid = bob.tx_lock.txid(); - - let alice_bitcoin_wallet = Arc::new(alice_node.bitcoin_wallet); - let bob_bitcoin_wallet = Arc::new(bob_node.bitcoin_wallet); - let alice_monero_wallet = Arc::new(alice_node.monero_wallet); - let bob_monero_wallet = Arc::new(bob_node.monero_wallet); - - let (alice_network, bob_sender) = Network::::new(); - let (bob_network, alice_sender) = Network::::new(); - - try_join( - swap_as_alice( - Arc::new(Mutex::new(alice_network)), - alice_sender, - alice_monero_wallet.clone(), - alice_bitcoin_wallet.clone(), - AliceBehaviour::default(), - alice, - ), - swap_as_bob( - Arc::new(Mutex::new(bob_network)), - bob_sender, - bob_monero_wallet.clone(), - bob_bitcoin_wallet.clone(), - BobBehaviour::default(), - bob, - ), - ) - .await - .unwrap(); - - let alice_final_btc_balance = alice_bitcoin_wallet.balance().await.unwrap(); - let bob_final_btc_balance = bob_bitcoin_wallet.balance().await.unwrap(); - - let lock_tx_bitcoin_fee = bob_bitcoin_wallet - .transaction_fee(tx_lock_txid) - .await - .unwrap(); - - let alice_final_xmr_balance = alice_monero_wallet.get_balance().await.unwrap(); - - monero.wallet("bob").unwrap().refresh().await.unwrap(); - let bob_final_xmr_balance = bob_monero_wallet.get_balance().await.unwrap(); - - assert_eq!( - alice_final_btc_balance, - initial_balances.alice_btc + swap_amounts.btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE) - ); - assert_eq!( - bob_final_btc_balance, - initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee - ); - - // Getting the Monero LockTx fee is tricky in a clean way, I think checking this - // condition is sufficient - assert!(alice_final_xmr_balance <= initial_balances.alice_xmr - swap_amounts.xmr); - assert_eq!( - bob_final_xmr_balance, - initial_balances.bob_xmr + swap_amounts.xmr - ); -} - -#[tokio::test] -async fn on_chain_both_refund_if_alice_never_redeems() { - let cli = Cli::default(); - let (monero, _container) = Monero::new(&cli, Some("ocbr".to_string()), vec![ - "alice".to_string(), - "bob".to_string(), - ]) - .await - .unwrap(); - let bitcoind = init_bitcoind(&cli).await; - - let (alice_state0, bob_state0, mut alice_node, mut bob_node, initial_balances, swap_amounts) = - init_test(&monero, &bitcoind, Some(10), Some(10)).await; - - // run the handshake as part of the setup - let (alice_state, bob_state) = try_join( - run_alice_until( - &mut alice_node, - alice_state0.into(), - harness::alice::is_state3, - &mut OsRng, - ), - run_bob_until( - &mut bob_node, - bob_state0.into(), - harness::bob::is_state2, - &mut OsRng, - ), - ) - .await - .unwrap(); - let alice: alice::State3 = alice_state.try_into().unwrap(); - let bob: bob::State2 = bob_state.try_into().unwrap(); - let tx_lock_txid = bob.tx_lock.txid(); - - let alice_bitcoin_wallet = Arc::new(alice_node.bitcoin_wallet); - let bob_bitcoin_wallet = Arc::new(bob_node.bitcoin_wallet); - let alice_monero_wallet = Arc::new(alice_node.monero_wallet); - let bob_monero_wallet = Arc::new(bob_node.monero_wallet); - - let (alice_network, bob_sender) = Network::::new(); - let (bob_network, alice_sender) = Network::::new(); - - try_join( - swap_as_alice( - Arc::new(Mutex::new(alice_network)), - alice_sender, - alice_monero_wallet.clone(), - alice_bitcoin_wallet.clone(), - AliceBehaviour { - redeem_btc: false, - ..Default::default() - }, - alice, - ), - swap_as_bob( - Arc::new(Mutex::new(bob_network)), - bob_sender, - bob_monero_wallet.clone(), - bob_bitcoin_wallet.clone(), - BobBehaviour::default(), - bob, - ), - ) - .await - .unwrap(); - - let alice_final_btc_balance = alice_bitcoin_wallet.balance().await.unwrap(); - let bob_final_btc_balance = bob_bitcoin_wallet.balance().await.unwrap(); - - let lock_tx_bitcoin_fee = bob_bitcoin_wallet - .transaction_fee(tx_lock_txid) - .await - .unwrap(); - - monero.wallet("alice").unwrap().refresh().await.unwrap(); - let alice_final_xmr_balance = alice_monero_wallet.get_balance().await.unwrap(); - - let bob_final_xmr_balance = bob_monero_wallet.get_balance().await.unwrap(); - - assert_eq!(alice_final_btc_balance, initial_balances.alice_btc); - assert_eq!( - bob_final_btc_balance, - // The 2 * TX_FEE corresponds to tx_refund and tx_cancel. - initial_balances.bob_btc - - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) - - lock_tx_bitcoin_fee - ); - - // Because we create a new wallet when claiming Monero, we can only assert on - // this new wallet owning all of `xmr_amount` after refund - assert_eq!(alice_final_xmr_balance, swap_amounts.xmr); - assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr); -} - -#[tokio::test] -async fn on_chain_alice_punishes_if_bob_never_acts_after_fund() { - let cli = Cli::default(); - let (monero, _container) = Monero::new(&cli, Some("ocap".to_string()), vec![ - "alice".to_string(), - "bob".to_string(), - ]) - .await - .unwrap(); - let bitcoind = init_bitcoind(&cli).await; - - let (alice_state0, bob_state0, mut alice_node, mut bob_node, initial_balances, swap_amounts) = - init_test(&monero, &bitcoind, Some(10), Some(10)).await; - - // run the handshake as part of the setup - let (alice_state, bob_state) = try_join( - run_alice_until( - &mut alice_node, - alice_state0.into(), - harness::alice::is_state3, - &mut OsRng, - ), - run_bob_until( - &mut bob_node, - bob_state0.into(), - harness::bob::is_state2, - &mut OsRng, - ), - ) - .await - .unwrap(); - let alice: alice::State3 = alice_state.try_into().unwrap(); - let bob: bob::State2 = bob_state.try_into().unwrap(); - let tx_lock_txid = bob.tx_lock.txid(); - - let alice_bitcoin_wallet = Arc::new(alice_node.bitcoin_wallet); - let bob_bitcoin_wallet = Arc::new(bob_node.bitcoin_wallet); - let alice_monero_wallet = Arc::new(alice_node.monero_wallet); - let bob_monero_wallet = Arc::new(bob_node.monero_wallet); - - let (alice_network, bob_sender) = Network::::new(); - let (bob_network, alice_sender) = Network::::new(); - - let alice_swap = swap_as_alice( - Arc::new(Mutex::new(alice_network)), - alice_sender, - alice_monero_wallet.clone(), - alice_bitcoin_wallet.clone(), - AliceBehaviour::default(), - alice, - ); - let bob_swap = swap_as_bob( - Arc::new(Mutex::new(bob_network)), - bob_sender, - bob_monero_wallet.clone(), - bob_bitcoin_wallet.clone(), - BobBehaviour { - send_btc_redeem_encsig: false, - create_monero_wallet_for_output: false, - cancel_btc: false, - refund_btc: false, - ..Default::default() - }, - bob, - ); - - pin_mut!(alice_swap); - pin_mut!(bob_swap); - - // since we model Bob as inactive after locking bitcoin, his future does not - // resolve, so we wait for one of the two (Alice's) to resolve via select - select(alice_swap, bob_swap).await; - - let alice_final_btc_balance = alice_bitcoin_wallet.balance().await.unwrap(); - let bob_final_btc_balance = bob_bitcoin_wallet.balance().await.unwrap(); - - let lock_tx_bitcoin_fee = bob_bitcoin_wallet - .transaction_fee(tx_lock_txid) - .await - .unwrap(); - - let alice_final_xmr_balance = alice_monero_wallet.get_balance().await.unwrap(); - let bob_final_xmr_balance = bob_monero_wallet.get_balance().await.unwrap(); - - assert_eq!( - alice_final_btc_balance, - initial_balances.alice_btc + swap_amounts.btc - - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) - ); - assert_eq!( - bob_final_btc_balance, - initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee - ); - - // Getting the Monero LockTx fee is tricky in a clean way, I think checking this - // condition is sufficient - assert!(alice_final_xmr_balance <= initial_balances.alice_xmr - swap_amounts.xmr,); - assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr); -}