From dd07e2f8828c369ae85e27710cb2577cf0092ece Mon Sep 17 00:00:00 2001 From: rishflab Date: Thu, 12 Nov 2020 11:06:34 +1100 Subject: [PATCH] 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(), + }) +}