From dd07e2f8828c369ae85e27710cb2577cf0092ece Mon Sep 17 00:00:00 2001 From: rishflab Date: Thu, 12 Nov 2020 11:06:34 +1100 Subject: [PATCH 01/27] Add Alice execution path Consolidate and simplify swap execution. Generators are no longer needed. Consolidate recovery and swap data structures. The recursive calls can be replaced with a loop if returning prior to completion is desired for testing purposes. Fill out alice abort path Move state machine executors into seperate files Not compiling due to recursion/async issues Fix async recursion compilation errors Fix Bob swap execution Remove check for ack message from Alice. Seems like a bad idea to rely on an acknowledgement message instead of looking at the blockchain. Fix Bob abort Fix warnings Xmr lock complete Add TxCancel submit to XmrLocked Bob swap completed Remove alice --- swap/Cargo.toml | 1 + swap/src/alice.rs | 461 ------------------------------ swap/src/bin/simple_swap.rs | 30 ++ swap/src/{main.rs => bin/swap.rs} | 11 +- swap/src/bitcoin.rs | 7 + swap/src/bob.rs | 43 +-- swap/src/bob_simple.rs | 304 ++++++++++++++++++++ swap/src/lib.rs | 3 +- xmr-btc/src/bitcoin.rs | 5 + xmr-btc/src/bob.rs | 183 ++++++++++-- 10 files changed, 538 insertions(+), 510 deletions(-) delete mode 100644 swap/src/alice.rs create mode 100644 swap/src/bin/simple_swap.rs rename swap/src/{main.rs => bin/swap.rs} (98%) create mode 100644 swap/src/bob_simple.rs diff --git a/swap/Cargo.toml b/swap/Cargo.toml index e1371f91..bf5fa649 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -8,6 +8,7 @@ description = "XMR/BTC trustless atomic swaps." [dependencies] anyhow = "1" async-trait = "0.1" +async-recursion = "0.3.1" atty = "0.2" backoff = { version = "0.2", features = ["tokio"] } base64 = "0.12" diff --git a/swap/src/alice.rs b/swap/src/alice.rs deleted file mode 100644 index 52ae7cc4..00000000 --- a/swap/src/alice.rs +++ /dev/null @@ -1,461 +0,0 @@ -//! Run an XMR/BTC swap in the role of Alice. -//! Alice holds XMR and wishes receive BTC. -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; - -mod amounts; -mod message0; -mod message1; -mod message2; -mod message3; - -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, -}; -use xmr_btc::{ - alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0}, - bitcoin::BroadcastSignedTransaction, - bob, - monero::{CreateWalletForOutput, Transfer}, -}; - -pub async fn swap( - bitcoin_wallet: Arc, - monero_wallet: Arc, - db: Database, - listen: Multiaddr, - transport: SwapTransport, - behaviour: Alice, -) -> 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 state = State0::new( - rng, - 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::Handshaken(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; - -fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> Result { - use anyhow::Context as _; - - let local_peer_id = behaviour.peer_id(); - - let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) - .executor(Box::new(TokioExecutor { - handle: tokio::runtime::Handle::current(), - })) - .build(); - - Swarm::listen_on(&mut swarm, listen.clone()) - .with_context(|| format!("Address is not supported: {:#}", listen))?; - - tracing::info!("Initialized swarm: {}", local_peer_id); - - Ok(swarm) -} - -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] -pub enum OutEvent { - ConnectionEstablished(PeerId), - Request(amounts::OutEvent), // Not-uniform with Bob on purpose, ready for adding Xmr event. - Message0(bob::Message0), - Message1 { - msg: bob::Message1, - channel: ResponseChannel, - }, - Message2 { - msg: bob::Message2, - channel: ResponseChannel, - }, - Message3(bob::Message3), -} - -impl From for OutEvent { - fn from(event: peer_tracker::OutEvent) -> Self { - match event { - peer_tracker::OutEvent::ConnectionEstablished(id) => { - OutEvent::ConnectionEstablished(id) - } - } - } -} - -impl From for OutEvent { - fn from(event: amounts::OutEvent) -> Self { - OutEvent::Request(event) - } -} - -impl From for OutEvent { - fn from(event: message0::OutEvent) -> Self { - match event { - message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), - } - } -} - -impl From for OutEvent { - fn from(event: message1::OutEvent) -> Self { - match event { - message1::OutEvent::Msg { msg, channel } => OutEvent::Message1 { msg, channel }, - } - } -} - -impl From for OutEvent { - fn from(event: message2::OutEvent) -> Self { - match event { - message2::OutEvent::Msg { msg, channel } => OutEvent::Message2 { msg, channel }, - } - } -} - -impl From for OutEvent { - fn from(event: message3::OutEvent) -> Self { - match event { - message3::OutEvent::Msg(msg) => OutEvent::Message3(msg), - } - } -} - -/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice. -#[derive(NetworkBehaviour)] -#[behaviour(out_event = "OutEvent", event_process = false)] -#[allow(missing_debug_implementations)] -pub struct Alice { - pt: PeerTracker, - amounts: Amounts, - message0: Message0, - message1: Message1, - message2: Message2, - message3: Message3, - #[behaviour(ignore)] - identity: Keypair, -} - -impl Alice { - pub fn identity(&self) -> Keypair { - self.identity.clone() - } - - pub fn peer_id(&self) -> PeerId { - PeerId::from(self.identity.public()) - } - - /// Alice always sends her messages as a response to a request from Bob. - pub fn send_amounts(&mut self, channel: ResponseChannel, amounts: SwapAmounts) { - let msg = AliceToBob::Amounts(amounts); - self.amounts.send(channel, msg); - info!("Sent amounts response"); - } - - /// Message0 gets sent within the network layer using this state0. - pub fn set_state0(&mut self, state: State0) { - debug!("Set state 0"); - let _ = self.message0.set_state(state); - } - - /// Send Message1 to Bob in response to receiving his Message1. - pub fn send_message1( - &mut self, - channel: ResponseChannel, - msg: xmr_btc::alice::Message1, - ) { - self.message1.send(channel, msg); - debug!("Sent Message1"); - } - - /// Send Message2 to Bob in response to receiving his Message2. - pub fn send_message2( - &mut self, - channel: ResponseChannel, - msg: xmr_btc::alice::Message2, - ) { - self.message2.send(channel, msg); - debug!("Sent Message2"); - } -} - -impl Default for Alice { - fn default() -> Self { - let identity = Keypair::generate_ed25519(); - - Self { - pt: PeerTracker::default(), - amounts: Amounts::default(), - message0: Message0::default(), - message1: Message1::default(), - message2: Message2::default(), - message3: Message3::default(), - identity, - } - } -} - -fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapAmounts { - // TODO: Get this from an exchange. - // 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/simple_swap.rs b/swap/src/bin/simple_swap.rs new file mode 100644 index 00000000..cc3ed027 --- /dev/null +++ b/swap/src/bin/simple_swap.rs @@ -0,0 +1,30 @@ +use anyhow::Result; +use structopt::StructOpt; +use swap::{ + bob_simple::{simple_swap, BobState}, + cli::Options, + storage::Database, +}; + +#[tokio::main] +async fn main() -> Result<()> { + let opt = Options::from_args(); + + let db = Database::open(std::path::Path::new("./.swap-db/")).unwrap(); + let swarm = unimplemented!(); + let bitcoin_wallet = unimplemented!(); + let monero_wallet = unimplemented!(); + let mut rng = unimplemented!(); + let bob_state = unimplemented!(); + + match opt { + Options::Alice { .. } => { + simple_swap(bob_state, swarm, db, bitcoin_wallet, monero_wallet, rng).await?; + } + Options::Recover { .. } => { + let _stored_state: BobState = unimplemented!("io.get_state(uuid)?"); + // abort(_stored_state, _io); + } + _ => {} + }; +} diff --git a/swap/src/main.rs b/swap/src/bin/swap.rs similarity index 98% rename from swap/src/main.rs rename to swap/src/bin/swap.rs index afdf110c..b9a9e45f 100644 --- a/swap/src/main.rs +++ b/swap/src/bin/swap.rs @@ -15,7 +15,6 @@ use anyhow::Result; use futures::{channel::mpsc, StreamExt}; use libp2p::Multiaddr; -use log::LevelFilter; use prettytable::{row, Table}; use std::{io, io::Write, process, sync::Arc}; use structopt::StructOpt; @@ -23,9 +22,11 @@ use swap::{ alice::{self, Alice}, bitcoin, bob::{self, Bob}, + cli::Options, monero, network::transport::{build, build_tor, SwapTransport}, recover::recover, + storage::Database, Cmd, Rsp, SwapAmounts, }; use tracing::info; @@ -33,20 +34,12 @@ use tracing::info; #[macro_use] extern crate prettytable; -mod cli; -mod trace; - -use cli::Options; -use swap::storage::Database; - // TODO: Add root seed file instead of generating new seed each run. #[tokio::main] async fn main() -> Result<()> { let opt = Options::from_args(); - trace::init_tracing(LevelFilter::Debug)?; - // This currently creates the directory if it's not there in the first place let db = Database::open(std::path::Path::new("./.swap-db/")).unwrap(); diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 15c1e76b..7d5eb916 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -110,6 +110,13 @@ impl WatchForRawTransaction for Wallet { } } +#[async_trait] +impl GetRawTransaction for Wallet { + async fn get_raw_transaction(&self, txid: Txid) -> Result { + Ok(self.0.get_raw_transaction(txid).await?) + } +} + #[async_trait] impl BlockHeight for Wallet { async fn block_height(&self) -> u32 { diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 3b5c5936..9651d12f 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -1,5 +1,18 @@ //! Run an XMR/BTC swap in the role of Bob. //! Bob holds BTC and wishes receive XMR. +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 anyhow::Result; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; @@ -14,26 +27,6 @@ use std::{process, sync::Arc, time::Duration}; use tokio::sync::Mutex; use tracing::{debug, info, warn}; use uuid::Uuid; - -mod amounts; -mod message0; -mod message1; -mod message2; -mod message3; - -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}, @@ -41,6 +34,12 @@ use xmr_btc::{ monero::CreateWalletForOutput, }; +mod amounts; +mod message0; +mod message1; +mod message2; +mod message3; + #[allow(clippy::too_many_arguments)] pub async fn swap( bitcoin_wallet: Arc, @@ -98,6 +97,9 @@ pub async fn swap( swarm.request_amounts(alice.clone(), btc); + // What is going on here, shouldn't this be a simple req/resp?? + // Why do we need mspc channels? + // Todo: simplify this code let (btc, xmr) = match swarm.next().await { OutEvent::Amounts(amounts) => { info!("Got amounts from Alice: {:?}", amounts); @@ -108,7 +110,6 @@ pub async fn swap( info!("User rejected amounts proposed by Alice, aborting..."); process::exit(0); } - info!("User accepted amounts proposed by Alice"); (amounts.btc, amounts.xmr) } diff --git a/swap/src/bob_simple.rs b/swap/src/bob_simple.rs new file mode 100644 index 00000000..1f0a0e75 --- /dev/null +++ b/swap/src/bob_simple.rs @@ -0,0 +1,304 @@ +use crate::{ + bitcoin::{self}, + bob::{OutEvent, Swarm}, + network::{transport::SwapTransport, TokioExecutor}, + state, + storage::Database, + Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, +}; +use anyhow::Result; +use async_recursion::async_recursion; +use async_trait::async_trait; +use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; +use futures::{ + channel::mpsc::{Receiver, Sender}, + future::Either, + FutureExt, StreamExt, +}; +use genawaiter::GeneratorState; +use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; +use rand::{rngs::OsRng, CryptoRng, RngCore}; +use std::{process, sync::Arc, time::Duration}; +use tokio::sync::Mutex; +use tracing::{debug, info, warn}; +use uuid::Uuid; +use xmr_btc::{ + alice, + bitcoin::{ + poll_until_block_height_is_gte, BroadcastSignedTransaction, EncryptedSignature, SignTxLock, + TransactionBlockHeight, + }, + bob::{self, action_generator, ReceiveTransferProof, State0}, + monero::CreateWalletForOutput, +}; + +// The same data structure is used for swap execution and recovery. +// This allows for a seamless transition from a failed swap to recovery. +pub enum BobState { + Started(Sender, Receiver, u64, PeerId), + Negotiated(bob::State2, PeerId), + BtcLocked(bob::State3, PeerId), + XmrLocked(bob::State4, PeerId), + EncSigSent(bob::State4, PeerId), + BtcRedeemed(bob::State5), + Cancelled(bob::State4), + BtcRefunded, + XmrRedeemed, + Punished, + SafelyAborted, +} + +// State machine driver for swap execution +#[async_recursion] +pub async fn simple_swap( + state: BobState, + mut swarm: Swarm, + db: Database, + bitcoin_wallet: Arc, + monero_wallet: Arc, + mut rng: OsRng, +) -> Result { + match state { + BobState::Started(mut cmd_tx, mut rsp_rx, btc, alice_peer_id) => { + // todo: dial the swarm outside + // 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); + + // todo: remove mspc channel + 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?; + + let state0 = State0::new( + &mut rng, + btc, + xmr, + REFUND_TIMELOCK, + PUNISH_TIMELOCK, + refund_address, + ); + + info!("Commencing handshake"); + + swarm.send_message0(alice.clone(), state0.next_message(&mut 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)?, + 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"); + + simple_swap( + BobState::Negotiated(state2, alice_peer_id), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + ) + .await + } + BobState::Negotiated(state2, alice_peer_id) => { + // Alice and Bob have exchanged info + let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; + simple_swap( + BobState::BtcLocked(state3, alice_peer_id), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + ) + .await + } + // Bob has locked Btc + // Watch for Alice to Lock Xmr or for t1 to elapse + BobState::BtcLocked(state3, alice_peer_id) => { + // todo: watch until t1, not indefinetely + let state4 = match swarm.next().await { + OutEvent::Message2(msg) => { + state3 + .watch_for_lock_xmr(monero_wallet.as_ref(), msg) + .await? + } + other => panic!("unexpected event: {:?}", other), + }; + simple_swap( + BobState::XmrLocked(state4, alice_peer_id), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + ) + .await + } + BobState::XmrLocked(state, alice_peer_id) => { + // Alice has locked Xmr + // Bob sends Alice his key + // let cloned = state.clone(); + let tx_redeem_encsig = state.tx_redeem_encsig(); + // Do we have to wait for a response? + // What if Alice fails to receive this? Should we always resend? + // todo: If we cannot dial Alice we should go to EncSigSent. Maybe dialing + // should happen in this arm? + swarm.send_message3(alice_peer_id.clone(), tx_redeem_encsig); + + simple_swap( + BobState::EncSigSent(state, alice_peer_id), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + ) + .await + } + BobState::EncSigSent(state, ..) => { + // Watch for redeem + let redeem_watcher = state.watch_for_redeem_btc(bitcoin_wallet.as_ref()); + let t1_timeout = state.wait_for_t1(bitcoin_wallet.as_ref()); + + tokio::select! { + val = redeem_watcher => { + simple_swap( + BobState::BtcRedeemed(val?), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + ) + .await + } + val = t1_timeout => { + // Check whether TxCancel has been published. + // We should not fail if the transaction is already on the blockchain + if let Err(_e) = state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await { + state.submit_tx_cancel(bitcoin_wallet.as_ref()).await; + } + + simple_swap( + BobState::Cancelled(state), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + ) + .await + + } + } + } + BobState::BtcRedeemed(state) => { + // Bob redeems XMR using revealed s_a + state.claim_xmr(monero_wallet.as_ref()).await?; + simple_swap( + BobState::XmrRedeemed, + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + ) + .await + } + BobState::Cancelled(_state) => Ok(BobState::BtcRefunded), + BobState::BtcRefunded => Ok(BobState::BtcRefunded), + BobState::Punished => Ok(BobState::Punished), + BobState::SafelyAborted => Ok(BobState::SafelyAborted), + BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), + } +} +// // State machine driver for recovery execution +// #[async_recursion] +// pub async fn abort(state: BobState, io: Io) -> Result { +// match state { +// BobState::Started => { +// // Nothing has been commited by either party, abort swap. +// abort(BobState::SafelyAborted, io).await +// } +// BobState::Negotiated => { +// // Nothing has been commited by either party, abort swap. +// abort(BobState::SafelyAborted, io).await +// } +// BobState::BtcLocked => { +// // Bob has locked BTC and must refund it +// // Bob waits for alice to publish TxRedeem or t1 +// if unimplemented!("TxRedeemSeen") { +// // Alice has redeemed revealing s_a +// abort(BobState::BtcRedeemed, io).await +// } else if unimplemented!("T1Elapsed") { +// // publish TxCancel or see if it has been published +// abort(BobState::Cancelled, io).await +// } else { +// Err(unimplemented!()) +// } +// } +// BobState::XmrLocked => { +// // Alice has locked Xmr +// // Wait until t1 +// if unimplemented!(">t1 and t2 +// // submit TxCancel +// abort(BobState::Punished, io).await +// } +// } +// BobState::Cancelled => { +// // Bob has cancelled the swap +// // If { +// // Bob uses revealed s_a to redeem XMR +// abort(BobState::XmrRedeemed, io).await +// } +// BobState::BtcRefunded => Ok(BobState::BtcRefunded), +// BobState::Punished => Ok(BobState::Punished), +// BobState::SafelyAborted => Ok(BobState::SafelyAborted), +// BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), +// } +// } diff --git a/swap/src/lib.rs b/swap/src/lib.rs index cdc8673f..e9003112 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -1,9 +1,10 @@ use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; -pub mod alice; pub mod bitcoin; pub mod bob; +pub mod bob_simple; +pub mod cli; pub mod monero; pub mod network; pub mod recover; diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index a095d64f..95b06ef2 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -186,6 +186,11 @@ pub trait WatchForRawTransaction { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction; } +#[async_trait] +pub trait GetRawTransaction { + async fn get_raw_transaction(&self, txid: Txid) -> Result; +} + #[async_trait] pub trait BlockHeight { async fn block_height(&self) -> u32; diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index e9b98ac8..b0e4b4f9 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -32,7 +32,11 @@ use tokio::{sync::Mutex, time::timeout}; use tracing::error; pub mod message; -use crate::monero::{CreateWalletForOutput, WatchForTransfer}; +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)] @@ -679,23 +683,23 @@ impl State3 { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct State4 { - A: bitcoin::PublicKey, - b: bitcoin::SecretKey, + pub A: bitcoin::PublicKey, + pub b: bitcoin::SecretKey, s_b: cross_curve_dleq::Scalar, S_a_monero: monero::PublicKey, - S_a_bitcoin: bitcoin::PublicKey, + pub S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, #[serde(with = "::bitcoin::util::amount::serde::as_sat")] btc: bitcoin::Amount, xmr: monero::Amount, - refund_timelock: u32, + pub refund_timelock: u32, punish_timelock: u32, refund_address: bitcoin::Address, - redeem_address: bitcoin::Address, + pub redeem_address: bitcoin::Address, punish_address: bitcoin::Address, - tx_lock: bitcoin::TxLock, + pub tx_lock: bitcoin::TxLock, tx_cancel_sig_a: Signature, tx_refund_encsig: EncryptedSignature, } @@ -708,7 +712,77 @@ impl State4 { Message3 { tx_redeem_encsig } } - pub async fn watch_for_redeem_btc(self, bitcoin_wallet: &W) -> Result + pub fn tx_redeem_encsig(&self) -> EncryptedSignature { + let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); + self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest()) + } + + pub async fn check_for_tx_cancel(&self, bitcoin_wallet: &W) -> Result + where + W: GetRawTransaction, + { + let tx_cancel = bitcoin::TxCancel::new( + &self.tx_lock, + self.refund_timelock, + self.A.clone(), + self.b.public(), + ); + + // todo: check if this is correct + let sig_a = self.tx_cancel_sig_a.clone(); + let sig_b = self.b.sign(tx_cancel.digest()); + + let tx_cancel = tx_cancel + .clone() + .add_signatures( + &self.tx_lock, + (self.A.clone(), sig_a), + (self.b.public(), sig_b), + ) + .expect( + "sig_{a,b} to be valid signatures for + tx_cancel", + ); + + let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?; + + Ok(tx) + } + + pub async fn submit_tx_cancel(&self, bitcoin_wallet: &W) -> Result + where + W: BroadcastSignedTransaction, + { + let tx_cancel = bitcoin::TxCancel::new( + &self.tx_lock, + self.refund_timelock, + self.A.clone(), + self.b.public(), + ); + + // todo: check if this is correct + let sig_a = self.tx_cancel_sig_a.clone(); + let sig_b = self.b.sign(tx_cancel.digest()); + + let tx_cancel = tx_cancel + .clone() + .add_signatures( + &self.tx_lock, + (self.A.clone(), sig_a), + (self.b.public(), sig_b), + ) + .expect( + "sig_{a,b} to be valid signatures for + tx_cancel", + ); + + let tx_id = bitcoin_wallet + .broadcast_signed_transaction(tx_cancel) + .await?; + Ok(tx_id) + } + + pub async fn watch_for_redeem_btc(&self, bitcoin_wallet: &W) -> Result where W: WatchForRawTransaction, { @@ -725,25 +799,38 @@ impl State4 { let s_a = monero::private_key_from_secp256k1_scalar(s_a.into()); Ok(State5 { - A: self.A, - b: self.b, + A: self.A.clone(), + b: self.b.clone(), s_a, s_b: self.s_b, S_a_monero: self.S_a_monero, - S_a_bitcoin: self.S_a_bitcoin, + S_a_bitcoin: self.S_a_bitcoin.clone(), v: self.v, btc: self.btc, xmr: self.xmr, refund_timelock: self.refund_timelock, punish_timelock: self.punish_timelock, - refund_address: self.refund_address, - redeem_address: self.redeem_address, - punish_address: self.punish_address, - tx_lock: self.tx_lock, - tx_refund_encsig: self.tx_refund_encsig, - tx_cancel_sig: self.tx_cancel_sig_a, + refund_address: self.refund_address.clone(), + redeem_address: self.redeem_address.clone(), + punish_address: self.punish_address.clone(), + tx_lock: self.tx_lock.clone(), + tx_refund_encsig: self.tx_refund_encsig.clone(), + tx_cancel_sig: self.tx_cancel_sig_a.clone(), }) } + + pub async fn wait_for_t1(&self, bitcoin_wallet: &W) -> Result<()> + where + W: WatchForRawTransaction + TransactionBlockHeight + BlockHeight, + { + let tx_id = self.tx_lock.txid().clone(); + let tx_lock_height = bitcoin_wallet.transaction_block_height(tx_id).await; + + let t1_timeout = + poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + self.refund_timelock); + t1_timeout.await; + Ok(()) + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -792,3 +879,63 @@ impl State5 { self.tx_lock.txid() } } + +/// Watch for the refund transaction on the blockchain. Watch until t2 has +/// elapsed. +pub async fn watch_for_refund_btc(state: State5, bitcoin_wallet: &W) -> Result<()> +where + W: WatchForRawTransaction, +{ + let tx_cancel = bitcoin::TxCancel::new( + &state.tx_lock, + state.refund_timelock, + state.A.clone(), + state.b.public(), + ); + + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address); + + let tx_refund_watcher = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + + Ok(()) +} + +// Watch for refund transaction on the blockchain +pub async fn watch_for_redeem_btc(state: State4, bitcoin_wallet: &W) -> Result +where + W: WatchForRawTransaction, +{ + let tx_redeem = bitcoin::TxRedeem::new(&state.tx_lock, &state.redeem_address); + let tx_redeem_encsig = state + .b + .encsign(state.S_a_bitcoin.clone(), tx_redeem.digest()); + + let tx_redeem_candidate = bitcoin_wallet + .watch_for_raw_transaction(tx_redeem.txid()) + .await; + + let tx_redeem_sig = + tx_redeem.extract_signature_by_key(tx_redeem_candidate, state.b.public())?; + let s_a = bitcoin::recover(state.S_a_bitcoin.clone(), tx_redeem_sig, tx_redeem_encsig)?; + let s_a = monero::private_key_from_secp256k1_scalar(s_a.into()); + + Ok(State5 { + A: state.A.clone(), + b: state.b.clone(), + s_a, + s_b: state.s_b, + S_a_monero: state.S_a_monero, + S_a_bitcoin: state.S_a_bitcoin.clone(), + v: state.v, + btc: state.btc, + xmr: state.xmr, + refund_timelock: state.refund_timelock, + punish_timelock: state.punish_timelock, + refund_address: state.refund_address.clone(), + redeem_address: state.redeem_address.clone(), + punish_address: state.punish_address.clone(), + tx_lock: state.tx_lock.clone(), + tx_refund_encsig: state.tx_refund_encsig.clone(), + tx_cancel_sig: state.tx_cancel_sig_a.clone(), + }) +} From ff7daf16f36c4171ec18b237cb0e9a53c6265d32 Mon Sep 17 00:00:00 2001 From: rishflab Date: Thu, 26 Nov 2020 13:55:56 +1100 Subject: [PATCH 02/27] WIP: Test simplified swap execution --- swap/src/alice.rs | 420 ++++++++++++++++++++++++++++++++++++ swap/src/bin/simple_swap.rs | 12 +- swap/src/bob.rs | 2 +- swap/src/bob_simple.rs | 11 +- swap/src/lib.rs | 1 + swap/tests/e2e.rs | 110 +++++++++- 6 files changed, 552 insertions(+), 4 deletions(-) create mode 100644 swap/src/alice.rs diff --git a/swap/src/alice.rs b/swap/src/alice.rs new file mode 100644 index 00000000..9bf8e51c --- /dev/null +++ b/swap/src/alice.rs @@ -0,0 +1,420 @@ +//! Run an XMR/BTC swap in the role of Alice. +//! Alice holds XMR and wishes receive BTC. +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; + +mod amounts; +mod message0; +mod message1; +mod message2; +mod message3; + +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, +}; +use xmr_btc::{ + alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0}, + bitcoin::BroadcastSignedTransaction, + bob, + monero::{CreateWalletForOutput, Transfer}, +}; + +pub async fn swap( + bitcoin_wallet: Arc, + monero_wallet: Arc, + db: Database, + listen: Multiaddr, + transport: SwapTransport, + behaviour: Alice, +) -> Result<()> { + struct Network(Arc>); + + // 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.0.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 state = State0::new( + rng, + 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 = match swarm.next().await { + OutEvent::Message2(msg) => state2.receive(msg)?, + other => panic!("Unexpected event: {:?}", other), + }; + + let swap_id = Uuid::new_v4(); + db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into()) + .await?; + + info!("Handshake complete, we now have State3 for Alice."); + + let network = Arc::new(Mutex::new(Network(unimplemented!()))); + + let mut action_generator = action_generator( + network, + 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 _ = monero_wallet + .transfer(public_spend_key, public_view_key, amount) + .await?; + + db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into()) + .await?; + } + + 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; + +fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> Result { + use anyhow::Context as _; + + let local_peer_id = behaviour.peer_id(); + + let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) + .executor(Box::new(TokioExecutor { + handle: tokio::runtime::Handle::current(), + })) + .build(); + + Swarm::listen_on(&mut swarm, listen.clone()) + .with_context(|| format!("Address is not supported: {:#}", listen))?; + + tracing::info!("Initialized swarm: {}", local_peer_id); + + Ok(swarm) +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum OutEvent { + ConnectionEstablished(PeerId), + Request(amounts::OutEvent), // Not-uniform with Bob on purpose, ready for adding Xmr event. + Message0(bob::Message0), + Message1 { + msg: bob::Message1, + channel: ResponseChannel, + }, + Message2(bob::Message2), + Message3(bob::Message3), +} + +impl From for OutEvent { + fn from(event: peer_tracker::OutEvent) -> Self { + match event { + peer_tracker::OutEvent::ConnectionEstablished(id) => { + OutEvent::ConnectionEstablished(id) + } + } + } +} + +impl From for OutEvent { + fn from(event: amounts::OutEvent) -> Self { + OutEvent::Request(event) + } +} + +impl From for OutEvent { + fn from(event: message0::OutEvent) -> Self { + match event { + message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), + } + } +} + +impl From for OutEvent { + fn from(event: message1::OutEvent) -> Self { + match event { + message1::OutEvent::Msg { msg, channel } => OutEvent::Message1 { msg, channel }, + } + } +} + +impl From for OutEvent { + fn from(event: message2::OutEvent) -> Self { + match event { + message2::OutEvent::Msg { msg, .. } => OutEvent::Message2(msg), + } + } +} + +impl From for OutEvent { + fn from(event: message3::OutEvent) -> Self { + match event { + message3::OutEvent::Msg(msg) => OutEvent::Message3(msg), + } + } +} + +/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "OutEvent", event_process = false)] +#[allow(missing_debug_implementations)] +pub struct Alice { + pt: PeerTracker, + amounts: Amounts, + message0: Message0, + message1: Message1, + message2: Message2, + message3: Message3, + #[behaviour(ignore)] + identity: Keypair, +} + +impl Alice { + pub fn identity(&self) -> Keypair { + self.identity.clone() + } + + pub fn peer_id(&self) -> PeerId { + PeerId::from(self.identity.public()) + } + + /// Alice always sends her messages as a response to a request from Bob. + pub fn send_amounts(&mut self, channel: ResponseChannel, amounts: SwapAmounts) { + let msg = AliceToBob::Amounts(amounts); + self.amounts.send(channel, msg); + info!("Sent amounts response"); + } + + /// Message0 gets sent within the network layer using this state0. + pub fn set_state0(&mut self, state: State0) { + debug!("Set state 0"); + let _ = self.message0.set_state(state); + } + + /// Send Message1 to Bob in response to receiving his Message1. + pub fn send_message1( + &mut self, + channel: ResponseChannel, + msg: xmr_btc::alice::Message1, + ) { + self.message1.send(channel, msg); + debug!("Sent Message1"); + } +} + +impl Default for Alice { + fn default() -> Self { + let identity = Keypair::generate_ed25519(); + + Self { + pt: PeerTracker::default(), + amounts: Amounts::default(), + message0: Message0::default(), + message1: Message1::default(), + message2: Message2::default(), + message3: Message3::default(), + identity, + } + } +} + +fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapAmounts { + // TODO: Get this from an exchange. + // 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/simple_swap.rs b/swap/src/bin/simple_swap.rs index cc3ed027..a7f0952a 100644 --- a/swap/src/bin/simple_swap.rs +++ b/swap/src/bin/simple_swap.rs @@ -5,6 +5,7 @@ use swap::{ cli::Options, storage::Database, }; +use uuid::Uuid; #[tokio::main] async fn main() -> Result<()> { @@ -19,7 +20,16 @@ async fn main() -> Result<()> { match opt { Options::Alice { .. } => { - simple_swap(bob_state, swarm, db, bitcoin_wallet, monero_wallet, rng).await?; + simple_swap( + bob_state, + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + Uuid::new_v4(), + ) + .await?; } Options::Recover { .. } => { let _stored_state: BobState = unimplemented!("io.get_state(uuid)?"); diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 9651d12f..ea424e18 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -237,7 +237,7 @@ pub async fn swap( pub type Swarm = libp2p::Swarm; -fn new_swarm(transport: SwapTransport, behaviour: Bob) -> Result { +pub fn new_swarm(transport: SwapTransport, behaviour: Bob) -> Result { let local_peer_id = behaviour.peer_id(); let swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) diff --git a/swap/src/bob_simple.rs b/swap/src/bob_simple.rs index 1f0a0e75..db5c5b6c 100644 --- a/swap/src/bob_simple.rs +++ b/swap/src/bob_simple.rs @@ -57,6 +57,7 @@ pub async fn simple_swap( bitcoin_wallet: Arc, monero_wallet: Arc, mut rng: OsRng, + swap_id: Uuid, ) -> Result { match state { BobState::Started(mut cmd_tx, mut rsp_rx, btc, alice_peer_id) => { @@ -128,12 +129,14 @@ pub async fn simple_swap( bitcoin_wallet, monero_wallet, rng, + swap_id, ) .await } BobState::Negotiated(state2, alice_peer_id) => { // Alice and Bob have exchanged info let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; + // db.insert_latest_state(state); simple_swap( BobState::BtcLocked(state3, alice_peer_id), swarm, @@ -141,6 +144,7 @@ pub async fn simple_swap( bitcoin_wallet, monero_wallet, rng, + swap_id, ) .await } @@ -163,6 +167,7 @@ pub async fn simple_swap( bitcoin_wallet, monero_wallet, rng, + swap_id, ) .await } @@ -184,6 +189,7 @@ pub async fn simple_swap( bitcoin_wallet, monero_wallet, rng, + swap_id, ) .await } @@ -201,6 +207,7 @@ pub async fn simple_swap( bitcoin_wallet, monero_wallet, rng, + swap_id, ) .await } @@ -217,7 +224,8 @@ pub async fn simple_swap( db, bitcoin_wallet, monero_wallet, - rng, + rng, + swap_id ) .await @@ -234,6 +242,7 @@ pub async fn simple_swap( bitcoin_wallet, monero_wallet, rng, + swap_id, ) .await } diff --git a/swap/src/lib.rs b/swap/src/lib.rs index e9003112..5ec6d6be 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; +pub mod alice; pub mod bitcoin; pub mod bob; pub mod bob_simple; diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 358657ca..7323262f 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -2,10 +2,15 @@ use bitcoin_harness::Bitcoind; use futures::{channel::mpsc, future::try_join}; use libp2p::Multiaddr; use monero_harness::Monero; +use rand::rngs::OsRng; use std::sync::Arc; -use swap::{alice, bob, network::transport::build, storage::Database}; +use swap::{ + alice, bob, bob::new_swarm, bob_simple, bob_simple::BobState, network::transport::build, + storage::Database, +}; use tempfile::tempdir; use testcontainers::clients::Cli; +use uuid::Uuid; use xmr_btc::bitcoin; // NOTE: For some reason running these tests overflows the stack. In order to @@ -122,3 +127,106 @@ async fn swap() { assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr); assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr); } + +#[tokio::test] +async fn simple_swap_happy_path() { + 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::Alice::default(); + let alice_transport = build(alice_behaviour.identity()).unwrap(); + + let db = Database::open(std::path::Path::new("../.swap-db/")).unwrap(); + let alice_swap = todo!(); + + 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::Bob::default(); + let bob_transport = build(bob_behaviour.identity()).unwrap(); + let bob_state = BobState::Started(cmd_tx, rsp_rx, btc_bob.as_sat(), alice_behaviour.peer_id()); + let bob_swarm = new_swarm(bob_transport, bob_behaviour).unwrap(); + let bob_swap = bob_simple::simple_swap( + bob_state, + bob_swarm, + db, + bob_btc_wallet.clone(), + bob_xmr_wallet.clone(), + OsRng, + Uuid::new_v4(), + ); + + // 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); +} From ae94b170fd878c89c1799efa45699ce79c88d9f9 Mon Sep 17 00:00:00 2001 From: rishflab Date: Mon, 16 Nov 2020 10:21:17 +1100 Subject: [PATCH 03/27] Cleanup Move state machine executors into seperate files Remove check for ack message from Alice. Seems like a bad idea to rely on an acknowledgement message instead of looking at the blockchain. Fix warnings --- swap/src/alice.rs | 117 ++++++++++++++++++++++++++++++++ swap/src/bob.rs | 169 ++++++++++++++++++++++++++++++++++++++++------ swap/src/io.rs | 8 +++ swap/src/lib.rs | 1 + 4 files changed, 273 insertions(+), 22 deletions(-) create mode 100644 swap/src/io.rs diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 9bf8e51c..c5aaa159 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -1,6 +1,7 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. use anyhow::Result; +use async_recursion::async_recursion; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use genawaiter::GeneratorState; @@ -25,6 +26,7 @@ use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use crate::{ bitcoin, bitcoin::TX_LOCK_MINE_TIMEOUT, + io::Io, monero, network::{ peer_tracker::{self, PeerTracker}, @@ -36,6 +38,7 @@ use crate::{ storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; + use xmr_btc::{ alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0}, bitcoin::BroadcastSignedTransaction, @@ -43,6 +46,120 @@ use xmr_btc::{ monero::{CreateWalletForOutput, Transfer}, }; +// The same data structure is used for swap execution and recovery. +// This allows for a seamless transition from a failed swap to recovery. +pub enum AliceState { + Started, + Negotiated, + BtcLocked, + XmrLocked, + BtcRedeemed, + XmrRefunded, + Cancelled, + Punished, + SafelyAborted, +} + +// State machine driver for swap execution +#[async_recursion] +pub async fn simple_swap(state: AliceState, io: Io) -> Result { + match state { + AliceState::Started => { + // Alice and Bob exchange swap info + // Todo: Poll the swarm here until Alice and Bob have exchanged info + simple_swap(AliceState::Negotiated, io).await + } + AliceState::Negotiated => { + // Alice and Bob have exchanged info + // Todo: Alice watches for BTC to be locked on chain + // Todo: Timeout at t1? + simple_swap(AliceState::BtcLocked, io).await + } + AliceState::BtcLocked => { + // Alice has seen that Bob has locked BTC + // Todo: Alice locks XMR + simple_swap(AliceState::XmrLocked, io).await + } + AliceState::XmrLocked => { + // Alice has locked Xmr + // Alice waits until Bob sends her key to redeem BTC + // Todo: Poll the swarm here until msg from Bob arrives or t1 + if unimplemented!("key_received") { + // Alice redeems BTC + simple_swap(AliceState::BtcRedeemed, io).await + } else { + // submit TxCancel + simple_swap(AliceState::Cancelled, io).await + } + } + AliceState::Cancelled => { + // Wait until t2 or if TxRefund is seen + // If Bob has refunded the Alice should extract Bob's monero secret key and move + // the TxLockXmr output to her wallet. + if unimplemented!("refunded") { + simple_swap(AliceState::XmrRefunded, io).await + } else { + simple_swap(AliceState::Punished, io).await + } + } + AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), + AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), + AliceState::Punished => Ok(AliceState::Punished), + AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), + } +} + +// State machine driver for recovery execution +#[async_recursion] +pub async fn abort(state: AliceState, io: Io) -> Result { + match state { + AliceState::Started => { + // Nothing has been commited by either party, abort swap. + abort(AliceState::SafelyAborted, io).await + } + AliceState::Negotiated => { + // Nothing has been commited by either party, abort swap. + abort(AliceState::SafelyAborted, io).await + } + AliceState::BtcLocked => { + // Alice has seen that Bob has locked BTC + // Alice does not need to do anything to recover + abort(AliceState::SafelyAborted, io).await + } + AliceState::XmrLocked => { + // Alice has locked XMR + // Alice watches for TxRedeem until t1 + if unimplemented!("TxRedeemSeen") { + // Alice has successfully redeemed, protocol was a success + abort(AliceState::BtcRedeemed, io).await + } else if unimplemented!("T1Elapsed") { + // publish TxCancel or see if it has been published + abort(AliceState::Cancelled, io).await + } else { + Err(unimplemented!()) + } + } + AliceState::Cancelled => { + // Alice has cancelled the swap + // Alice waits watches for t2 or TxRefund + if unimplemented!("TxRefundSeen") { + // Bob has refunded and leaked s_b + abort(AliceState::XmrRefunded, io).await + } else if unimplemented!("T1Elapsed") { + // publish TxCancel or see if it has been published + // Wait until t2 and publish TxPunish + abort(AliceState::Punished, io).await + } else { + Err(unimplemented!()) + } + } + AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), + AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), + AliceState::Punished => Ok(AliceState::Punished), + AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), + } +} + pub async fn swap( bitcoin_wallet: Arc, monero_wallet: Arc, diff --git a/swap/src/bob.rs b/swap/src/bob.rs index ea424e18..f7bf16af 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -1,19 +1,7 @@ //! Run an XMR/BTC swap in the role of Bob. //! Bob holds BTC and wishes receive XMR. -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 anyhow::Result; +use async_recursion::async_recursion; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use futures::{ @@ -27,6 +15,28 @@ use std::{process, sync::Arc, time::Duration}; use tokio::sync::Mutex; use tracing::{debug, info, warn}; use uuid::Uuid; + +mod amounts; +mod message0; +mod message1; +mod message2; +mod message3; + +use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; +use crate::{ + bitcoin::{self, TX_LOCK_MINE_TIMEOUT}, + io::Io, + 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}, @@ -34,11 +44,128 @@ use xmr_btc::{ monero::CreateWalletForOutput, }; -mod amounts; -mod message0; -mod message1; -mod message2; -mod message3; +// The same data structure is used for swap execution and recovery. +// This allows for a seamless transition from a failed swap to recovery. +pub enum BobState { + Started, + Negotiated, + BtcLocked, + XmrLocked, + BtcRedeemed, + BtcRefunded, + XmrRedeemed, + Cancelled, + Punished, + SafelyAborted, +} + +// State machine driver for swap execution +#[async_recursion] +pub async fn simple_swap(state: BobState, io: Io) -> Result { + match state { + BobState::Started => { + // Alice and Bob exchange swap info + // Todo: Poll the swarm here until Alice and Bob have exchanged info + simple_swap(BobState::Negotiated, io).await + } + BobState::Negotiated => { + // Alice and Bob have exchanged info + // Bob Locks Btc + simple_swap(BobState::BtcLocked, io).await + } + BobState::BtcLocked => { + // Bob has locked Btc + // Watch for Alice to Lock Xmr + simple_swap(BobState::XmrLocked, io).await + } + BobState::XmrLocked => { + // Alice has locked Xmr + // Bob sends Alice his key + // Todo: This should be a oneshot + if unimplemented!("Redeemed before t1") { + simple_swap(BobState::BtcRedeemed, io).await + } else { + // submit TxCancel + simple_swap(BobState::Cancelled, io).await + } + } + BobState::Cancelled => { + if unimplemented!(" Ok(BobState::BtcRefunded), + BobState::BtcRedeemed => { + // Bob redeems XMR using revealed s_a + simple_swap(BobState::XmrRedeemed, io).await + } + BobState::Punished => Ok(BobState::Punished), + BobState::SafelyAborted => Ok(BobState::SafelyAborted), + BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), + } +} + +// State machine driver for recovery execution +#[async_recursion] +pub async fn abort(state: BobState, io: Io) -> Result { + match state { + BobState::Started => { + // Nothing has been commited by either party, abort swap. + abort(BobState::SafelyAborted, io).await + } + BobState::Negotiated => { + // Nothing has been commited by either party, abort swap. + abort(BobState::SafelyAborted, io).await + } + BobState::BtcLocked => { + // Bob has locked BTC and must refund it + // Bob waits for alice to publish TxRedeem or t1 + if unimplemented!("TxRedeemSeen") { + // Alice has redeemed revealing s_a + abort(BobState::BtcRedeemed, io).await + } else if unimplemented!("T1Elapsed") { + // publish TxCancel or see if it has been published + abort(BobState::Cancelled, io).await + } else { + Err(unimplemented!()) + } + } + BobState::XmrLocked => { + // Alice has locked Xmr + // Wait until t1 + if unimplemented!(">t1 and t2 + // submit TxCancel + abort(BobState::Punished, io).await + } + } + BobState::Cancelled => { + // Bob has cancelled the swap + // If { + // Bob uses revealed s_a to redeem XMR + abort(BobState::XmrRedeemed, io).await + } + BobState::BtcRefunded => Ok(BobState::BtcRefunded), + BobState::Punished => Ok(BobState::Punished), + BobState::SafelyAborted => Ok(BobState::SafelyAborted), + BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), + } +} #[allow(clippy::too_many_arguments)] pub async fn swap( @@ -97,9 +224,6 @@ pub async fn swap( swarm.request_amounts(alice.clone(), btc); - // What is going on here, shouldn't this be a simple req/resp?? - // Why do we need mspc channels? - // Todo: simplify this code let (btc, xmr) = match swarm.next().await { OutEvent::Amounts(amounts) => { info!("Got amounts from Alice: {:?}", amounts); @@ -110,6 +234,7 @@ pub async fn swap( info!("User rejected amounts proposed by Alice, aborting..."); process::exit(0); } + info!("User accepted amounts proposed by Alice"); (amounts.btc, amounts.xmr) } @@ -237,7 +362,7 @@ pub async fn swap( pub type Swarm = libp2p::Swarm; -pub fn new_swarm(transport: SwapTransport, behaviour: Bob) -> Result { +fn new_swarm(transport: SwapTransport, behaviour: Bob) -> Result { let local_peer_id = behaviour.peer_id(); let swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) diff --git a/swap/src/io.rs b/swap/src/io.rs new file mode 100644 index 00000000..5b26d09a --- /dev/null +++ b/swap/src/io.rs @@ -0,0 +1,8 @@ +// This struct contains all the I/O required to execute a swap +pub struct Io { + // swarm: libp2p::Swarm<>, +// bitcoind_rpc: _, +// monerod_rpc: _, +// monero_wallet_rpc: _, +// db: _, +} diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 5ec6d6be..ea801e64 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -6,6 +6,7 @@ pub mod bitcoin; pub mod bob; pub mod bob_simple; pub mod cli; +pub mod io; pub mod monero; pub mod network; pub mod recover; From 0fe5131a8a0233f7fd361000ce9c4ef71830e2c2 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 17 Nov 2020 10:06:45 +1100 Subject: [PATCH 04/27] Fix import format --- swap/src/alice.rs | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index c5aaa159..4803534c 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -1,27 +1,5 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. -use anyhow::Result; -use async_recursion::async_recursion; -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; - -mod amounts; -mod message0; -mod message1; -mod message2; -mod message3; - use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use crate::{ bitcoin, @@ -38,7 +16,21 @@ use crate::{ storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; - +use anyhow::Result; +use async_recursion::async_recursion; +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, @@ -46,6 +38,12 @@ use xmr_btc::{ monero::{CreateWalletForOutput, Transfer}, }; +mod amounts; +mod message0; +mod message1; +mod message2; +mod message3; + // The same data structure is used for swap execution and recovery. // This allows for a seamless transition from a failed swap to recovery. pub enum AliceState { From c4cd64d134e4e2a2c9c01e270fe280015644976c Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 17 Nov 2020 10:24:59 +1100 Subject: [PATCH 05/27] Implemented Alice transition from `Started` to `Negotiated` --- swap/src/alice.rs | 133 ++++++++++++++++++++++++++++++++++--------- swap/src/bin/swap.rs | 6 +- swap/src/bob.rs | 1 - swap/tests/e2e.rs | 2 +- 4 files changed, 109 insertions(+), 33 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 4803534c..6c44cdac 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -4,7 +4,6 @@ use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use crate::{ bitcoin, bitcoin::TX_LOCK_MINE_TIMEOUT, - io::Io, monero, network::{ peer_tracker::{self, PeerTracker}, @@ -16,7 +15,7 @@ use crate::{ storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; -use anyhow::Result; +use anyhow::{bail, Result}; use async_recursion::async_recursion; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; @@ -60,23 +59,99 @@ pub enum AliceState { // State machine driver for swap execution #[async_recursion] -pub async fn simple_swap(state: AliceState, io: Io) -> Result { +pub async fn simple_swap( + state: AliceState, + &mut rng: R, + mut swarm: Swarm, + bitcoin_wallet: Arc, +) -> Result { match state { AliceState::Started => { - // Alice and Bob exchange swap info - // Todo: Poll the swarm here until Alice and Bob have exchanged info - simple_swap(AliceState::Negotiated, io).await + // Bob dials us + let bob_peer_id = match swarm.next().await { + OutEvent::ConnectionEstablished(bob_peer_id) => bob_peer_id, + other => bail!("Unexpected event received: {:?}", other), + }; + + // Bob sends us a request + let (btc, channel) = match swarm.next().await { + OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => (btc, channel), + other => bail!("Unexpected event received: {:?}", other), + }; + + let amounts = calculate_amounts(btc); + 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(); + + let state0 = State0::new( + rng, + btc, + xmr, + REFUND_TIMELOCK, + PUNISH_TIMELOCK, + redeem_address, + punish_address, + ); + + // TODO(Franck) This is not needed + // Review if the swarm really needs to store states + swarm.set_state0(state0.clone()); + + // Bob sends us message0 + let message0 = match swarm.next().await { + OutEvent::Message0(msg) => msg, + other => bail!("Unexpected event received: {:?}", other), + }; + + let state1 = state0.receive(message0)?; + + // TODO(Franck) We should use the same channel everytime, + // Can we remove this response channel? + let (state2, channel) = match swarm.next().await { + OutEvent::Message1 { msg, channel } => { + let state2 = state1.receive(msg); + (state2, channel) + } + other => bail!("Unexpected event: {:?}", other), + }; + + let message1 = state2.next_message(); + swarm.send_message1(channel, message1); + + let (state3, channel) = match swarm.next().await { + OutEvent::Message2 { msg, channel } => { + let state3 = state2.receive(msg)?; + (state3, channel) + } + other => bail!("Unexpected event: {:?}", other), + }; + + let swap_id = Uuid::new_v4(); + // TODO(Franck): Use the same terminology (negotiated) to describe this state. + db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into()) + .await?; + + info!( + "State transitioned from Started to Negotiated, Bob peer id is {}", + bob_peer_id + ); + + simple_swap(AliceState::Negotiated, swarm, rng, bitcoin_wallet).await } AliceState::Negotiated => { // Alice and Bob have exchanged info // Todo: Alice watches for BTC to be locked on chain // Todo: Timeout at t1? - simple_swap(AliceState::BtcLocked, io).await + simple_swap(AliceState::BtcLocked, swarm, rng, bitcoin_wallet).await } AliceState::BtcLocked => { // Alice has seen that Bob has locked BTC // Todo: Alice locks XMR - simple_swap(AliceState::XmrLocked, io).await + simple_swap(AliceState::XmrLocked, swarm, bitcoin_wallet).await } AliceState::XmrLocked => { // Alice has locked Xmr @@ -84,10 +159,10 @@ pub async fn simple_swap(state: AliceState, io: Io) -> Result { // Todo: Poll the swarm here until msg from Bob arrives or t1 if unimplemented!("key_received") { // Alice redeems BTC - simple_swap(AliceState::BtcRedeemed, io).await + simple_swap(AliceState::BtcRedeemed, swarm, bitcoin_wallet).await } else { // submit TxCancel - simple_swap(AliceState::Cancelled, io).await + simple_swap(AliceState::Cancelled, swarm, bitcoin_wallet).await } } AliceState::Cancelled => { @@ -95,9 +170,9 @@ pub async fn simple_swap(state: AliceState, io: Io) -> Result { // If Bob has refunded the Alice should extract Bob's monero secret key and move // the TxLockXmr output to her wallet. if unimplemented!("refunded") { - simple_swap(AliceState::XmrRefunded, io).await + simple_swap(AliceState::XmrRefunded, swarm, bitcoin_wallet).await } else { - simple_swap(AliceState::Punished, io).await + simple_swap(AliceState::Punished, swarm, bitcoin_wallet).await } } AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), @@ -109,30 +184,30 @@ pub async fn simple_swap(state: AliceState, io: Io) -> Result { // State machine driver for recovery execution #[async_recursion] -pub async fn abort(state: AliceState, io: Io) -> Result { +pub async fn abort(state: AliceState) -> Result { match state { AliceState::Started => { // Nothing has been commited by either party, abort swap. - abort(AliceState::SafelyAborted, io).await + abort(AliceState::SafelyAborted).await } AliceState::Negotiated => { // Nothing has been commited by either party, abort swap. - abort(AliceState::SafelyAborted, io).await + abort(AliceState::SafelyAborted).await } AliceState::BtcLocked => { // Alice has seen that Bob has locked BTC // Alice does not need to do anything to recover - abort(AliceState::SafelyAborted, io).await + abort(AliceState::SafelyAborted).await } AliceState::XmrLocked => { // Alice has locked XMR // Alice watches for TxRedeem until t1 if unimplemented!("TxRedeemSeen") { // Alice has successfully redeemed, protocol was a success - abort(AliceState::BtcRedeemed, io).await + abort(AliceState::BtcRedeemed).await } else if unimplemented!("T1Elapsed") { // publish TxCancel or see if it has been published - abort(AliceState::Cancelled, io).await + abort(AliceState::Cancelled).await } else { Err(unimplemented!()) } @@ -142,11 +217,11 @@ pub async fn abort(state: AliceState, io: Io) -> Result { // Alice waits watches for t2 or TxRefund if unimplemented!("TxRefundSeen") { // Bob has refunded and leaked s_b - abort(AliceState::XmrRefunded, io).await + abort(AliceState::XmrRefunded).await } else if unimplemented!("T1Elapsed") { // publish TxCancel or see if it has been published // Wait until t2 and publish TxPunish - abort(AliceState::Punished, io).await + abort(AliceState::Punished).await } else { Err(unimplemented!()) } @@ -164,7 +239,7 @@ pub async fn swap( db: Database, listen: Multiaddr, transport: SwapTransport, - behaviour: Alice, + behaviour: Behaviour, ) -> Result<()> { struct Network(Arc>); @@ -359,9 +434,9 @@ pub async fn swap( } } -pub type Swarm = libp2p::Swarm; +pub type Swarm = libp2p::Swarm; -fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> Result { +fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Behaviour) -> Result { use anyhow::Context as _; let local_peer_id = behaviour.peer_id(); @@ -384,6 +459,8 @@ fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> R #[derive(Debug)] pub enum OutEvent { ConnectionEstablished(PeerId), + // TODO (Franck): Change this to get both amounts so parties can verify the amounts are + // expected early on. Request(amounts::OutEvent), // Not-uniform with Bob on purpose, ready for adding Xmr event. Message0(bob::Message0), Message1 { @@ -446,7 +523,7 @@ impl From for OutEvent { #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", event_process = false)] #[allow(missing_debug_implementations)] -pub struct Alice { +pub struct Behaviour { pt: PeerTracker, amounts: Amounts, message0: Message0, @@ -457,7 +534,7 @@ pub struct Alice { identity: Keypair, } -impl Alice { +impl Behaviour { pub fn identity(&self) -> Keypair { self.identity.clone() } @@ -490,7 +567,7 @@ impl Alice { } } -impl Default for Alice { +impl Default for Behaviour { fn default() -> Self { let identity = Keypair::generate_ed25519(); @@ -507,8 +584,8 @@ impl Default for Alice { } fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapAmounts { - // TODO: Get this from an exchange. - // This value corresponds to 100 XMR per BTC + // 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; diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index b9a9e45f..2e24c418 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -19,7 +19,7 @@ use prettytable::{row, Table}; use std::{io, io::Write, process, sync::Arc}; use structopt::StructOpt; use swap::{ - alice::{self, Alice}, + alice::{self, Behaviour}, bitcoin, bob::{self, Bob}, cli::Options, @@ -52,7 +52,7 @@ async fn main() -> Result<()> { } => { info!("running swap node as Alice ..."); - let behaviour = Alice::default(); + let behaviour = Behaviour::default(); let local_key_pair = behaviour.identity(); let (listen_addr, _ac, transport) = match tor_port { @@ -180,7 +180,7 @@ async fn swap_as_alice( db: Database, addr: Multiaddr, transport: SwapTransport, - behaviour: Alice, + behaviour: Behaviour, ) -> Result<()> { alice::swap( bitcoin_wallet, diff --git a/swap/src/bob.rs b/swap/src/bob.rs index f7bf16af..31a2819b 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -36,7 +36,6 @@ use crate::{ storage::Database, Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; - use xmr_btc::{ alice, bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock}, diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 7323262f..5abbe191 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -74,7 +74,7 @@ async fn swap() { )); let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client())); - let alice_behaviour = alice::Alice::default(); + 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(); From 75e7fedfed41aef3a9bcd3221e242e050c0811ee Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 17 Nov 2020 11:26:29 +1100 Subject: [PATCH 06/27] Implemented Alice transition from `Negotiated` to `BtcLocked` --- swap/src/alice.rs | 64 +++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 6c44cdac..21ec5021 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -15,7 +15,7 @@ use crate::{ storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use async_recursion::async_recursion; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; @@ -25,14 +25,14 @@ use libp2p::{ request_response::ResponseChannel, NetworkBehaviour, PeerId, }; -use rand::rngs::OsRng; +use rand::{rngs::OsRng, CryptoRng, RngCore}; use std::{sync::Arc, time::Duration}; -use tokio::sync::Mutex; +use tokio::{sync::Mutex, time::timeout}; use tracing::{debug, info, warn}; use uuid::Uuid; use xmr_btc::{ - alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0}, - bitcoin::BroadcastSignedTransaction, + alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0, State3}, + bitcoin::{BroadcastSignedTransaction, WatchForRawTransaction}, bob, monero::{CreateWalletForOutput, Transfer}, }; @@ -47,7 +47,7 @@ mod message3; // This allows for a seamless transition from a failed swap to recovery. pub enum AliceState { Started, - Negotiated, + Negotiated(State3), BtcLocked, XmrLocked, BtcRedeemed, @@ -59,12 +59,16 @@ pub enum AliceState { // State machine driver for swap execution #[async_recursion] -pub async fn simple_swap( +pub async fn simple_swap( state: AliceState, - &mut rng: R, + rng: &mut R, mut swarm: Swarm, + db: Database, bitcoin_wallet: Arc, -) -> Result { +) -> Result +where + R: RngCore + CryptoRng + Send, +{ match state { AliceState::Started => { // Bob dials us @@ -97,10 +101,6 @@ pub async fn simple_swap( punish_address, ); - // TODO(Franck) This is not needed - // Review if the swarm really needs to store states - swarm.set_state0(state0.clone()); - // Bob sends us message0 let message0 = match swarm.next().await { OutEvent::Message0(msg) => msg, @@ -140,18 +140,31 @@ pub async fn simple_swap( bob_peer_id ); - simple_swap(AliceState::Negotiated, swarm, rng, bitcoin_wallet).await + simple_swap( + AliceState::Negotiated(state3), + rng, + swarm, + db, + bitcoin_wallet, + ) + .await } - AliceState::Negotiated => { - // Alice and Bob have exchanged info - // Todo: Alice watches for BTC to be locked on chain - // Todo: Timeout at t1? - simple_swap(AliceState::BtcLocked, swarm, rng, bitcoin_wallet).await + AliceState::Negotiated(state3) => { + // TODO(1): Do a future select with watch bitcoin blockchain time + // TODO(2): Implement a proper safe expiry module + timeout( + Duration::from_secs(TX_LOCK_MINE_TIMEOUT), + bitcoin_wallet.watch_for_raw_transaction(state3.tx_lock.txid()), + ) + .await + .context("Timed out, Bob did not lock Bitcoin in time")?; + + simple_swap(AliceState::BtcLocked, rng, swarm, db, bitcoin_wallet).await } AliceState::BtcLocked => { // Alice has seen that Bob has locked BTC // Todo: Alice locks XMR - simple_swap(AliceState::XmrLocked, swarm, bitcoin_wallet).await + simple_swap(AliceState::XmrLocked, rng, swarm, db, bitcoin_wallet).await } AliceState::XmrLocked => { // Alice has locked Xmr @@ -159,10 +172,10 @@ pub async fn simple_swap( // Todo: Poll the swarm here until msg from Bob arrives or t1 if unimplemented!("key_received") { // Alice redeems BTC - simple_swap(AliceState::BtcRedeemed, swarm, bitcoin_wallet).await + simple_swap(AliceState::BtcRedeemed, rng, swarm, db, bitcoin_wallet).await } else { // submit TxCancel - simple_swap(AliceState::Cancelled, swarm, bitcoin_wallet).await + simple_swap(AliceState::Cancelled, rng, swarm, db, bitcoin_wallet).await } } AliceState::Cancelled => { @@ -170,9 +183,9 @@ pub async fn simple_swap( // If Bob has refunded the Alice should extract Bob's monero secret key and move // the TxLockXmr output to her wallet. if unimplemented!("refunded") { - simple_swap(AliceState::XmrRefunded, swarm, bitcoin_wallet).await + simple_swap(AliceState::XmrRefunded, rng, swarm, db, bitcoin_wallet).await } else { - simple_swap(AliceState::Punished, swarm, bitcoin_wallet).await + simple_swap(AliceState::Punished, rng, swarm, db, bitcoin_wallet).await } } AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), @@ -190,7 +203,7 @@ pub async fn abort(state: AliceState) -> Result { // Nothing has been commited by either party, abort swap. abort(AliceState::SafelyAborted).await } - AliceState::Negotiated => { + AliceState::Negotiated(_) => { // Nothing has been commited by either party, abort swap. abort(AliceState::SafelyAborted).await } @@ -550,6 +563,7 @@ impl Behaviour { info!("Sent amounts response"); } + // TODO(Franck) remove /// Message0 gets sent within the network layer using this state0. pub fn set_state0(&mut self, state: State0) { debug!("Set state 0"); From 8976a03b3d1cbce4747a3cbae3daf67d9993fd20 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 18 Nov 2020 16:27:50 +1100 Subject: [PATCH 07/27] Implemented Alice transition from `BtcLocked` to `XmrLocked` --- swap/src/alice.rs | 158 +++++++++++++++++++++++++++++++++++++------ swap/src/lib.rs | 1 + xmr-btc/src/alice.rs | 1 + 3 files changed, 140 insertions(+), 20 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 21ec5021..e3c56752 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -15,7 +15,7 @@ use crate::{ storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use async_recursion::async_recursion; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; @@ -46,9 +46,21 @@ mod message3; // The same data structure is used for swap execution and recovery. // This allows for a seamless transition from a failed swap to recovery. pub enum AliceState { - Started, - Negotiated(State3), - BtcLocked, + Started { + amounts: SwapAmounts, + }, + Negotiated { + swap_id: Uuid, + channel: ResponseChannel, + amounts: SwapAmounts, + state3: State3, + }, + BtcLocked { + swap_id: Uuid, + channel: ResponseChannel, + amounts: SwapAmounts, + state3: State3, + }, XmrLocked, BtcRedeemed, XmrRefunded, @@ -61,16 +73,19 @@ pub enum AliceState { #[async_recursion] pub async fn simple_swap( state: AliceState, + // TODO: Would it make it better if it's in the `Started` enum variant so we don't carry it + // along? rng: &mut R, mut swarm: Swarm, db: Database, bitcoin_wallet: Arc, + monero_wallet: Arc, ) -> Result where R: RngCore + CryptoRng + Send, { match state { - AliceState::Started => { + AliceState::Started { amounts } => { // Bob dials us let bob_peer_id = match swarm.next().await { OutEvent::ConnectionEstablished(bob_peer_id) => bob_peer_id, @@ -83,7 +98,13 @@ where other => bail!("Unexpected event received: {:?}", other), }; - let amounts = calculate_amounts(btc); + if btc != amounts.btc { + bail!( + "Bob proposed a different amount; got {}, expected: {}", + btc, + amounts.btc + ); + } swarm.send_amounts(channel, amounts); let SwapAmounts { btc, xmr } = amounts; @@ -141,30 +162,95 @@ where ); simple_swap( - AliceState::Negotiated(state3), + AliceState::Negotiated { + swap_id, + state3, + channel, + amounts, + }, rng, swarm, db, bitcoin_wallet, + monero_wallet, ) .await } - AliceState::Negotiated(state3) => { + AliceState::Negotiated { + swap_id, + state3, + channel, + amounts, + } => { // TODO(1): Do a future select with watch bitcoin blockchain time // TODO(2): Implement a proper safe expiry module timeout( Duration::from_secs(TX_LOCK_MINE_TIMEOUT), + // TODO(Franck): Need to check amount? bitcoin_wallet.watch_for_raw_transaction(state3.tx_lock.txid()), ) .await .context("Timed out, Bob did not lock Bitcoin in time")?; - simple_swap(AliceState::BtcLocked, rng, swarm, db, bitcoin_wallet).await + db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into()) + .await?; + + simple_swap( + AliceState::BtcLocked { + swap_id, + channel, + amounts, + state3, + }, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await } - AliceState::BtcLocked => { - // Alice has seen that Bob has locked BTC - // Todo: Alice locks XMR - simple_swap(AliceState::XmrLocked, rng, swarm, db, bitcoin_wallet).await + AliceState::BtcLocked { + swap_id, + channel, + amounts, + state3, + } => { + let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { + scalar: state3.s_a.into_ed25519(), + }); + + let public_spend_key = S_a + state3.S_b_monero; + let public_view_key = state3.v.public(); + + // TODO(Franck): Probably need to wait at least 1 confirmation to be sure that + // we don't wrongfully think this is done. + let (transfer_proof, _) = monero_wallet + .transfer(public_spend_key, public_view_key, amounts.xmr) + .await?; + + swarm.send_message2(channel, alice::Message2 { + tx_lock_proof: transfer_proof, + }); + + // TODO(Franck): we should merge state::Alice and AliceState. + // There should be only 2 states: + // 1. the cryptographic state (State0, etc) which only aware of the crypto + // primitive to execute the protocol 2. the more general/business + // state that contains the crypto + other business data such as network + // communication, amounts to verify, swap id, etc. + db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3).into()) + .await?; + + simple_swap( + AliceState::XmrLocked, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await } AliceState::XmrLocked => { // Alice has locked Xmr @@ -172,10 +258,26 @@ where // Todo: Poll the swarm here until msg from Bob arrives or t1 if unimplemented!("key_received") { // Alice redeems BTC - simple_swap(AliceState::BtcRedeemed, rng, swarm, db, bitcoin_wallet).await + simple_swap( + AliceState::BtcRedeemed, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await } else { // submit TxCancel - simple_swap(AliceState::Cancelled, rng, swarm, db, bitcoin_wallet).await + simple_swap( + AliceState::Cancelled, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await } } AliceState::Cancelled => { @@ -183,9 +285,25 @@ where // If Bob has refunded the Alice should extract Bob's monero secret key and move // the TxLockXmr output to her wallet. if unimplemented!("refunded") { - simple_swap(AliceState::XmrRefunded, rng, swarm, db, bitcoin_wallet).await + simple_swap( + AliceState::XmrRefunded, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await } else { - simple_swap(AliceState::Punished, rng, swarm, db, bitcoin_wallet).await + simple_swap( + AliceState::Punished, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await } } AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), @@ -199,15 +317,15 @@ where #[async_recursion] pub async fn abort(state: AliceState) -> Result { match state { - AliceState::Started => { + AliceState::Started { .. } => { // Nothing has been commited by either party, abort swap. abort(AliceState::SafelyAborted).await } - AliceState::Negotiated(_) => { + AliceState::Negotiated { .. } => { // Nothing has been commited by either party, abort swap. abort(AliceState::SafelyAborted).await } - AliceState::BtcLocked => { + AliceState::BtcLocked { .. } => { // Alice has seen that Bob has locked BTC // Alice does not need to do anything to recover abort(AliceState::SafelyAborted).await diff --git a/swap/src/lib.rs b/swap/src/lib.rs index ea801e64..7f22637e 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -34,6 +34,7 @@ pub enum Rsp { /// XMR/BTC swap amounts. #[derive(Copy, Clone, Debug, Serialize, Deserialize)] +// TODO(Franck): review necessity of this struct pub struct SwapAmounts { /// Amount of BTC to swap. #[serde(with = "::bitcoin::util::amount::serde::as_sat")] diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index fa35a140..87a0a051 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -617,6 +617,7 @@ impl State2 { S_b_monero: self.S_b_monero, S_b_bitcoin: self.S_b_bitcoin, v: self.v, + // TODO(Franck): Review if these amounts are actually needed btc: self.btc, xmr: self.xmr, refund_timelock: self.refund_timelock, From 8e31a3af6aa4f53f970ab3b12af2dcb598240275 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 19 Nov 2020 16:27:17 +1100 Subject: [PATCH 08/27] Add steps from `EncSignLearned` --- swap/src/alice.rs | 165 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 130 insertions(+), 35 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index e3c56752..2b28c3d3 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -3,7 +3,7 @@ use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use crate::{ bitcoin, - bitcoin::TX_LOCK_MINE_TIMEOUT, + bitcoin::{EncryptedSignature, TX_LOCK_MINE_TIMEOUT}, monero, network::{ peer_tracker::{self, PeerTracker}, @@ -19,6 +19,7 @@ use anyhow::{anyhow, bail, Context, Result}; use async_recursion::async_recursion; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; +use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; use genawaiter::GeneratorState; use libp2p::{ core::{identity::Keypair, Multiaddr}, @@ -26,6 +27,7 @@ use libp2p::{ NetworkBehaviour, PeerId, }; use rand::{rngs::OsRng, CryptoRng, RngCore}; +use sha2::Sha256; use std::{sync::Arc, time::Duration}; use tokio::{sync::Mutex, time::timeout}; use tracing::{debug, info, warn}; @@ -61,10 +63,16 @@ pub enum AliceState { amounts: SwapAmounts, state3: State3, }, - XmrLocked, + XmrLocked { + state3: State3, + }, + EncSignLearned { + state3: State3, + encrypted_signature: EncryptedSignature, + }, BtcRedeemed, XmrRefunded, - Cancelled, + WaitingToCancel, Punished, SafelyAborted, } @@ -239,11 +247,11 @@ where // primitive to execute the protocol 2. the more general/business // state that contains the crypto + other business data such as network // communication, amounts to verify, swap id, etc. - db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3).into()) + db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into()) .await?; simple_swap( - AliceState::XmrLocked, + AliceState::XmrLocked { state3 }, rng, swarm, db, @@ -252,35 +260,121 @@ where ) .await } - AliceState::XmrLocked => { - // Alice has locked Xmr - // Alice waits until Bob sends her key to redeem BTC - // Todo: Poll the swarm here until msg from Bob arrives or t1 - if unimplemented!("key_received") { - // Alice redeems BTC - simple_swap( - AliceState::BtcRedeemed, - rng, - swarm, - db, - bitcoin_wallet, - monero_wallet, - ) - .await - } else { - // submit TxCancel - simple_swap( - AliceState::Cancelled, - rng, - swarm, - db, - bitcoin_wallet, - monero_wallet, - ) - .await + AliceState::XmrLocked { state3 } => { + let encsig = timeout( + // TODO(Franck): This is now inefficient as time has been spent since btc was + // locked + Duration::from_secs(TX_LOCK_MINE_TIMEOUT), + async { + match swarm.next().await { + OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig), + other => Err(anyhow!( + "Expected Bob's Bitcoin redeem encsig, got: {:?}", + other + )), + } + }, + ) + .await + .context("Timed out, Bob did not send redeem encsign in time"); + + match encsig { + Err(_timeout_error) => { + // TODO(Franck): Insert in DB + + simple_swap( + AliceState::WaitingToCancel, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await + } + Ok(Err(_unexpected_msg_error)) => { + // TODO(Franck): Insert in DB + + simple_swap( + AliceState::WaitingToCancel, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await + } + Ok(Ok(encrypted_signature)) => { + // TODO(Franck): Insert in DB + + simple_swap( + AliceState::EncSignLearned { + state3, + encrypted_signature, + }, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await + } } } - AliceState::Cancelled => { + AliceState::EncSignLearned { + state3, + encrypted_signature, + } => { + let (signed_tx_redeem, tx_redeem_txid) = { + let adaptor = Adaptor::>::default(); + + let tx_redeem = bitcoin::TxRedeem::new(&state3.tx_lock, &state3.redeem_address); + + bitcoin::verify_encsig( + state3.B.clone(), + state3.s_a.into_secp256k1().into(), + &tx_redeem.digest(), + &encrypted_signature, + ) + .context("Invalid encrypted signature received")?; + + let sig_a = state3.a.sign(tx_redeem.digest()); + let sig_b = adaptor + .decrypt_signature(&state3.s_a.into_secp256k1(), encrypted_signature.clone()); + + let tx = tx_redeem + .add_signatures( + &state3.tx_lock, + (state3.a.public(), sig_a), + (state3.B.clone(), sig_b), + ) + .expect("sig_{a,b} to be valid signatures for tx_redeem"); + let txid = tx.txid(); + + (tx, txid) + }; + + // TODO(Franck): Insert in db + + let _ = bitcoin_wallet + .broadcast_signed_transaction(signed_tx_redeem) + .await?; + + // TODO(Franck) Wait for confirmations + + simple_swap( + AliceState::BtcRedeemed, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await + } + AliceState::WaitingToCancel => { // Wait until t2 or if TxRefund is seen // If Bob has refunded the Alice should extract Bob's monero secret key and move // the TxLockXmr output to her wallet. @@ -330,7 +424,7 @@ pub async fn abort(state: AliceState) -> Result { // Alice does not need to do anything to recover abort(AliceState::SafelyAborted).await } - AliceState::XmrLocked => { + AliceState::XmrLocked { state3 } => { // Alice has locked XMR // Alice watches for TxRedeem until t1 if unimplemented!("TxRedeemSeen") { @@ -338,12 +432,13 @@ pub async fn abort(state: AliceState) -> Result { abort(AliceState::BtcRedeemed).await } else if unimplemented!("T1Elapsed") { // publish TxCancel or see if it has been published - abort(AliceState::Cancelled).await + abort(AliceState::WaitingToCancel).await } else { Err(unimplemented!()) } } - AliceState::Cancelled => { + AliceState::EncSignLearned { .. } => todo!(), + AliceState::WaitingToCancel => { // Alice has cancelled the swap // Alice waits watches for t2 or TxRefund if unimplemented!("TxRefundSeen") { From bff83bbe99f4f4059b77bb65ea08b05aef0891bf Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 24 Nov 2020 14:42:51 +1100 Subject: [PATCH 09/27] Progress on Cancellation path --- swap/src/alice.rs | 146 +++++++++++++++++++++++++++++++---------- swap/src/bitcoin.rs | 7 ++ xmr-btc/src/bitcoin.rs | 10 +++ 3 files changed, 130 insertions(+), 33 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 2b28c3d3..a05fe27b 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -20,6 +20,10 @@ use async_recursion::async_recursion; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; +use futures::{ + future::{select, Either}, + pin_mut, +}; use genawaiter::GeneratorState; use libp2p::{ core::{identity::Keypair, Multiaddr}, @@ -34,7 +38,10 @@ use tracing::{debug, info, warn}; use uuid::Uuid; use xmr_btc::{ alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0, State3}, - bitcoin::{BroadcastSignedTransaction, WatchForRawTransaction}, + bitcoin::{ + poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction, + TransactionBlockHeight, TxCancel, WatchForRawTransaction, + }, bob, monero::{CreateWalletForOutput, Transfer}, }; @@ -71,8 +78,18 @@ pub enum AliceState { encrypted_signature: EncryptedSignature, }, BtcRedeemed, + BtcCancelled { + state3: State3, + tx_cancel: TxCancel, + }, + BtcRefunded { + refund_tx: ::bitcoin::Transaction, + }, + BtcPunishable, XmrRefunded, - WaitingToCancel, + WaitingToCancel { + state3: State3, + }, Punished, SafelyAborted, } @@ -283,7 +300,7 @@ where // TODO(Franck): Insert in DB simple_swap( - AliceState::WaitingToCancel, + AliceState::WaitingToCancel { state3 }, rng, swarm, db, @@ -296,7 +313,7 @@ where // TODO(Franck): Insert in DB simple_swap( - AliceState::WaitingToCancel, + AliceState::WaitingToCancel { state3 }, rng, swarm, db, @@ -327,7 +344,7 @@ where state3, encrypted_signature, } => { - let (signed_tx_redeem, tx_redeem_txid) = { + let (signed_tx_redeem, _tx_redeem_txid) = { let adaptor = Adaptor::>::default(); let tx_redeem = bitcoin::TxRedeem::new(&state3.tx_lock, &state3.redeem_address); @@ -374,32 +391,92 @@ where ) .await } - AliceState::WaitingToCancel => { - // Wait until t2 or if TxRefund is seen - // If Bob has refunded the Alice should extract Bob's monero secret key and move - // the TxLockXmr output to her wallet. - if unimplemented!("refunded") { - simple_swap( - AliceState::XmrRefunded, - rng, - swarm, - db, - bitcoin_wallet, - monero_wallet, - ) - .await - } else { - simple_swap( - AliceState::Punished, - rng, - swarm, - db, - bitcoin_wallet, - monero_wallet, - ) - .await + AliceState::WaitingToCancel { state3 } => { + let tx_lock_height = bitcoin_wallet + .transaction_block_height(state3.tx_lock.txid()) + .await; + poll_until_block_height_is_gte( + bitcoin_wallet.as_ref(), + tx_lock_height + state3.refund_timelock, + ) + .await; + + let tx_cancel = bitcoin::TxCancel::new( + &state3.tx_lock, + state3.refund_timelock, + state3.a.public(), + state3.B.clone(), + ); + + if let None = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await { + let sig_a = state3.a.sign(tx_cancel.digest()); + let sig_b = state3.tx_cancel_sig_bob.clone(); + + let tx_cancel = tx_cancel + .clone() + .add_signatures( + &state3.tx_lock, + (state3.a.public(), sig_a), + (state3.B.clone(), sig_b), + ) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + bitcoin_wallet.broadcast_signed_transaction(tx_cancel).await; + } + + simple_swap( + AliceState::BtcCancelled { state3, tx_cancel }, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await + } + AliceState::BtcCancelled { state3, tx_cancel } => { + let tx_cancel_height = bitcoin_wallet + .transaction_block_height(tx_cancel.txid()) + .await; + + let reached_t2 = poll_until_block_height_is_gte( + bitcoin_wallet.as_ref(), + tx_cancel_height + state3.punish_timelock, + ); + + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state3.refund_address); + let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + + pin_mut!(reached_t2); + pin_mut!(seen_refund_tx); + + match select(reached_t2, seen_refund_tx).await { + Either::Left(_) => { + simple_swap( + AliceState::BtcPunishable, + rng, + swarm, + db, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + Either::Right((refund_tx, _)) => { + simple_swap( + AliceState::BtcRefunded { refund_tx }, + rng, + swarm, + db, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } } } + AliceState::BtcRefunded { .. } => todo!(), + AliceState::BtcPunishable => todo!(), AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), AliceState::Punished => Ok(AliceState::Punished), @@ -412,11 +489,11 @@ where pub async fn abort(state: AliceState) -> Result { match state { AliceState::Started { .. } => { - // Nothing has been commited by either party, abort swap. + // Nothing has been committed by either party, abort swap. abort(AliceState::SafelyAborted).await } AliceState::Negotiated { .. } => { - // Nothing has been commited by either party, abort swap. + // Nothing has been committed by either party, abort swap. abort(AliceState::SafelyAborted).await } AliceState::BtcLocked { .. } => { @@ -432,13 +509,13 @@ pub async fn abort(state: AliceState) -> Result { abort(AliceState::BtcRedeemed).await } else if unimplemented!("T1Elapsed") { // publish TxCancel or see if it has been published - abort(AliceState::WaitingToCancel).await + abort(AliceState::WaitingToCancel { state3 }).await } else { Err(unimplemented!()) } } AliceState::EncSignLearned { .. } => todo!(), - AliceState::WaitingToCancel => { + AliceState::WaitingToCancel { state3 } => { // Alice has cancelled the swap // Alice waits watches for t2 or TxRefund if unimplemented!("TxRefundSeen") { @@ -456,6 +533,9 @@ pub async fn abort(state: AliceState) -> Result { AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), AliceState::Punished => Ok(AliceState::Punished), AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), + AliceState::BtcCancelled { .. } => todo!(), + AliceState::BtcRefunded { .. } => todo!(), + AliceState::BtcPunishable => todo!(), } } diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 7d5eb916..aefa8dae 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -153,3 +153,10 @@ impl TransactionBlockHeight for Wallet { .expect("transient errors to be retried") } } + +#[async_trait] +impl GetRawTransaction for Wallet { + async fn get_raw_transaction(&self, txid: Txid) -> Option { + todo!() + } +} diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 95b06ef2..9ee13b73 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -201,6 +201,16 @@ pub trait TransactionBlockHeight { async fn transaction_block_height(&self, txid: Txid) -> u32; } +#[async_trait] +pub trait WaitForBlockHeight { + async fn wait_for_block_height(&self, height: u32); +} + +#[async_trait] +pub trait GetRawTransaction { + async fn get_raw_transaction(&self, txid: Txid) -> Option; +} + pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { let adaptor = Adaptor::>::default(); From 4c4a10098c1d707b2dafba021a7a2b181bc8fe87 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 24 Nov 2020 16:44:27 +1100 Subject: [PATCH 10/27] Complete Refund path --- swap/src/alice.rs | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index a05fe27b..dad3453c 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -40,7 +40,7 @@ use xmr_btc::{ alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0, State3}, bitcoin::{ poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction, - TransactionBlockHeight, TxCancel, WatchForRawTransaction, + TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction, }, bob, monero::{CreateWalletForOutput, Transfer}, @@ -83,7 +83,9 @@ pub enum AliceState { tx_cancel: TxCancel, }, BtcRefunded { - refund_tx: ::bitcoin::Transaction, + tx_refund: TxRefund, + published_refund_tx: ::bitcoin::Transaction, + state3: State3, }, BtcPunishable, XmrRefunded, @@ -462,9 +464,13 @@ where ) .await } - Either::Right((refund_tx, _)) => { + Either::Right((published_refund_tx, _)) => { simple_swap( - AliceState::BtcRefunded { refund_tx }, + AliceState::BtcRefunded { + tx_refund, + published_refund_tx, + state3, + }, rng, swarm, db, @@ -475,7 +481,35 @@ where } } } - AliceState::BtcRefunded { .. } => todo!(), + AliceState::BtcRefunded { + tx_refund, + published_refund_tx, + state3, + } => { + let s_a = monero::PrivateKey { + scalar: state3.s_a.into_ed25519(), + }; + + let tx_refund_sig = tx_refund + .extract_signature_by_key(published_refund_tx, state3.a.public()) + .context("Failed to extract signature from Bitcoin refund tx")?; + let tx_refund_encsig = state3 + .a + .encsign(state3.S_b_bitcoin.clone(), tx_refund.digest()); + + let s_b = bitcoin::recover(state3.S_b_bitcoin, tx_refund_sig, tx_refund_encsig) + .context("Failed to recover Monero secret key from Bitcoin signature")?; + let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); + + let spend_key = s_a + s_b; + let view_key = state3.v; + + monero_wallet + .create_and_load_wallet_for_output(spend_key, view_key) + .await?; + + Ok(AliceState::XmrRefunded) + } AliceState::BtcPunishable => todo!(), AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), From c501d8427839fb55a292183b994440446be00165 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 24 Nov 2020 16:45:00 +1100 Subject: [PATCH 11/27] Park abort function for now --- swap/src/alice.rs | 53 ++--------------------------------------------- 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index dad3453c..ea8cd44f 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -520,57 +520,8 @@ where // State machine driver for recovery execution #[async_recursion] -pub async fn abort(state: AliceState) -> Result { - match state { - AliceState::Started { .. } => { - // Nothing has been committed by either party, abort swap. - abort(AliceState::SafelyAborted).await - } - AliceState::Negotiated { .. } => { - // Nothing has been committed by either party, abort swap. - abort(AliceState::SafelyAborted).await - } - AliceState::BtcLocked { .. } => { - // Alice has seen that Bob has locked BTC - // Alice does not need to do anything to recover - abort(AliceState::SafelyAborted).await - } - AliceState::XmrLocked { state3 } => { - // Alice has locked XMR - // Alice watches for TxRedeem until t1 - if unimplemented!("TxRedeemSeen") { - // Alice has successfully redeemed, protocol was a success - abort(AliceState::BtcRedeemed).await - } else if unimplemented!("T1Elapsed") { - // publish TxCancel or see if it has been published - abort(AliceState::WaitingToCancel { state3 }).await - } else { - Err(unimplemented!()) - } - } - AliceState::EncSignLearned { .. } => todo!(), - AliceState::WaitingToCancel { state3 } => { - // Alice has cancelled the swap - // Alice waits watches for t2 or TxRefund - if unimplemented!("TxRefundSeen") { - // Bob has refunded and leaked s_b - abort(AliceState::XmrRefunded).await - } else if unimplemented!("T1Elapsed") { - // publish TxCancel or see if it has been published - // Wait until t2 and publish TxPunish - abort(AliceState::Punished).await - } else { - Err(unimplemented!()) - } - } - AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), - AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), - AliceState::Punished => Ok(AliceState::Punished), - AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), - AliceState::BtcCancelled { .. } => todo!(), - AliceState::BtcRefunded { .. } => todo!(), - AliceState::BtcPunishable => todo!(), - } +pub async fn abort(_state: AliceState) -> Result { + todo!() } pub async fn swap( From 58ca33dd04ccd380c65d584cb7466332d0eea65a Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 24 Nov 2020 16:49:47 +1100 Subject: [PATCH 12/27] Remove warnings --- swap/src/alice.rs | 4 +++- swap/src/bitcoin.rs | 2 +- swap/src/lib.rs | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index ea8cd44f..e605ea01 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -423,7 +423,9 @@ where ) .expect("sig_{a,b} to be valid signatures for tx_cancel"); - bitcoin_wallet.broadcast_signed_transaction(tx_cancel).await; + bitcoin_wallet + .broadcast_signed_transaction(tx_cancel) + .await?; } simple_swap( diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index aefa8dae..b22c83b7 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -156,7 +156,7 @@ impl TransactionBlockHeight for Wallet { #[async_trait] impl GetRawTransaction for Wallet { - async fn get_raw_transaction(&self, txid: Txid) -> Option { + async fn get_raw_transaction(&self, _txid: Txid) -> Option { todo!() } } diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 7f22637e..cd87f36d 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; From 8ff1824126f67d178dcc06ba183eb83b76babf17 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 25 Nov 2020 10:57:24 +1100 Subject: [PATCH 13/27] Remove abort function --- swap/src/alice.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index e605ea01..9dff090c 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -520,12 +520,6 @@ where } } -// State machine driver for recovery execution -#[async_recursion] -pub async fn abort(_state: AliceState) -> Result { - todo!() -} - pub async fn swap( bitcoin_wallet: Arc, monero_wallet: Arc, From 6437b529eb079bb57457d7a4ccb6bf6c7a233fae Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 25 Nov 2020 12:01:42 +1100 Subject: [PATCH 14/27] Punish Bob if t2 is reached --- swap/src/alice.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 9dff090c..e8701e51 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -87,7 +87,13 @@ pub enum AliceState { published_refund_tx: ::bitcoin::Transaction, state3: State3, }, - BtcPunishable, + BtcPunishable { + state3: State3, + }, + BtcPunished { + punished_tx_id: bitcoin::Txid, + state3: State3, + }, XmrRefunded, WaitingToCancel { state3: State3, @@ -457,7 +463,7 @@ where match select(reached_t2, seen_refund_tx).await { Either::Left(_) => { simple_swap( - AliceState::BtcPunishable, + AliceState::BtcPunishable { state3 }, rng, swarm, db, @@ -512,7 +518,53 @@ where Ok(AliceState::XmrRefunded) } - AliceState::BtcPunishable => todo!(), + AliceState::BtcPunished { + punished_tx_id, + state3, + } => { + // TODO(Franck): Watch for Btc refund in mempool and punish + // transaction being mined. + todo!() + } + AliceState::BtcPunishable { state3 } => { + let tx_cancel = bitcoin::TxCancel::new( + &state3.tx_lock, + state3.refund_timelock, + state3.a.public(), + state3.B.clone(), + ); + let tx_punish = + bitcoin::TxPunish::new(&tx_cancel, &state3.punish_address, state3.punish_timelock); + let punished_tx_id = tx_punish.txid(); + + let sig_a = state3.a.sign(tx_punish.digest()); + let sig_b = state3.tx_punish_sig_bob.clone(); + + let signed_tx_punish = tx_punish + .add_signatures( + &tx_cancel, + (state3.a.public(), sig_a), + (state3.B.clone(), sig_b), + ) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + let _ = bitcoin_wallet + .broadcast_signed_transaction(signed_tx_punish) + .await?; + + simple_swap( + AliceState::BtcPunished { + punished_tx_id, + state3, + }, + rng, + swarm, + db, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), AliceState::Punished => Ok(AliceState::Punished), From 6c0df836caf45b1859f4d3a59e85079956b87b06 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 25 Nov 2020 12:22:52 +1100 Subject: [PATCH 15/27] Deal with Btc Punished. --- swap/src/alice.rs | 58 +++++++++++++++++++++++++++++++++++++-------- swap/src/bitcoin.rs | 7 ++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index e8701e51..87efdecc 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -41,6 +41,7 @@ use xmr_btc::{ bitcoin::{ poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction, TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction, + WatchForTransactionFinality, }, bob, monero::{CreateWalletForOutput, Transfer}, @@ -88,9 +89,11 @@ pub enum AliceState { state3: State3, }, BtcPunishable { + tx_refund: TxRefund, state3: State3, }, BtcPunished { + tx_refund: TxRefund, punished_tx_id: bitcoin::Txid, state3: State3, }, @@ -463,7 +466,7 @@ where match select(reached_t2, seen_refund_tx).await { Either::Left(_) => { simple_swap( - AliceState::BtcPunishable { state3 }, + AliceState::BtcPunishable { tx_refund, state3 }, rng, swarm, db, @@ -518,15 +521,7 @@ where Ok(AliceState::XmrRefunded) } - AliceState::BtcPunished { - punished_tx_id, - state3, - } => { - // TODO(Franck): Watch for Btc refund in mempool and punish - // transaction being mined. - todo!() - } - AliceState::BtcPunishable { state3 } => { + AliceState::BtcPunishable { tx_refund, state3 } => { let tx_cancel = bitcoin::TxCancel::new( &state3.tx_lock, state3.refund_timelock, @@ -554,6 +549,7 @@ where simple_swap( AliceState::BtcPunished { + tx_refund, punished_tx_id, state3, }, @@ -565,6 +561,48 @@ where ) .await } + AliceState::BtcPunished { + punished_tx_id, + tx_refund, + state3, + } => { + let punish_tx_finalised = bitcoin_wallet.watch_for_transaction_finality(punished_tx_id); + + let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + + pin_mut!(punish_tx_finalised); + pin_mut!(refund_tx_seen); + + match select(punish_tx_finalised, refund_tx_seen).await { + Either::Left(_) => { + simple_swap( + AliceState::Punished, + rng, + swarm, + db, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + Either::Right((published_refund_tx, _)) => { + simple_swap( + AliceState::BtcRefunded { + tx_refund, + published_refund_tx, + state3, + }, + rng, + swarm, + db, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + } + } + AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), AliceState::Punished => Ok(AliceState::Punished), diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index b22c83b7..4661d179 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -160,3 +160,10 @@ impl GetRawTransaction for Wallet { todo!() } } + +#[async_trait] +impl WatchForTransactionFinality for Wallet { + async fn watch_for_transaction_finality(&self, _txid: Txid) { + todo!() + } +} From 66866f8fbd5c14ef5f68bb0230337fa5eba2750c Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 25 Nov 2020 14:37:37 +1100 Subject: [PATCH 16/27] Remove rng argument --- swap/src/alice.rs | 50 ++++++++++++++++++++++---------------------- xmr-btc/src/alice.rs | 19 ++++++++++------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 87efdecc..362cf396 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -43,7 +43,7 @@ use xmr_btc::{ TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction, WatchForTransactionFinality, }, - bob, + bob, cross_curve_dleq, monero::{CreateWalletForOutput, Transfer}, }; @@ -53,11 +53,18 @@ mod message1; mod message2; mod message3; +trait Rng: RngCore + CryptoRng + Send {} + +impl Rng for T where T: RngCore + CryptoRng + Send {} + // The same data structure is used for swap execution and recovery. // This allows for a seamless transition from a failed swap to recovery. pub enum AliceState { Started { amounts: SwapAmounts, + a: bitcoin::SecretKey, + s_a: cross_curve_dleq::Scalar, + v_a: monero::PrivateViewKey, }, Negotiated { swap_id: Uuid, @@ -107,21 +114,20 @@ pub enum AliceState { // State machine driver for swap execution #[async_recursion] -pub async fn simple_swap( +pub async fn simple_swap( state: AliceState, - // TODO: Would it make it better if it's in the `Started` enum variant so we don't carry it - // along? - rng: &mut R, mut swarm: Swarm, db: Database, bitcoin_wallet: Arc, monero_wallet: Arc, -) -> Result -where - R: RngCore + CryptoRng + Send, -{ +) -> Result { match state { - AliceState::Started { amounts } => { + AliceState::Started { + amounts, + a, + s_a, + v_a, + } => { // Bob dials us let bob_peer_id = match swarm.next().await { OutEvent::ConnectionEstablished(bob_peer_id) => bob_peer_id, @@ -149,7 +155,9 @@ where let punish_address = redeem_address.clone(); let state0 = State0::new( - rng, + a, + s_a, + v_a, btc, xmr, REFUND_TIMELOCK, @@ -204,7 +212,6 @@ where channel, amounts, }, - rng, swarm, db, bitcoin_wallet, @@ -238,7 +245,6 @@ where amounts, state3, }, - rng, swarm, db, bitcoin_wallet, @@ -280,7 +286,6 @@ where simple_swap( AliceState::XmrLocked { state3 }, - rng, swarm, db, bitcoin_wallet, @@ -312,7 +317,6 @@ where simple_swap( AliceState::WaitingToCancel { state3 }, - rng, swarm, db, bitcoin_wallet, @@ -325,7 +329,6 @@ where simple_swap( AliceState::WaitingToCancel { state3 }, - rng, swarm, db, bitcoin_wallet, @@ -341,7 +344,6 @@ where state3, encrypted_signature, }, - rng, swarm, db, bitcoin_wallet, @@ -394,7 +396,6 @@ where simple_swap( AliceState::BtcRedeemed, - rng, swarm, db, bitcoin_wallet, @@ -439,7 +440,6 @@ where simple_swap( AliceState::BtcCancelled { state3, tx_cancel }, - rng, swarm, db, bitcoin_wallet, @@ -467,7 +467,6 @@ where Either::Left(_) => { simple_swap( AliceState::BtcPunishable { tx_refund, state3 }, - rng, swarm, db, bitcoin_wallet.clone(), @@ -482,7 +481,6 @@ where published_refund_tx, state3, }, - rng, swarm, db, bitcoin_wallet.clone(), @@ -553,7 +551,6 @@ where punished_tx_id, state3, }, - rng, swarm, db, bitcoin_wallet.clone(), @@ -577,7 +574,6 @@ where Either::Left(_) => { simple_swap( AliceState::Punished, - rng, swarm, db, bitcoin_wallet.clone(), @@ -592,7 +588,6 @@ where published_refund_tx, state3, }, - rng, swarm, db, bitcoin_wallet.clone(), @@ -677,8 +672,13 @@ pub async fn swap( // 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( - rng, + a, + s_a, + v_a, btc, xmr, REFUND_TIMELOCK, diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 87a0a051..dc5cc9c6 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -416,8 +416,14 @@ impl State { redeem_address: bitcoin::Address, punish_address: bitcoin::Address, ) -> Self { + let a = bitcoin::SecretKey::new_random(rng); + let s_a = cross_curve_dleq::Scalar::random(rng); + let v_a = monero::PrivateViewKey::new_random(rng); + Self::State0(State0::new( - rng, + a, + s_a, + v_a, btc, xmr, refund_timelock, @@ -443,8 +449,10 @@ pub struct State0 { } impl State0 { - pub fn new( - rng: &mut R, + pub fn new( + a: bitcoin::SecretKey, + s_a: cross_curve_dleq::Scalar, + v_a: monero::PrivateViewKey, btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -452,11 +460,6 @@ impl State0 { redeem_address: bitcoin::Address, punish_address: bitcoin::Address, ) -> Self { - let a = bitcoin::SecretKey::new_random(rng); - - let s_a = cross_curve_dleq::Scalar::random(rng); - let v_a = monero::PrivateViewKey::new_random(rng); - Self { a, s_a, From 437c1cbb80c7b5f45871488af32494b6fe612bba Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 25 Nov 2020 16:16:04 +1100 Subject: [PATCH 17/27] Started to extract some steps Tealised that the whole point is for them to be idempotent to be useful --- swap/src/alice.rs | 154 +++++------------------------------- swap/src/alice/execution.rs | 119 ++++++++++++++++++++++++++++ swap/src/bitcoin.rs | 4 +- xmr-btc/src/bitcoin.rs | 7 +- 4 files changed, 143 insertions(+), 141 deletions(-) create mode 100644 swap/src/alice/execution.rs diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 362cf396..d4ca7ece 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -2,6 +2,7 @@ //! Alice holds XMR and wishes receive BTC. use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use crate::{ + alice::execution::{lock_xmr, negotiate}, bitcoin, bitcoin::{EncryptedSignature, TX_LOCK_MINE_TIMEOUT}, monero, @@ -15,7 +16,7 @@ use crate::{ storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, Context, Result}; use async_recursion::async_recursion; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; @@ -40,14 +41,15 @@ use xmr_btc::{ alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0, State3}, bitcoin::{ poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction, - TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction, - WatchForTransactionFinality, + TransactionBlockHeight, TxCancel, TxRefund, WaitForTransactionFinality, + WatchForRawTransaction, }, bob, cross_curve_dleq, monero::{CreateWalletForOutput, Transfer}, }; mod amounts; +mod execution; mod message0; mod message1; mod message2; @@ -67,13 +69,11 @@ pub enum AliceState { v_a: monero::PrivateViewKey, }, Negotiated { - swap_id: Uuid, channel: ResponseChannel, amounts: SwapAmounts, state3: State3, }, BtcLocked { - swap_id: Uuid, channel: ResponseChannel, amounts: SwapAmounts, state3: State3, @@ -117,7 +117,6 @@ pub enum AliceState { pub async fn simple_swap( state: AliceState, mut swarm: Swarm, - db: Database, bitcoin_wallet: Arc, monero_wallet: Arc, ) -> Result { @@ -128,166 +127,62 @@ pub async fn simple_swap( s_a, v_a, } => { - // Bob dials us - let bob_peer_id = match swarm.next().await { - OutEvent::ConnectionEstablished(bob_peer_id) => bob_peer_id, - other => bail!("Unexpected event received: {:?}", other), - }; - - // Bob sends us a request - let (btc, channel) = match swarm.next().await { - OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => (btc, channel), - other => bail!("Unexpected event received: {:?}", other), - }; - - if btc != amounts.btc { - bail!( - "Bob proposed a different amount; got {}, expected: {}", - btc, - amounts.btc - ); - } - 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(); - - let state0 = State0::new( - a, - s_a, - v_a, - btc, - xmr, - REFUND_TIMELOCK, - PUNISH_TIMELOCK, - redeem_address, - punish_address, - ); - - // Bob sends us message0 - let message0 = match swarm.next().await { - OutEvent::Message0(msg) => msg, - other => bail!("Unexpected event received: {:?}", other), - }; - - let state1 = state0.receive(message0)?; - - // TODO(Franck) We should use the same channel everytime, - // Can we remove this response channel? - let (state2, channel) = match swarm.next().await { - OutEvent::Message1 { msg, channel } => { - let state2 = state1.receive(msg); - (state2, channel) - } - other => bail!("Unexpected event: {:?}", other), - }; - - let message1 = state2.next_message(); - swarm.send_message1(channel, message1); - - let (state3, channel) = match swarm.next().await { - OutEvent::Message2 { msg, channel } => { - let state3 = state2.receive(msg)?; - (state3, channel) - } - other => bail!("Unexpected event: {:?}", other), - }; - - let swap_id = Uuid::new_v4(); - // TODO(Franck): Use the same terminology (negotiated) to describe this state. - db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into()) - .await?; - - info!( - "State transitioned from Started to Negotiated, Bob peer id is {}", - bob_peer_id - ); + let (channel, amounts, state3) = + negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?; simple_swap( AliceState::Negotiated { - swap_id, - state3, channel, amounts, + state3, }, swarm, - db, bitcoin_wallet, monero_wallet, ) .await } AliceState::Negotiated { - swap_id, state3, channel, amounts, } => { - // TODO(1): Do a future select with watch bitcoin blockchain time - // TODO(2): Implement a proper safe expiry module timeout( Duration::from_secs(TX_LOCK_MINE_TIMEOUT), - // TODO(Franck): Need to check amount? - bitcoin_wallet.watch_for_raw_transaction(state3.tx_lock.txid()), + bitcoin_wallet.wait_for_transaction_finality(state3.tx_lock.txid()), ) .await .context("Timed out, Bob did not lock Bitcoin in time")?; - db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into()) - .await?; - simple_swap( AliceState::BtcLocked { - swap_id, channel, amounts, state3, }, swarm, - db, bitcoin_wallet, monero_wallet, ) .await } AliceState::BtcLocked { - swap_id, channel, amounts, state3, } => { - let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { - scalar: state3.s_a.into_ed25519(), - }); - - let public_spend_key = S_a + state3.S_b_monero; - let public_view_key = state3.v.public(); - - // TODO(Franck): Probably need to wait at least 1 confirmation to be sure that - // we don't wrongfully think this is done. - let (transfer_proof, _) = monero_wallet - .transfer(public_spend_key, public_view_key, amounts.xmr) - .await?; - - swarm.send_message2(channel, alice::Message2 { - tx_lock_proof: transfer_proof, - }); - - // TODO(Franck): we should merge state::Alice and AliceState. - // There should be only 2 states: - // 1. the cryptographic state (State0, etc) which only aware of the crypto - // primitive to execute the protocol 2. the more general/business - // state that contains the crypto + other business data such as network - // communication, amounts to verify, swap id, etc. - db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into()) - .await?; + lock_xmr( + channel, + amounts, + state3.clone(), + &mut swarm, + monero_wallet.clone(), + ) + .await?; simple_swap( AliceState::XmrLocked { state3 }, swarm, - db, bitcoin_wallet, monero_wallet, ) @@ -295,8 +190,7 @@ pub async fn simple_swap( } AliceState::XmrLocked { state3 } => { let encsig = timeout( - // TODO(Franck): This is now inefficient as time has been spent since btc was - // locked + // Give a set arbitrary time to Bob to send us `tx_redeem_encsign` Duration::from_secs(TX_LOCK_MINE_TIMEOUT), async { match swarm.next().await { @@ -318,7 +212,6 @@ pub async fn simple_swap( simple_swap( AliceState::WaitingToCancel { state3 }, swarm, - db, bitcoin_wallet, monero_wallet, ) @@ -330,7 +223,6 @@ pub async fn simple_swap( simple_swap( AliceState::WaitingToCancel { state3 }, swarm, - db, bitcoin_wallet, monero_wallet, ) @@ -345,7 +237,6 @@ pub async fn simple_swap( encrypted_signature, }, swarm, - db, bitcoin_wallet, monero_wallet, ) @@ -397,7 +288,6 @@ pub async fn simple_swap( simple_swap( AliceState::BtcRedeemed, swarm, - db, bitcoin_wallet, monero_wallet, ) @@ -441,7 +331,6 @@ pub async fn simple_swap( simple_swap( AliceState::BtcCancelled { state3, tx_cancel }, swarm, - db, bitcoin_wallet, monero_wallet, ) @@ -468,7 +357,6 @@ pub async fn simple_swap( simple_swap( AliceState::BtcPunishable { tx_refund, state3 }, swarm, - db, bitcoin_wallet.clone(), monero_wallet, ) @@ -482,7 +370,6 @@ pub async fn simple_swap( state3, }, swarm, - db, bitcoin_wallet.clone(), monero_wallet, ) @@ -552,7 +439,6 @@ pub async fn simple_swap( state3, }, swarm, - db, bitcoin_wallet.clone(), monero_wallet, ) @@ -563,7 +449,7 @@ pub async fn simple_swap( tx_refund, state3, } => { - let punish_tx_finalised = bitcoin_wallet.watch_for_transaction_finality(punished_tx_id); + let punish_tx_finalised = bitcoin_wallet.wait_for_transaction_finality(punished_tx_id); let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); @@ -575,7 +461,6 @@ pub async fn simple_swap( simple_swap( AliceState::Punished, swarm, - db, bitcoin_wallet.clone(), monero_wallet, ) @@ -589,7 +474,6 @@ pub async fn simple_swap( state3, }, swarm, - db, bitcoin_wallet.clone(), monero_wallet, ) diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs new file mode 100644 index 00000000..e798c94d --- /dev/null +++ b/swap/src/alice/execution.rs @@ -0,0 +1,119 @@ +use crate::{ + alice::{amounts, OutEvent, Swarm}, + network::request_response::AliceToBob, + SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, +}; +use anyhow::{bail, Result}; +use libp2p::request_response::ResponseChannel; +use std::sync::Arc; +use xmr_btc::{ + alice, + alice::{State0, State3}, + cross_curve_dleq, + monero::Transfer, +}; + +// TODO(Franck): Make all methods here idempotent using db + +pub async fn negotiate( + amounts: SwapAmounts, + a: crate::bitcoin::SecretKey, + s_a: cross_curve_dleq::Scalar, + v_a: crate::monero::PrivateViewKey, + swarm: &mut Swarm, + bitcoin_wallet: Arc, +) -> Result<(ResponseChannel, SwapAmounts, State3)> { + // Bob dials us + match swarm.next().await { + OutEvent::ConnectionEstablished(_bob_peer_id) => {} + other => bail!("Unexpected event received: {:?}", other), + }; + + // Bob sends us a request + let (btc, channel) = match swarm.next().await { + OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => (btc, channel), + other => bail!("Unexpected event received: {:?}", other), + }; + + if btc != amounts.btc { + bail!( + "Bob proposed a different amount; got {}, expected: {}", + btc, + amounts.btc + ); + } + 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(); + + let state0 = State0::new( + a, + s_a, + v_a, + btc, + xmr, + REFUND_TIMELOCK, + PUNISH_TIMELOCK, + redeem_address, + punish_address, + ); + + // Bob sends us message0 + let message0 = match swarm.next().await { + OutEvent::Message0(msg) => msg, + other => bail!("Unexpected event received: {:?}", other), + }; + + let state1 = state0.receive(message0)?; + + let (state2, channel) = match swarm.next().await { + OutEvent::Message1 { msg, channel } => { + let state2 = state1.receive(msg); + (state2, channel) + } + other => bail!("Unexpected event: {:?}", other), + }; + + let message1 = state2.next_message(); + swarm.send_message1(channel, message1); + + let (state3, channel) = match swarm.next().await { + OutEvent::Message2 { msg, channel } => { + let state3 = state2.receive(msg)?; + (state3, channel) + } + other => bail!("Unexpected event: {:?}", other), + }; + + Ok((channel, amounts, state3)) +} + +pub async fn lock_xmr( + channel: ResponseChannel, + amounts: SwapAmounts, + state3: State3, + swarm: &mut Swarm, + monero_wallet: Arc, +) -> Result<()> { + let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { + scalar: state3.s_a.into_ed25519(), + }); + + let public_spend_key = S_a + state3.S_b_monero; + let public_view_key = state3.v.public(); + + let (transfer_proof, _) = monero_wallet + .transfer(public_spend_key, public_view_key, amounts.xmr) + .await?; + + swarm.send_message2(channel, alice::Message2 { + tx_lock_proof: transfer_proof, + }); + + // TODO(Franck): Wait for Monero to be mined/finalised + + Ok(()) +} diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 4661d179..91292bbb 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -162,8 +162,8 @@ impl GetRawTransaction for Wallet { } #[async_trait] -impl WatchForTransactionFinality for Wallet { - async fn watch_for_transaction_finality(&self, _txid: Txid) { +impl WaitForTransactionFinality for Wallet { + async fn wait_for_transaction_finality(&self, _txid: Txid) { todo!() } } diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 9ee13b73..bcd00b66 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -186,9 +186,8 @@ pub trait WatchForRawTransaction { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction; } -#[async_trait] -pub trait GetRawTransaction { - async fn get_raw_transaction(&self, txid: Txid) -> Result; +pub trait WaitForTransactionFinality { + async fn wait_for_transaction_finality(&self, txid: Txid); } #[async_trait] @@ -208,7 +207,7 @@ pub trait WaitForBlockHeight { #[async_trait] pub trait GetRawTransaction { - async fn get_raw_transaction(&self, txid: Txid) -> Option; + async fn get_raw_transaction(&self, txid: Txid) -> Result; } pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { From 3b005bd15cf5dacc523a3c6ef0f688ae8b9aa617 Mon Sep 17 00:00:00 2001 From: rishflab Date: Fri, 27 Nov 2020 11:30:07 +1100 Subject: [PATCH 18/27] Resolve rebase issues, restructure code and fix warnings --- swap/src/alice.rs | 518 +++--------------------- swap/src/alice/swap.rs | 472 +++++++++++++++++++++ swap/src/bin/simple_swap.rs | 18 +- swap/src/bitcoin.rs | 9 +- swap/src/bob.rs | 127 +----- swap/src/{bob_simple.rs => bob/swap.rs} | 55 +-- swap/src/io.rs | 8 - swap/src/lib.rs | 2 - xmr-btc/src/bitcoin.rs | 1 + xmr-btc/src/bob.rs | 60 --- 10 files changed, 557 insertions(+), 713 deletions(-) create mode 100644 swap/src/alice/swap.rs rename swap/src/{bob_simple.rs => bob/swap.rs} (89%) delete mode 100644 swap/src/io.rs diff --git a/swap/src/alice.rs b/swap/src/alice.rs index d4ca7ece..6a623660 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -2,9 +2,8 @@ //! Alice holds XMR and wishes receive BTC. use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use crate::{ - alice::execution::{lock_xmr, negotiate}, bitcoin, - bitcoin::{EncryptedSignature, TX_LOCK_MINE_TIMEOUT}, + bitcoin::TX_LOCK_MINE_TIMEOUT, monero, network::{ peer_tracker::{self, PeerTracker}, @@ -16,34 +15,23 @@ use crate::{ storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; -use anyhow::{anyhow, Context, Result}; -use async_recursion::async_recursion; +use anyhow::Result; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; -use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; -use futures::{ - future::{select, Either}, - pin_mut, -}; use genawaiter::GeneratorState; use libp2p::{ core::{identity::Keypair, Multiaddr}, request_response::ResponseChannel, NetworkBehaviour, PeerId, }; -use rand::{rngs::OsRng, CryptoRng, RngCore}; -use sha2::Sha256; +use rand::rngs::OsRng; use std::{sync::Arc, time::Duration}; -use tokio::{sync::Mutex, time::timeout}; +use tokio::sync::Mutex; use tracing::{debug, info, warn}; use uuid::Uuid; use xmr_btc::{ - alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0, State3}, - bitcoin::{ - poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction, - TransactionBlockHeight, TxCancel, TxRefund, WaitForTransactionFinality, - WatchForRawTransaction, - }, + alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0}, + bitcoin::BroadcastSignedTransaction, bob, cross_curve_dleq, monero::{CreateWalletForOutput, Transfer}, }; @@ -54,440 +42,7 @@ mod message0; mod message1; mod message2; mod message3; - -trait Rng: RngCore + CryptoRng + Send {} - -impl Rng for T where T: RngCore + CryptoRng + Send {} - -// The same data structure is used for swap execution and recovery. -// This allows for a seamless transition from a failed swap to recovery. -pub enum AliceState { - Started { - amounts: SwapAmounts, - a: bitcoin::SecretKey, - s_a: cross_curve_dleq::Scalar, - v_a: monero::PrivateViewKey, - }, - Negotiated { - channel: ResponseChannel, - amounts: SwapAmounts, - state3: State3, - }, - BtcLocked { - channel: ResponseChannel, - amounts: SwapAmounts, - state3: State3, - }, - XmrLocked { - state3: State3, - }, - EncSignLearned { - state3: State3, - encrypted_signature: EncryptedSignature, - }, - BtcRedeemed, - BtcCancelled { - state3: State3, - tx_cancel: TxCancel, - }, - BtcRefunded { - tx_refund: TxRefund, - published_refund_tx: ::bitcoin::Transaction, - state3: State3, - }, - BtcPunishable { - tx_refund: TxRefund, - state3: State3, - }, - BtcPunished { - tx_refund: TxRefund, - punished_tx_id: bitcoin::Txid, - state3: State3, - }, - XmrRefunded, - WaitingToCancel { - state3: State3, - }, - Punished, - SafelyAborted, -} - -// State machine driver for swap execution -#[async_recursion] -pub async fn simple_swap( - state: AliceState, - mut swarm: Swarm, - bitcoin_wallet: Arc, - monero_wallet: Arc, -) -> Result { - match state { - AliceState::Started { - amounts, - a, - s_a, - v_a, - } => { - let (channel, amounts, state3) = - negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?; - - simple_swap( - AliceState::Negotiated { - channel, - amounts, - state3, - }, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } - AliceState::Negotiated { - state3, - channel, - amounts, - } => { - timeout( - Duration::from_secs(TX_LOCK_MINE_TIMEOUT), - bitcoin_wallet.wait_for_transaction_finality(state3.tx_lock.txid()), - ) - .await - .context("Timed out, Bob did not lock Bitcoin in time")?; - - simple_swap( - AliceState::BtcLocked { - channel, - amounts, - state3, - }, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } - AliceState::BtcLocked { - channel, - amounts, - state3, - } => { - lock_xmr( - channel, - amounts, - state3.clone(), - &mut swarm, - monero_wallet.clone(), - ) - .await?; - - simple_swap( - AliceState::XmrLocked { state3 }, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } - AliceState::XmrLocked { state3 } => { - let encsig = timeout( - // Give a set arbitrary time to Bob to send us `tx_redeem_encsign` - Duration::from_secs(TX_LOCK_MINE_TIMEOUT), - async { - match swarm.next().await { - OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig), - other => Err(anyhow!( - "Expected Bob's Bitcoin redeem encsig, got: {:?}", - other - )), - } - }, - ) - .await - .context("Timed out, Bob did not send redeem encsign in time"); - - match encsig { - Err(_timeout_error) => { - // TODO(Franck): Insert in DB - - simple_swap( - AliceState::WaitingToCancel { state3 }, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } - Ok(Err(_unexpected_msg_error)) => { - // TODO(Franck): Insert in DB - - simple_swap( - AliceState::WaitingToCancel { state3 }, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } - Ok(Ok(encrypted_signature)) => { - // TODO(Franck): Insert in DB - - simple_swap( - AliceState::EncSignLearned { - state3, - encrypted_signature, - }, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } - } - } - AliceState::EncSignLearned { - state3, - encrypted_signature, - } => { - let (signed_tx_redeem, _tx_redeem_txid) = { - let adaptor = Adaptor::>::default(); - - let tx_redeem = bitcoin::TxRedeem::new(&state3.tx_lock, &state3.redeem_address); - - bitcoin::verify_encsig( - state3.B.clone(), - state3.s_a.into_secp256k1().into(), - &tx_redeem.digest(), - &encrypted_signature, - ) - .context("Invalid encrypted signature received")?; - - let sig_a = state3.a.sign(tx_redeem.digest()); - let sig_b = adaptor - .decrypt_signature(&state3.s_a.into_secp256k1(), encrypted_signature.clone()); - - let tx = tx_redeem - .add_signatures( - &state3.tx_lock, - (state3.a.public(), sig_a), - (state3.B.clone(), sig_b), - ) - .expect("sig_{a,b} to be valid signatures for tx_redeem"); - let txid = tx.txid(); - - (tx, txid) - }; - - // TODO(Franck): Insert in db - - let _ = bitcoin_wallet - .broadcast_signed_transaction(signed_tx_redeem) - .await?; - - // TODO(Franck) Wait for confirmations - - simple_swap( - AliceState::BtcRedeemed, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } - AliceState::WaitingToCancel { state3 } => { - let tx_lock_height = bitcoin_wallet - .transaction_block_height(state3.tx_lock.txid()) - .await; - poll_until_block_height_is_gte( - bitcoin_wallet.as_ref(), - tx_lock_height + state3.refund_timelock, - ) - .await; - - let tx_cancel = bitcoin::TxCancel::new( - &state3.tx_lock, - state3.refund_timelock, - state3.a.public(), - state3.B.clone(), - ); - - if let None = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await { - let sig_a = state3.a.sign(tx_cancel.digest()); - let sig_b = state3.tx_cancel_sig_bob.clone(); - - let tx_cancel = tx_cancel - .clone() - .add_signatures( - &state3.tx_lock, - (state3.a.public(), sig_a), - (state3.B.clone(), sig_b), - ) - .expect("sig_{a,b} to be valid signatures for tx_cancel"); - - bitcoin_wallet - .broadcast_signed_transaction(tx_cancel) - .await?; - } - - simple_swap( - AliceState::BtcCancelled { state3, tx_cancel }, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } - AliceState::BtcCancelled { state3, tx_cancel } => { - let tx_cancel_height = bitcoin_wallet - .transaction_block_height(tx_cancel.txid()) - .await; - - let reached_t2 = poll_until_block_height_is_gte( - bitcoin_wallet.as_ref(), - tx_cancel_height + state3.punish_timelock, - ); - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state3.refund_address); - let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); - - pin_mut!(reached_t2); - pin_mut!(seen_refund_tx); - - match select(reached_t2, seen_refund_tx).await { - Either::Left(_) => { - simple_swap( - AliceState::BtcPunishable { tx_refund, state3 }, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - ) - .await - } - Either::Right((published_refund_tx, _)) => { - simple_swap( - AliceState::BtcRefunded { - tx_refund, - published_refund_tx, - state3, - }, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - ) - .await - } - } - } - AliceState::BtcRefunded { - tx_refund, - published_refund_tx, - state3, - } => { - let s_a = monero::PrivateKey { - scalar: state3.s_a.into_ed25519(), - }; - - let tx_refund_sig = tx_refund - .extract_signature_by_key(published_refund_tx, state3.a.public()) - .context("Failed to extract signature from Bitcoin refund tx")?; - let tx_refund_encsig = state3 - .a - .encsign(state3.S_b_bitcoin.clone(), tx_refund.digest()); - - let s_b = bitcoin::recover(state3.S_b_bitcoin, tx_refund_sig, tx_refund_encsig) - .context("Failed to recover Monero secret key from Bitcoin signature")?; - let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); - - let spend_key = s_a + s_b; - let view_key = state3.v; - - monero_wallet - .create_and_load_wallet_for_output(spend_key, view_key) - .await?; - - Ok(AliceState::XmrRefunded) - } - AliceState::BtcPunishable { tx_refund, state3 } => { - let tx_cancel = bitcoin::TxCancel::new( - &state3.tx_lock, - state3.refund_timelock, - state3.a.public(), - state3.B.clone(), - ); - let tx_punish = - bitcoin::TxPunish::new(&tx_cancel, &state3.punish_address, state3.punish_timelock); - let punished_tx_id = tx_punish.txid(); - - let sig_a = state3.a.sign(tx_punish.digest()); - let sig_b = state3.tx_punish_sig_bob.clone(); - - let signed_tx_punish = tx_punish - .add_signatures( - &tx_cancel, - (state3.a.public(), sig_a), - (state3.B.clone(), sig_b), - ) - .expect("sig_{a,b} to be valid signatures for tx_cancel"); - - let _ = bitcoin_wallet - .broadcast_signed_transaction(signed_tx_punish) - .await?; - - simple_swap( - AliceState::BtcPunished { - tx_refund, - punished_tx_id, - state3, - }, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - ) - .await - } - AliceState::BtcPunished { - punished_tx_id, - tx_refund, - state3, - } => { - let punish_tx_finalised = bitcoin_wallet.wait_for_transaction_finality(punished_tx_id); - - let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); - - pin_mut!(punish_tx_finalised); - pin_mut!(refund_tx_seen); - - match select(punish_tx_finalised, refund_tx_seen).await { - Either::Left(_) => { - simple_swap( - AliceState::Punished, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - ) - .await - } - Either::Right((published_refund_tx, _)) => { - simple_swap( - AliceState::BtcRefunded { - tx_refund, - published_refund_tx, - state3, - }, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - ) - .await - } - } - } - - AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), - AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), - AliceState::Punished => Ok(AliceState::Punished), - AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), - } -} +pub mod swap; pub async fn swap( bitcoin_wallet: Arc, @@ -497,7 +52,25 @@ pub async fn swap( transport: SwapTransport, behaviour: Behaviour, ) -> Result<()> { - struct Network(Arc>); + 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`. @@ -508,7 +81,7 @@ pub async fn swap( struct UnexpectedMessage; let encsig = (|| async { - let mut guard = self.0.lock().await; + let mut guard = self.swarm.lock().await; let encsig = match guard.next().await { OutEvent::Message3(msg) => msg.tx_redeem_encsig, other => { @@ -602,8 +175,11 @@ pub async fn swap( let msg = state2.next_message(); swarm.send_message1(channel, msg); - let state3 = match swarm.next().await { - OutEvent::Message2(msg) => state2.receive(msg)?, + let (state3, channel) = match swarm.next().await { + OutEvent::Message2 { msg, channel } => { + let state3 = state2.receive(msg)?; + (state3, channel) + } other => panic!("Unexpected event: {:?}", other), }; @@ -613,10 +189,13 @@ pub async fn swap( info!("Handshake complete, we now have State3 for Alice."); - let network = Arc::new(Mutex::new(Network(unimplemented!()))); + let network = Arc::new(Mutex::new(Network { + swarm: Arc::new(Mutex::new(swarm)), + channel: Some(channel), + })); let mut action_generator = action_generator( - network, + network.clone(), bitcoin_wallet.clone(), state3.clone(), TX_LOCK_MINE_TIMEOUT, @@ -636,12 +215,16 @@ pub async fn swap( db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into()) .await?; - let _ = monero_wallet + 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)) => { @@ -728,7 +311,10 @@ pub enum OutEvent { msg: bob::Message1, channel: ResponseChannel, }, - Message2(bob::Message2), + Message2 { + msg: bob::Message2, + channel: ResponseChannel, + }, Message3(bob::Message3), } @@ -767,7 +353,7 @@ impl From for OutEvent { impl From for OutEvent { fn from(event: message2::OutEvent) -> Self { match event { - message2::OutEvent::Msg { msg, .. } => OutEvent::Message2(msg), + message2::OutEvent::Msg { msg, channel } => OutEvent::Message2 { msg, channel }, } } } @@ -827,6 +413,16 @@ impl Behaviour { self.message1.send(channel, msg); debug!("Sent Message1"); } + + /// Send Message2 to Bob in response to receiving his Message2. + pub fn send_message2( + &mut self, + channel: ResponseChannel, + msg: xmr_btc::alice::Message2, + ) { + self.message2.send(channel, msg); + debug!("Sent Message2"); + } } impl Default for Behaviour { diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs new file mode 100644 index 00000000..25655f30 --- /dev/null +++ b/swap/src/alice/swap.rs @@ -0,0 +1,472 @@ +//! Run an XMR/BTC swap in the role of Alice. +//! Alice holds XMR and wishes receive BTC. +use crate::{ + alice::{ + execution::{lock_xmr, negotiate}, + OutEvent, Swarm, + }, + bitcoin, + bitcoin::{EncryptedSignature, TX_LOCK_MINE_TIMEOUT}, + monero, + network::request_response::AliceToBob, + SwapAmounts, +}; +use anyhow::{anyhow, Context, Result}; +use async_recursion::async_recursion; + +use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; +use futures::{ + future::{select, Either}, + pin_mut, +}; + +use libp2p::request_response::ResponseChannel; +use rand::{CryptoRng, RngCore}; +use sha2::Sha256; +use std::{sync::Arc, time::Duration}; +use tokio::time::timeout; + +use xmr_btc::{ + alice::State3, + bitcoin::{ + poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction, + TransactionBlockHeight, TxCancel, TxRefund, WaitForTransactionFinality, + WatchForRawTransaction, + }, + cross_curve_dleq, + monero::CreateWalletForOutput, +}; + +trait Rng: RngCore + CryptoRng + Send {} + +impl Rng for T where T: RngCore + CryptoRng + Send {} + +// The same data structure is used for swap execution and recovery. +// This allows for a seamless transition from a failed swap to recovery. +pub enum AliceState { + Started { + amounts: SwapAmounts, + a: bitcoin::SecretKey, + s_a: cross_curve_dleq::Scalar, + v_a: monero::PrivateViewKey, + }, + Negotiated { + channel: ResponseChannel, + amounts: SwapAmounts, + state3: State3, + }, + BtcLocked { + channel: ResponseChannel, + amounts: SwapAmounts, + state3: State3, + }, + XmrLocked { + state3: State3, + }, + EncSignLearned { + state3: State3, + encrypted_signature: EncryptedSignature, + }, + BtcRedeemed, + BtcCancelled { + state3: State3, + tx_cancel: TxCancel, + }, + BtcRefunded { + tx_refund: TxRefund, + published_refund_tx: ::bitcoin::Transaction, + state3: State3, + }, + BtcPunishable { + tx_refund: TxRefund, + state3: State3, + }, + BtcPunished { + tx_refund: TxRefund, + punished_tx_id: bitcoin::Txid, + state3: State3, + }, + XmrRefunded, + WaitingToCancel { + state3: State3, + }, + Punished, + SafelyAborted, +} + +// State machine driver for swap execution +#[async_recursion] +pub async fn swap( + state: AliceState, + mut swarm: Swarm, + bitcoin_wallet: Arc, + monero_wallet: Arc, +) -> Result { + match state { + AliceState::Started { + amounts, + a, + s_a, + v_a, + } => { + let (channel, amounts, state3) = + negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?; + + swap( + AliceState::Negotiated { + channel, + amounts, + state3, + }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + AliceState::Negotiated { + state3, + channel, + amounts, + } => { + timeout( + Duration::from_secs(TX_LOCK_MINE_TIMEOUT), + bitcoin_wallet.wait_for_transaction_finality(state3.tx_lock.txid()), + ) + .await + .context("Timed out, Bob did not lock Bitcoin in time")?; + + swap( + AliceState::BtcLocked { + channel, + amounts, + state3, + }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + AliceState::BtcLocked { + channel, + amounts, + state3, + } => { + lock_xmr( + channel, + amounts, + state3.clone(), + &mut swarm, + monero_wallet.clone(), + ) + .await?; + + swap( + AliceState::XmrLocked { state3 }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + AliceState::XmrLocked { state3 } => { + let encsig = timeout( + // Give a set arbitrary time to Bob to send us `tx_redeem_encsign` + Duration::from_secs(TX_LOCK_MINE_TIMEOUT), + async { + match swarm.next().await { + OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig), + other => Err(anyhow!( + "Expected Bob's Bitcoin redeem encsig, got: {:?}", + other + )), + } + }, + ) + .await + .context("Timed out, Bob did not send redeem encsign in time"); + + match encsig { + Err(_timeout_error) => { + // TODO(Franck): Insert in DB + + swap( + AliceState::WaitingToCancel { state3 }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + Ok(Err(_unexpected_msg_error)) => { + // TODO(Franck): Insert in DB + + swap( + AliceState::WaitingToCancel { state3 }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + Ok(Ok(encrypted_signature)) => { + // TODO(Franck): Insert in DB + + swap( + AliceState::EncSignLearned { + state3, + encrypted_signature, + }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + } + } + AliceState::EncSignLearned { + state3, + encrypted_signature, + } => { + let (signed_tx_redeem, _tx_redeem_txid) = { + let adaptor = Adaptor::>::default(); + + let tx_redeem = bitcoin::TxRedeem::new(&state3.tx_lock, &state3.redeem_address); + + bitcoin::verify_encsig( + state3.B.clone(), + state3.s_a.into_secp256k1().into(), + &tx_redeem.digest(), + &encrypted_signature, + ) + .context("Invalid encrypted signature received")?; + + let sig_a = state3.a.sign(tx_redeem.digest()); + let sig_b = adaptor + .decrypt_signature(&state3.s_a.into_secp256k1(), encrypted_signature.clone()); + + let tx = tx_redeem + .add_signatures( + &state3.tx_lock, + (state3.a.public(), sig_a), + (state3.B.clone(), sig_b), + ) + .expect("sig_{a,b} to be valid signatures for tx_redeem"); + let txid = tx.txid(); + + (tx, txid) + }; + + // TODO(Franck): Insert in db + + let _ = bitcoin_wallet + .broadcast_signed_transaction(signed_tx_redeem) + .await?; + + // TODO(Franck) Wait for confirmations + + swap( + AliceState::BtcRedeemed, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + AliceState::WaitingToCancel { state3 } => { + let tx_lock_height = bitcoin_wallet + .transaction_block_height(state3.tx_lock.txid()) + .await; + poll_until_block_height_is_gte( + bitcoin_wallet.as_ref(), + tx_lock_height + state3.refund_timelock, + ) + .await; + + let tx_cancel = bitcoin::TxCancel::new( + &state3.tx_lock, + state3.refund_timelock, + state3.a.public(), + state3.B.clone(), + ); + + if let Err(_e) = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await { + let sig_a = state3.a.sign(tx_cancel.digest()); + let sig_b = state3.tx_cancel_sig_bob.clone(); + + let tx_cancel = tx_cancel + .clone() + .add_signatures( + &state3.tx_lock, + (state3.a.public(), sig_a), + (state3.B.clone(), sig_b), + ) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + bitcoin_wallet + .broadcast_signed_transaction(tx_cancel) + .await?; + } + + swap( + AliceState::BtcCancelled { state3, tx_cancel }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + AliceState::BtcCancelled { state3, tx_cancel } => { + let tx_cancel_height = bitcoin_wallet + .transaction_block_height(tx_cancel.txid()) + .await; + + let reached_t2 = poll_until_block_height_is_gte( + bitcoin_wallet.as_ref(), + tx_cancel_height + state3.punish_timelock, + ); + + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state3.refund_address); + let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + + pin_mut!(reached_t2); + pin_mut!(seen_refund_tx); + + match select(reached_t2, seen_refund_tx).await { + Either::Left(_) => { + swap( + AliceState::BtcPunishable { tx_refund, state3 }, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + Either::Right((published_refund_tx, _)) => { + swap( + AliceState::BtcRefunded { + tx_refund, + published_refund_tx, + state3, + }, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + } + } + AliceState::BtcRefunded { + tx_refund, + published_refund_tx, + state3, + } => { + let s_a = monero::PrivateKey { + scalar: state3.s_a.into_ed25519(), + }; + + let tx_refund_sig = tx_refund + .extract_signature_by_key(published_refund_tx, state3.a.public()) + .context("Failed to extract signature from Bitcoin refund tx")?; + let tx_refund_encsig = state3 + .a + .encsign(state3.S_b_bitcoin.clone(), tx_refund.digest()); + + let s_b = bitcoin::recover(state3.S_b_bitcoin, tx_refund_sig, tx_refund_encsig) + .context("Failed to recover Monero secret key from Bitcoin signature")?; + let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); + + let spend_key = s_a + s_b; + let view_key = state3.v; + + monero_wallet + .create_and_load_wallet_for_output(spend_key, view_key) + .await?; + + Ok(AliceState::XmrRefunded) + } + AliceState::BtcPunishable { tx_refund, state3 } => { + let tx_cancel = bitcoin::TxCancel::new( + &state3.tx_lock, + state3.refund_timelock, + state3.a.public(), + state3.B.clone(), + ); + let tx_punish = + bitcoin::TxPunish::new(&tx_cancel, &state3.punish_address, state3.punish_timelock); + let punished_tx_id = tx_punish.txid(); + + let sig_a = state3.a.sign(tx_punish.digest()); + let sig_b = state3.tx_punish_sig_bob.clone(); + + let signed_tx_punish = tx_punish + .add_signatures( + &tx_cancel, + (state3.a.public(), sig_a), + (state3.B.clone(), sig_b), + ) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + let _ = bitcoin_wallet + .broadcast_signed_transaction(signed_tx_punish) + .await?; + + swap( + AliceState::BtcPunished { + tx_refund, + punished_tx_id, + state3, + }, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + AliceState::BtcPunished { + punished_tx_id, + tx_refund, + state3, + } => { + let punish_tx_finalised = bitcoin_wallet.wait_for_transaction_finality(punished_tx_id); + + let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + + pin_mut!(punish_tx_finalised); + pin_mut!(refund_tx_seen); + + match select(punish_tx_finalised, refund_tx_seen).await { + Either::Left(_) => { + swap( + AliceState::Punished, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + Either::Right((published_refund_tx, _)) => { + swap( + AliceState::BtcRefunded { + tx_refund, + published_refund_tx, + state3, + }, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + } + } + + AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), + AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), + AliceState::Punished => Ok(AliceState::Punished), + AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), + } +} diff --git a/swap/src/bin/simple_swap.rs b/swap/src/bin/simple_swap.rs index a7f0952a..776758eb 100644 --- a/swap/src/bin/simple_swap.rs +++ b/swap/src/bin/simple_swap.rs @@ -1,11 +1,6 @@ use anyhow::Result; use structopt::StructOpt; -use swap::{ - bob_simple::{simple_swap, BobState}, - cli::Options, - storage::Database, -}; -use uuid::Uuid; +use swap::{alice::swap::swap, bob::swap::BobState, cli::Options, storage::Database}; #[tokio::main] async fn main() -> Result<()> { @@ -20,16 +15,7 @@ async fn main() -> Result<()> { match opt { Options::Alice { .. } => { - simple_swap( - bob_state, - swarm, - db, - bitcoin_wallet, - monero_wallet, - rng, - Uuid::new_v4(), - ) - .await?; + swap(bob_state, swarm, bitcoin_wallet, monero_wallet).await?; } Options::Recover { .. } => { let _stored_state: BobState = unimplemented!("io.get_state(uuid)?"); diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 91292bbb..55ed05b7 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -99,7 +99,6 @@ impl BroadcastSignedTransaction for Wallet { // TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed // to `ConstantBackoff`. - #[async_trait] impl WatchForRawTransaction for Wallet { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { @@ -112,6 +111,7 @@ impl WatchForRawTransaction for Wallet { #[async_trait] impl GetRawTransaction for Wallet { + // todo: potentially replace with option async fn get_raw_transaction(&self, txid: Txid) -> Result { Ok(self.0.get_raw_transaction(txid).await?) } @@ -154,13 +154,6 @@ impl TransactionBlockHeight for Wallet { } } -#[async_trait] -impl GetRawTransaction for Wallet { - async fn get_raw_transaction(&self, _txid: Txid) -> Option { - todo!() - } -} - #[async_trait] impl WaitForTransactionFinality for Wallet { async fn wait_for_transaction_finality(&self, _txid: Txid) { diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 31a2819b..194dbb3d 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -1,7 +1,7 @@ //! Run an XMR/BTC swap in the role of Bob. //! Bob holds BTC and wishes receive XMR. use anyhow::Result; -use async_recursion::async_recursion; + use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use futures::{ @@ -21,11 +21,11 @@ mod message0; mod message1; mod message2; mod message3; +pub mod swap; use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use crate::{ bitcoin::{self, TX_LOCK_MINE_TIMEOUT}, - io::Io, monero, network::{ peer_tracker::{self, PeerTracker}, @@ -43,129 +43,6 @@ use xmr_btc::{ monero::CreateWalletForOutput, }; -// The same data structure is used for swap execution and recovery. -// This allows for a seamless transition from a failed swap to recovery. -pub enum BobState { - Started, - Negotiated, - BtcLocked, - XmrLocked, - BtcRedeemed, - BtcRefunded, - XmrRedeemed, - Cancelled, - Punished, - SafelyAborted, -} - -// State machine driver for swap execution -#[async_recursion] -pub async fn simple_swap(state: BobState, io: Io) -> Result { - match state { - BobState::Started => { - // Alice and Bob exchange swap info - // Todo: Poll the swarm here until Alice and Bob have exchanged info - simple_swap(BobState::Negotiated, io).await - } - BobState::Negotiated => { - // Alice and Bob have exchanged info - // Bob Locks Btc - simple_swap(BobState::BtcLocked, io).await - } - BobState::BtcLocked => { - // Bob has locked Btc - // Watch for Alice to Lock Xmr - simple_swap(BobState::XmrLocked, io).await - } - BobState::XmrLocked => { - // Alice has locked Xmr - // Bob sends Alice his key - // Todo: This should be a oneshot - if unimplemented!("Redeemed before t1") { - simple_swap(BobState::BtcRedeemed, io).await - } else { - // submit TxCancel - simple_swap(BobState::Cancelled, io).await - } - } - BobState::Cancelled => { - if unimplemented!(" Ok(BobState::BtcRefunded), - BobState::BtcRedeemed => { - // Bob redeems XMR using revealed s_a - simple_swap(BobState::XmrRedeemed, io).await - } - BobState::Punished => Ok(BobState::Punished), - BobState::SafelyAborted => Ok(BobState::SafelyAborted), - BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), - } -} - -// State machine driver for recovery execution -#[async_recursion] -pub async fn abort(state: BobState, io: Io) -> Result { - match state { - BobState::Started => { - // Nothing has been commited by either party, abort swap. - abort(BobState::SafelyAborted, io).await - } - BobState::Negotiated => { - // Nothing has been commited by either party, abort swap. - abort(BobState::SafelyAborted, io).await - } - BobState::BtcLocked => { - // Bob has locked BTC and must refund it - // Bob waits for alice to publish TxRedeem or t1 - if unimplemented!("TxRedeemSeen") { - // Alice has redeemed revealing s_a - abort(BobState::BtcRedeemed, io).await - } else if unimplemented!("T1Elapsed") { - // publish TxCancel or see if it has been published - abort(BobState::Cancelled, io).await - } else { - Err(unimplemented!()) - } - } - BobState::XmrLocked => { - // Alice has locked Xmr - // Wait until t1 - if unimplemented!(">t1 and t2 - // submit TxCancel - abort(BobState::Punished, io).await - } - } - BobState::Cancelled => { - // Bob has cancelled the swap - // If { - // Bob uses revealed s_a to redeem XMR - abort(BobState::XmrRedeemed, io).await - } - BobState::BtcRefunded => Ok(BobState::BtcRefunded), - BobState::Punished => Ok(BobState::Punished), - BobState::SafelyAborted => Ok(BobState::SafelyAborted), - BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), - } -} - #[allow(clippy::too_many_arguments)] pub async fn swap( bitcoin_wallet: Arc, diff --git a/swap/src/bob_simple.rs b/swap/src/bob/swap.rs similarity index 89% rename from swap/src/bob_simple.rs rename to swap/src/bob/swap.rs index db5c5b6c..a7c194a4 100644 --- a/swap/src/bob_simple.rs +++ b/swap/src/bob/swap.rs @@ -1,36 +1,24 @@ use crate::{ - bitcoin::{self}, bob::{OutEvent, Swarm}, - network::{transport::SwapTransport, TokioExecutor}, state, storage::Database, - Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, + Cmd, Rsp, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; use anyhow::Result; use async_recursion::async_recursion; -use async_trait::async_trait; -use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; + use futures::{ channel::mpsc::{Receiver, Sender}, - future::Either, - FutureExt, StreamExt, + StreamExt, }; -use genawaiter::GeneratorState; -use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; -use rand::{rngs::OsRng, CryptoRng, RngCore}; -use std::{process, sync::Arc, time::Duration}; -use tokio::sync::Mutex; -use tracing::{debug, info, warn}; + +use libp2p::PeerId; +use rand::rngs::OsRng; +use std::{process, sync::Arc}; + +use tracing::info; use uuid::Uuid; -use xmr_btc::{ - alice, - bitcoin::{ - poll_until_block_height_is_gte, BroadcastSignedTransaction, EncryptedSignature, SignTxLock, - TransactionBlockHeight, - }, - bob::{self, action_generator, ReceiveTransferProof, State0}, - monero::CreateWalletForOutput, -}; +use xmr_btc::bob::{self, State0}; // The same data structure is used for swap execution and recovery. // This allows for a seamless transition from a failed swap to recovery. @@ -50,7 +38,7 @@ pub enum BobState { // State machine driver for swap execution #[async_recursion] -pub async fn simple_swap( +pub async fn swap( state: BobState, mut swarm: Swarm, db: Database, @@ -122,7 +110,7 @@ pub async fn simple_swap( info!("Handshake complete"); - simple_swap( + swap( BobState::Negotiated(state2, alice_peer_id), swarm, db, @@ -137,7 +125,7 @@ pub async fn simple_swap( // Alice and Bob have exchanged info let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; // db.insert_latest_state(state); - simple_swap( + swap( BobState::BtcLocked(state3, alice_peer_id), swarm, db, @@ -160,7 +148,7 @@ pub async fn simple_swap( } other => panic!("unexpected event: {:?}", other), }; - simple_swap( + swap( BobState::XmrLocked(state4, alice_peer_id), swarm, db, @@ -182,7 +170,7 @@ pub async fn simple_swap( // should happen in this arm? swarm.send_message3(alice_peer_id.clone(), tx_redeem_encsig); - simple_swap( + swap( BobState::EncSigSent(state, alice_peer_id), swarm, db, @@ -200,7 +188,7 @@ pub async fn simple_swap( tokio::select! { val = redeem_watcher => { - simple_swap( + swap( BobState::BtcRedeemed(val?), swarm, db, @@ -211,14 +199,14 @@ pub async fn simple_swap( ) .await } - val = t1_timeout => { + _ = t1_timeout => { // Check whether TxCancel has been published. // We should not fail if the transaction is already on the blockchain - if let Err(_e) = state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await { - state.submit_tx_cancel(bitcoin_wallet.as_ref()).await; + if let Err(_) = state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await { + state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; } - simple_swap( + swap( BobState::Cancelled(state), swarm, db, @@ -235,7 +223,7 @@ pub async fn simple_swap( BobState::BtcRedeemed(state) => { // Bob redeems XMR using revealed s_a state.claim_xmr(monero_wallet.as_ref()).await?; - simple_swap( + swap( BobState::XmrRedeemed, swarm, db, @@ -253,6 +241,7 @@ pub async fn simple_swap( BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), } } + // // State machine driver for recovery execution // #[async_recursion] // pub async fn abort(state: BobState, io: Io) -> Result { diff --git a/swap/src/io.rs b/swap/src/io.rs deleted file mode 100644 index 5b26d09a..00000000 --- a/swap/src/io.rs +++ /dev/null @@ -1,8 +0,0 @@ -// This struct contains all the I/O required to execute a swap -pub struct Io { - // swarm: libp2p::Swarm<>, -// bitcoind_rpc: _, -// monerod_rpc: _, -// monero_wallet_rpc: _, -// db: _, -} diff --git a/swap/src/lib.rs b/swap/src/lib.rs index cd87f36d..4cf8c15e 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -6,9 +6,7 @@ use std::fmt::{self, Display}; pub mod alice; pub mod bitcoin; pub mod bob; -pub mod bob_simple; pub mod cli; -pub mod io; pub mod monero; pub mod network; pub mod recover; diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index bcd00b66..1cbb49f5 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -186,6 +186,7 @@ pub trait WatchForRawTransaction { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction; } +#[async_trait] pub trait WaitForTransactionFinality { async fn wait_for_transaction_finality(&self, txid: Txid); } diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index b0e4b4f9..50599a33 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -879,63 +879,3 @@ impl State5 { self.tx_lock.txid() } } - -/// Watch for the refund transaction on the blockchain. Watch until t2 has -/// elapsed. -pub async fn watch_for_refund_btc(state: State5, bitcoin_wallet: &W) -> Result<()> -where - W: WatchForRawTransaction, -{ - let tx_cancel = bitcoin::TxCancel::new( - &state.tx_lock, - state.refund_timelock, - state.A.clone(), - state.b.public(), - ); - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address); - - let tx_refund_watcher = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); - - Ok(()) -} - -// Watch for refund transaction on the blockchain -pub async fn watch_for_redeem_btc(state: State4, bitcoin_wallet: &W) -> Result -where - W: WatchForRawTransaction, -{ - let tx_redeem = bitcoin::TxRedeem::new(&state.tx_lock, &state.redeem_address); - let tx_redeem_encsig = state - .b - .encsign(state.S_a_bitcoin.clone(), tx_redeem.digest()); - - let tx_redeem_candidate = bitcoin_wallet - .watch_for_raw_transaction(tx_redeem.txid()) - .await; - - let tx_redeem_sig = - tx_redeem.extract_signature_by_key(tx_redeem_candidate, state.b.public())?; - let s_a = bitcoin::recover(state.S_a_bitcoin.clone(), tx_redeem_sig, tx_redeem_encsig)?; - let s_a = monero::private_key_from_secp256k1_scalar(s_a.into()); - - Ok(State5 { - A: state.A.clone(), - b: state.b.clone(), - s_a, - s_b: state.s_b, - S_a_monero: state.S_a_monero, - S_a_bitcoin: state.S_a_bitcoin.clone(), - v: state.v, - btc: state.btc, - xmr: state.xmr, - refund_timelock: state.refund_timelock, - punish_timelock: state.punish_timelock, - refund_address: state.refund_address.clone(), - redeem_address: state.redeem_address.clone(), - punish_address: state.punish_address.clone(), - tx_lock: state.tx_lock.clone(), - tx_refund_encsig: state.tx_refund_encsig.clone(), - tx_cancel_sig: state.tx_cancel_sig_a.clone(), - }) -} From dca15b6872cf5611babd3313169b0cd3318427c1 Mon Sep 17 00:00:00 2001 From: rishflab Date: Fri, 27 Nov 2020 13:50:48 +1100 Subject: [PATCH 19/27] Removed unused binary --- swap/src/bin/simple_swap.rs | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 swap/src/bin/simple_swap.rs diff --git a/swap/src/bin/simple_swap.rs b/swap/src/bin/simple_swap.rs deleted file mode 100644 index 776758eb..00000000 --- a/swap/src/bin/simple_swap.rs +++ /dev/null @@ -1,26 +0,0 @@ -use anyhow::Result; -use structopt::StructOpt; -use swap::{alice::swap::swap, bob::swap::BobState, cli::Options, storage::Database}; - -#[tokio::main] -async fn main() -> Result<()> { - let opt = Options::from_args(); - - let db = Database::open(std::path::Path::new("./.swap-db/")).unwrap(); - let swarm = unimplemented!(); - let bitcoin_wallet = unimplemented!(); - let monero_wallet = unimplemented!(); - let mut rng = unimplemented!(); - let bob_state = unimplemented!(); - - match opt { - Options::Alice { .. } => { - swap(bob_state, swarm, bitcoin_wallet, monero_wallet).await?; - } - Options::Recover { .. } => { - let _stored_state: BobState = unimplemented!("io.get_state(uuid)?"); - // abort(_stored_state, _io); - } - _ => {} - }; -} From 24631d464d82d387bef7b7cf61aef74e158a7b76 Mon Sep 17 00:00:00 2001 From: rishflab Date: Mon, 30 Nov 2020 13:25:11 +1100 Subject: [PATCH 20/27] Add test for recursive executor --- swap/src/alice.rs | 6 ++- swap/src/alice/swap.rs | 2 - swap/src/bin/swap.rs | 12 ++--- swap/src/bob.rs | 15 +++--- swap/src/bob/execution.rs | 65 ++++++++++++++++++++++ swap/src/bob/swap.rs | 110 ++++++++++---------------------------- swap/src/lib.rs | 4 +- swap/tests/e2e.rs | 86 ++++++++++++++++++++--------- xmr-btc/src/bob.rs | 2 +- 9 files changed, 175 insertions(+), 127 deletions(-) create mode 100644 swap/src/bob/execution.rs diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 6a623660..f230cb05 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -280,7 +280,11 @@ pub async fn swap( pub type Swarm = libp2p::Swarm; -fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Behaviour) -> Result { +pub fn new_swarm( + listen: Multiaddr, + transport: SwapTransport, + behaviour: Behaviour, +) -> Result { use anyhow::Context as _; let local_peer_id = behaviour.peer_id(); diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index 25655f30..fa9b5288 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -13,13 +13,11 @@ use crate::{ }; use anyhow::{anyhow, Context, Result}; use async_recursion::async_recursion; - use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; use futures::{ future::{select, Either}, pin_mut, }; - use libp2p::request_response::ResponseChannel; use rand::{CryptoRng, RngCore}; use sha2::Sha256; diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 2e24c418..c6e3d82d 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -19,9 +19,7 @@ use prettytable::{row, Table}; use std::{io, io::Write, process, sync::Arc}; use structopt::StructOpt; use swap::{ - alice::{self, Behaviour}, - bitcoin, - bob::{self, Bob}, + alice, bitcoin, bob, cli::Options, monero, network::transport::{build, build_tor, SwapTransport}, @@ -52,7 +50,7 @@ async fn main() -> Result<()> { } => { info!("running swap node as Alice ..."); - let behaviour = Behaviour::default(); + let behaviour = alice::Behaviour::default(); let local_key_pair = behaviour.identity(); let (listen_addr, _ac, transport) = match tor_port { @@ -100,7 +98,7 @@ async fn main() -> Result<()> { } => { info!("running swap node as Bob ..."); - let behaviour = Bob::default(); + let behaviour = bob::Behaviour::default(); let local_key_pair = behaviour.identity(); let transport = match tor { @@ -180,7 +178,7 @@ async fn swap_as_alice( db: Database, addr: Multiaddr, transport: SwapTransport, - behaviour: Behaviour, + behaviour: alice::Behaviour, ) -> Result<()> { alice::swap( bitcoin_wallet, @@ -200,7 +198,7 @@ async fn swap_as_bob( sats: u64, alice: Multiaddr, transport: SwapTransport, - behaviour: Bob, + behaviour: bob::Behaviour, ) -> Result<()> { let (cmd_tx, mut cmd_rx) = mpsc::channel(1); let (mut rsp_tx, rsp_rx) = mpsc::channel(1); diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 194dbb3d..f5602b8a 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -17,6 +17,7 @@ use tracing::{debug, info, warn}; use uuid::Uuid; mod amounts; +mod execution; mod message0; mod message1; mod message2; @@ -53,7 +54,7 @@ pub async fn swap( mut cmd_tx: Sender, mut rsp_rx: Receiver, transport: SwapTransport, - behaviour: Bob, + behaviour: Behaviour, ) -> Result<()> { struct Network(Swarm); @@ -236,9 +237,9 @@ pub async fn swap( } } -pub type Swarm = libp2p::Swarm; +pub type Swarm = libp2p::Swarm; -fn new_swarm(transport: SwapTransport, behaviour: Bob) -> Result { +pub fn new_swarm(transport: SwapTransport, behaviour: Behaviour) -> Result { let local_peer_id = behaviour.peer_id(); let swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) @@ -317,7 +318,7 @@ impl From for OutEvent { #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", event_process = false)] #[allow(missing_debug_implementations)] -pub struct Bob { +pub struct Behaviour { pt: PeerTracker, amounts: Amounts, message0: Message0, @@ -328,7 +329,7 @@ pub struct Bob { identity: Keypair, } -impl Bob { +impl Behaviour { pub fn identity(&self) -> Keypair { self.identity.clone() } @@ -375,8 +376,8 @@ impl Bob { } } -impl Default for Bob { - fn default() -> Bob { +impl Default for Behaviour { + fn default() -> Behaviour { let identity = Keypair::generate_ed25519(); Self { diff --git a/swap/src/bob/execution.rs b/swap/src/bob/execution.rs new file mode 100644 index 00000000..c63739ea --- /dev/null +++ b/swap/src/bob/execution.rs @@ -0,0 +1,65 @@ +use crate::{ + bob::{OutEvent, Swarm}, + Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, +}; +use anyhow::Result; +use rand::{CryptoRng, RngCore}; +use std::sync::Arc; +use tokio::{stream::StreamExt, sync::mpsc}; + +use xmr_btc::bob::{State0, State2}; + +pub async fn negotiate( + state0: xmr_btc::bob::State0, + amounts: SwapAmounts, + swarm: &mut Swarm, + mut rng: R, + bitcoin_wallet: Arc, +) -> Result<(SwapAmounts, State2)> +where + R: RngCore + CryptoRng + Send, +{ + let (mut cmd_tx, _cmd_rx) = mpsc::channel(1); + let (_rsp_tx, mut rsp_rx) = mpsc::channel(1); + + // todo: dial the swarm outside + // libp2p::Swarm::dial_addr(&mut swarm, addr)?; + let alice = match swarm.next().await { + OutEvent::ConnectionEstablished(alice) => alice, + other => panic!("unexpected event: {:?}", other), + }; + + swarm.request_amounts(alice.clone(), amounts.btc.as_sat()); + + // todo: remove/refactor mspc channel + let (btc, xmr) = match swarm.next().await { + OutEvent::Amounts(amounts) => { + let cmd = Cmd::VerifyAmounts(amounts); + cmd_tx.try_send(cmd)?; + let response = rsp_rx.next().await; + if response == Some(Rsp::Abort) { + panic!("abort response"); + } + (amounts.btc, amounts.xmr) + } + other => panic!("unexpected event: {:?}", other), + }; + + let refund_address = bitcoin_wallet.as_ref().new_address().await?; + + swarm.send_message0(alice.clone(), state0.next_message(&mut 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)?, + other => panic!("unexpected event: {:?}", other), + }; + + swarm.send_message2(alice.clone(), state2.next_message()); + + Ok((amounts, state2)) +} diff --git a/swap/src/bob/swap.rs b/swap/src/bob/swap.rs index a7c194a4..603b8203 100644 --- a/swap/src/bob/swap.rs +++ b/swap/src/bob/swap.rs @@ -1,29 +1,24 @@ use crate::{ - bob::{OutEvent, Swarm}, - state, + bob::{execution::negotiate, OutEvent, Swarm}, storage::Database, - Cmd, Rsp, PUNISH_TIMELOCK, REFUND_TIMELOCK, + SwapAmounts, }; use anyhow::Result; use async_recursion::async_recursion; - -use futures::{ - channel::mpsc::{Receiver, Sender}, - StreamExt, -}; - use libp2p::PeerId; -use rand::rngs::OsRng; -use std::{process, sync::Arc}; - -use tracing::info; +use rand::{CryptoRng, RngCore}; +use std::sync::Arc; use uuid::Uuid; -use xmr_btc::bob::{self, State0}; +use xmr_btc::bob::{self}; // The same data structure is used for swap execution and recovery. // This allows for a seamless transition from a failed swap to recovery. pub enum BobState { - Started(Sender, Receiver, u64, PeerId), + Started { + state0: bob::State0, + amounts: SwapAmounts, + peer_id: PeerId, + }, Negotiated(bob::State2, PeerId), BtcLocked(bob::State3, PeerId), XmrLocked(bob::State4, PeerId), @@ -38,80 +33,34 @@ pub enum BobState { // State machine driver for swap execution #[async_recursion] -pub async fn swap( +pub async fn swap( state: BobState, mut swarm: Swarm, db: Database, bitcoin_wallet: Arc, monero_wallet: Arc, - mut rng: OsRng, + mut rng: R, swap_id: Uuid, -) -> Result { +) -> Result +where + R: RngCore + CryptoRng + Send, +{ match state { - BobState::Started(mut cmd_tx, mut rsp_rx, btc, alice_peer_id) => { - // todo: dial the swarm outside - // 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); - - // todo: remove mspc channel - 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?; - - let state0 = State0::new( + BobState::Started { + state0, + amounts, + peer_id, + } => { + let (swap_amounts, state2) = negotiate( + state0, + amounts, + &mut swarm, &mut rng, - btc, - xmr, - REFUND_TIMELOCK, - PUNISH_TIMELOCK, - refund_address, - ); - - info!("Commencing handshake"); - - swarm.send_message0(alice.clone(), state0.next_message(&mut 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)?, - 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"); - + bitcoin_wallet.clone(), + ) + .await?; swap( - BobState::Negotiated(state2, alice_peer_id), + BobState::Negotiated(state2, peer_id), swarm, db, bitcoin_wallet, @@ -162,7 +111,6 @@ pub async fn swap( BobState::XmrLocked(state, alice_peer_id) => { // Alice has locked Xmr // Bob sends Alice his key - // let cloned = state.clone(); let tx_redeem_encsig = state.tx_redeem_encsig(); // Do we have to wait for a response? // What if Alice fails to receive this? Should we always resend? diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 4cf8c15e..68fb45c4 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -14,8 +14,8 @@ pub mod state; pub mod storage; pub mod tor; -const REFUND_TIMELOCK: u32 = 10; // Relative timelock, this is number of blocks. TODO: What should it be? -const PUNISH_TIMELOCK: u32 = 10; // FIXME: What should this be? +pub const REFUND_TIMELOCK: u32 = 10; // Relative timelock, this is number of blocks. TODO: What should it be? +pub const PUNISH_TIMELOCK: u32 = 10; // FIXME: What should this be? pub type Never = std::convert::Infallible; diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 5abbe191..7b318b6c 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -1,22 +1,23 @@ use bitcoin_harness::Bitcoind; -use futures::{channel::mpsc, future::try_join}; +use futures::{ + channel::{ + mpsc, + mpsc::{Receiver, Sender}, + }, + future::try_join, +}; use libp2p::Multiaddr; use monero_harness::Monero; use rand::rngs::OsRng; use std::sync::Arc; use swap::{ - alice, bob, bob::new_swarm, bob_simple, bob_simple::BobState, network::transport::build, - storage::Database, + alice, alice::swap::AliceState, bob, bob::swap::BobState, network::transport::build, + storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; use tempfile::tempdir; use testcontainers::clients::Cli; use uuid::Uuid; -use xmr_btc::bitcoin; - -// NOTE: For some reason running these tests overflows the stack. In order to -// mitigate this run them with: -// -// RUST_MIN_STACK=100000000 cargo test +use xmr_btc::{bitcoin, cross_curve_dleq}; #[tokio::test] async fn swap() { @@ -91,7 +92,7 @@ async fn swap() { 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::Bob::default(); + let bob_behaviour = bob::Behaviour::default(); let bob_transport = build(bob_behaviour.identity()).unwrap(); let bob_swap = bob::swap( bob_btc_wallet.clone(), @@ -184,33 +185,66 @@ async fn simple_swap_happy_path() { )); let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client())); - let alice_behaviour = alice::Alice::default(); - let alice_transport = build(alice_behaviour.identity()).unwrap(); + // let redeem_address = bitcoin_wallet.as_ref().new_address().await?; + // let punish_address = redeem_address.clone(); + let amounts = SwapAmounts { + btc, + xmr: xmr_btc::monero::Amount::from_piconero(xmr), + }; - let db = Database::open(std::path::Path::new("../.swap-db/")).unwrap(); - let alice_swap = todo!(); + let alice_behaviour = alice::Behaviour::default(); + let alice_peer_id = alice_behaviour.peer_id().clone(); + let alice_transport = build(alice_behaviour.identity()).unwrap(); + let rng = &mut OsRng; + let alice_state = { + let a = bitcoin::SecretKey::new_random(rng); + let s_a = cross_curve_dleq::Scalar::random(rng); + let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng); + AliceState::Started { + amounts, + a, + s_a, + v_a, + } + }; + let alice_swarm = alice::new_swarm(alice_multiaddr, alice_transport, alice_behaviour).unwrap(); + let alice_swap = alice::swap::swap( + alice_state, + alice_swarm, + alice_btc_wallet.clone(), + alice_xmr_wallet.clone(), + ); - 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::Bob::default(); + let bob_db_dir = tempdir().unwrap(); + let bob_db = Database::open(bob_db_dir.path()).unwrap(); + let bob_behaviour = bob::Behaviour::default(); let bob_transport = build(bob_behaviour.identity()).unwrap(); - let bob_state = BobState::Started(cmd_tx, rsp_rx, btc_bob.as_sat(), alice_behaviour.peer_id()); - let bob_swarm = new_swarm(bob_transport, bob_behaviour).unwrap(); - let bob_swap = bob_simple::simple_swap( + + let refund_address = bob_btc_wallet.new_address().await.unwrap(); + let state0 = xmr_btc::bob::State0::new( + rng, + btc, + xmr_btc::monero::Amount::from_piconero(xmr), + REFUND_TIMELOCK, + PUNISH_TIMELOCK, + refund_address, + ); + let bob_state = BobState::Started { + state0, + amounts, + peer_id: alice_peer_id, + }; + let bob_swarm = bob::new_swarm(bob_transport, bob_behaviour).unwrap(); + let bob_swap = bob::swap::swap( bob_state, bob_swarm, - db, + bob_db, bob_btc_wallet.clone(), bob_xmr_wallet.clone(), OsRng, Uuid::new_v4(), ); - // 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(); diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 50599a33..64c45f48 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -823,7 +823,7 @@ impl State4 { where W: WatchForRawTransaction + TransactionBlockHeight + BlockHeight, { - let tx_id = self.tx_lock.txid().clone(); + let tx_id = self.tx_lock.txid(); let tx_lock_height = bitcoin_wallet.transaction_block_height(tx_id).await; let t1_timeout = From 9e13034e54151bafe16b0d35d34b3efae9190a44 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 25 Nov 2020 16:27:57 +1100 Subject: [PATCH 21/27] Extract execution details from driving loop --- swap/Cargo.toml | 1 + swap/src/alice.rs | 8 +- swap/src/alice/execution.rs | 320 +++++++++++++++++++++++++++++++---- swap/src/alice/swap.rs | 276 +++++++++--------------------- swap/src/bitcoin.rs | 14 +- swap/src/bob/execution.rs | 4 +- swap/src/recover.rs | 2 +- swap/src/state.rs | 4 +- swap/tests/e2e.rs | 8 +- xmr-btc/tests/harness/mod.rs | 26 ++- 10 files changed, 394 insertions(+), 269 deletions(-) diff --git a/swap/Cargo.toml b/swap/Cargo.toml index bf5fa649..e63aa5b5 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -14,6 +14,7 @@ backoff = { version = "0.2", features = ["tokio"] } base64 = "0.12" bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version. bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "3be644cd9512c157d3337a189298b8257ed54d04" } +conquer-once = "0.3" derivative = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] } futures = { version = "0.3", default-features = false } diff --git a/swap/src/alice.rs b/swap/src/alice.rs index f230cb05..adcfdfc0 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -184,7 +184,7 @@ pub async fn swap( }; let swap_id = Uuid::new_v4(); - db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into()) + db.insert_latest_state(swap_id, state::Alice::Negotiated(state3.clone()).into()) .await?; info!("Handshake complete, we now have State3 for Alice."); @@ -280,11 +280,7 @@ pub async fn swap( pub type Swarm = libp2p::Swarm; -pub fn new_swarm( - listen: Multiaddr, - transport: SwapTransport, - behaviour: Behaviour, -) -> Result { +fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Behaviour) -> Result { use anyhow::Context as _; let local_peer_id = behaviour.peer_id(); diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs index e798c94d..3c74d4d9 100644 --- a/swap/src/alice/execution.rs +++ b/swap/src/alice/execution.rs @@ -1,36 +1,62 @@ use crate::{ alice::{amounts, OutEvent, Swarm}, + bitcoin, monero, network::request_response::AliceToBob, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; +use conquer_once::Lazy; +use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; +use futures::{ + future::{select, Either}, + pin_mut, +}; use libp2p::request_response::ResponseChannel; -use std::sync::Arc; +use sha2::Sha256; +use std::{sync::Arc, time::Duration}; +use tokio::time::timeout; use xmr_btc::{ alice, alice::{State0, State3}, + bitcoin::{ + poll_until_block_height_is_gte, BlockHeight, BroadcastSignedTransaction, + EncryptedSignature, GetRawTransaction, TransactionBlockHeight, TxCancel, TxLock, TxRefund, + WaitForTransactionFinality, WatchForRawTransaction, + }, cross_curve_dleq, monero::Transfer, }; -// TODO(Franck): Make all methods here idempotent using db +// For each step, we are giving Bob 10 minutes to act. +static BOB_TIME_TO_ACT: Lazy = Lazy::new(|| Duration::from_secs(10 * 60)); + +// The maximum we assume we need to wait from the moment the monero transaction +// is mined to the moment it reaches finality. We set 15 confirmations for now +// (based on Kraken). 1.5 multiplier in case the blockchain is slower than +// usually. Average of 2 minutes block time +static MONERO_MAX_FINALITY_TIME: Lazy = + Lazy::new(|| Duration::from_secs_f64(15f64 * 1.5 * 2f64 * 60f64)); pub async fn negotiate( amounts: SwapAmounts, - a: crate::bitcoin::SecretKey, + a: bitcoin::SecretKey, s_a: cross_curve_dleq::Scalar, - v_a: crate::monero::PrivateViewKey, + v_a: monero::PrivateViewKey, swarm: &mut Swarm, - bitcoin_wallet: Arc, -) -> Result<(ResponseChannel, SwapAmounts, State3)> { - // Bob dials us - match swarm.next().await { + bitcoin_wallet: Arc, +) -> Result<(ResponseChannel, State3)> { + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive dial connection from Bob")?; + match event { OutEvent::ConnectionEstablished(_bob_peer_id) => {} other => bail!("Unexpected event received: {:?}", other), - }; + } - // Bob sends us a request - let (btc, channel) = match swarm.next().await { + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive amounts from Bob")?; + let (btc, channel) = match event { OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => (btc, channel), other => bail!("Unexpected event received: {:?}", other), }; @@ -42,8 +68,17 @@ pub async fn negotiate( amounts.btc ); } + // TODO: get an ack from libp2p2 swarm.send_amounts(channel, amounts); + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive message 0 from Bob")?; + let message0 = match event { + OutEvent::Message0(msg) => msg, + other => bail!("Unexpected event received: {:?}", other), + }; + let SwapAmounts { btc, xmr } = amounts; let redeem_address = bitcoin_wallet.as_ref().new_address().await?; @@ -61,43 +96,67 @@ pub async fn negotiate( punish_address, ); - // Bob sends us message0 - let message0 = match swarm.next().await { - OutEvent::Message0(msg) => msg, - other => bail!("Unexpected event received: {:?}", other), - }; - let state1 = state0.receive(message0)?; - let (state2, channel) = match swarm.next().await { - OutEvent::Message1 { msg, channel } => { - let state2 = state1.receive(msg); - (state2, channel) - } + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive message 1 from Bob")?; + let (msg, channel) = match event { + OutEvent::Message1 { msg, channel } => (msg, channel), other => bail!("Unexpected event: {:?}", other), }; + let state2 = state1.receive(msg); + let message1 = state2.next_message(); swarm.send_message1(channel, message1); - let (state3, channel) = match swarm.next().await { - OutEvent::Message2 { msg, channel } => { - let state3 = state2.receive(msg)?; - (state3, channel) - } + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive message 2 from Bob")?; + let (msg, channel) = match event { + OutEvent::Message2 { msg, channel } => (msg, channel), other => bail!("Unexpected event: {:?}", other), }; - Ok((channel, amounts, state3)) + let state3 = state2.receive(msg)?; + + Ok((channel, state3)) +} + +pub async fn wait_for_locked_bitcoin( + lock_bitcoin_txid: bitcoin::Txid, + bitcoin_wallet: Arc, +) -> Result<()> +where + W: WatchForRawTransaction + WaitForTransactionFinality, +{ + // We assume we will see Bob's transaction in the mempool first. + timeout( + *BOB_TIME_TO_ACT, + bitcoin_wallet.watch_for_raw_transaction(lock_bitcoin_txid), + ) + .await + .context("Failed to find lock Bitcoin tx")?; + + // We saw the transaction in the mempool, waiting for it to be confirmed. + bitcoin_wallet + .wait_for_transaction_finality(lock_bitcoin_txid) + .await; + + Ok(()) } -pub async fn lock_xmr( +pub async fn lock_xmr( channel: ResponseChannel, amounts: SwapAmounts, state3: State3, swarm: &mut Swarm, - monero_wallet: Arc, -) -> Result<()> { + monero_wallet: Arc, +) -> Result<()> +where + W: Transfer, +{ let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: state3.s_a.into_ed25519(), }); @@ -109,11 +168,206 @@ pub async fn lock_xmr( .transfer(public_spend_key, public_view_key, amounts.xmr) .await?; + // TODO(Franck): Wait for Monero to be confirmed once + swarm.send_message2(channel, alice::Message2 { tx_lock_proof: transfer_proof, }); - // TODO(Franck): Wait for Monero to be mined/finalised + Ok(()) +} + +pub async fn wait_for_bitcoin_encrypted_signature(swarm: &mut Swarm) -> Result { + let event = timeout(*MONERO_MAX_FINALITY_TIME, swarm.next()) + .await + .context("Failed to receive Bitcoin encrypted signature from Bob")?; + + match event { + OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig), + other => bail!( + "Expected Bob's Bitcoin redeem encrypted signature, got: {:?}", + other + ), + } +} + +pub fn build_bitcoin_redeem_transaction( + encrypted_signature: EncryptedSignature, + tx_lock: &TxLock, + a: bitcoin::SecretKey, + s_a: cross_curve_dleq::Scalar, + B: bitcoin::PublicKey, + redeem_address: &bitcoin::Address, +) -> Result { + 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(), + &encrypted_signature, + ) + .context("Invalid encrypted signature received")?; + + let sig_a = a.sign(tx_redeem.digest()); + let sig_b = adaptor.decrypt_signature(&s_a.into_secp256k1(), encrypted_signature.clone()); + + let tx = tx_redeem + .add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b)) + .context("sig_{a,b} are invalid for tx_redeem")?; + + Ok(tx) +} + +pub async fn publish_bitcoin_redeem_transaction( + redeem_tx: bitcoin::Transaction, + bitcoin_wallet: Arc, +) -> Result<()> +where + W: BroadcastSignedTransaction + WaitForTransactionFinality, +{ + let tx_id = bitcoin_wallet + .broadcast_signed_transaction(redeem_tx) + .await?; + // TODO(Franck): Not sure if we wait for finality here or just mined + bitcoin_wallet.wait_for_transaction_finality(tx_id).await; Ok(()) } + +pub async fn publish_cancel_transaction( + tx_lock: TxLock, + a: bitcoin::SecretKey, + B: bitcoin::PublicKey, + refund_timelock: u32, + tx_cancel_sig_bob: bitcoin::Signature, + bitcoin_wallet: Arc, +) -> Result +where + W: GetRawTransaction + TransactionBlockHeight + BlockHeight + BroadcastSignedTransaction, +{ + // First wait for t1 to expire + let tx_lock_height = bitcoin_wallet + .transaction_block_height(tx_lock.txid()) + .await; + poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), tx_lock_height + refund_timelock).await; + + let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); + + // If Bob hasn't yet broadcasted the tx cancel, we do it + if let Err(_) = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await { + // TODO(Franck): Maybe the cancel transaction is already mined, in this case, + // the broadcast will error out. + + let sig_a = a.sign(tx_cancel.digest()); + let sig_b = tx_cancel_sig_bob.clone(); + + let tx_cancel = 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"); + + // TODO(Franck): Error handling is delicate, why can't we broadcast? + bitcoin_wallet + .broadcast_signed_transaction(tx_cancel) + .await?; + + // TODO(Franck): Wait until transaction is mined and returned mined + // block height + } + + Ok(tx_cancel) +} + +pub async fn wait_for_bitcoin_refund( + tx_cancel: &TxCancel, + cancel_tx_height: u32, + punish_timelock: u32, + refund_address: &bitcoin::Address, + bitcoin_wallet: Arc, +) -> Result<(bitcoin::TxRefund, Option)> +where + W: BlockHeight + WatchForRawTransaction, +{ + let punish_timelock_expired = + poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), cancel_tx_height + punish_timelock); + + let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address); + + // TODO(Franck): This only checks the mempool, need to cater for the case where + // the transaction goes directly in a block + let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + + pin_mut!(punish_timelock_expired); + pin_mut!(seen_refund_tx); + + match select(punish_timelock_expired, seen_refund_tx).await { + Either::Left(_) => Ok((tx_refund, None)), + Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx))), + } +} + +pub fn extract_monero_private_key( + published_refund_tx: bitcoin::Transaction, + tx_refund: TxRefund, + s_a: cross_curve_dleq::Scalar, + a: bitcoin::SecretKey, + S_b_bitcoin: bitcoin::PublicKey, +) -> Result { + let s_a = monero::PrivateKey { + scalar: s_a.into_ed25519(), + }; + + let tx_refund_sig = tx_refund + .extract_signature_by_key(published_refund_tx, a.public()) + .context("Failed to extract signature from Bitcoin refund tx")?; + 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) + .context("Failed to recover Monero secret key from Bitcoin signature")?; + let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); + + let spend_key = s_a + s_b; + + Ok(spend_key) +} + +pub fn build_bitcoin_punish_transaction( + tx_lock: &TxLock, + refund_timelock: u32, + punish_address: &bitcoin::Address, + punish_timelock: u32, + tx_punish_sig_bob: bitcoin::Signature, + a: bitcoin::SecretKey, + B: bitcoin::PublicKey, +) -> 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 sig_a = a.sign(tx_punish.digest()); + let sig_b = tx_punish_sig_bob.clone(); + + let signed_tx_punish = tx_punish + .add_signatures(&tx_cancel, (a.public(), sig_a), (B.clone(), sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + Ok(signed_tx_punish) +} + +pub async fn publish_bitcoin_punish_transaction( + punish_tx: bitcoin::Transaction, + bitcoin_wallet: Arc, +) -> Result +where + W: BroadcastSignedTransaction + WaitForTransactionFinality, +{ + let txid = bitcoin_wallet + .broadcast_signed_transaction(punish_tx) + .await?; + + bitcoin_wallet.wait_for_transaction_finality(txid).await; + + Ok(txid) +} diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index fa9b5288..e096eb30 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -2,35 +2,32 @@ //! Alice holds XMR and wishes receive BTC. use crate::{ alice::{ - execution::{lock_xmr, negotiate}, - OutEvent, Swarm, + execution::{ + build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, + extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction, + publish_bitcoin_redeem_transaction, publish_cancel_transaction, + wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin, + }, + Swarm, }, bitcoin, - bitcoin::{EncryptedSignature, TX_LOCK_MINE_TIMEOUT}, + bitcoin::EncryptedSignature, monero, network::request_response::AliceToBob, SwapAmounts, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::Result; use async_recursion::async_recursion; -use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; use futures::{ future::{select, Either}, pin_mut, }; use libp2p::request_response::ResponseChannel; use rand::{CryptoRng, RngCore}; -use sha2::Sha256; -use std::{sync::Arc, time::Duration}; -use tokio::time::timeout; - +use std::sync::Arc; use xmr_btc::{ alice::State3, - bitcoin::{ - poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction, - TransactionBlockHeight, TxCancel, TxRefund, WaitForTransactionFinality, - WatchForRawTransaction, - }, + bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction}, cross_curve_dleq, monero::CreateWalletForOutput, }; @@ -79,11 +76,6 @@ pub enum AliceState { tx_refund: TxRefund, state3: State3, }, - BtcPunished { - tx_refund: TxRefund, - punished_tx_id: bitcoin::Txid, - state3: State3, - }, XmrRefunded, WaitingToCancel { state3: State3, @@ -107,7 +99,7 @@ pub async fn swap( s_a, v_a, } => { - let (channel, amounts, state3) = + let (channel, state3) = negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?; swap( @@ -127,12 +119,7 @@ pub async fn swap( channel, amounts, } => { - timeout( - Duration::from_secs(TX_LOCK_MINE_TIMEOUT), - bitcoin_wallet.wait_for_transaction_finality(state3.tx_lock.txid()), - ) - .await - .context("Timed out, Bob did not lock Bitcoin in time")?; + let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone()).await?; swap( AliceState::BtcLocked { @@ -169,37 +156,22 @@ pub async fn swap( .await } AliceState::XmrLocked { state3 } => { - let encsig = timeout( - // Give a set arbitrary time to Bob to send us `tx_redeem_encsign` - Duration::from_secs(TX_LOCK_MINE_TIMEOUT), - async { - match swarm.next().await { - OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig), - other => Err(anyhow!( - "Expected Bob's Bitcoin redeem encsig, got: {:?}", - other - )), - } - }, - ) - .await - .context("Timed out, Bob did not send redeem encsign in time"); - - match encsig { - Err(_timeout_error) => { - // TODO(Franck): Insert in DB - + // Our Monero is locked, we need to go through the cancellation process if this + // step fails + match wait_for_bitcoin_encrypted_signature(&mut swarm).await { + Ok(encrypted_signature) => { swap( - AliceState::WaitingToCancel { state3 }, + AliceState::EncSignLearned { + state3, + encrypted_signature, + }, swarm, bitcoin_wallet, monero_wallet, ) .await } - Ok(Err(_unexpected_msg_error)) => { - // TODO(Franck): Insert in DB - + Err(_) => { swap( AliceState::WaitingToCancel { state3 }, swarm, @@ -208,62 +180,36 @@ pub async fn swap( ) .await } - Ok(Ok(encrypted_signature)) => { - // TODO(Franck): Insert in DB - - swap( - AliceState::EncSignLearned { - state3, - encrypted_signature, - }, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } } } AliceState::EncSignLearned { state3, encrypted_signature, } => { - let (signed_tx_redeem, _tx_redeem_txid) = { - let adaptor = Adaptor::>::default(); - - let tx_redeem = bitcoin::TxRedeem::new(&state3.tx_lock, &state3.redeem_address); - - bitcoin::verify_encsig( - state3.B.clone(), - state3.s_a.into_secp256k1().into(), - &tx_redeem.digest(), - &encrypted_signature, - ) - .context("Invalid encrypted signature received")?; - - let sig_a = state3.a.sign(tx_redeem.digest()); - let sig_b = adaptor - .decrypt_signature(&state3.s_a.into_secp256k1(), encrypted_signature.clone()); - - let tx = tx_redeem - .add_signatures( - &state3.tx_lock, - (state3.a.public(), sig_a), - (state3.B.clone(), sig_b), + let signed_tx_redeem = match build_bitcoin_redeem_transaction( + encrypted_signature, + &state3.tx_lock, + state3.a.clone(), + state3.s_a, + state3.B.clone(), + &state3.redeem_address, + ) { + Ok(tx) => tx, + Err(_) => { + return swap( + AliceState::WaitingToCancel { state3 }, + swarm, + bitcoin_wallet, + monero_wallet, ) - .expect("sig_{a,b} to be valid signatures for tx_redeem"); - let txid = tx.txid(); - - (tx, txid) + .await; + } }; - // TODO(Franck): Insert in db - - let _ = bitcoin_wallet - .broadcast_signed_transaction(signed_tx_redeem) - .await?; - - // TODO(Franck) Wait for confirmations + // TODO(Franck): Error handling is delicate here. + // If Bob sees this transaction he can redeem Monero + // e.g. If the Bitcoin node is down then the user needs to take action. + publish_bitcoin_redeem_transaction(signed_tx_redeem, bitcoin_wallet.clone()).await?; swap( AliceState::BtcRedeemed, @@ -274,39 +220,15 @@ pub async fn swap( .await } AliceState::WaitingToCancel { state3 } => { - let tx_lock_height = bitcoin_wallet - .transaction_block_height(state3.tx_lock.txid()) - .await; - poll_until_block_height_is_gte( - bitcoin_wallet.as_ref(), - tx_lock_height + state3.refund_timelock, - ) - .await; - - let tx_cancel = bitcoin::TxCancel::new( - &state3.tx_lock, - state3.refund_timelock, - state3.a.public(), + let tx_cancel = publish_cancel_transaction( + state3.tx_lock.clone(), + state3.a.clone(), state3.B.clone(), - ); - - if let Err(_e) = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await { - let sig_a = state3.a.sign(tx_cancel.digest()); - let sig_b = state3.tx_cancel_sig_bob.clone(); - - let tx_cancel = tx_cancel - .clone() - .add_signatures( - &state3.tx_lock, - (state3.a.public(), sig_a), - (state3.B.clone(), sig_b), - ) - .expect("sig_{a,b} to be valid signatures for tx_cancel"); - - bitcoin_wallet - .broadcast_signed_transaction(tx_cancel) - .await?; - } + state3.refund_timelock, + state3.tx_cancel_sig_bob.clone(), + bitcoin_wallet.clone(), + ) + .await?; swap( AliceState::BtcCancelled { state3, tx_cancel }, @@ -321,19 +243,18 @@ pub async fn swap( .transaction_block_height(tx_cancel.txid()) .await; - let reached_t2 = poll_until_block_height_is_gte( - bitcoin_wallet.as_ref(), - tx_cancel_height + state3.punish_timelock, - ); - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state3.refund_address); - let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); - - pin_mut!(reached_t2); - pin_mut!(seen_refund_tx); + let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund( + &tx_cancel, + tx_cancel_height, + state3.punish_timelock, + &state3.refund_address, + bitcoin_wallet.clone(), + ) + .await?; - match select(reached_t2, seen_refund_tx).await { - Either::Left(_) => { + // TODO(Franck): Review error handling + match published_refund_tx { + None => { swap( AliceState::BtcPunishable { tx_refund, state3 }, swarm, @@ -342,7 +263,7 @@ pub async fn swap( ) .await } - Either::Right((published_refund_tx, _)) => { + Some(published_refund_tx) => { swap( AliceState::BtcRefunded { tx_refund, @@ -362,22 +283,13 @@ pub async fn swap( published_refund_tx, state3, } => { - let s_a = monero::PrivateKey { - scalar: state3.s_a.into_ed25519(), - }; - - let tx_refund_sig = tx_refund - .extract_signature_by_key(published_refund_tx, state3.a.public()) - .context("Failed to extract signature from Bitcoin refund tx")?; - let tx_refund_encsig = state3 - .a - .encsign(state3.S_b_bitcoin.clone(), tx_refund.digest()); - - let s_b = bitcoin::recover(state3.S_b_bitcoin, tx_refund_sig, tx_refund_encsig) - .context("Failed to recover Monero secret key from Bitcoin signature")?; - let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); - - let spend_key = s_a + s_b; + let spend_key = extract_monero_private_key( + published_refund_tx, + tx_refund, + state3.s_a, + state3.a.clone(), + state3.S_b_bitcoin, + )?; let view_key = state3.v; monero_wallet @@ -387,49 +299,18 @@ pub async fn swap( Ok(AliceState::XmrRefunded) } AliceState::BtcPunishable { tx_refund, state3 } => { - let tx_cancel = bitcoin::TxCancel::new( + let signed_tx_punish = build_bitcoin_punish_transaction( &state3.tx_lock, state3.refund_timelock, - state3.a.public(), + &state3.punish_address, + state3.punish_timelock, + state3.tx_punish_sig_bob.clone(), + state3.a.clone(), state3.B.clone(), - ); - let tx_punish = - bitcoin::TxPunish::new(&tx_cancel, &state3.punish_address, state3.punish_timelock); - let punished_tx_id = tx_punish.txid(); - - let sig_a = state3.a.sign(tx_punish.digest()); - let sig_b = state3.tx_punish_sig_bob.clone(); + )?; - let signed_tx_punish = tx_punish - .add_signatures( - &tx_cancel, - (state3.a.public(), sig_a), - (state3.B.clone(), sig_b), - ) - .expect("sig_{a,b} to be valid signatures for tx_cancel"); - - let _ = bitcoin_wallet - .broadcast_signed_transaction(signed_tx_punish) - .await?; - - swap( - AliceState::BtcPunished { - tx_refund, - punished_tx_id, - state3, - }, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - ) - .await - } - AliceState::BtcPunished { - punished_tx_id, - tx_refund, - state3, - } => { - let punish_tx_finalised = bitcoin_wallet.wait_for_transaction_finality(punished_tx_id); + let punish_tx_finalised = + publish_bitcoin_punish_transaction(signed_tx_punish, bitcoin_wallet.clone()); let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); @@ -461,7 +342,6 @@ pub async fn swap( } } } - AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), AliceState::Punished => Ok(AliceState::Punished), diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 55ed05b7..8425fb85 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -3,15 +3,15 @@ use std::time::Duration; 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::util::psbt::PartiallySignedTransaction; use bitcoin_harness::bitcoind_rpc::PsbtBase64; use reqwest::Url; -use tokio::time; use xmr_btc::bitcoin::{ BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight, WatchForRawTransaction, }; +pub use ::bitcoin::{Address, Transaction}; pub use xmr_btc::bitcoin::*; pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600; @@ -85,15 +85,7 @@ impl SignTxLock for Wallet { #[async_trait] impl BroadcastSignedTransaction for Wallet { async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result { - let txid = self.0.send_raw_transaction(transaction).await?; - - // TODO: Instead of guessing how long it will take for the transaction to be - // mined we should ask bitcoind for the number of confirmations on `txid` - - // give time for transaction to be mined - time::delay_for(Duration::from_millis(1100)).await; - - Ok(txid) + Ok(self.0.send_raw_transaction(transaction).await?) } } diff --git a/swap/src/bob/execution.rs b/swap/src/bob/execution.rs index c63739ea..52f8cda1 100644 --- a/swap/src/bob/execution.rs +++ b/swap/src/bob/execution.rs @@ -1,13 +1,13 @@ use crate::{ bob::{OutEvent, Swarm}, - Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, + Cmd, Rsp, SwapAmounts, }; use anyhow::Result; use rand::{CryptoRng, RngCore}; use std::sync::Arc; use tokio::{stream::StreamExt, sync::mpsc}; -use xmr_btc::bob::{State0, State2}; +use xmr_btc::bob::State2; pub async fn negotiate( state0: xmr_btc::bob::State0, diff --git a/swap/src/recover.rs b/swap/src/recover.rs index b71b31fd..5f41112f 100644 --- a/swap/src/recover.rs +++ b/swap/src/recover.rs @@ -45,7 +45,7 @@ pub async fn alice_recover( state: Alice, ) -> Result<()> { match state { - Alice::Handshaken(_) | Alice::BtcLocked(_) | Alice::SwapComplete => { + Alice::Negotiated(_) | Alice::BtcLocked(_) | Alice::SwapComplete => { info!("Nothing to do"); } Alice::XmrLocked(state) => { diff --git a/swap/src/state.rs b/swap/src/state.rs index 701c61e0..e59c976d 100644 --- a/swap/src/state.rs +++ b/swap/src/state.rs @@ -12,7 +12,7 @@ pub enum Swap { #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum Alice { - Handshaken(alice::State3), + Negotiated(alice::State3), BtcLocked(alice::State3), XmrLocked(alice::State3), BtcRedeemable { @@ -63,7 +63,7 @@ impl Display for Swap { impl Display for Alice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Alice::Handshaken(_) => f.write_str("Handshake complete"), + Alice::Negotiated(_) => f.write_str("Handshake complete"), Alice::BtcLocked(_) => f.write_str("Bitcoin locked"), Alice::XmrLocked(_) => f.write_str("Monero locked"), Alice::BtcRedeemable { .. } => f.write_str("Bitcoin redeemable"), diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 7b318b6c..66672874 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -1,11 +1,5 @@ use bitcoin_harness::Bitcoind; -use futures::{ - channel::{ - mpsc, - mpsc::{Receiver, Sender}, - }, - future::try_join, -}; +use futures::{channel::mpsc, future::try_join}; use libp2p::Multiaddr; use monero_harness::Monero; use rand::rngs::OsRng; diff --git a/xmr-btc/tests/harness/mod.rs b/xmr-btc/tests/harness/mod.rs index 3d789b8f..c1efa323 100644 --- a/xmr-btc/tests/harness/mod.rs +++ b/xmr-btc/tests/harness/mod.rs @@ -160,15 +160,23 @@ pub async fn init_test( let punish_address = redeem_address.clone(); let refund_address = bob.bitcoin_wallet.new_address().await.unwrap(); - let alice_state0 = xmr_btc::alice::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK), - punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK), - redeem_address.clone(), - punish_address.clone(), - ); + let alice_state0 = { + let a = bitcoin::SecretKey::new_random(&mut OsRng); + let s_a = cross_curve_dleq::Scalar::random(&mut OsRng); + let v_a = monero::PrivateViewKey::new_random(&mut OsRng); + + xmr_btc::alice::State0::new( + a, + s_a, + v_a, + btc_amount, + xmr_amount, + refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK), + punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK), + redeem_address.clone(), + punish_address.clone(), + ) + }; let bob_state0 = xmr_btc::bob::State0::new( &mut OsRng, btc_amount, From 2db9ebd28b6fa4579aad8dc17a89a19cd0e33f4e Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Mon, 30 Nov 2020 14:48:53 +1100 Subject: [PATCH 22/27] make new_swarm public --- swap/src/alice.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index adcfdfc0..0aa0eedf 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -280,7 +280,11 @@ pub async fn swap( pub type Swarm = libp2p::Swarm; -fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Behaviour) -> Result { +pub fn new_swarm( + listen: Multiaddr, + transport: SwapTransport, + behaviour: Behaviour, +) -> Result { use anyhow::Context as _; let local_peer_id = behaviour.peer_id(); From d8218a7bea6cb19686ac3bf0884acd192d588d23 Mon Sep 17 00:00:00 2001 From: rishflab Date: Mon, 30 Nov 2020 15:16:56 +1100 Subject: [PATCH 23/27] Remove mspc channel from new executor --- swap/src/bob/execution.rs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/swap/src/bob/execution.rs b/swap/src/bob/execution.rs index 52f8cda1..0b0f2aab 100644 --- a/swap/src/bob/execution.rs +++ b/swap/src/bob/execution.rs @@ -19,9 +19,6 @@ pub async fn negotiate( where R: RngCore + CryptoRng + Send, { - let (mut cmd_tx, _cmd_rx) = mpsc::channel(1); - let (_rsp_tx, mut rsp_rx) = mpsc::channel(1); - // todo: dial the swarm outside // libp2p::Swarm::dial_addr(&mut swarm, addr)?; let alice = match swarm.next().await { @@ -31,22 +28,12 @@ where swarm.request_amounts(alice.clone(), amounts.btc.as_sat()); - // todo: remove/refactor mspc channel + // todo: see if we can remove let (btc, xmr) = match swarm.next().await { - OutEvent::Amounts(amounts) => { - let cmd = Cmd::VerifyAmounts(amounts); - cmd_tx.try_send(cmd)?; - let response = rsp_rx.next().await; - if response == Some(Rsp::Abort) { - panic!("abort response"); - } - (amounts.btc, amounts.xmr) - } + OutEvent::Amounts(amounts) => (amounts.btc, amounts.xmr), other => panic!("unexpected event: {:?}", other), }; - let refund_address = bitcoin_wallet.as_ref().new_address().await?; - swarm.send_message0(alice.clone(), state0.next_message(&mut rng)); let state1 = match swarm.next().await { OutEvent::Message0(msg) => state0.receive(bitcoin_wallet.as_ref(), msg).await?, From 2abeba17f9278ccaa39da30f3772a327ab5f725a Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 1 Dec 2020 10:41:10 +1100 Subject: [PATCH 24/27] Save state0 in the swarm to process message0 --- swap/src/alice/execution.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs index 3c74d4d9..c1c83db4 100644 --- a/swap/src/alice/execution.rs +++ b/swap/src/alice/execution.rs @@ -71,16 +71,6 @@ pub async fn negotiate( // TODO: get an ack from libp2p2 swarm.send_amounts(channel, amounts); - let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) - .await - .context("Failed to receive message 0 from Bob")?; - let message0 = match event { - OutEvent::Message0(msg) => msg, - other => bail!("Unexpected event received: {:?}", other), - }; - - let SwapAmounts { btc, xmr } = amounts; - let redeem_address = bitcoin_wallet.as_ref().new_address().await?; let punish_address = redeem_address.clone(); @@ -88,14 +78,27 @@ pub async fn negotiate( a, s_a, v_a, - btc, - xmr, + amounts.btc, + amounts.xmr, REFUND_TIMELOCK, PUNISH_TIMELOCK, redeem_address, punish_address, ); + // TODO(Franck): Understand why this is needed. + swarm.set_state0(state0.clone()); + + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive message 0 from Bob")?; + let message0 = match event { + OutEvent::Message0(msg) => msg, + other => bail!("Unexpected event received: {:?}", other), + }; + + let SwapAmounts { btc, xmr } = amounts; + let state1 = state0.receive(message0)?; let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) From 47c3ddc6f7a0867759e4544076ab576068df8852 Mon Sep 17 00:00:00 2001 From: rishflab Date: Tue, 1 Dec 2020 14:30:02 +1100 Subject: [PATCH 25/27] Fix happy path test Poll bob swarm to send encsig after calling send message3. Comment out unimplmented wait for transaction finality trait. Dial alice. --- swap/src/alice/execution.rs | 15 ++++++++------- swap/src/bob/execution.rs | 7 ++++--- swap/src/bob/swap.rs | 17 ++++++++++++++++- swap/tests/e2e.rs | 6 +++--- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs index c1c83db4..aea07512 100644 --- a/swap/src/alice/execution.rs +++ b/swap/src/alice/execution.rs @@ -142,10 +142,10 @@ where .await .context("Failed to find lock Bitcoin tx")?; - // We saw the transaction in the mempool, waiting for it to be confirmed. - bitcoin_wallet - .wait_for_transaction_finality(lock_bitcoin_txid) - .await; + // // We saw the transaction in the mempool, waiting for it to be confirmed. + // bitcoin_wallet + // .wait_for_transaction_finality(lock_bitcoin_txid) + // .await; Ok(()) } @@ -235,8 +235,8 @@ where .broadcast_signed_transaction(redeem_tx) .await?; - // TODO(Franck): Not sure if we wait for finality here or just mined - bitcoin_wallet.wait_for_transaction_finality(tx_id).await; + // // TODO(Franck): Not sure if we wait for finality here or just mined + // bitcoin_wallet.wait_for_transaction_finality(tx_id).await; Ok(()) } @@ -370,7 +370,8 @@ where .broadcast_signed_transaction(punish_tx) .await?; - bitcoin_wallet.wait_for_transaction_finality(txid).await; + // todo: enable this once trait is implemented + // bitcoin_wallet.wait_for_transaction_finality(txid).await; Ok(txid) } diff --git a/swap/src/bob/execution.rs b/swap/src/bob/execution.rs index 0b0f2aab..86f839c2 100644 --- a/swap/src/bob/execution.rs +++ b/swap/src/bob/execution.rs @@ -3,24 +3,25 @@ use crate::{ Cmd, Rsp, SwapAmounts, }; use anyhow::Result; +use libp2p::core::Multiaddr; use rand::{CryptoRng, RngCore}; use std::sync::Arc; use tokio::{stream::StreamExt, sync::mpsc}; - use xmr_btc::bob::State2; pub async fn negotiate( state0: xmr_btc::bob::State0, amounts: SwapAmounts, swarm: &mut Swarm, + addr: Multiaddr, mut rng: R, bitcoin_wallet: Arc, ) -> Result<(SwapAmounts, State2)> where R: RngCore + CryptoRng + Send, { - // todo: dial the swarm outside - // libp2p::Swarm::dial_addr(&mut swarm, addr)?; + libp2p::Swarm::dial_addr(swarm, addr)?; + let alice = match swarm.next().await { OutEvent::ConnectionEstablished(alice) => alice, other => panic!("unexpected event: {:?}", other), diff --git a/swap/src/bob/swap.rs b/swap/src/bob/swap.rs index 603b8203..2a3a3a3f 100644 --- a/swap/src/bob/swap.rs +++ b/swap/src/bob/swap.rs @@ -5,9 +5,10 @@ use crate::{ }; use anyhow::Result; use async_recursion::async_recursion; -use libp2p::PeerId; +use libp2p::{core::Multiaddr, PeerId}; use rand::{CryptoRng, RngCore}; use std::sync::Arc; +use tracing::debug; use uuid::Uuid; use xmr_btc::bob::{self}; @@ -18,6 +19,7 @@ pub enum BobState { state0: bob::State0, amounts: SwapAmounts, peer_id: PeerId, + addr: Multiaddr, }, Negotiated(bob::State2, PeerId), BtcLocked(bob::State3, PeerId), @@ -50,11 +52,13 @@ where state0, amounts, peer_id, + addr, } => { let (swap_amounts, state2) = negotiate( state0, amounts, &mut swarm, + addr, &mut rng, bitcoin_wallet.clone(), ) @@ -118,6 +122,17 @@ where // should happen in this arm? swarm.send_message3(alice_peer_id.clone(), tx_redeem_encsig); + // Sadly we have to poll the swarm to get make sure the message is sent? + // 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 swarm.next().await { + OutEvent::Message3 => { + debug!("Got Message3 empty response"); + } + other => panic!("unexpected event: {:?}", other), + }; + swap( BobState::EncSigSent(state, alice_peer_id), swarm, diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 66672874..656d4408 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -179,8 +179,6 @@ async fn simple_swap_happy_path() { )); let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client())); - // let redeem_address = bitcoin_wallet.as_ref().new_address().await?; - // let punish_address = redeem_address.clone(); let amounts = SwapAmounts { btc, xmr: xmr_btc::monero::Amount::from_piconero(xmr), @@ -201,7 +199,8 @@ async fn simple_swap_happy_path() { v_a, } }; - let alice_swarm = alice::new_swarm(alice_multiaddr, alice_transport, alice_behaviour).unwrap(); + let alice_swarm = + alice::new_swarm(alice_multiaddr.clone(), alice_transport, alice_behaviour).unwrap(); let alice_swap = alice::swap::swap( alice_state, alice_swarm, @@ -227,6 +226,7 @@ async fn simple_swap_happy_path() { state0, amounts, peer_id: alice_peer_id, + addr: alice_multiaddr, }; let bob_swarm = bob::new_swarm(bob_transport, bob_behaviour).unwrap(); let bob_swap = bob::swap::swap( From b06321a40f0279aa1f243b00004f42836bbc66fd Mon Sep 17 00:00:00 2001 From: rishflab Date: Tue, 1 Dec 2020 14:46:53 +1100 Subject: [PATCH 26/27] WIP: cleanup --- swap/src/alice/execution.rs | 2 -- swap/src/bob/execution.rs | 9 ++++----- swap/src/bob/swap.rs | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs index aea07512..afe6d544 100644 --- a/swap/src/alice/execution.rs +++ b/swap/src/alice/execution.rs @@ -97,8 +97,6 @@ pub async fn negotiate( other => bail!("Unexpected event received: {:?}", other), }; - let SwapAmounts { btc, xmr } = amounts; - let state1 = state0.receive(message0)?; let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) diff --git a/swap/src/bob/execution.rs b/swap/src/bob/execution.rs index 86f839c2..abe31815 100644 --- a/swap/src/bob/execution.rs +++ b/swap/src/bob/execution.rs @@ -1,12 +1,11 @@ use crate::{ bob::{OutEvent, Swarm}, - Cmd, Rsp, SwapAmounts, + SwapAmounts, }; use anyhow::Result; use libp2p::core::Multiaddr; use rand::{CryptoRng, RngCore}; use std::sync::Arc; -use tokio::{stream::StreamExt, sync::mpsc}; use xmr_btc::bob::State2; pub async fn negotiate( @@ -16,7 +15,7 @@ pub async fn negotiate( addr: Multiaddr, mut rng: R, bitcoin_wallet: Arc, -) -> Result<(SwapAmounts, State2)> +) -> Result where R: RngCore + CryptoRng + Send, { @@ -30,7 +29,7 @@ where swarm.request_amounts(alice.clone(), amounts.btc.as_sat()); // todo: see if we can remove - let (btc, xmr) = match swarm.next().await { + let (_btc, _xmr) = match swarm.next().await { OutEvent::Amounts(amounts) => (amounts.btc, amounts.xmr), other => panic!("unexpected event: {:?}", other), }; @@ -49,5 +48,5 @@ where swarm.send_message2(alice.clone(), state2.next_message()); - Ok((amounts, state2)) + Ok(state2) } diff --git a/swap/src/bob/swap.rs b/swap/src/bob/swap.rs index 2a3a3a3f..21b97b3d 100644 --- a/swap/src/bob/swap.rs +++ b/swap/src/bob/swap.rs @@ -54,7 +54,7 @@ where peer_id, addr, } => { - let (swap_amounts, state2) = negotiate( + let state2 = negotiate( state0, amounts, &mut swarm, From 1c401aad31e1858e0df38209c8b2b23ae77dd033 Mon Sep 17 00:00:00 2001 From: rishflab Date: Tue, 1 Dec 2020 15:38:24 +1100 Subject: [PATCH 27/27] Fix clippy warnings and formatting --- swap/Cargo.toml | 2 +- swap/src/alice/execution.rs | 16 ++++++++++------ swap/src/alice/swap.rs | 1 + swap/src/bob/swap.rs | 4 ++-- swap/tests/e2e.rs | 3 ++- xmr-btc/src/alice.rs | 1 + 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/swap/Cargo.toml b/swap/Cargo.toml index e63aa5b5..5d606654 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -7,8 +7,8 @@ description = "XMR/BTC trustless atomic swaps." [dependencies] anyhow = "1" -async-trait = "0.1" async-recursion = "0.3.1" +async-trait = "0.1" atty = "0.2" backoff = { version = "0.2", features = ["tokio"] } base64 = "0.12" diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs index afe6d544..0350eeb3 100644 --- a/swap/src/alice/execution.rs +++ b/swap/src/alice/execution.rs @@ -213,10 +213,10 @@ pub fn build_bitcoin_redeem_transaction( .context("Invalid encrypted signature received")?; let sig_a = a.sign(tx_redeem.digest()); - let sig_b = adaptor.decrypt_signature(&s_a.into_secp256k1(), encrypted_signature.clone()); + let sig_b = adaptor.decrypt_signature(&s_a.into_secp256k1(), encrypted_signature); let tx = tx_redeem - .add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b)) + .add_signatures(&tx_lock, (a.public(), sig_a), (B, sig_b)) .context("sig_{a,b} are invalid for tx_redeem")?; Ok(tx) @@ -229,7 +229,7 @@ pub async fn publish_bitcoin_redeem_transaction( where W: BroadcastSignedTransaction + WaitForTransactionFinality, { - let tx_id = bitcoin_wallet + let _tx_id = bitcoin_wallet .broadcast_signed_transaction(redeem_tx) .await?; @@ -258,7 +258,11 @@ where let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); // If Bob hasn't yet broadcasted the tx cancel, we do it - if let Err(_) = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await { + if bitcoin_wallet + .get_raw_transaction(tx_cancel.txid()) + .await + .is_err() + { // TODO(Franck): Maybe the cancel transaction is already mined, in this case, // the broadcast will error out. @@ -348,10 +352,10 @@ pub fn build_bitcoin_punish_transaction( let tx_punish = bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock); let sig_a = a.sign(tx_punish.digest()); - let sig_b = tx_punish_sig_bob.clone(); + let sig_b = tx_punish_sig_bob; let signed_tx_punish = tx_punish - .add_signatures(&tx_cancel, (a.public(), sig_a), (B.clone(), sig_b)) + .add_signatures(&tx_cancel, (a.public(), sig_a), (B, sig_b)) .expect("sig_{a,b} to be valid signatures for tx_cancel"); Ok(signed_tx_punish) diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index e096eb30..a6330205 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -38,6 +38,7 @@ impl Rng for T where T: RngCore + CryptoRng + Send {} // The same data structure is used for swap execution and recovery. // This allows for a seamless transition from a failed swap to recovery. +#[allow(clippy::large_enum_variant)] pub enum AliceState { Started { amounts: SwapAmounts, diff --git a/swap/src/bob/swap.rs b/swap/src/bob/swap.rs index 21b97b3d..0d8827e4 100644 --- a/swap/src/bob/swap.rs +++ b/swap/src/bob/swap.rs @@ -165,8 +165,8 @@ where _ = t1_timeout => { // Check whether TxCancel has been published. // We should not fail if the transaction is already on the blockchain - if let Err(_) = state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await { - state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; + if state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await.is_err() { + state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; } swap( diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 656d4408..442fba5f 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -13,6 +13,7 @@ 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 _; @@ -124,7 +125,7 @@ async fn swap() { } #[tokio::test] -async fn simple_swap_happy_path() { +async fn happy_path_recursive_executor() { use tracing_subscriber::util::SubscriberInitExt as _; let _guard = tracing_subscriber::fmt() .with_env_filter("swap=info,xmr_btc=info") diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index dc5cc9c6..477df236 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -449,6 +449,7 @@ pub struct State0 { } impl State0 { + #[allow(clippy::too_many_arguments)] pub fn new( a: bitcoin::SecretKey, s_a: cross_curve_dleq::Scalar,