diff --git a/swap/Cargo.toml b/swap/Cargo.toml index e1371f91..5d606654 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -7,12 +7,14 @@ description = "XMR/BTC trustless atomic swaps." [dependencies] anyhow = "1" +async-recursion = "0.3.1" async-trait = "0.1" atty = "0.2" backoff = { version = "0.2", features = ["tokio"] } base64 = "0.12" bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version. bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "3be644cd9512c157d3337a189298b8257ed54d04" } +conquer-once = "0.3" derivative = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] } futures = { version = "0.3", default-features = false } diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 52ae7cc4..0aa0eedf 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -1,26 +1,5 @@ //! 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, @@ -36,20 +15,42 @@ use crate::{ storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; +use anyhow::Result; +use async_trait::async_trait; +use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; +use genawaiter::GeneratorState; +use libp2p::{ + core::{identity::Keypair, Multiaddr}, + request_response::ResponseChannel, + NetworkBehaviour, PeerId, +}; +use rand::rngs::OsRng; +use std::{sync::Arc, time::Duration}; +use tokio::sync::Mutex; +use tracing::{debug, info, warn}; +use uuid::Uuid; use xmr_btc::{ alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0}, bitcoin::BroadcastSignedTransaction, - bob, + bob, cross_curve_dleq, monero::{CreateWalletForOutput, Transfer}, }; +mod amounts; +mod execution; +mod message0; +mod message1; +mod message2; +mod message3; +pub mod swap; + pub async fn swap( bitcoin_wallet: Arc, monero_wallet: Arc, db: Database, listen: Multiaddr, transport: SwapTransport, - behaviour: Alice, + behaviour: Behaviour, ) -> Result<()> { struct Network { swarm: Arc>, @@ -128,8 +129,13 @@ pub async fn swap( // TODO: Pass this in using let rng = &mut OsRng; + let a = bitcoin::SecretKey::new_random(rng); + let s_a = cross_curve_dleq::Scalar::random(rng); + let v_a = monero::PrivateViewKey::new_random(rng); let state = State0::new( - rng, + a, + s_a, + v_a, btc, xmr, REFUND_TIMELOCK, @@ -178,7 +184,7 @@ pub async fn swap( }; let swap_id = Uuid::new_v4(); - db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into()) + db.insert_latest_state(swap_id, state::Alice::Negotiated(state3.clone()).into()) .await?; info!("Handshake complete, we now have State3 for Alice."); @@ -272,9 +278,13 @@ pub async fn swap( } } -pub type Swarm = libp2p::Swarm; +pub type Swarm = libp2p::Swarm; -fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> Result { +pub fn new_swarm( + listen: Multiaddr, + transport: SwapTransport, + behaviour: Behaviour, +) -> Result { use anyhow::Context as _; let local_peer_id = behaviour.peer_id(); @@ -297,6 +307,8 @@ fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> R #[derive(Debug)] pub enum OutEvent { ConnectionEstablished(PeerId), + // TODO (Franck): Change this to get both amounts so parties can verify the amounts are + // expected early on. Request(amounts::OutEvent), // Not-uniform with Bob on purpose, ready for adding Xmr event. Message0(bob::Message0), Message1 { @@ -362,7 +374,7 @@ impl From for OutEvent { #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", event_process = false)] #[allow(missing_debug_implementations)] -pub struct Alice { +pub struct Behaviour { pt: PeerTracker, amounts: Amounts, message0: Message0, @@ -373,7 +385,7 @@ pub struct Alice { identity: Keypair, } -impl Alice { +impl Behaviour { pub fn identity(&self) -> Keypair { self.identity.clone() } @@ -389,6 +401,7 @@ impl Alice { info!("Sent amounts response"); } + // TODO(Franck) remove /// Message0 gets sent within the network layer using this state0. pub fn set_state0(&mut self, state: State0) { debug!("Set state 0"); @@ -416,7 +429,7 @@ impl Alice { } } -impl Default for Alice { +impl Default for Behaviour { fn default() -> Self { let identity = Keypair::generate_ed25519(); @@ -433,8 +446,8 @@ impl Default for Alice { } fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapAmounts { - // TODO: Get this from an exchange. - // This value corresponds to 100 XMR per BTC + // TODO (Franck): This should instead verify that the received amounts matches + // the command line arguments This value corresponds to 100 XMR per BTC const PICONERO_PER_SAT: u64 = 1_000_000; let picos = btc.as_sat() * PICONERO_PER_SAT; diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs new file mode 100644 index 00000000..0350eeb3 --- /dev/null +++ b/swap/src/alice/execution.rs @@ -0,0 +1,379 @@ +use crate::{ + alice::{amounts, OutEvent, Swarm}, + bitcoin, monero, + network::request_response::AliceToBob, + SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, +}; +use anyhow::{bail, Context, Result}; +use conquer_once::Lazy; +use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; +use futures::{ + future::{select, Either}, + pin_mut, +}; +use libp2p::request_response::ResponseChannel; +use sha2::Sha256; +use std::{sync::Arc, time::Duration}; +use tokio::time::timeout; +use xmr_btc::{ + alice, + alice::{State0, State3}, + bitcoin::{ + poll_until_block_height_is_gte, BlockHeight, BroadcastSignedTransaction, + EncryptedSignature, GetRawTransaction, TransactionBlockHeight, TxCancel, TxLock, TxRefund, + WaitForTransactionFinality, WatchForRawTransaction, + }, + cross_curve_dleq, + monero::Transfer, +}; + +// For each step, we are giving Bob 10 minutes to act. +static BOB_TIME_TO_ACT: Lazy = Lazy::new(|| Duration::from_secs(10 * 60)); + +// The maximum we assume we need to wait from the moment the monero transaction +// is mined to the moment it reaches finality. We set 15 confirmations for now +// (based on Kraken). 1.5 multiplier in case the blockchain is slower than +// usually. Average of 2 minutes block time +static MONERO_MAX_FINALITY_TIME: Lazy = + Lazy::new(|| Duration::from_secs_f64(15f64 * 1.5 * 2f64 * 60f64)); + +pub async fn negotiate( + amounts: SwapAmounts, + a: bitcoin::SecretKey, + s_a: cross_curve_dleq::Scalar, + v_a: monero::PrivateViewKey, + swarm: &mut Swarm, + bitcoin_wallet: Arc, +) -> Result<(ResponseChannel, State3)> { + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive dial connection from Bob")?; + match event { + OutEvent::ConnectionEstablished(_bob_peer_id) => {} + other => bail!("Unexpected event received: {:?}", other), + } + + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive amounts from Bob")?; + let (btc, channel) = match event { + OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => (btc, channel), + other => bail!("Unexpected event received: {:?}", other), + }; + + if btc != amounts.btc { + bail!( + "Bob proposed a different amount; got {}, expected: {}", + btc, + amounts.btc + ); + } + // TODO: get an ack from libp2p2 + swarm.send_amounts(channel, amounts); + + let redeem_address = bitcoin_wallet.as_ref().new_address().await?; + let punish_address = redeem_address.clone(); + + let state0 = State0::new( + a, + s_a, + v_a, + amounts.btc, + amounts.xmr, + REFUND_TIMELOCK, + PUNISH_TIMELOCK, + redeem_address, + punish_address, + ); + + // TODO(Franck): Understand why this is needed. + swarm.set_state0(state0.clone()); + + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive message 0 from Bob")?; + let message0 = match event { + OutEvent::Message0(msg) => msg, + other => bail!("Unexpected event received: {:?}", other), + }; + + let state1 = state0.receive(message0)?; + + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive message 1 from Bob")?; + let (msg, channel) = match event { + OutEvent::Message1 { msg, channel } => (msg, channel), + other => bail!("Unexpected event: {:?}", other), + }; + + let state2 = state1.receive(msg); + + let message1 = state2.next_message(); + swarm.send_message1(channel, message1); + + let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + .await + .context("Failed to receive message 2 from Bob")?; + let (msg, channel) = match event { + OutEvent::Message2 { msg, channel } => (msg, channel), + other => bail!("Unexpected event: {:?}", other), + }; + + let state3 = state2.receive(msg)?; + + Ok((channel, state3)) +} + +pub async fn wait_for_locked_bitcoin( + lock_bitcoin_txid: bitcoin::Txid, + bitcoin_wallet: Arc, +) -> Result<()> +where + W: WatchForRawTransaction + WaitForTransactionFinality, +{ + // We assume we will see Bob's transaction in the mempool first. + timeout( + *BOB_TIME_TO_ACT, + bitcoin_wallet.watch_for_raw_transaction(lock_bitcoin_txid), + ) + .await + .context("Failed to find lock Bitcoin tx")?; + + // // We saw the transaction in the mempool, waiting for it to be confirmed. + // bitcoin_wallet + // .wait_for_transaction_finality(lock_bitcoin_txid) + // .await; + + Ok(()) +} + +pub async fn lock_xmr( + channel: ResponseChannel, + amounts: SwapAmounts, + state3: State3, + swarm: &mut Swarm, + monero_wallet: Arc, +) -> Result<()> +where + W: Transfer, +{ + let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { + scalar: state3.s_a.into_ed25519(), + }); + + let public_spend_key = S_a + state3.S_b_monero; + let public_view_key = state3.v.public(); + + let (transfer_proof, _) = monero_wallet + .transfer(public_spend_key, public_view_key, amounts.xmr) + .await?; + + // TODO(Franck): Wait for Monero to be confirmed once + + swarm.send_message2(channel, alice::Message2 { + tx_lock_proof: transfer_proof, + }); + + Ok(()) +} + +pub async fn wait_for_bitcoin_encrypted_signature(swarm: &mut Swarm) -> Result { + let event = timeout(*MONERO_MAX_FINALITY_TIME, swarm.next()) + .await + .context("Failed to receive Bitcoin encrypted signature from Bob")?; + + match event { + OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig), + other => bail!( + "Expected Bob's Bitcoin redeem encrypted signature, got: {:?}", + other + ), + } +} + +pub fn build_bitcoin_redeem_transaction( + encrypted_signature: EncryptedSignature, + tx_lock: &TxLock, + a: bitcoin::SecretKey, + s_a: cross_curve_dleq::Scalar, + B: bitcoin::PublicKey, + redeem_address: &bitcoin::Address, +) -> Result { + let adaptor = Adaptor::>::default(); + + let tx_redeem = bitcoin::TxRedeem::new(tx_lock, redeem_address); + + bitcoin::verify_encsig( + B.clone(), + s_a.into_secp256k1().into(), + &tx_redeem.digest(), + &encrypted_signature, + ) + .context("Invalid encrypted signature received")?; + + let sig_a = a.sign(tx_redeem.digest()); + let sig_b = adaptor.decrypt_signature(&s_a.into_secp256k1(), encrypted_signature); + + let tx = tx_redeem + .add_signatures(&tx_lock, (a.public(), sig_a), (B, sig_b)) + .context("sig_{a,b} are invalid for tx_redeem")?; + + Ok(tx) +} + +pub async fn publish_bitcoin_redeem_transaction( + redeem_tx: bitcoin::Transaction, + bitcoin_wallet: Arc, +) -> Result<()> +where + W: BroadcastSignedTransaction + WaitForTransactionFinality, +{ + let _tx_id = bitcoin_wallet + .broadcast_signed_transaction(redeem_tx) + .await?; + + // // TODO(Franck): Not sure if we wait for finality here or just mined + // bitcoin_wallet.wait_for_transaction_finality(tx_id).await; + Ok(()) +} + +pub async fn publish_cancel_transaction( + tx_lock: TxLock, + a: bitcoin::SecretKey, + B: bitcoin::PublicKey, + refund_timelock: u32, + tx_cancel_sig_bob: bitcoin::Signature, + bitcoin_wallet: Arc, +) -> Result +where + W: GetRawTransaction + TransactionBlockHeight + BlockHeight + BroadcastSignedTransaction, +{ + // First wait for t1 to expire + let tx_lock_height = bitcoin_wallet + .transaction_block_height(tx_lock.txid()) + .await; + poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), tx_lock_height + refund_timelock).await; + + let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); + + // If Bob hasn't yet broadcasted the tx cancel, we do it + if bitcoin_wallet + .get_raw_transaction(tx_cancel.txid()) + .await + .is_err() + { + // TODO(Franck): Maybe the cancel transaction is already mined, in this case, + // the broadcast will error out. + + let sig_a = a.sign(tx_cancel.digest()); + let sig_b = tx_cancel_sig_bob.clone(); + + let tx_cancel = tx_cancel + .clone() + .add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + // TODO(Franck): Error handling is delicate, why can't we broadcast? + bitcoin_wallet + .broadcast_signed_transaction(tx_cancel) + .await?; + + // TODO(Franck): Wait until transaction is mined and returned mined + // block height + } + + Ok(tx_cancel) +} + +pub async fn wait_for_bitcoin_refund( + tx_cancel: &TxCancel, + cancel_tx_height: u32, + punish_timelock: u32, + refund_address: &bitcoin::Address, + bitcoin_wallet: Arc, +) -> Result<(bitcoin::TxRefund, Option)> +where + W: BlockHeight + WatchForRawTransaction, +{ + let punish_timelock_expired = + poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), cancel_tx_height + punish_timelock); + + let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address); + + // TODO(Franck): This only checks the mempool, need to cater for the case where + // the transaction goes directly in a block + let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + + pin_mut!(punish_timelock_expired); + pin_mut!(seen_refund_tx); + + match select(punish_timelock_expired, seen_refund_tx).await { + Either::Left(_) => Ok((tx_refund, None)), + Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx))), + } +} + +pub fn extract_monero_private_key( + published_refund_tx: bitcoin::Transaction, + tx_refund: TxRefund, + s_a: cross_curve_dleq::Scalar, + a: bitcoin::SecretKey, + S_b_bitcoin: bitcoin::PublicKey, +) -> Result { + let s_a = monero::PrivateKey { + scalar: s_a.into_ed25519(), + }; + + let tx_refund_sig = tx_refund + .extract_signature_by_key(published_refund_tx, a.public()) + .context("Failed to extract signature from Bitcoin refund tx")?; + let tx_refund_encsig = a.encsign(S_b_bitcoin.clone(), tx_refund.digest()); + + let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig) + .context("Failed to recover Monero secret key from Bitcoin signature")?; + let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); + + let spend_key = s_a + s_b; + + Ok(spend_key) +} + +pub fn build_bitcoin_punish_transaction( + tx_lock: &TxLock, + refund_timelock: u32, + punish_address: &bitcoin::Address, + punish_timelock: u32, + tx_punish_sig_bob: bitcoin::Signature, + a: bitcoin::SecretKey, + B: bitcoin::PublicKey, +) -> Result { + let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); + let tx_punish = bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock); + + let sig_a = a.sign(tx_punish.digest()); + let sig_b = tx_punish_sig_bob; + + let signed_tx_punish = tx_punish + .add_signatures(&tx_cancel, (a.public(), sig_a), (B, sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + Ok(signed_tx_punish) +} + +pub async fn publish_bitcoin_punish_transaction( + punish_tx: bitcoin::Transaction, + bitcoin_wallet: Arc, +) -> Result +where + W: BroadcastSignedTransaction + WaitForTransactionFinality, +{ + let txid = bitcoin_wallet + .broadcast_signed_transaction(punish_tx) + .await?; + + // todo: enable this once trait is implemented + // bitcoin_wallet.wait_for_transaction_finality(txid).await; + + Ok(txid) +} diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs new file mode 100644 index 00000000..a6330205 --- /dev/null +++ b/swap/src/alice/swap.rs @@ -0,0 +1,351 @@ +//! Run an XMR/BTC swap in the role of Alice. +//! Alice holds XMR and wishes receive BTC. +use crate::{ + alice::{ + execution::{ + build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, + extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction, + publish_bitcoin_redeem_transaction, publish_cancel_transaction, + wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin, + }, + Swarm, + }, + bitcoin, + bitcoin::EncryptedSignature, + monero, + network::request_response::AliceToBob, + SwapAmounts, +}; +use anyhow::Result; +use async_recursion::async_recursion; +use futures::{ + future::{select, Either}, + pin_mut, +}; +use libp2p::request_response::ResponseChannel; +use rand::{CryptoRng, RngCore}; +use std::sync::Arc; +use xmr_btc::{ + alice::State3, + bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, 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. +#[allow(clippy::large_enum_variant)] +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, + }, + 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, 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, + } => { + let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone()).await?; + + 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 } => { + // Our Monero is locked, we need to go through the cancellation process if this + // step fails + match wait_for_bitcoin_encrypted_signature(&mut swarm).await { + Ok(encrypted_signature) => { + swap( + AliceState::EncSignLearned { + state3, + encrypted_signature, + }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + Err(_) => { + swap( + AliceState::WaitingToCancel { state3 }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + } + } + AliceState::EncSignLearned { + state3, + encrypted_signature, + } => { + let signed_tx_redeem = match build_bitcoin_redeem_transaction( + encrypted_signature, + &state3.tx_lock, + state3.a.clone(), + state3.s_a, + state3.B.clone(), + &state3.redeem_address, + ) { + Ok(tx) => tx, + Err(_) => { + return swap( + AliceState::WaitingToCancel { state3 }, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await; + } + }; + + // TODO(Franck): Error handling is delicate here. + // If Bob sees this transaction he can redeem Monero + // e.g. If the Bitcoin node is down then the user needs to take action. + publish_bitcoin_redeem_transaction(signed_tx_redeem, bitcoin_wallet.clone()).await?; + + swap( + AliceState::BtcRedeemed, + swarm, + bitcoin_wallet, + monero_wallet, + ) + .await + } + AliceState::WaitingToCancel { state3 } => { + let tx_cancel = publish_cancel_transaction( + state3.tx_lock.clone(), + state3.a.clone(), + state3.B.clone(), + state3.refund_timelock, + state3.tx_cancel_sig_bob.clone(), + bitcoin_wallet.clone(), + ) + .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 (tx_refund, published_refund_tx) = wait_for_bitcoin_refund( + &tx_cancel, + tx_cancel_height, + state3.punish_timelock, + &state3.refund_address, + bitcoin_wallet.clone(), + ) + .await?; + + // TODO(Franck): Review error handling + match published_refund_tx { + None => { + swap( + AliceState::BtcPunishable { tx_refund, state3 }, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + Some(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 spend_key = extract_monero_private_key( + published_refund_tx, + tx_refund, + state3.s_a, + state3.a.clone(), + state3.S_b_bitcoin, + )?; + let view_key = state3.v; + + monero_wallet + .create_and_load_wallet_for_output(spend_key, view_key) + .await?; + + Ok(AliceState::XmrRefunded) + } + AliceState::BtcPunishable { tx_refund, state3 } => { + let signed_tx_punish = build_bitcoin_punish_transaction( + &state3.tx_lock, + state3.refund_timelock, + &state3.punish_address, + state3.punish_timelock, + state3.tx_punish_sig_bob.clone(), + state3.a.clone(), + state3.B.clone(), + )?; + + let punish_tx_finalised = + publish_bitcoin_punish_transaction(signed_tx_punish, bitcoin_wallet.clone()); + + let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + + 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/main.rs b/swap/src/bin/swap.rs similarity index 95% rename from swap/src/main.rs rename to swap/src/bin/swap.rs index afdf110c..c6e3d82d 100644 --- a/swap/src/main.rs +++ b/swap/src/bin/swap.rs @@ -15,17 +15,16 @@ 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; use swap::{ - alice::{self, Alice}, - bitcoin, - bob::{self, Bob}, + alice, bitcoin, bob, + cli::Options, monero, network::transport::{build, build_tor, SwapTransport}, recover::recover, + storage::Database, Cmd, Rsp, SwapAmounts, }; use tracing::info; @@ -33,20 +32,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(); @@ -59,7 +50,7 @@ async fn main() -> Result<()> { } => { info!("running swap node as Alice ..."); - let behaviour = Alice::default(); + let behaviour = alice::Behaviour::default(); let local_key_pair = behaviour.identity(); let (listen_addr, _ac, transport) = match tor_port { @@ -107,7 +98,7 @@ async fn main() -> Result<()> { } => { info!("running swap node as Bob ..."); - let behaviour = Bob::default(); + let behaviour = bob::Behaviour::default(); let local_key_pair = behaviour.identity(); let transport = match tor { @@ -187,7 +178,7 @@ async fn swap_as_alice( db: Database, addr: Multiaddr, transport: SwapTransport, - behaviour: Alice, + behaviour: alice::Behaviour, ) -> Result<()> { alice::swap( bitcoin_wallet, @@ -207,7 +198,7 @@ async fn swap_as_bob( sats: u64, alice: Multiaddr, transport: SwapTransport, - behaviour: Bob, + behaviour: bob::Behaviour, ) -> Result<()> { let (cmd_tx, mut cmd_rx) = mpsc::channel(1); let (mut rsp_tx, rsp_rx) = mpsc::channel(1); diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 15c1e76b..8425fb85 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -3,15 +3,15 @@ use std::time::Duration; use anyhow::Result; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; -use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Transaction}; +use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin_harness::bitcoind_rpc::PsbtBase64; use reqwest::Url; -use tokio::time; use xmr_btc::bitcoin::{ BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight, WatchForRawTransaction, }; +pub use ::bitcoin::{Address, Transaction}; pub use xmr_btc::bitcoin::*; pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600; @@ -85,21 +85,12 @@ impl SignTxLock for Wallet { #[async_trait] impl BroadcastSignedTransaction for Wallet { async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result { - let txid = self.0.send_raw_transaction(transaction).await?; - - // TODO: Instead of guessing how long it will take for the transaction to be - // mined we should ask bitcoind for the number of confirmations on `txid` - - // give time for transaction to be mined - time::delay_for(Duration::from_millis(1100)).await; - - Ok(txid) + Ok(self.0.send_raw_transaction(transaction).await?) } } // 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 { @@ -110,6 +101,14 @@ 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?) + } +} + #[async_trait] impl BlockHeight for Wallet { async fn block_height(&self) -> u32 { @@ -146,3 +145,10 @@ impl TransactionBlockHeight for Wallet { .expect("transient errors to be retried") } } + +#[async_trait] +impl WaitForTransactionFinality for Wallet { + async fn wait_for_transaction_finality(&self, _txid: Txid) { + todo!() + } +} diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 3b5c5936..f5602b8a 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -1,6 +1,7 @@ //! Run an XMR/BTC swap in the role of Bob. //! Bob holds BTC and wishes receive XMR. use anyhow::Result; + use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use futures::{ @@ -16,10 +17,12 @@ use tracing::{debug, info, warn}; use uuid::Uuid; mod amounts; +mod execution; mod message0; mod message1; mod message2; mod message3; +pub mod swap; use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use crate::{ @@ -51,7 +54,7 @@ pub async fn swap( mut cmd_tx: Sender, mut rsp_rx: Receiver, transport: SwapTransport, - behaviour: Bob, + behaviour: Behaviour, ) -> Result<()> { struct Network(Swarm); @@ -234,9 +237,9 @@ pub async fn swap( } } -pub type Swarm = libp2p::Swarm; +pub type Swarm = libp2p::Swarm; -fn new_swarm(transport: SwapTransport, behaviour: Bob) -> Result { +pub fn new_swarm(transport: SwapTransport, behaviour: Behaviour) -> Result { let local_peer_id = behaviour.peer_id(); let swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) @@ -315,7 +318,7 @@ impl From for OutEvent { #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", event_process = false)] #[allow(missing_debug_implementations)] -pub struct Bob { +pub struct Behaviour { pt: PeerTracker, amounts: Amounts, message0: Message0, @@ -326,7 +329,7 @@ pub struct Bob { identity: Keypair, } -impl Bob { +impl Behaviour { pub fn identity(&self) -> Keypair { self.identity.clone() } @@ -373,8 +376,8 @@ impl Bob { } } -impl Default for Bob { - fn default() -> Bob { +impl Default for Behaviour { + fn default() -> Behaviour { let identity = Keypair::generate_ed25519(); Self { diff --git a/swap/src/bob/execution.rs b/swap/src/bob/execution.rs new file mode 100644 index 00000000..abe31815 --- /dev/null +++ b/swap/src/bob/execution.rs @@ -0,0 +1,52 @@ +use crate::{ + bob::{OutEvent, Swarm}, + SwapAmounts, +}; +use anyhow::Result; +use libp2p::core::Multiaddr; +use rand::{CryptoRng, RngCore}; +use std::sync::Arc; +use xmr_btc::bob::State2; + +pub async fn negotiate( + state0: xmr_btc::bob::State0, + amounts: SwapAmounts, + swarm: &mut Swarm, + addr: Multiaddr, + mut rng: R, + bitcoin_wallet: Arc, +) -> Result +where + R: RngCore + CryptoRng + Send, +{ + libp2p::Swarm::dial_addr(swarm, addr)?; + + let alice = match swarm.next().await { + OutEvent::ConnectionEstablished(alice) => alice, + other => panic!("unexpected event: {:?}", other), + }; + + swarm.request_amounts(alice.clone(), amounts.btc.as_sat()); + + // todo: see if we can remove + let (_btc, _xmr) = match swarm.next().await { + OutEvent::Amounts(amounts) => (amounts.btc, amounts.xmr), + other => panic!("unexpected event: {:?}", other), + }; + + swarm.send_message0(alice.clone(), state0.next_message(&mut rng)); + let state1 = match swarm.next().await { + OutEvent::Message0(msg) => state0.receive(bitcoin_wallet.as_ref(), msg).await?, + other => panic!("unexpected event: {:?}", other), + }; + + swarm.send_message1(alice.clone(), state1.next_message()); + let state2 = match swarm.next().await { + OutEvent::Message1(msg) => state1.receive(msg)?, + other => panic!("unexpected event: {:?}", other), + }; + + swarm.send_message2(alice.clone(), state2.next_message()); + + Ok(state2) +} diff --git a/swap/src/bob/swap.rs b/swap/src/bob/swap.rs new file mode 100644 index 00000000..0d8827e4 --- /dev/null +++ b/swap/src/bob/swap.rs @@ -0,0 +1,265 @@ +use crate::{ + bob::{execution::negotiate, OutEvent, Swarm}, + storage::Database, + SwapAmounts, +}; +use anyhow::Result; +use async_recursion::async_recursion; +use libp2p::{core::Multiaddr, PeerId}; +use rand::{CryptoRng, RngCore}; +use std::sync::Arc; +use tracing::debug; +use uuid::Uuid; +use xmr_btc::bob::{self}; + +// The same data structure is used for swap execution and recovery. +// This allows for a seamless transition from a failed swap to recovery. +pub enum BobState { + Started { + state0: bob::State0, + amounts: SwapAmounts, + peer_id: PeerId, + addr: Multiaddr, + }, + 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 swap( + state: BobState, + mut swarm: Swarm, + db: Database, + bitcoin_wallet: Arc, + monero_wallet: Arc, + mut rng: R, + swap_id: Uuid, +) -> Result +where + R: RngCore + CryptoRng + Send, +{ + match state { + BobState::Started { + state0, + amounts, + peer_id, + addr, + } => { + let state2 = negotiate( + state0, + amounts, + &mut swarm, + addr, + &mut rng, + bitcoin_wallet.clone(), + ) + .await?; + swap( + BobState::Negotiated(state2, peer_id), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + swap_id, + ) + .await + } + BobState::Negotiated(state2, alice_peer_id) => { + // Alice and Bob have exchanged info + let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; + // db.insert_latest_state(state); + swap( + BobState::BtcLocked(state3, alice_peer_id), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + swap_id, + ) + .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), + }; + swap( + BobState::XmrLocked(state4, alice_peer_id), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + swap_id, + ) + .await + } + BobState::XmrLocked(state, alice_peer_id) => { + // Alice has locked Xmr + // Bob sends Alice his key + 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); + + // Sadly we have to poll the swarm to get make sure the message is sent? + // FIXME: Having to wait for Alice's response here is a big problem, because + // we're stuck if she doesn't send her response back. I believe this is + // currently necessary, so we may have to rework this and/or how we use libp2p + match swarm.next().await { + OutEvent::Message3 => { + debug!("Got Message3 empty response"); + } + other => panic!("unexpected event: {:?}", other), + }; + + swap( + BobState::EncSigSent(state, alice_peer_id), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + swap_id, + ) + .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 => { + swap( + BobState::BtcRedeemed(val?), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + swap_id, + ) + .await + } + _ = t1_timeout => { + // Check whether TxCancel has been published. + // We should not fail if the transaction is already on the blockchain + if state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await.is_err() { + state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; + } + + swap( + BobState::Cancelled(state), + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + swap_id + ) + .await + + } + } + } + BobState::BtcRedeemed(state) => { + // Bob redeems XMR using revealed s_a + state.claim_xmr(monero_wallet.as_ref()).await?; + swap( + BobState::XmrRedeemed, + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + swap_id, + ) + .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..68fb45c4 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -1,9 +1,12 @@ +#![allow(non_snake_case)] + use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; pub mod alice; pub mod bitcoin; pub mod bob; +pub mod cli; pub mod monero; pub mod network; pub mod recover; @@ -11,8 +14,8 @@ pub mod state; pub mod storage; pub mod tor; -const REFUND_TIMELOCK: u32 = 10; // Relative timelock, this is number of blocks. TODO: What should it be? -const PUNISH_TIMELOCK: u32 = 10; // FIXME: What should this be? +pub const REFUND_TIMELOCK: u32 = 10; // Relative timelock, this is number of blocks. TODO: What should it be? +pub const PUNISH_TIMELOCK: u32 = 10; // FIXME: What should this be? pub type Never = std::convert::Infallible; @@ -31,6 +34,7 @@ pub enum Rsp { /// XMR/BTC swap amounts. #[derive(Copy, Clone, Debug, Serialize, Deserialize)] +// TODO(Franck): review necessity of this struct pub struct SwapAmounts { /// Amount of BTC to swap. #[serde(with = "::bitcoin::util::amount::serde::as_sat")] diff --git a/swap/src/recover.rs b/swap/src/recover.rs index b71b31fd..5f41112f 100644 --- a/swap/src/recover.rs +++ b/swap/src/recover.rs @@ -45,7 +45,7 @@ pub async fn alice_recover( state: Alice, ) -> Result<()> { match state { - Alice::Handshaken(_) | Alice::BtcLocked(_) | Alice::SwapComplete => { + Alice::Negotiated(_) | Alice::BtcLocked(_) | Alice::SwapComplete => { info!("Nothing to do"); } Alice::XmrLocked(state) => { diff --git a/swap/src/state.rs b/swap/src/state.rs index 701c61e0..e59c976d 100644 --- a/swap/src/state.rs +++ b/swap/src/state.rs @@ -12,7 +12,7 @@ pub enum Swap { #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum Alice { - Handshaken(alice::State3), + Negotiated(alice::State3), BtcLocked(alice::State3), XmrLocked(alice::State3), BtcRedeemable { @@ -63,7 +63,7 @@ impl Display for Swap { impl Display for Alice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Alice::Handshaken(_) => f.write_str("Handshake complete"), + Alice::Negotiated(_) => f.write_str("Handshake complete"), Alice::BtcLocked(_) => f.write_str("Bitcoin locked"), Alice::XmrLocked(_) => f.write_str("Monero locked"), Alice::BtcRedeemable { .. } => f.write_str("Bitcoin redeemable"), diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 358657ca..442fba5f 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -2,17 +2,18 @@ use bitcoin_harness::Bitcoind; use futures::{channel::mpsc, future::try_join}; use libp2p::Multiaddr; use monero_harness::Monero; +use rand::rngs::OsRng; use std::sync::Arc; -use swap::{alice, bob, network::transport::build, storage::Database}; +use swap::{ + alice, alice::swap::AliceState, bob, bob::swap::BobState, network::transport::build, + storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, +}; use tempfile::tempdir; use testcontainers::clients::Cli; -use xmr_btc::bitcoin; - -// NOTE: For some reason running these tests overflows the stack. In order to -// mitigate this run them with: -// -// RUST_MIN_STACK=100000000 cargo test +use uuid::Uuid; +use xmr_btc::{bitcoin, cross_curve_dleq}; +#[ignore] #[tokio::test] async fn swap() { use tracing_subscriber::util::SubscriberInitExt as _; @@ -69,7 +70,7 @@ async fn swap() { )); let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client())); - let alice_behaviour = alice::Alice::default(); + let alice_behaviour = alice::Behaviour::default(); let alice_transport = build(alice_behaviour.identity()).unwrap(); let db = Database::open(std::path::Path::new("../.swap-db/")).unwrap(); @@ -86,7 +87,7 @@ async fn swap() { let db = Database::open(db_dir.path()).unwrap(); let (cmd_tx, mut _cmd_rx) = mpsc::channel(1); let (mut rsp_tx, rsp_rx) = mpsc::channel(1); - let bob_behaviour = bob::Bob::default(); + let bob_behaviour = bob::Behaviour::default(); let bob_transport = build(bob_behaviour.identity()).unwrap(); let bob_swap = bob::swap( bob_btc_wallet.clone(), @@ -122,3 +123,139 @@ async fn swap() { assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr); assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr); } + +#[tokio::test] +async fn happy_path_recursive_executor() { + use tracing_subscriber::util::SubscriberInitExt as _; + let _guard = tracing_subscriber::fmt() + .with_env_filter("swap=info,xmr_btc=info") + .with_ansi(false) + .set_default(); + + let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876" + .parse() + .expect("failed to parse Alice's address"); + + let cli = Cli::default(); + let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); + dbg!(&bitcoind.node_url); + let _ = bitcoind.init(5).await; + + let btc = bitcoin::Amount::from_sat(1_000_000); + let btc_alice = bitcoin::Amount::ZERO; + let btc_bob = btc * 10; + + // this xmr value matches the logic of alice::calculate_amounts i.e. btc * + // 10_000 * 100 + let xmr = 1_000_000_000_000; + let xmr_alice = xmr * 10; + let xmr_bob = 0; + + let alice_btc_wallet = Arc::new( + swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone()) + .await + .unwrap(), + ); + let bob_btc_wallet = Arc::new( + swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone()) + .await + .unwrap(), + ); + bitcoind + .mint(bob_btc_wallet.0.new_address().await.unwrap(), btc_bob) + .await + .unwrap(); + + let (monero, _container) = + Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) + .await + .unwrap(); + monero + .init(vec![("alice", xmr_alice), ("bob", xmr_bob)]) + .await + .unwrap(); + + let alice_xmr_wallet = Arc::new(swap::monero::Wallet( + monero.wallet("alice").unwrap().client(), + )); + let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client())); + + let amounts = SwapAmounts { + btc, + xmr: xmr_btc::monero::Amount::from_piconero(xmr), + }; + + let alice_behaviour = alice::Behaviour::default(); + let alice_peer_id = alice_behaviour.peer_id().clone(); + let alice_transport = build(alice_behaviour.identity()).unwrap(); + let rng = &mut OsRng; + let alice_state = { + let a = bitcoin::SecretKey::new_random(rng); + let s_a = cross_curve_dleq::Scalar::random(rng); + let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng); + AliceState::Started { + amounts, + a, + s_a, + v_a, + } + }; + let alice_swarm = + alice::new_swarm(alice_multiaddr.clone(), alice_transport, alice_behaviour).unwrap(); + let alice_swap = alice::swap::swap( + alice_state, + alice_swarm, + alice_btc_wallet.clone(), + alice_xmr_wallet.clone(), + ); + + let bob_db_dir = tempdir().unwrap(); + let bob_db = Database::open(bob_db_dir.path()).unwrap(); + let bob_behaviour = bob::Behaviour::default(); + let bob_transport = build(bob_behaviour.identity()).unwrap(); + + let refund_address = bob_btc_wallet.new_address().await.unwrap(); + let state0 = xmr_btc::bob::State0::new( + rng, + btc, + xmr_btc::monero::Amount::from_piconero(xmr), + REFUND_TIMELOCK, + PUNISH_TIMELOCK, + refund_address, + ); + let bob_state = BobState::Started { + state0, + amounts, + peer_id: alice_peer_id, + addr: alice_multiaddr, + }; + let bob_swarm = bob::new_swarm(bob_transport, bob_behaviour).unwrap(); + let bob_swap = bob::swap::swap( + bob_state, + bob_swarm, + bob_db, + bob_btc_wallet.clone(), + bob_xmr_wallet.clone(), + OsRng, + Uuid::new_v4(), + ); + + try_join(alice_swap, bob_swap).await.unwrap(); + + let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap(); + let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap(); + + let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap(); + + bob_xmr_wallet.as_ref().0.refresh().await.unwrap(); + let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap(); + + assert_eq!( + btc_alice_final, + btc_alice + btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE) + ); + assert!(btc_bob_final <= btc_bob - btc); + + assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr); + assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr); +} diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index fa35a140..477df236 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -416,8 +416,14 @@ impl State { redeem_address: bitcoin::Address, punish_address: bitcoin::Address, ) -> Self { + let a = bitcoin::SecretKey::new_random(rng); + let s_a = cross_curve_dleq::Scalar::random(rng); + let v_a = monero::PrivateViewKey::new_random(rng); + Self::State0(State0::new( - rng, + a, + s_a, + v_a, btc, xmr, refund_timelock, @@ -443,8 +449,11 @@ pub struct State0 { } impl State0 { - pub fn new( - rng: &mut R, + #[allow(clippy::too_many_arguments)] + pub fn new( + a: bitcoin::SecretKey, + s_a: cross_curve_dleq::Scalar, + v_a: monero::PrivateViewKey, btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -452,11 +461,6 @@ impl State0 { redeem_address: bitcoin::Address, punish_address: bitcoin::Address, ) -> Self { - let a = bitcoin::SecretKey::new_random(rng); - - let s_a = cross_curve_dleq::Scalar::random(rng); - let v_a = monero::PrivateViewKey::new_random(rng); - Self { a, s_a, @@ -617,6 +621,7 @@ impl State2 { S_b_monero: self.S_b_monero, S_b_bitcoin: self.S_b_bitcoin, v: self.v, + // TODO(Franck): Review if these amounts are actually needed btc: self.btc, xmr: self.xmr, refund_timelock: self.refund_timelock, diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index a095d64f..1cbb49f5 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 WaitForTransactionFinality { + async fn wait_for_transaction_finality(&self, txid: Txid); +} + #[async_trait] pub trait BlockHeight { async fn block_height(&self) -> u32; @@ -196,6 +201,16 @@ pub trait TransactionBlockHeight { async fn transaction_block_height(&self, txid: Txid) -> u32; } +#[async_trait] +pub trait WaitForBlockHeight { + async fn wait_for_block_height(&self, height: u32); +} + +#[async_trait] +pub trait GetRawTransaction { + async fn get_raw_transaction(&self, txid: Txid) -> Result; +} + pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { let adaptor = Adaptor::>::default(); diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index e9b98ac8..64c45f48 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(); + 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)] diff --git a/xmr-btc/tests/harness/mod.rs b/xmr-btc/tests/harness/mod.rs index 3d789b8f..c1efa323 100644 --- a/xmr-btc/tests/harness/mod.rs +++ b/xmr-btc/tests/harness/mod.rs @@ -160,15 +160,23 @@ pub async fn init_test( let punish_address = redeem_address.clone(); let refund_address = bob.bitcoin_wallet.new_address().await.unwrap(); - let alice_state0 = xmr_btc::alice::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK), - punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK), - redeem_address.clone(), - punish_address.clone(), - ); + let alice_state0 = { + let a = bitcoin::SecretKey::new_random(&mut OsRng); + let s_a = cross_curve_dleq::Scalar::random(&mut OsRng); + let v_a = monero::PrivateViewKey::new_random(&mut OsRng); + + xmr_btc::alice::State0::new( + a, + s_a, + v_a, + btc_amount, + xmr_amount, + refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK), + punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK), + redeem_address.clone(), + punish_address.clone(), + ) + }; let bob_state0 = xmr_btc::bob::State0::new( &mut OsRng, btc_amount,