diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea67a01c..0602667f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,7 @@ jobs: run: cargo test --workspace --all-features env: MONERO_ADDITIONAL_SLEEP_PERIOD: 60000 + RUST_MIN_STACK: 100000000000 - name: Build binary run: | diff --git a/Cargo.lock b/Cargo.lock index 8d4ec1a7..116a079b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3369,7 +3369,6 @@ dependencies = [ "bitcoin", "bitcoin-harness", "conquer-once", - "conquer-once", "derivative", "ecdsa_fun", "futures", diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs index 89ffa8fa..7ff638ae 100644 --- a/swap/src/alice/execution.rs +++ b/swap/src/alice/execution.rs @@ -5,7 +5,6 @@ use crate::{ 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}, @@ -29,13 +28,6 @@ use xmr_btc::{ monero::Transfer, }; -// 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, @@ -180,8 +172,11 @@ where Ok(()) } -pub async fn wait_for_bitcoin_encrypted_signature(swarm: &mut Swarm) -> Result { - let event = timeout(*MONERO_MAX_FINALITY_TIME, swarm.next()) +pub async fn wait_for_bitcoin_encrypted_signature( + swarm: &mut Swarm, + timeout_duration: Duration, +) -> Result { + let event = timeout(timeout_duration, swarm.next()) .await .context("Failed to receive Bitcoin encrypted signature from Bob")?; diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index 0142527d..b96117ba 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -24,7 +24,8 @@ use futures::{ }; use libp2p::request_response::ResponseChannel; use rand::{CryptoRng, RngCore}; -use std::sync::Arc; +use std::{fmt, sync::Arc}; +use tracing::info; use xmr_btc::{ alice::State3, bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction}, @@ -86,328 +87,377 @@ pub enum AliceState { SafelyAborted, } -pub async fn swap( +impl fmt::Display for AliceState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AliceState::Started { .. } => write!(f, "started"), + AliceState::Negotiated { .. } => write!(f, "negotiated"), + AliceState::BtcLocked { .. } => write!(f, "btc_locked"), + AliceState::XmrLocked { .. } => write!(f, "xmr_locked"), + AliceState::EncSignLearned { .. } => write!(f, "encsig_sent"), + AliceState::BtcRedeemed => write!(f, "btc_redeemed"), + AliceState::BtcCancelled { .. } => write!(f, "btc_cancelled"), + AliceState::BtcRefunded { .. } => write!(f, "btc_refunded"), + AliceState::Punished => write!(f, "punished"), + AliceState::SafelyAborted => write!(f, "safely_aborted"), + AliceState::BtcPunishable { .. } => write!(f, "btc_punishable"), + AliceState::XmrRefunded => write!(f, "xmr_refunded"), + AliceState::WaitingToCancel { .. } => write!(f, "waiting_to_cancel"), + } + } +} + +pub async fn swap( state: AliceState, swarm: Swarm, bitcoin_wallet: Arc, monero_wallet: Arc, -) -> Result - where - R: RngCore + CryptoRng + Send, -{ - run_until(state, is_complete, swarm, bitcoin_wallet, monero_wallet).await + config: Config, +) -> Result<(AliceState, Swarm)> { + run_until( + state, + is_complete, + swarm, + bitcoin_wallet, + monero_wallet, + config, + ) + .await } // TODO: use macro or generics pub fn is_complete(state: &AliceState) -> bool { - matches!(state, AliceState::XmrRefunded| AliceState::BtcRedeemed | AliceState::Punished | AliceState::SafelyAborted) + matches!( + state, + AliceState::XmrRefunded + | AliceState::BtcRedeemed + | AliceState::Punished + | AliceState::SafelyAborted + ) } +pub fn is_xmr_locked(state: &AliceState) -> bool { + matches!( + state, + AliceState::XmrLocked{..} + ) +} // State machine driver for swap execution #[async_recursion] pub async fn run_until( state: AliceState, - is_state: fn(&AliceState) -> bool, + is_target_state: fn(&AliceState) -> bool, mut swarm: Swarm, bitcoin_wallet: Arc, monero_wallet: Arc, config: Config, -) -> Result { - if is_state(&state) { - Ok(state) +) -> Result<(AliceState, Swarm)> { + info!("{}", state); + if is_target_state(&state) { + Ok((state, swarm)) } else { match state { - AliceState::Started { - amounts, - a, - s_a, - v_a, - } => { - let (channel, state3) = negotiate( + AliceState::Started { amounts, a, s_a, v_a, - &mut swarm, - bitcoin_wallet.clone(), - config, - ) - .await?; - - run_until( - AliceState::Negotiated { - channel, + } => { + let (channel, state3) = negotiate( amounts, - state3, - }, - is_state, - swarm, - bitcoin_wallet, - monero_wallet, - config, - ) - .await - } - AliceState::Negotiated { - state3, - channel, - amounts, - } => { - let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone(), config) + a, + s_a, + v_a, + &mut swarm, + bitcoin_wallet.clone(), + config, + ) .await?; - run_until( - AliceState::BtcLocked { - channel, - amounts, - state3, - }, - is_state, - swarm, - bitcoin_wallet, - monero_wallet, - config, - ) - .await - } - AliceState::BtcLocked { - channel, - amounts, - state3, - } => { - lock_xmr( + run_until( + AliceState::Negotiated { + channel, + amounts, + state3, + }, + is_target_state, + swarm, + bitcoin_wallet, + monero_wallet, + config, + ) + .await + } + AliceState::Negotiated { + state3, channel, amounts, - state3.clone(), - &mut swarm, - monero_wallet.clone(), - ) - .await?; + } => { + let _ = + wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone(), config) + .await?; - run_until( - AliceState::XmrLocked { state3 }, - is_state, - swarm, - bitcoin_wallet, - monero_wallet, - config, - ) - .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) => { - run_until( - AliceState::EncSignLearned { - state3, - encrypted_signature, - }, - is_state, - swarm, - bitcoin_wallet, - monero_wallet, - config, - ) - .await - } - Err(_) => { - run_until( - AliceState::WaitingToCancel { state3 }, - is_state, - swarm, - bitcoin_wallet, - monero_wallet, - config, - ) - .await + run_until( + AliceState::BtcLocked { + channel, + amounts, + state3, + }, + is_target_state, + swarm, + bitcoin_wallet, + monero_wallet, + config, + ) + .await + } + AliceState::BtcLocked { + channel, + amounts, + state3, + } => { + lock_xmr( + channel, + amounts, + state3.clone(), + &mut swarm, + monero_wallet.clone(), + ) + .await?; + + run_until( + AliceState::XmrLocked { state3 }, + is_target_state, + swarm, + bitcoin_wallet, + monero_wallet, + config, + ) + .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, + config.monero_max_finality_time, + ) + .await + { + Ok(encrypted_signature) => { + run_until( + AliceState::EncSignLearned { + state3, + encrypted_signature, + }, + is_target_state, + swarm, + bitcoin_wallet, + monero_wallet, + config, + ) + .await + } + Err(_) => { + run_until( + AliceState::WaitingToCancel { state3 }, + is_target_state, + swarm, + bitcoin_wallet, + monero_wallet, + config, + ) + .await + } } } - } - AliceState::EncSignLearned { - state3, - encrypted_signature, - } => { - let signed_tx_redeem = match build_bitcoin_redeem_transaction( + AliceState::EncSignLearned { + state3, encrypted_signature, - &state3.tx_lock, - state3.a.clone(), - state3.s_a, - state3.B, - &state3.redeem_address, - ) { - Ok(tx) => tx, - Err(_) => { - return run_until( - AliceState::WaitingToCancel { state3 }, - is_state, - swarm, - bitcoin_wallet, - monero_wallet, - config, - ) + } => { + let signed_tx_redeem = match build_bitcoin_redeem_transaction( + encrypted_signature, + &state3.tx_lock, + state3.a.clone(), + state3.s_a, + state3.B, + &state3.redeem_address, + ) { + Ok(tx) => tx, + Err(_) => { + return run_until( + AliceState::WaitingToCancel { state3 }, + is_target_state, + swarm, + bitcoin_wallet, + monero_wallet, + config, + ) + .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(), + config, + ) + .await?; + + run_until( + AliceState::BtcRedeemed, + is_target_state, + swarm, + bitcoin_wallet, + monero_wallet, + config, + ) + .await + } + AliceState::WaitingToCancel { state3 } => { + let tx_cancel = publish_cancel_transaction( + state3.tx_lock.clone(), + state3.a.clone(), + state3.B, + state3.refund_timelock, + state3.tx_cancel_sig_bob.clone(), + bitcoin_wallet.clone(), + ) + .await?; + + run_until( + AliceState::BtcCancelled { state3, tx_cancel }, + is_target_state, + swarm, + bitcoin_wallet, + monero_wallet, + config, + ) + .await + } + AliceState::BtcCancelled { state3, tx_cancel } => { + let tx_cancel_height = bitcoin_wallet + .transaction_block_height(tx_cancel.txid()) .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(), config) + 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?; - run_until( - AliceState::BtcRedeemed, - is_state, - swarm, - bitcoin_wallet, - monero_wallet, - config, - ) - .await - } - AliceState::WaitingToCancel { state3 } => { - let tx_cancel = publish_cancel_transaction( - state3.tx_lock.clone(), - state3.a.clone(), - state3.B, - state3.refund_timelock, - state3.tx_cancel_sig_bob.clone(), - bitcoin_wallet.clone(), - ) - .await?; - - run_until( - AliceState::BtcCancelled { state3, tx_cancel }, - is_state, - swarm, - bitcoin_wallet, - monero_wallet, - config, - ) - .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 => { - run_until( - AliceState::BtcPunishable { tx_refund, state3 }, - is_state, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - config, - ) - .await - } - Some(published_refund_tx) => { - run_until( - AliceState::BtcRefunded { - tx_refund, - published_refund_tx, - state3, - }, - is_state, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - config, - ) - .await + // TODO(Franck): Review error handling + match published_refund_tx { + None => { + run_until( + AliceState::BtcPunishable { tx_refund, state3 }, + is_target_state, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + config, + ) + .await + } + Some(published_refund_tx) => { + run_until( + AliceState::BtcRefunded { + tx_refund, + published_refund_tx, + state3, + }, + is_target_state, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + config, + ) + .await + } } } - } - AliceState::BtcRefunded { - tx_refund, - published_refund_tx, - state3, - } => { - let spend_key = extract_monero_private_key( - published_refund_tx, + AliceState::BtcRefunded { tx_refund, - state3.s_a, - state3.a.clone(), - state3.S_b_bitcoin, - )?; - let view_key = state3.v; + 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?; + 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, - )?; + Ok((AliceState::XmrRefunded, swarm)) + } + 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, + )?; - let punish_tx_finalised = publish_bitcoin_punish_transaction( - signed_tx_punish, - bitcoin_wallet.clone(), - config, - ); + let punish_tx_finalised = publish_bitcoin_punish_transaction( + signed_tx_punish, + bitcoin_wallet.clone(), + config, + ); - let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); - pin_mut!(punish_tx_finalised); - pin_mut!(refund_tx_seen); + pin_mut!(punish_tx_finalised); + pin_mut!(refund_tx_seen); - match select(punish_tx_finalised, refund_tx_seen).await { - Either::Left(_) => { - run_until( - AliceState::Punished, - is_state, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - config, - ) - .await - } - Either::Right((published_refund_tx, _)) => { - run_until( - AliceState::BtcRefunded { - tx_refund, - published_refund_tx, - state3, - }, - is_state, - swarm, - bitcoin_wallet.clone(), - monero_wallet, - config, - ) - .await + match select(punish_tx_finalised, refund_tx_seen).await { + Either::Left(_) => { + run_until( + AliceState::Punished, + is_target_state, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + config, + ) + .await + } + Either::Right((published_refund_tx, _)) => { + run_until( + AliceState::BtcRefunded { + tx_refund, + published_refund_tx, + state3, + }, + is_target_state, + swarm, + bitcoin_wallet.clone(), + monero_wallet, + config, + ) + .await + } } } + AliceState::XmrRefunded => Ok((AliceState::XmrRefunded, swarm)), + AliceState::BtcRedeemed => Ok((AliceState::BtcRedeemed, swarm)), + AliceState::Punished => Ok((AliceState::Punished, swarm)), + AliceState::SafelyAborted => Ok((AliceState::SafelyAborted, swarm)), } - 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/bob/swap.rs b/swap/src/bob/swap.rs index 602adeb0..443a86af 100644 --- a/swap/src/bob/swap.rs +++ b/swap/src/bob/swap.rs @@ -7,8 +7,8 @@ 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 std::{fmt, sync::Arc}; +use tracing::{debug, info}; use uuid::Uuid; use xmr_btc::bob::{self}; @@ -33,6 +33,24 @@ pub enum BobState { SafelyAborted, } +impl fmt::Display for BobState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BobState::Started { .. } => write!(f, "started"), + BobState::Negotiated(..) => write!(f, "negotiated"), + BobState::BtcLocked(..) => write!(f, "btc_locked"), + BobState::XmrLocked(..) => write!(f, "xmr_locked"), + BobState::EncSigSent(..) => write!(f, "encsig_sent"), + BobState::BtcRedeemed(_) => write!(f, "btc_redeemed"), + BobState::Cancelled(_) => write!(f, "cancelled"), + BobState::BtcRefunded => write!(f, "btc_refunded"), + BobState::XmrRedeemed => write!(f, "xmr_redeemed"), + BobState::Punished => write!(f, "punished"), + BobState::SafelyAborted => write!(f, "safely_aborted"), + } + } +} + pub async fn swap( state: BobState, swarm: Swarm, @@ -42,22 +60,47 @@ pub async fn swap( rng: R, swap_id: Uuid, ) -> Result - where - R: RngCore + CryptoRng + Send, +where + R: RngCore + CryptoRng + Send, { - run_until(state, is_complete, swarm, db, bitcoin_wallet, monero_wallet, rng, swap_id).await + run_until( + state, + is_complete, + swarm, + db, + bitcoin_wallet, + monero_wallet, + rng, + swap_id, + ) + .await } // TODO: use macro or generics pub fn is_complete(state: &BobState) -> bool { - matches!(state, BobState::BtcRefunded| BobState::XmrRedeemed | BobState::Punished | BobState::SafelyAborted) + matches!( + state, + BobState::BtcRefunded + | BobState::XmrRedeemed + | BobState::Punished + | BobState::SafelyAborted + ) +} + +pub fn is_btc_locked(state: &BobState) -> bool { + matches!(state, BobState::BtcLocked(..)) +} + +pub fn is_xmr_locked(state: &BobState) -> bool { + matches!(state, BobState::XmrLocked(..)) } // State machine driver for swap execution +#[allow(clippy::too_many_arguments)] #[async_recursion] pub async fn run_until( state: BobState, - is_state: fn(&BobState) -> bool, + is_target_state: fn(&BobState) -> bool, mut swarm: Swarm, db: Database, bitcoin_wallet: Arc, @@ -68,7 +111,8 @@ pub async fn run_until( where R: RngCore + CryptoRng + Send, { - if is_state(&state) { + info!("{}", state); + if is_target_state(&state) { Ok(state) } else { match state { @@ -86,10 +130,10 @@ where &mut rng, bitcoin_wallet.clone(), ) - .await?; + .await?; run_until( BobState::Negotiated(state2, peer_id), - is_state, + is_target_state, swarm, db, bitcoin_wallet, @@ -97,7 +141,7 @@ where rng, swap_id, ) - .await + .await } BobState::Negotiated(state2, alice_peer_id) => { // Alice and Bob have exchanged info @@ -105,7 +149,7 @@ where // db.insert_latest_state(state); run_until( BobState::BtcLocked(state3, alice_peer_id), - is_state, + is_target_state, swarm, db, bitcoin_wallet, @@ -113,7 +157,7 @@ where rng, swap_id, ) - .await + .await } // Bob has locked Btc // Watch for Alice to Lock Xmr or for t1 to elapse @@ -129,7 +173,7 @@ where }; run_until( BobState::XmrLocked(state4, alice_peer_id), - is_state, + is_target_state, swarm, db, bitcoin_wallet, @@ -137,7 +181,7 @@ where rng, swap_id, ) - .await + .await } BobState::XmrLocked(state, alice_peer_id) => { // Alice has locked Xmr @@ -162,7 +206,7 @@ where run_until( BobState::EncSigSent(state, alice_peer_id), - is_state, + is_target_state, swarm, db, bitcoin_wallet, @@ -170,7 +214,7 @@ where rng, swap_id, ) - .await + .await } BobState::EncSigSent(state, ..) => { // Watch for redeem @@ -178,47 +222,47 @@ where let t1_timeout = state.wait_for_t1(bitcoin_wallet.as_ref()); tokio::select! { - val = redeem_watcher => { - run_until( - BobState::BtcRedeemed(val?), - is_state, - 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?; + val = redeem_watcher => { + run_until( + BobState::BtcRedeemed(val?), + is_target_state, + 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?; + } - run_until( - BobState::Cancelled(state), - is_state, - swarm, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id - ) - .await + run_until( + BobState::Cancelled(state), + is_target_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?; run_until( BobState::XmrRedeemed, - is_state, + is_target_state, swarm, db, bitcoin_wallet, @@ -226,21 +270,40 @@ where rng, swap_id, ) - .await + .await + } + BobState::Cancelled(_state) => { + // Bob has cancelled the swap + // If { + info!("btc refunded"); + Ok(BobState::BtcRefunded) + } + BobState::Punished => { + info!("punished"); + Ok(BobState::Punished) + } + BobState::SafelyAborted => { + info!("safely aborted"); + Ok(BobState::SafelyAborted) + } + BobState::XmrRedeemed => { + info!("xmr redeemed"); + Ok(BobState::XmrRedeemed) } - 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 { diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 5807a913..f19143fc 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -1,6 +1,6 @@ use bitcoin_harness::Bitcoind; -use futures::{channel::mpsc, future::try_join}; -use libp2p::Multiaddr; +use futures::future::try_join; +use libp2p::{Multiaddr, PeerId}; use monero_harness::Monero; use rand::rngs::OsRng; use std::sync::Arc; @@ -11,25 +11,20 @@ use swap::{ use tempfile::tempdir; use testcontainers::clients::Cli; use uuid::Uuid; -use xmr_btc::{bitcoin, cross_curve_dleq}; +use xmr_btc::{bitcoin, config::Config, cross_curve_dleq}; + +/// Run the following tests with RUST_MIN_STACK=100000000000 -#[ignore] #[tokio::test] -async fn swap() { - 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"); - +async fn happy_path() { + init_tracing(); let cli = Cli::default(); let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); - dbg!(&bitcoind.node_url); let _ = bitcoind.init(5).await; + let (monero, _container) = + Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) + .await + .unwrap(); let btc = bitcoin::Amount::from_sat(1_000_000); let btc_alice = bitcoin::Amount::ZERO; @@ -41,197 +36,35 @@ async fn swap() { let xmr_alice = xmr * 10; let xmr_bob = 0; - let alice_btc_wallet = Arc::new( - swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone()) - .await - .unwrap(), - ); - let bob_btc_wallet = Arc::new( - swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone()) - .await - .unwrap(), - ); - bitcoind - .mint(bob_btc_wallet.0.new_address().await.unwrap(), btc_bob) - .await - .unwrap(); - - let (monero, _container) = - Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) - .await - .unwrap(); - monero - .init(vec![("alice", xmr_alice), ("bob", xmr_bob)]) - .await - .unwrap(); - - let alice_xmr_wallet = Arc::new(swap::monero::Wallet( - monero.wallet("alice").unwrap().client(), - )); - let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client())); - - let alice_behaviour = alice::Behaviour::default(); - let alice_transport = build(alice_behaviour.identity()).unwrap(); - - let db = Database::open(std::path::Path::new("../.swap-db/")).unwrap(); - let alice_swap = alice::swap( - alice_btc_wallet.clone(), - alice_xmr_wallet.clone(), - db, - alice_multiaddr.clone(), - alice_transport, - alice_behaviour, - ); - - let db_dir = tempdir().unwrap(); - let db = Database::open(db_dir.path()).unwrap(); - let (cmd_tx, mut _cmd_rx) = mpsc::channel(1); - let (mut rsp_tx, rsp_rx) = mpsc::channel(1); - let bob_behaviour = bob::Behaviour::default(); - let bob_transport = build(bob_behaviour.identity()).unwrap(); - let bob_swap = bob::swap( - bob_btc_wallet.clone(), - bob_xmr_wallet.clone(), - db, - btc.as_sat(), + let ( + alice_state, + alice_swarm, + alice_btc_wallet, + alice_xmr_wallet, + alice_peer_id, alice_multiaddr, - cmd_tx, - rsp_rx, - bob_transport, - bob_behaviour, - ); + ) = init_alice(&bitcoind, &monero, btc, btc_alice, xmr, xmr_alice).await; - // automate the verification step by accepting any amounts sent over by Alice - rsp_tx.try_send(swap::Rsp::VerifiedAmounts).unwrap(); - - try_join(alice_swap, bob_swap).await.unwrap(); - - let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap(); - let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap(); - - let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap(); - - bob_xmr_wallet.as_ref().0.refresh().await.unwrap(); - let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap(); - - assert_eq!( - btc_alice_final, - btc_alice + btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE) - ); - assert!(btc_bob_final <= btc_bob - btc); - - assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr); - assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr); -} - -#[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 { + let (bob_state, bob_swarm, bob_btc_wallet, bob_xmr_wallet, bob_db) = init_bob( + alice_multiaddr, + alice_peer_id, + &bitcoind, + &monero, btc, - xmr: xmr_btc::monero::Amount::from_piconero(xmr), - }; + btc_bob, + xmr, + xmr_bob, + ) + .await; - 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 config = xmr_btc::config::Config::regtest(); let alice_swap = alice::swap::swap( alice_state, alice_swarm, alice_btc_wallet.clone(), alice_xmr_wallet.clone(), - config, + Config::regtest(), ); - 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, @@ -261,3 +94,241 @@ async fn happy_path_recursive_executor() { assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr); assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr); } + +/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice +/// the encsig and fail to refund or redeem. Alice punishes. +#[tokio::test] +async fn alice_punishes_if_bob_never_acts_after_fund() { + init_tracing(); + let cli = Cli::default(); + let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); + let _ = bitcoind.init(5).await; + let (monero, _container) = + Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) + .await + .unwrap(); + + let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); + let xmr_to_swap = 1_000_000_000_000; + + let bob_btc_starting_balance = btc_to_swap * 10; + let bob_xmr_starting_balance = 0; + + let alice_btc_starting_balance = bitcoin::Amount::ZERO; + let alice_xmr_starting_balance = xmr_to_swap * 10; + + let ( + alice_state, + alice_swarm, + alice_btc_wallet, + alice_xmr_wallet, + alice_peer_id, + alice_multiaddr, + ) = init_alice( + &bitcoind, + &monero, + btc_to_swap, + alice_btc_starting_balance, + xmr_to_swap, + alice_xmr_starting_balance, + ) + .await; + + let (bob_state, bob_swarm, bob_btc_wallet, bob_xmr_wallet, bob_db) = init_bob( + alice_multiaddr, + alice_peer_id, + &bitcoind, + &monero, + btc_to_swap, + bob_btc_starting_balance, + xmr_to_swap, + bob_xmr_starting_balance, + ) + .await; + + let bob_xmr_locked_fut = bob::swap::run_until( + bob_state, + bob::swap::is_xmr_locked, + bob_swarm, + bob_db, + bob_btc_wallet.clone(), + bob_xmr_wallet.clone(), + OsRng, + Uuid::new_v4(), + ); + + let alice_xmr_locked_fut = alice::swap::run_until( + alice_state, + alice::swap::is_xmr_locked, + alice_swarm, + alice_btc_wallet.clone(), + alice_xmr_wallet.clone(), + Config::regtest(), + ); + + // Wait until alice has locked xmr and bob has locked btc + let ((alice_state, alice_swarm), _bob_state) = + try_join(alice_xmr_locked_fut, bob_xmr_locked_fut) + .await + .unwrap(); + + let (punished, _) = alice::swap::swap( + alice_state, + alice_swarm, + alice_btc_wallet.clone(), + alice_xmr_wallet.clone(), + Config::regtest(), + ) + .await + .unwrap(); + + assert!(matches!(punished, AliceState::Punished)); + + // todo: Add balance assertions +} + +#[allow(clippy::too_many_arguments)] +async fn init_alice( + bitcoind: &Bitcoind<'_>, + monero: &Monero, + btc_to_swap: bitcoin::Amount, + _btc_starting_balance: bitcoin::Amount, + xmr_to_swap: u64, + xmr_starting_balance: u64, +) -> ( + AliceState, + alice::Swarm, + Arc, + Arc, + PeerId, + Multiaddr, +) { + monero + .init(vec![("alice", xmr_starting_balance)]) + .await + .unwrap(); + + let alice_xmr_wallet = Arc::new(swap::monero::Wallet( + monero.wallet("alice").unwrap().client(), + )); + + let alice_btc_wallet = Arc::new( + swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone()) + .await + .unwrap(), + ); + + let amounts = SwapAmounts { + btc: btc_to_swap, + xmr: xmr_btc::monero::Amount::from_piconero(xmr_to_swap), + }; + + let alice_behaviour = alice::Behaviour::default(); + let alice_peer_id = alice_behaviour.peer_id(); + 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_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876" + .parse() + .expect("failed to parse Alice's address"); + + let alice_swarm = + alice::new_swarm(alice_multiaddr.clone(), alice_transport, alice_behaviour).unwrap(); + + ( + alice_state, + alice_swarm, + alice_btc_wallet, + alice_xmr_wallet, + alice_peer_id, + alice_multiaddr, + ) +} + +#[allow(clippy::too_many_arguments)] +async fn init_bob( + alice_multiaddr: Multiaddr, + alice_peer_id: PeerId, + bitcoind: &Bitcoind<'_>, + monero: &Monero, + btc_to_swap: bitcoin::Amount, + btc_starting_balance: bitcoin::Amount, + xmr_to_swap: u64, + xmr_stating_balance: u64, +) -> ( + BobState, + bob::Swarm, + Arc, + Arc, + Database, +) { + 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_starting_balance, + ) + .await + .unwrap(); + + monero + .init(vec![("bob", xmr_stating_balance)]) + .await + .unwrap(); + + let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client())); + + let amounts = SwapAmounts { + btc: btc_to_swap, + xmr: xmr_btc::monero::Amount::from_piconero(xmr_to_swap), + }; + + let rng = &mut OsRng; + + 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_to_swap, + xmr_btc::monero::Amount::from_piconero(xmr_to_swap), + 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(); + + (bob_state, bob_swarm, bob_btc_wallet, bob_xmr_wallet, bob_db) +} + +fn init_tracing() { + use tracing_subscriber::util::SubscriberInitExt as _; + let _guard = tracing_subscriber::fmt() + .with_env_filter("swap=info,xmr_btc=info") + .with_ansi(false) + .set_default(); +} diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 3d0c15ae..8f52b788 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -799,6 +799,9 @@ impl State4 { t1_timeout.await; Ok(()) } + pub fn tx_lock_id(&self) -> bitcoin::Txid { + self.tx_lock.txid() + } } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/xmr-btc/src/config.rs b/xmr-btc/src/config.rs index cc0a7d5a..48594091 100644 --- a/xmr-btc/src/config.rs +++ b/xmr-btc/src/config.rs @@ -6,6 +6,7 @@ pub struct Config { pub bob_time_to_act: Duration, pub bitcoin_finality_confirmations: u32, pub bitcoin_avg_block_time: Duration, + pub monero_max_finality_time: Duration, } impl Config { @@ -14,6 +15,7 @@ impl Config { bob_time_to_act: *mainnet::BOB_TIME_TO_ACT, bitcoin_finality_confirmations: mainnet::BITCOIN_FINALITY_CONFIRMATIONS, bitcoin_avg_block_time: *mainnet::BITCOIN_AVG_BLOCK_TIME, + monero_max_finality_time: *mainnet::MONERO_MAX_FINALITY_TIME, } } @@ -22,6 +24,7 @@ impl Config { bob_time_to_act: *regtest::BOB_TIME_TO_ACT, bitcoin_finality_confirmations: regtest::BITCOIN_FINALITY_CONFIRMATIONS, bitcoin_avg_block_time: *regtest::BITCOIN_AVG_BLOCK_TIME, + monero_max_finality_time: *regtest::MONERO_MAX_FINALITY_TIME, } } } @@ -35,15 +38,21 @@ mod mainnet { pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 3; pub static BITCOIN_AVG_BLOCK_TIME: Lazy = Lazy::new(|| Duration::from_secs(10 * 60)); + + pub static MONERO_MAX_FINALITY_TIME: Lazy = + Lazy::new(|| Duration::from_secs_f64(15f64 * 1.5 * 2f64 * 60f64)); } mod regtest { use super::*; // In test, set to 5 seconds to fail fast - pub static BOB_TIME_TO_ACT: Lazy = Lazy::new(|| Duration::from_secs(10)); + pub static BOB_TIME_TO_ACT: Lazy = Lazy::new(|| Duration::from_secs(30)); pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 1; pub static BITCOIN_AVG_BLOCK_TIME: Lazy = Lazy::new(|| Duration::from_secs(5)); + + pub static MONERO_MAX_FINALITY_TIME: Lazy = + Lazy::new(|| Duration::from_secs_f64(60f64)); }