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(), + }) +}