From 5395303a99b0e66a5380e8d14ba48f573d6950fc Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Thu, 22 Oct 2020 10:57:42 +1100 Subject: [PATCH] Test on-chain protocol happy path --- xmr-btc/Cargo.toml | 3 +- xmr-btc/src/bob.rs | 6 +- xmr-btc/src/lib.rs | 130 +++++++++++------- xmr-btc/src/monero.rs | 20 ++- xmr-btc/tests/e2e.rs | 170 ++---------------------- xmr-btc/tests/harness/mod.rs | 153 +++++++++++++++++++++ xmr-btc/tests/harness/wallet/bitcoin.rs | 47 +++++-- xmr-btc/tests/harness/wallet/monero.rs | 18 ++- xmr-btc/tests/on_chain.rs | 155 ++++++++++++++++++--- 9 files changed, 457 insertions(+), 245 deletions(-) diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 3e1457de..4e22e421 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -19,12 +19,13 @@ monero = "0.9" rand = "0.7" sha2 = "0.9" thiserror = "1" +tokio = { version = "0.2", features = ["time"] } tracing = "0.1" [dev-dependencies] backoff = { version = "0.2", features = ["tokio"] } base64 = "0.12" -bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" } +bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "7ff30a559ab57cc3aa71189e71433ef6b2a6c3a2" } futures = "0.3" monero-harness = { path = "../monero-harness" } reqwest = { version = "0.10", default-features = false } diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 2b62c749..51bd2a88 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -50,13 +50,15 @@ pub async fn next_state< let message1 = transport.receive_message().await?.try_into()?; let state2 = state1.receive(message1)?; + + let message2 = state2.next_message(); + transport.send_message(message2.into()).await?; Ok(state2.into()) } State::State2(state2) => { - let message2 = state2.next_message(); let state3 = state2.lock_btc(bitcoin_wallet).await?; tracing::info!("bob has locked btc"); - transport.send_message(message2.into()).await?; + Ok(state3.into()) } State::State3(state3) => { diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 1b2b6e24..c51ca999 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -59,6 +59,14 @@ use futures::{ }; use genawaiter::sync::{Gen, GenBoxed}; use sha2::Sha256; +use std::{sync::Arc, time::Duration}; +use tokio::time::timeout; +use tracing::error; + +// TODO: Replace this with something configurable, such as an function argument. +/// Time that Bob has to publish the Bitcoin lock transaction before both +/// parties will abort, in seconds. +const SECS_TO_ACT_BOB: u64 = 60; #[allow(clippy::large_enum_variant)] #[derive(Debug)] @@ -80,8 +88,13 @@ pub trait ReceiveTransferProof { } #[async_trait] -pub trait MedianTime { - async fn median_time(&self) -> u32; +pub trait BlockHeight { + async fn block_height(&self) -> u32; +} + +#[async_trait] +pub trait TransactionBlockHeight { + async fn transaction_block_height(&self, txid: bitcoin::Txid) -> u32; } /// Perform the on-chain protocol to swap monero and bitcoin as Bob. @@ -89,9 +102,9 @@ pub trait MedianTime { /// This is called post handshake, after all the keys, addresses and most of the /// signatures have been exchanged. pub fn action_generator_bob( - network: &'static mut N, - monero_client: &'static M, - bitcoin_client: &'static B, + mut network: N, + monero_client: Arc, + bitcoin_client: Arc, // TODO: Replace this with a new, slimmer struct? bob::State2 { A, @@ -111,10 +124,16 @@ pub fn action_generator_bob( }: bob::State2, ) -> GenBoxed where - N: ReceiveTransferProof + Send + Sync, - M: monero::WatchForTransfer + Send + Sync, - B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync, + N: ReceiveTransferProof + Send + Sync + 'static, + M: monero::WatchForTransfer + Send + Sync + 'static, + B: BlockHeight + + TransactionBlockHeight + + bitcoin::WatchForRawTransaction + + Send + + Sync + + 'static, { + #[derive(Debug)] enum SwapFailed { BeforeBtcLock, AfterBtcLock(Reason), @@ -122,6 +141,7 @@ where } /// Reason why the swap has failed. + #[derive(Debug)] enum Reason { /// The refund timelock has been reached. BtcExpired, @@ -140,37 +160,40 @@ where if condition_future.clone().await { return; } + + tokio::time::delay_for(std::time::Duration::from_secs(1)).await; } } - async fn bitcoin_time_is_gte(bitcoin_client: &B, timestamp: u32) -> bool + async fn bitcoin_block_height_is_gte(bitcoin_client: &B, n_blocks: u32) -> bool where - B: MedianTime, + B: BlockHeight, { - bitcoin_client.median_time().await >= timestamp + bitcoin_client.block_height().await >= n_blocks } Gen::new_boxed(|co| async move { let swap_result: Result<(), SwapFailed> = async { - let btc_has_expired = bitcoin_time_is_gte(bitcoin_client, refund_timelock).shared(); - let poll_until_btc_has_expired = poll_until(btc_has_expired.clone()).shared(); - futures::pin_mut!(poll_until_btc_has_expired); - - if btc_has_expired.clone().await { - return Err(SwapFailed::BeforeBtcLock); - } - co.yield_(BobAction::LockBitcoin(tx_lock.clone())).await; - match select( + timeout( + Duration::from_secs(SECS_TO_ACT_BOB), bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - poll_until_btc_has_expired.clone(), ) .await - { - Either::Left(_) => {} - Either::Right(_) => return Err(SwapFailed::BeforeBtcLock), - } + .map(|tx| tx.txid()) + .map_err(|_| SwapFailed::BeforeBtcLock)?; + + let tx_lock_height = bitcoin_client + .transaction_block_height(tx_lock.txid()) + .await; + let btc_has_expired = bitcoin_block_height_is_gte( + bitcoin_client.as_ref(), + tx_lock_height + refund_timelock, + ) + .shared(); + let poll_until_btc_has_expired = poll_until(btc_has_expired).shared(); + futures::pin_mut!(poll_until_btc_has_expired); let transfer_proof = match select( network.receive_transfer_proof(), @@ -245,7 +268,9 @@ where } .await; - if let Err(SwapFailed::AfterBtcLock(_)) = swap_result { + if let Err(err @ SwapFailed::AfterBtcLock(_)) = swap_result { + error!("Swap failed, reason: {:?}", err); + let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public()); let tx_cancel_txid = tx_cancel.txid(); @@ -316,10 +341,9 @@ pub trait ReceiveBitcoinRedeemEncsig { /// /// This is called post handshake, after all the keys, addresses and most of the /// signatures have been exchanged. -pub fn action_generator_alice( - network: &'static mut N, - _monero_client: &'static M, - bitcoin_client: &'static B, +pub fn action_generator_alice( + mut network: N, + bitcoin_client: Arc, // TODO: Replace this with a new, slimmer struct? alice::State3 { a, @@ -341,16 +365,22 @@ pub fn action_generator_alice( }: alice::State3, ) -> GenBoxed where - N: ReceiveBitcoinRedeemEncsig + Send + Sync, - M: Send + Sync, - B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync, + N: ReceiveBitcoinRedeemEncsig + Send + Sync + 'static, + B: BlockHeight + + TransactionBlockHeight + + bitcoin::WatchForRawTransaction + + Send + + Sync + + 'static, { + #[derive(Debug)] enum SwapFailed { BeforeBtcLock, AfterXmrLock(Reason), } /// Reason why the swap has failed. + #[derive(Debug)] enum Reason { /// The refund timelock has been reached. BtcExpired, @@ -373,31 +403,37 @@ where if condition_future.clone().await { return; } + + tokio::time::delay_for(std::time::Duration::from_secs(1)).await; } } - async fn bitcoin_time_is_gte(bitcoin_client: &B, timestamp: u32) -> bool + async fn bitcoin_block_height_is_gte(bitcoin_client: &B, n_blocks: u32) -> bool where - B: MedianTime, + B: BlockHeight, { - bitcoin_client.median_time().await >= timestamp + bitcoin_client.block_height().await >= n_blocks } Gen::new_boxed(|co| async move { let swap_result: Result<(), SwapFailed> = async { - let btc_has_expired = bitcoin_time_is_gte(bitcoin_client, refund_timelock).shared(); - let poll_until_btc_has_expired = poll_until(btc_has_expired.clone()).shared(); - futures::pin_mut!(poll_until_btc_has_expired); - - match select( + timeout( + Duration::from_secs(SECS_TO_ACT_BOB), bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - poll_until_btc_has_expired.clone(), ) .await - { - Either::Left(_) => {} - Either::Right(_) => return Err(SwapFailed::BeforeBtcLock), - } + .map_err(|_| SwapFailed::BeforeBtcLock)?; + + let tx_lock_height = bitcoin_client + .transaction_block_height(tx_lock.txid()) + .await; + let btc_has_expired = bitcoin_block_height_is_gte( + bitcoin_client.as_ref(), + tx_lock_height + refund_timelock, + ) + .shared(); + let poll_until_btc_has_expired = poll_until(btc_has_expired).shared(); + futures::pin_mut!(poll_until_btc_has_expired); let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: s_a.into_ed25519(), @@ -459,7 +495,7 @@ where if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result { let refund_result: Result<(), RefundFailed> = async { let bob_can_be_punished = - bitcoin_time_is_gte(bitcoin_client, punish_timelock).shared(); + bitcoin_block_height_is_gte(bitcoin_client.as_ref(), punish_timelock).shared(); let poll_until_bob_can_be_punished = poll_until(bob_can_be_punished).shared(); futures::pin_mut!(poll_until_bob_can_be_punished); diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index 241666c7..f2459d04 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; pub use curve25519_dalek::scalar::Scalar; pub use monero::{Address, PrivateKey, PublicKey}; use rand::{CryptoRng, RngCore}; -use std::ops::Add; +use std::ops::{Add, Sub}; pub const MIN_CONFIRMATIONS: u32 = 10; @@ -51,7 +51,7 @@ impl From for PublicKey { #[derive(Clone, Copy, Debug)] pub struct PublicViewKey(PublicKey); -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Amount(u64); impl Amount { @@ -67,6 +67,22 @@ impl Amount { } } +impl Add for Amount { + type Output = Amount; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for Amount { + type Output = Amount; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + impl From for u64 { fn from(from: Amount) -> u64 { from.0 diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index 024fb1ca..a7203ab0 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -1,156 +1,14 @@ pub mod harness; -use crate::harness::wallet; -use bitcoin_harness::Bitcoind; -use harness::{ - node::{AliceNode, BobNode}, - transport::Transport, -}; -use monero_harness::Monero; -use rand::rngs::OsRng; -use testcontainers::clients::Cli; -use tokio::sync::{ - mpsc, - mpsc::{Receiver, Sender}, -}; -use xmr_btc::{alice, bitcoin, bob, monero}; - -const TEN_XMR: u64 = 10_000_000_000_000; -const RELATIVE_REFUND_TIMELOCK: u32 = 1; -const RELATIVE_PUNISH_TIMELOCK: u32 = 1; - -pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> { - let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind"); - let _ = bitcoind.init(5).await; - - bitcoind -} - -pub struct InitialBalances { - alice_xmr: u64, - alice_btc: bitcoin::Amount, - bob_xmr: u64, - bob_btc: bitcoin::Amount, -} - -pub struct SwapAmounts { - xmr: monero::Amount, - btc: bitcoin::Amount, -} - -pub fn init_alice_and_bob_transports() -> ( - Transport, - Transport, -) { - let (a_sender, b_receiver): (Sender, Receiver) = - mpsc::channel(5); - let (b_sender, a_receiver): (Sender, Receiver) = mpsc::channel(5); - - let a_transport = Transport { - sender: a_sender, - receiver: a_receiver, - }; - - let b_transport = Transport { - sender: b_sender, - receiver: b_receiver, - }; - - (a_transport, b_transport) -} - -pub async fn init_test( - monero: &Monero, - bitcoind: &Bitcoind<'_>, -) -> ( - alice::State0, - bob::State0, - AliceNode, - BobNode, - InitialBalances, - SwapAmounts, -) { - // must be bigger than our hardcoded fee of 10_000 - let btc_amount = bitcoin::Amount::from_sat(10_000_000); - let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000); - - let swap_amounts = SwapAmounts { - xmr: xmr_amount, - btc: btc_amount, - }; - - let fund_alice = TEN_XMR; - let fund_bob = 0; - monero.init(fund_alice, fund_bob).await.unwrap(); - - let alice_monero_wallet = wallet::monero::Wallet(monero.alice_wallet_rpc_client()); - let bob_monero_wallet = wallet::monero::Wallet(monero.bob_wallet_rpc_client()); - - let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url) - .await - .unwrap(); - let bob_btc_wallet = wallet::bitcoin::make_wallet("bob", &bitcoind, btc_amount) - .await - .unwrap(); - - let (alice_transport, bob_transport) = init_alice_and_bob_transports(); - let alice = AliceNode::new(alice_transport, alice_btc_wallet, alice_monero_wallet); - - let bob = BobNode::new(bob_transport, bob_btc_wallet, bob_monero_wallet); - - let alice_initial_btc_balance = alice.bitcoin_wallet.balance().await.unwrap(); - let bob_initial_btc_balance = bob.bitcoin_wallet.balance().await.unwrap(); - - let alice_initial_xmr_balance = alice.monero_wallet.0.get_balance(0).await.unwrap(); - let bob_initial_xmr_balance = bob.monero_wallet.0.get_balance(0).await.unwrap(); - - let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap(); - let punish_address = redeem_address.clone(); - let refund_address = bob.bitcoin_wallet.new_address().await.unwrap(); - - let alice_state0 = alice::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - RELATIVE_REFUND_TIMELOCK, - RELATIVE_PUNISH_TIMELOCK, - redeem_address.clone(), - punish_address.clone(), - ); - let bob_state0 = bob::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - RELATIVE_REFUND_TIMELOCK, - RELATIVE_PUNISH_TIMELOCK, - refund_address, - ); - let initial_balances = InitialBalances { - alice_xmr: alice_initial_xmr_balance, - alice_btc: alice_initial_btc_balance, - bob_xmr: bob_initial_xmr_balance, - bob_btc: bob_initial_btc_balance, - }; - ( - alice_state0, - bob_state0, - alice, - bob, - initial_balances, - swap_amounts, - ) -} - mod tests { // 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 crate::{ - harness, - harness::node::{run_alice_until, run_bob_until}, - init_bitcoind, init_test, + use crate::harness::{ + self, init_bitcoind, init_test, + node::{run_alice_until, run_bob_until}, }; use futures::future; use monero_harness::Monero; @@ -181,7 +39,7 @@ mod tests { mut bob_node, initial_balances, swap_amounts, - ) = init_test(&monero, &bitcoind).await; + ) = init_test(&monero, &bitcoind, None, None).await; let (alice_state, bob_state) = future::try_join( run_alice_until( @@ -212,11 +70,11 @@ mod tests { .await .unwrap(); - let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap(); + let alice_final_xmr_balance = alice_node.monero_wallet.get_balance().await.unwrap(); monero.wait_for_bob_wallet_block_height().await.unwrap(); - let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap(); + let bob_final_xmr_balance = bob_node.monero_wallet.get_balance().await.unwrap(); assert_eq!( alice_final_btc_balance, @@ -230,13 +88,11 @@ mod tests { assert_eq!( alice_final_xmr_balance, - initial_balances.alice_xmr - - u64::from(swap_amounts.xmr) - - u64::from(alice_state6.lock_xmr_fee()) + initial_balances.alice_xmr - swap_amounts.xmr - alice_state6.lock_xmr_fee() ); assert_eq!( bob_final_xmr_balance, - initial_balances.bob_xmr + u64::from(swap_amounts.xmr) + initial_balances.bob_xmr + swap_amounts.xmr ); } @@ -257,7 +113,7 @@ mod tests { mut bob_node, initial_balances, swap_amounts, - ) = init_test(&monero, &bitcoind).await; + ) = init_test(&monero, &bitcoind, None, None).await; let (alice_state, bob_state) = future::try_join( run_alice_until( @@ -300,8 +156,8 @@ mod tests { .unwrap(); monero.wait_for_alice_wallet_block_height().await.unwrap(); - let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap(); - let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap(); + let alice_final_xmr_balance = alice_node.monero_wallet.get_balance().await.unwrap(); + let bob_final_xmr_balance = bob_node.monero_wallet.get_balance().await.unwrap(); assert_eq!(alice_final_btc_balance, initial_balances.alice_btc); assert_eq!( @@ -312,7 +168,7 @@ mod tests { // Because we create a new wallet when claiming Monero, we can only assert on // this new wallet owning all of `xmr_amount` after refund - assert_eq!(alice_final_xmr_balance, u64::from(swap_amounts.xmr)); + assert_eq!(alice_final_xmr_balance, swap_amounts.xmr); assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr); } @@ -333,7 +189,7 @@ mod tests { mut bob_node, initial_balances, swap_amounts, - ) = init_test(&monero, &bitcoind).await; + ) = init_test(&monero, &bitcoind, None, None).await; let (alice_state, bob_state) = future::try_join( run_alice_until( diff --git a/xmr-btc/tests/harness/mod.rs b/xmr-btc/tests/harness/mod.rs index 4f2cba46..6e94edf2 100644 --- a/xmr-btc/tests/harness/mod.rs +++ b/xmr-btc/tests/harness/mod.rs @@ -5,6 +5,10 @@ pub mod wallet; pub mod bob { use xmr_btc::bob::State; + pub fn is_state2(state: &State) -> bool { + matches!(state, State::State2 { .. }) + } + // TODO: use macro or generics pub fn is_state5(state: &State) -> bool { matches!(state, State::State5 { .. }) @@ -19,6 +23,10 @@ pub mod bob { pub mod alice { use xmr_btc::alice::State; + pub fn is_state3(state: &State) -> bool { + matches!(state, State::State3 { .. }) + } + // TODO: use macro or generics pub fn is_state4(state: &State) -> bool { matches!(state, State::State4 { .. }) @@ -34,3 +42,148 @@ pub mod alice { matches!(state, State::State6 { .. }) } } + +use bitcoin_harness::Bitcoind; +use monero_harness::Monero; +use node::{AliceNode, BobNode}; +use rand::rngs::OsRng; +use testcontainers::clients::Cli; +use tokio::sync::{ + mpsc, + mpsc::{Receiver, Sender}, +}; +use transport::Transport; +use xmr_btc::{bitcoin, monero}; + +const TEN_XMR: u64 = 10_000_000_000_000; +const RELATIVE_REFUND_TIMELOCK: u32 = 1; +const RELATIVE_PUNISH_TIMELOCK: u32 = 1; + +pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> { + let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind"); + let _ = bitcoind.init(5).await; + + bitcoind +} + +pub struct InitialBalances { + pub alice_xmr: monero::Amount, + pub alice_btc: bitcoin::Amount, + pub bob_xmr: monero::Amount, + pub bob_btc: bitcoin::Amount, +} + +pub struct SwapAmounts { + pub xmr: monero::Amount, + pub btc: bitcoin::Amount, +} + +pub fn init_alice_and_bob_transports() -> ( + Transport, + Transport, +) { + let (a_sender, b_receiver): ( + Sender, + Receiver, + ) = mpsc::channel(5); + let (b_sender, a_receiver): ( + Sender, + Receiver, + ) = mpsc::channel(5); + + let a_transport = Transport { + sender: a_sender, + receiver: a_receiver, + }; + + let b_transport = Transport { + sender: b_sender, + receiver: b_receiver, + }; + + (a_transport, b_transport) +} + +pub async fn init_test( + monero: &Monero, + bitcoind: &Bitcoind<'_>, + refund_timelock: Option, + punish_timelock: Option, +) -> ( + xmr_btc::alice::State0, + xmr_btc::bob::State0, + AliceNode, + BobNode, + InitialBalances, + SwapAmounts, +) { + // must be bigger than our hardcoded fee of 10_000 + let btc_amount = bitcoin::Amount::from_sat(10_000_000); + let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000); + + let swap_amounts = SwapAmounts { + xmr: xmr_amount, + btc: btc_amount, + }; + + let fund_alice = TEN_XMR; + let fund_bob = 0; + monero.init(fund_alice, fund_bob).await.unwrap(); + + let alice_monero_wallet = wallet::monero::Wallet(monero.alice_wallet_rpc_client()); + let bob_monero_wallet = wallet::monero::Wallet(monero.bob_wallet_rpc_client()); + + let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url) + .await + .unwrap(); + let bob_btc_wallet = wallet::bitcoin::make_wallet("bob", &bitcoind, btc_amount) + .await + .unwrap(); + + let (alice_transport, bob_transport) = init_alice_and_bob_transports(); + let alice = AliceNode::new(alice_transport, alice_btc_wallet, alice_monero_wallet); + + let bob = BobNode::new(bob_transport, bob_btc_wallet, bob_monero_wallet); + + let alice_initial_btc_balance = alice.bitcoin_wallet.balance().await.unwrap(); + let bob_initial_btc_balance = bob.bitcoin_wallet.balance().await.unwrap(); + + let alice_initial_xmr_balance = alice.monero_wallet.get_balance().await.unwrap(); + let bob_initial_xmr_balance = bob.monero_wallet.get_balance().await.unwrap(); + + let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap(); + 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 bob_state0 = xmr_btc::bob::State0::new( + &mut OsRng, + btc_amount, + xmr_amount, + refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK), + punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK), + refund_address, + ); + let initial_balances = InitialBalances { + alice_xmr: alice_initial_xmr_balance, + alice_btc: alice_initial_btc_balance, + bob_xmr: bob_initial_xmr_balance, + bob_btc: bob_initial_btc_balance, + }; + ( + alice_state0, + bob_state0, + alice, + bob, + initial_balances, + swap_amounts, + ) +} diff --git a/xmr-btc/tests/harness/wallet/bitcoin.rs b/xmr-btc/tests/harness/wallet/bitcoin.rs index d31990e8..c39ba3f7 100644 --- a/xmr-btc/tests/harness/wallet/bitcoin.rs +++ b/xmr-btc/tests/harness/wallet/bitcoin.rs @@ -1,6 +1,6 @@ use anyhow::Result; use async_trait::async_trait; -use backoff::{future::FutureOperation as _, ExponentialBackoff}; +use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid}; use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind}; use reqwest::Url; @@ -10,7 +10,7 @@ use xmr_btc::{ bitcoin::{ BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction, }, - MedianTime, + BlockHeight, TransactionBlockHeight, }; #[derive(Debug)] @@ -114,24 +114,45 @@ impl BroadcastSignedTransaction for Wallet { impl WatchForRawTransaction for Wallet { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { (|| async { Ok(self.0.get_raw_transaction(txid).await?) }) - .retry(ExponentialBackoff { - max_elapsed_time: None, - ..Default::default() - }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) .await .expect("transient errors to be retried") } } #[async_trait] -impl MedianTime for Wallet { - async fn median_time(&self) -> u32 { - (|| async { Ok(self.0.median_time().await?) }) - .retry(ExponentialBackoff { - max_elapsed_time: None, - ..Default::default() - }) +impl BlockHeight for Wallet { + async fn block_height(&self) -> u32 { + (|| async { Ok(self.0.block_height().await?) }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) .await .expect("transient errors to be retried") } } + +#[async_trait] +impl TransactionBlockHeight for Wallet { + async fn transaction_block_height(&self, txid: Txid) -> u32 { + #[derive(Debug)] + enum Error { + Io, + NotYetMined, + } + + (|| async { + let block_height = self + .0 + .transaction_block_height(txid) + .await + .map_err(|_| backoff::Error::Transient(Error::Io))?; + + let block_height = + block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?; + + Result::<_, backoff::Error>::Ok(block_height) + }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) + .await + .expect("transient errors to be retried") + } +} diff --git a/xmr-btc/tests/harness/wallet/monero.rs b/xmr-btc/tests/harness/wallet/monero.rs index 5bac71c7..3cdc8a46 100644 --- a/xmr-btc/tests/harness/wallet/monero.rs +++ b/xmr-btc/tests/harness/wallet/monero.rs @@ -1,9 +1,9 @@ use anyhow::Result; use async_trait::async_trait; -use backoff::{future::FutureOperation as _, ExponentialBackoff}; +use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use monero::{Address, Network, PrivateKey}; use monero_harness::rpc::wallet; -use std::str::FromStr; +use std::{str::FromStr, time::Duration}; use xmr_btc::monero::{ Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey, Transfer, TransferProof, TxHash, WatchForTransfer, @@ -11,6 +11,15 @@ use xmr_btc::monero::{ pub struct Wallet(pub wallet::Client); +impl Wallet { + /// Get the balance of the primary account. + pub async fn get_balance(&self) -> Result { + let amount = self.0.get_balance(0).await?; + + Ok(Amount::from_piconero(amount)) + } +} + #[async_trait] impl Transfer for Wallet { async fn transfer( @@ -106,10 +115,7 @@ impl WatchForTransfer for Wallet { Ok(proof) }) - .retry(ExponentialBackoff { - max_elapsed_time: None, - ..Default::default() - }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) .await; if let Err(Error::InsufficientFunds { expected, actual }) = res { diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index 77fe0410..c2876fc3 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -1,13 +1,23 @@ pub mod harness; +use std::{convert::TryInto, sync::Arc}; + use anyhow::Result; use async_trait::async_trait; use futures::{ - channel::mpsc::{Receiver, Sender}, + channel::mpsc::{channel, Receiver, Sender}, + future::try_join, SinkExt, StreamExt, }; use genawaiter::GeneratorState; -use harness::wallet::{bitcoin, monero}; +use harness::{ + init_bitcoind, init_test, + node::{run_alice_until, run_bob_until}, +}; +use monero_harness::Monero; +use rand::rngs::OsRng; +use testcontainers::clients::Cli; +use tracing::info; use xmr_btc::{ action_generator_alice, action_generator_bob, alice, bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock}, @@ -20,10 +30,18 @@ type AliceNetwork = Network; type BobNetwork = Network; #[derive(Debug)] -struct Network { +struct Network { // TODO: It is weird to use mpsc's in a situation where only one message is expected, but the // ownership rules of Rust are making this painful - pub receiver: Receiver, + pub receiver: Receiver, +} + +impl Network { + pub fn new() -> (Network, Sender) { + let (sender, receiver) = channel(1); + + (Self { receiver }, sender) + } } #[async_trait] @@ -41,19 +59,22 @@ impl ReceiveBitcoinRedeemEncsig for AliceNetwork { } async fn swap_as_alice( - network: &'static mut AliceNetwork, + network: AliceNetwork, // FIXME: It would be more intuitive to have a single network/transport struct instead of // splitting into two, but Rust ownership rules make this tedious mut sender: Sender, - monero_wallet: &'static monero::Wallet, - bitcoin_wallet: &'static bitcoin::Wallet, + monero_wallet: &harness::wallet::monero::Wallet, + bitcoin_wallet: Arc, state: alice::State3, ) -> Result<()> { - let mut action_generator = - action_generator_alice(network, monero_wallet, bitcoin_wallet, state); + let mut action_generator = action_generator_alice(network, bitcoin_wallet.clone(), state); loop { - match action_generator.async_resume().await { + let state = action_generator.async_resume().await; + + info!("resumed execution of generator, got: {:?}", state); + + match state { GeneratorState::Yielded(AliceAction::LockXmr { amount, public_spend_key, @@ -84,16 +105,25 @@ async fn swap_as_alice( } async fn swap_as_bob( - network: &'static mut BobNetwork, + network: BobNetwork, mut sender: Sender, - monero_wallet: &'static monero::Wallet, - bitcoin_wallet: &'static bitcoin::Wallet, + monero_wallet: Arc, + bitcoin_wallet: Arc, state: bob::State2, ) -> Result<()> { - let mut action_generator = action_generator_bob(network, monero_wallet, bitcoin_wallet, state); + let mut action_generator = action_generator_bob( + network, + monero_wallet.clone(), + bitcoin_wallet.clone(), + state, + ); loop { - match action_generator.async_resume().await { + let state = action_generator.async_resume().await; + + info!("resumed execution of generator, got: {:?}", state); + + match state { GeneratorState::Yielded(BobAction::LockBitcoin(tx_lock)) => { let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?; let _ = bitcoin_wallet @@ -126,5 +156,96 @@ async fn swap_as_bob( } } -#[test] -fn on_chain_happy_path() {} +// NOTE: For some reason running these tests overflows the stack. In order to +// mitigate this run them with: +// +// RUST_MIN_STACK=100000000 cargo test + +#[tokio::test] +async fn on_chain_happy_path() { + let cli = Cli::default(); + let (monero, _container) = Monero::new(&cli); + let bitcoind = init_bitcoind(&cli).await; + + let (alice_state0, bob_state0, mut alice_node, mut bob_node, initial_balances, swap_amounts) = + init_test(&monero, &bitcoind, Some(100), Some(100)).await; + + // run the handshake as part of the setup + let (alice_state, bob_state) = try_join( + run_alice_until( + &mut alice_node, + alice_state0.into(), + harness::alice::is_state3, + &mut OsRng, + ), + run_bob_until( + &mut bob_node, + bob_state0.into(), + harness::bob::is_state2, + &mut OsRng, + ), + ) + .await + .unwrap(); + let alice: alice::State3 = alice_state.try_into().unwrap(); + let bob: bob::State2 = bob_state.try_into().unwrap(); + let tx_lock_txid = bob.tx_lock.txid(); + + let alice_bitcoin_wallet = Arc::new(alice_node.bitcoin_wallet); + let bob_bitcoin_wallet = Arc::new(bob_node.bitcoin_wallet); + let alice_monero_wallet = Arc::new(alice_node.monero_wallet); + let bob_monero_wallet = Arc::new(bob_node.monero_wallet); + + let (alice_network, bob_sender) = Network::::new(); + let (bob_network, alice_sender) = Network::::new(); + + try_join( + swap_as_alice( + alice_network, + alice_sender, + &alice_monero_wallet.clone(), + alice_bitcoin_wallet.clone(), + alice, + ), + swap_as_bob( + bob_network, + bob_sender, + bob_monero_wallet.clone(), + bob_bitcoin_wallet.clone(), + bob, + ), + ) + .await + .unwrap(); + + let alice_final_btc_balance = alice_bitcoin_wallet.balance().await.unwrap(); + let bob_final_btc_balance = bob_bitcoin_wallet.balance().await.unwrap(); + + let lock_tx_bitcoin_fee = bob_bitcoin_wallet + .transaction_fee(tx_lock_txid) + .await + .unwrap(); + + let alice_final_xmr_balance = alice_monero_wallet.get_balance().await.unwrap(); + + monero.wait_for_bob_wallet_block_height().await.unwrap(); + let bob_final_xmr_balance = bob_monero_wallet.get_balance().await.unwrap(); + + assert_eq!( + alice_final_btc_balance, + initial_balances.alice_btc + swap_amounts.btc + - bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE) + ); + assert_eq!( + bob_final_btc_balance, + initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee + ); + + // Getting the Monero LockTx fee is tricky in a clean way, I think checking this + // condition is sufficient + assert!(alice_final_xmr_balance <= initial_balances.alice_xmr - swap_amounts.xmr,); + assert_eq!( + bob_final_xmr_balance, + initial_balances.bob_xmr + swap_amounts.xmr + ); +}