Test on-chain protocol happy path

This commit is contained in:
Lucas Soriano del Pino 2020-10-22 10:57:42 +11:00
parent d3a7689059
commit 5395303a99
9 changed files with 457 additions and 245 deletions

View File

@ -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 }

View File

@ -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) => {

View File

@ -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<N, M, B>(
network: &'static mut N,
monero_client: &'static M,
bitcoin_client: &'static B,
mut network: N,
monero_client: Arc<M>,
bitcoin_client: Arc<B>,
// TODO: Replace this with a new, slimmer struct?
bob::State2 {
A,
@ -111,10 +124,16 @@ pub fn action_generator_bob<N, M, B>(
}: bob::State2,
) -> GenBoxed<BobAction, (), ()>
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<B>(bitcoin_client: &B, timestamp: u32) -> bool
async fn bitcoin_block_height_is_gte<B>(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<N, M, B>(
network: &'static mut N,
_monero_client: &'static M,
bitcoin_client: &'static B,
pub fn action_generator_alice<N, B>(
mut network: N,
bitcoin_client: Arc<B>,
// TODO: Replace this with a new, slimmer struct?
alice::State3 {
a,
@ -341,16 +365,22 @@ pub fn action_generator_alice<N, M, B>(
}: alice::State3,
) -> GenBoxed<AliceAction, (), ()>
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<B>(bitcoin_client: &B, timestamp: u32) -> bool
async fn bitcoin_block_height_is_gte<B>(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);

View File

@ -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<PublicViewKey> 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<Amount> for u64 {
fn from(from: Amount) -> u64 {
from.0

View File

@ -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<alice::Message, bob::Message>,
Transport<bob::Message, alice::Message>,
) {
let (a_sender, b_receiver): (Sender<alice::Message>, Receiver<alice::Message>) =
mpsc::channel(5);
let (b_sender, a_receiver): (Sender<bob::Message>, Receiver<bob::Message>) = 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(

View File

@ -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<xmr_btc::alice::Message, xmr_btc::bob::Message>,
Transport<xmr_btc::bob::Message, xmr_btc::alice::Message>,
) {
let (a_sender, b_receiver): (
Sender<xmr_btc::alice::Message>,
Receiver<xmr_btc::alice::Message>,
) = mpsc::channel(5);
let (b_sender, a_receiver): (
Sender<xmr_btc::bob::Message>,
Receiver<xmr_btc::bob::Message>,
) = 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<u32>,
punish_timelock: Option<u32>,
) -> (
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,
)
}

View File

@ -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<Error>>::Ok(block_height)
})
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried")
}
}

View File

@ -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<Amount> {
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 {

View File

@ -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<EncryptedSignature>;
type BobNetwork = Network<TransferProof>;
#[derive(Debug)]
struct Network<RecvMsg> {
struct Network<M> {
// 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<RecvMsg>,
pub receiver: Receiver<M>,
}
impl<M> Network<M> {
pub fn new() -> (Network<M>, Sender<M>) {
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<TransferProof>,
monero_wallet: &'static monero::Wallet,
bitcoin_wallet: &'static bitcoin::Wallet,
monero_wallet: &harness::wallet::monero::Wallet,
bitcoin_wallet: Arc<harness::wallet::bitcoin::Wallet>,
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<EncryptedSignature>,
monero_wallet: &'static monero::Wallet,
bitcoin_wallet: &'static bitcoin::Wallet,
monero_wallet: Arc<harness::wallet::monero::Wallet>,
bitcoin_wallet: Arc<harness::wallet::bitcoin::Wallet>,
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::<EncryptedSignature>::new();
let (bob_network, alice_sender) = Network::<TransferProof>::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
);
}