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