mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
Merge branch 'on-chain-protocol' of github.com:comit-network/xmr-btc-swap into on-chain-protocol
This commit is contained in:
commit
f8adf6d7e0
@ -20,12 +20,13 @@ rand = "0.7"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
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 }
|
||||
|
@ -52,13 +52,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) => {
|
||||
|
@ -60,6 +60,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)]
|
||||
@ -81,8 +89,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.
|
||||
@ -90,9 +103,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,
|
||||
@ -112,10 +125,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),
|
||||
@ -123,6 +142,7 @@ where
|
||||
}
|
||||
|
||||
/// Reason why the swap has failed.
|
||||
#[derive(Debug)]
|
||||
enum Reason {
|
||||
/// The refund timelock has been reached.
|
||||
BtcExpired,
|
||||
@ -141,37 +161,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(),
|
||||
@ -246,7 +269,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();
|
||||
@ -317,10 +342,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,
|
||||
@ -342,16 +366,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,
|
||||
@ -374,31 +404,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(),
|
||||
@ -460,7 +496,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);
|
||||
|
||||
|
@ -5,7 +5,7 @@ pub use curve25519_dalek::scalar::Scalar;
|
||||
pub use monero::{Address, PrivateKey, PublicKey};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Add;
|
||||
use std::ops::{Add, Sub};
|
||||
|
||||
pub const MIN_CONFIRMATIONS: u32 = 10;
|
||||
|
||||
@ -54,7 +54,7 @@ impl From<PublicViewKey> for PublicKey {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PublicViewKey(PublicKey);
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
|
||||
pub struct Amount(u64);
|
||||
|
||||
impl Amount {
|
||||
@ -70,6 +70,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
|
||||
|
@ -1,148 +1,5 @@
|
||||
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;
|
||||
const ALICE_TEST_DB_FOLDER: &str = "../target/e2e-test-alice-recover";
|
||||
const BOB_TEST_DB_FOLDER: &str = "../target/e2e-test-bob-recover";
|
||||
|
||||
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<'a>(
|
||||
monero: &'a Monero,
|
||||
bitcoind: &Bitcoind<'a>,
|
||||
) -> (
|
||||
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:
|
||||
@ -184,7 +41,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(
|
||||
@ -215,11 +72,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,
|
||||
@ -233,13 +90,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
|
||||
);
|
||||
}
|
||||
|
||||
@ -260,7 +115,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(
|
||||
@ -303,8 +158,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!(
|
||||
@ -315,7 +170,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);
|
||||
}
|
||||
|
||||
@ -336,7 +191,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(
|
||||
|
@ -6,6 +6,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 { .. })
|
||||
@ -20,6 +24,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 { .. })
|
||||
@ -35,3 +43,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,
|
||||
)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user