Merge #85
85: Database & Swap Resume r=da-kami a=D4nte - Introduce Database for Alice and Bob - Save states in database - Resume from database (broken for Bob, will create test + fix it in follow-up) PR - Tests when alice restarts: Both happy and refund path Co-authored-by: Franck Royer <franck@coblox.tech> Co-authored-by: rishflab <rishflab@hotmail.com> Co-authored-by: Daniel Karzel <daniel@comit.network>pull/89/head
commit
6ef6fc894f
@ -1,43 +0,0 @@
|
||||
use crate::{bob::event_loop::EventLoopHandle, SwapAmounts};
|
||||
use anyhow::Result;
|
||||
use libp2p::core::Multiaddr;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::sync::Arc;
|
||||
use xmr_btc::bob::State2;
|
||||
|
||||
pub async fn negotiate<R>(
|
||||
state0: xmr_btc::bob::State0,
|
||||
amounts: SwapAmounts,
|
||||
swarm: &mut EventLoopHandle,
|
||||
addr: Multiaddr,
|
||||
mut rng: R,
|
||||
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||
) -> Result<State2>
|
||||
where
|
||||
R: RngCore + CryptoRng + Send,
|
||||
{
|
||||
tracing::trace!("Starting negotiate");
|
||||
swarm.dial_alice(addr).await?;
|
||||
|
||||
let alice = swarm.recv_conn_established().await?;
|
||||
|
||||
swarm.request_amounts(alice.clone(), amounts.btc).await?;
|
||||
|
||||
swarm
|
||||
.send_message0(alice.clone(), state0.next_message(&mut rng))
|
||||
.await?;
|
||||
let msg0 = swarm.recv_message0().await?;
|
||||
let state1 = state0.receive(bitcoin_wallet.as_ref(), msg0).await?;
|
||||
|
||||
swarm
|
||||
.send_message1(alice.clone(), state1.next_message())
|
||||
.await?;
|
||||
let msg1 = swarm.recv_message1().await?;
|
||||
let state2 = state1.receive(msg1)?;
|
||||
|
||||
swarm
|
||||
.send_message2(alice.clone(), state2.next_message())
|
||||
.await?;
|
||||
|
||||
Ok(state2)
|
||||
}
|
@ -1,463 +0,0 @@
|
||||
//! This module is used to attempt to recover an unfinished swap.
|
||||
//!
|
||||
//! Recovery is only supported for certain states and the strategy followed is
|
||||
//! to perform the simplest steps that require no further action from the
|
||||
//! counterparty.
|
||||
//!
|
||||
//! The quality of this module is bad because there is a lot of code
|
||||
//! duplication, both within the module and with respect to
|
||||
//! `xmr_btc/src/{alice,bob}.rs`. In my opinion, a better approach to support
|
||||
//! swap recovery would be through the `action_generator`s themselves, but this
|
||||
//! was deemed too complicated for the time being.
|
||||
|
||||
use crate::{
|
||||
bitcoin, monero,
|
||||
monero::CreateWalletForOutput,
|
||||
state::{Alice, Bob, Swap},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use bitcoin_harness::BitcoindRpcApi;
|
||||
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
|
||||
use futures::{
|
||||
future::{select, Either},
|
||||
pin_mut,
|
||||
};
|
||||
use sha2::Sha256;
|
||||
use tracing::info;
|
||||
use xmr_btc::bitcoin::{
|
||||
poll_until_block_height_is_gte, BroadcastSignedTransaction, TransactionBlockHeight,
|
||||
WatchForRawTransaction,
|
||||
};
|
||||
|
||||
pub async fn recover(
|
||||
bitcoin_wallet: bitcoin::Wallet,
|
||||
monero_wallet: monero::Wallet,
|
||||
state: Swap,
|
||||
) -> Result<()> {
|
||||
match state {
|
||||
Swap::Alice(state) => alice_recover(bitcoin_wallet, monero_wallet, state).await,
|
||||
Swap::Bob(state) => bob_recover(bitcoin_wallet, monero_wallet, state).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn alice_recover(
|
||||
bitcoin_wallet: bitcoin::Wallet,
|
||||
monero_wallet: monero::Wallet,
|
||||
state: Alice,
|
||||
) -> Result<()> {
|
||||
match state {
|
||||
Alice::Negotiated(_) | Alice::BtcLocked(_) | Alice::SwapComplete => {
|
||||
info!("Nothing to do");
|
||||
}
|
||||
Alice::XmrLocked(state) => {
|
||||
info!("Monero still locked up");
|
||||
|
||||
let tx_cancel = bitcoin::TxCancel::new(
|
||||
&state.tx_lock,
|
||||
state.refund_timelock,
|
||||
state.a.public(),
|
||||
state.B,
|
||||
);
|
||||
|
||||
info!("Checking if the Bitcoin cancel transaction has been published");
|
||||
if bitcoin_wallet
|
||||
.inner
|
||||
.get_raw_transaction(tx_cancel.txid())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
info!("Bitcoin cancel transaction not yet published");
|
||||
|
||||
let tx_lock_height = bitcoin_wallet
|
||||
.transaction_block_height(state.tx_lock.txid())
|
||||
.await;
|
||||
poll_until_block_height_is_gte(
|
||||
&bitcoin_wallet,
|
||||
tx_lock_height + state.refund_timelock,
|
||||
)
|
||||
.await;
|
||||
|
||||
let sig_a = state.a.sign(tx_cancel.digest());
|
||||
let sig_b = state.tx_cancel_sig_bob.clone();
|
||||
|
||||
let tx_cancel = tx_cancel
|
||||
.clone()
|
||||
.add_signatures(&state.tx_lock, (state.a.public(), sig_a), (state.B, sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||
|
||||
// TODO: We should not fail if the transaction is already on the blockchain
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(tx_cancel)
|
||||
.await?;
|
||||
}
|
||||
|
||||
info!("Confirmed that Bitcoin cancel transaction is on the blockchain");
|
||||
|
||||
let tx_cancel_height = bitcoin_wallet
|
||||
.transaction_block_height(tx_cancel.txid())
|
||||
.await;
|
||||
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
|
||||
&bitcoin_wallet,
|
||||
tx_cancel_height + state.punish_timelock,
|
||||
);
|
||||
pin_mut!(poll_until_bob_can_be_punished);
|
||||
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
|
||||
|
||||
info!("Waiting for either Bitcoin refund or punish timelock");
|
||||
match select(
|
||||
bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()),
|
||||
poll_until_bob_can_be_punished,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either::Left((tx_refund_published, ..)) => {
|
||||
info!("Found Bitcoin refund transaction");
|
||||
|
||||
let s_a = monero::PrivateKey {
|
||||
scalar: state.s_a.into_ed25519(),
|
||||
};
|
||||
|
||||
let tx_refund_sig = tx_refund
|
||||
.extract_signature_by_key(tx_refund_published, state.a.public())?;
|
||||
let tx_refund_encsig = state.a.encsign(state.S_b_bitcoin, tx_refund.digest());
|
||||
|
||||
let s_b = bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
|
||||
let s_b = monero::PrivateKey::from_scalar(
|
||||
monero::Scalar::from_bytes_mod_order(s_b.to_bytes()),
|
||||
);
|
||||
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(s_a + s_b, state.v)
|
||||
.await?;
|
||||
info!("Successfully refunded monero");
|
||||
}
|
||||
Either::Right(_) => {
|
||||
info!("Punish timelock reached, attempting to punish Bob");
|
||||
|
||||
let tx_punish = bitcoin::TxPunish::new(
|
||||
&tx_cancel,
|
||||
&state.punish_address,
|
||||
state.punish_timelock,
|
||||
);
|
||||
|
||||
let sig_a = state.a.sign(tx_punish.digest());
|
||||
let sig_b = state.tx_punish_sig_bob.clone();
|
||||
|
||||
let sig_tx_punish = tx_punish.add_signatures(
|
||||
&tx_cancel,
|
||||
(state.a.public(), sig_a),
|
||||
(state.B, sig_b),
|
||||
)?;
|
||||
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(sig_tx_punish)
|
||||
.await?;
|
||||
info!("Successfully punished Bob's inactivity by taking bitcoin");
|
||||
}
|
||||
};
|
||||
}
|
||||
Alice::BtcRedeemable { redeem_tx, state } => {
|
||||
info!("Have the means to redeem the Bitcoin");
|
||||
|
||||
let tx_lock_height = bitcoin_wallet
|
||||
.transaction_block_height(state.tx_lock.txid())
|
||||
.await;
|
||||
|
||||
let block_height = bitcoin_wallet.inner.client.getblockcount().await?;
|
||||
let refund_absolute_expiry = tx_lock_height + state.refund_timelock;
|
||||
|
||||
info!("Checking refund timelock");
|
||||
if block_height < refund_absolute_expiry {
|
||||
info!("Safe to redeem");
|
||||
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(redeem_tx)
|
||||
.await?;
|
||||
info!("Successfully redeemed bitcoin");
|
||||
} else {
|
||||
info!("Refund timelock reached");
|
||||
|
||||
let tx_cancel = bitcoin::TxCancel::new(
|
||||
&state.tx_lock,
|
||||
state.refund_timelock,
|
||||
state.a.public(),
|
||||
state.B,
|
||||
);
|
||||
|
||||
info!("Checking if the Bitcoin cancel transaction has been published");
|
||||
if bitcoin_wallet
|
||||
.inner
|
||||
.get_raw_transaction(tx_cancel.txid())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
let sig_a = state.a.sign(tx_cancel.digest());
|
||||
let sig_b = state.tx_cancel_sig_bob.clone();
|
||||
|
||||
let tx_cancel = tx_cancel
|
||||
.clone()
|
||||
.add_signatures(&state.tx_lock, (state.a.public(), sig_a), (state.B, sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||
|
||||
// TODO: We should not fail if the transaction is already on the blockchain
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(tx_cancel)
|
||||
.await?;
|
||||
}
|
||||
|
||||
info!("Confirmed that Bitcoin cancel transaction is on the blockchain");
|
||||
|
||||
let tx_cancel_height = bitcoin_wallet
|
||||
.transaction_block_height(tx_cancel.txid())
|
||||
.await;
|
||||
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
|
||||
&bitcoin_wallet,
|
||||
tx_cancel_height + state.punish_timelock,
|
||||
);
|
||||
pin_mut!(poll_until_bob_can_be_punished);
|
||||
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
|
||||
|
||||
info!("Waiting for either Bitcoin refund or punish timelock");
|
||||
match select(
|
||||
bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()),
|
||||
poll_until_bob_can_be_punished,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either::Left((tx_refund_published, ..)) => {
|
||||
info!("Found Bitcoin refund transaction");
|
||||
|
||||
let s_a = monero::PrivateKey {
|
||||
scalar: state.s_a.into_ed25519(),
|
||||
};
|
||||
|
||||
let tx_refund_sig = tx_refund
|
||||
.extract_signature_by_key(tx_refund_published, state.a.public())?;
|
||||
let tx_refund_encsig =
|
||||
state.a.encsign(state.S_b_bitcoin, tx_refund.digest());
|
||||
|
||||
let s_b =
|
||||
bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
|
||||
let s_b = monero::PrivateKey::from_scalar(
|
||||
monero::Scalar::from_bytes_mod_order(s_b.to_bytes()),
|
||||
);
|
||||
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(s_a + s_b, state.v)
|
||||
.await?;
|
||||
info!("Successfully refunded monero");
|
||||
}
|
||||
Either::Right(_) => {
|
||||
info!("Punish timelock reached, attempting to punish Bob");
|
||||
|
||||
let tx_punish = bitcoin::TxPunish::new(
|
||||
&tx_cancel,
|
||||
&state.punish_address,
|
||||
state.punish_timelock,
|
||||
);
|
||||
|
||||
let sig_a = state.a.sign(tx_punish.digest());
|
||||
let sig_b = state.tx_punish_sig_bob.clone();
|
||||
|
||||
let sig_tx_punish = tx_punish.add_signatures(
|
||||
&tx_cancel,
|
||||
(state.a.public(), sig_a),
|
||||
(state.B, sig_b),
|
||||
)?;
|
||||
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(sig_tx_punish)
|
||||
.await?;
|
||||
info!("Successfully punished Bob's inactivity by taking bitcoin");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Alice::BtcPunishable(state) => {
|
||||
info!("Punish timelock reached, attempting to punish Bob");
|
||||
|
||||
let tx_cancel = bitcoin::TxCancel::new(
|
||||
&state.tx_lock,
|
||||
state.refund_timelock,
|
||||
state.a.public(),
|
||||
state.B,
|
||||
);
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
|
||||
|
||||
info!("Checking if Bitcoin has already been refunded");
|
||||
|
||||
// TODO: Protect against transient errors so that we can correctly decide if the
|
||||
// bitcoin has been refunded
|
||||
match bitcoin_wallet
|
||||
.inner
|
||||
.get_raw_transaction(tx_refund.txid())
|
||||
.await
|
||||
{
|
||||
Ok(tx_refund_published) => {
|
||||
info!("Bitcoin already refunded");
|
||||
|
||||
let s_a = monero::PrivateKey {
|
||||
scalar: state.s_a.into_ed25519(),
|
||||
};
|
||||
|
||||
let tx_refund_sig = tx_refund
|
||||
.extract_signature_by_key(tx_refund_published, state.a.public())?;
|
||||
let tx_refund_encsig = state.a.encsign(state.S_b_bitcoin, tx_refund.digest());
|
||||
|
||||
let s_b = bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
|
||||
let s_b = monero::PrivateKey::from_scalar(
|
||||
monero::Scalar::from_bytes_mod_order(s_b.to_bytes()),
|
||||
);
|
||||
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(s_a + s_b, state.v)
|
||||
.await?;
|
||||
info!("Successfully refunded monero");
|
||||
}
|
||||
Err(_) => {
|
||||
info!("Bitcoin not yet refunded");
|
||||
|
||||
let tx_punish = bitcoin::TxPunish::new(
|
||||
&tx_cancel,
|
||||
&state.punish_address,
|
||||
state.punish_timelock,
|
||||
);
|
||||
|
||||
let sig_a = state.a.sign(tx_punish.digest());
|
||||
let sig_b = state.tx_punish_sig_bob.clone();
|
||||
|
||||
let sig_tx_punish = tx_punish.add_signatures(
|
||||
&tx_cancel,
|
||||
(state.a.public(), sig_a),
|
||||
(state.B, sig_b),
|
||||
)?;
|
||||
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(sig_tx_punish)
|
||||
.await?;
|
||||
info!("Successfully punished Bob's inactivity by taking bitcoin");
|
||||
}
|
||||
}
|
||||
}
|
||||
Alice::BtcRefunded {
|
||||
view_key,
|
||||
spend_key,
|
||||
..
|
||||
} => {
|
||||
info!("Bitcoin was refunded, attempting to refund monero");
|
||||
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||
.await?;
|
||||
info!("Successfully refunded monero");
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn bob_recover(
|
||||
bitcoin_wallet: crate::bitcoin::Wallet,
|
||||
monero_wallet: crate::monero::Wallet,
|
||||
state: Bob,
|
||||
) -> Result<()> {
|
||||
match state {
|
||||
Bob::Handshaken(_) | Bob::SwapComplete => {
|
||||
info!("Nothing to do");
|
||||
}
|
||||
Bob::BtcLocked(state) | Bob::XmrLocked(state) | Bob::BtcRefundable(state) => {
|
||||
info!("Bitcoin may still be locked up, attempting to refund");
|
||||
|
||||
let tx_cancel = bitcoin::TxCancel::new(
|
||||
&state.tx_lock,
|
||||
state.refund_timelock,
|
||||
state.A,
|
||||
state.b.public(),
|
||||
);
|
||||
|
||||
info!("Checking if the Bitcoin cancel transaction has been published");
|
||||
if bitcoin_wallet
|
||||
.inner
|
||||
.get_raw_transaction(tx_cancel.txid())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
info!("Bitcoin cancel transaction not yet published");
|
||||
|
||||
let tx_lock_height = bitcoin_wallet
|
||||
.transaction_block_height(state.tx_lock.txid())
|
||||
.await;
|
||||
poll_until_block_height_is_gte(
|
||||
&bitcoin_wallet,
|
||||
tx_lock_height + state.refund_timelock,
|
||||
)
|
||||
.await;
|
||||
|
||||
let sig_a = state.tx_cancel_sig_a.clone();
|
||||
let sig_b = state.b.sign(tx_cancel.digest());
|
||||
|
||||
let tx_cancel = tx_cancel
|
||||
.clone()
|
||||
.add_signatures(&state.tx_lock, (state.A, sig_a), (state.b.public(), sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||
|
||||
// TODO: We should not fail if the transaction is already on the blockchain
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(tx_cancel)
|
||||
.await?;
|
||||
}
|
||||
|
||||
info!("Confirmed that Bitcoin cancel transaction is on the blockchain");
|
||||
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
|
||||
let signed_tx_refund = {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
let sig_a = adaptor
|
||||
.decrypt_signature(&state.s_b.into_secp256k1(), state.tx_refund_encsig.clone());
|
||||
let sig_b = state.b.sign(tx_refund.digest());
|
||||
|
||||
tx_refund
|
||||
.add_signatures(&tx_cancel, (state.A, sig_a), (state.b.public(), sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_refund")
|
||||
};
|
||||
|
||||
// TODO: Check if Bitcoin has already been punished and provide a useful error
|
||||
// message/log to the user if so
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_refund)
|
||||
.await?;
|
||||
info!("Successfully refunded bitcoin");
|
||||
}
|
||||
Bob::BtcRedeemed(state) => {
|
||||
info!("Bitcoin was redeemed, attempting to redeem monero");
|
||||
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&state.tx_lock, &state.redeem_address);
|
||||
let tx_redeem_published = bitcoin_wallet
|
||||
.inner
|
||||
.get_raw_transaction(tx_redeem.txid())
|
||||
.await?;
|
||||
|
||||
let tx_redeem_encsig = state.b.encsign(state.S_a_bitcoin, tx_redeem.digest());
|
||||
let tx_redeem_sig =
|
||||
tx_redeem.extract_signature_by_key(tx_redeem_published, state.b.public())?;
|
||||
|
||||
let s_a = bitcoin::recover(state.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?;
|
||||
let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(
|
||||
s_a.to_bytes(),
|
||||
));
|
||||
|
||||
let s_b = monero::PrivateKey {
|
||||
scalar: state.s_b.into_ed25519(),
|
||||
};
|
||||
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(s_a + s_b, state.v)
|
||||
.await?;
|
||||
info!("Successfully redeemed monero")
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
pub mod peer_id {
|
||||
use libp2p::PeerId;
|
||||
use serde::{de::Error, Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S>(peer_id: &PeerId, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let string = peer_id.to_string();
|
||||
serializer.serialize_str(&string)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<PeerId, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let string = String::deserialize(deserializer)?;
|
||||
let peer_id = string.parse().map_err(D::Error::custom)?;
|
||||
|
||||
Ok(peer_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde::Serialize;
|
||||
use spectral::prelude::*;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SerializablePeerId(#[serde(with = "super")] PeerId);
|
||||
|
||||
#[test]
|
||||
fn maker_id_serializes_as_expected() {
|
||||
let peer_id = SerializablePeerId(
|
||||
"QmfUfpC2frwFvcDzpspnfZitHt5wct6n4kpG5jzgRdsxkY"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let got = serde_json::to_string(&peer_id).expect("failed to serialize peer id");
|
||||
|
||||
assert_that(&got)
|
||||
.is_equal_to(r#""QmfUfpC2frwFvcDzpspnfZitHt5wct6n4kpG5jzgRdsxkY""#.to_string());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,574 +0,0 @@
|
||||
use bitcoin_harness::Bitcoind;
|
||||
use futures::future::try_join;
|
||||
use libp2p::{Multiaddr, PeerId};
|
||||
use monero_harness::Monero;
|
||||
use rand::rngs::OsRng;
|
||||
use std::sync::Arc;
|
||||
use swap::{
|
||||
alice, alice::swap::AliceState, bob, bob::swap::BobState, network::transport::build,
|
||||
storage::Database, SwapAmounts,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
use testcontainers::clients::Cli;
|
||||
use tracing_core::dispatcher::DefaultGuard;
|
||||
use tracing_log::LogTracer;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::{alice::State0, bitcoin, config::Config, cross_curve_dleq};
|
||||
|
||||
/// Run the following tests with RUST_MIN_STACK=10000000
|
||||
|
||||
#[tokio::test]
|
||||
async fn happy_path() {
|
||||
let _guard = init_tracing();
|
||||
|
||||
let cli = Cli::default();
|
||||
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
||||
let _ = bitcoind.init(5).await;
|
||||
let (monero, _container) =
|
||||
Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
||||
let btc_alice = bitcoin::Amount::ZERO;
|
||||
let btc_bob = btc_to_swap * 10;
|
||||
|
||||
// this xmr value matches the logic of alice::calculate_amounts i.e. btc *
|
||||
// 10_000 * 100
|
||||
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
|
||||
let xmr_alice = xmr_to_swap * 10;
|
||||
let xmr_bob = xmr_btc::monero::Amount::from_piconero(0);
|
||||
|
||||
// todo: This should not be hardcoded
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
|
||||
let config = Config::regtest();
|
||||
|
||||
let (
|
||||
alice_state,
|
||||
mut alice_swarm_driver,
|
||||
alice_swarm_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
alice_peer_id,
|
||||
) = init_alice(
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
btc_alice,
|
||||
xmr_to_swap,
|
||||
xmr_alice,
|
||||
alice_multiaddr.clone(),
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (bob_state, bob_swarm_driver, bob_swarm_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) =
|
||||
init_bob(
|
||||
alice_multiaddr,
|
||||
alice_peer_id,
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
btc_bob,
|
||||
xmr_to_swap,
|
||||
xmr_bob,
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let alice_swap_fut = alice::swap::swap(
|
||||
alice_state,
|
||||
alice_swarm_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
config,
|
||||
);
|
||||
|
||||
let _alice_swarm_fut = tokio::spawn(async move { alice_swarm_driver.run().await });
|
||||
|
||||
let bob_swap_fut = bob::swap::swap(
|
||||
bob_state,
|
||||
bob_swarm_handle,
|
||||
bob_db,
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
OsRng,
|
||||
Uuid::new_v4(),
|
||||
);
|
||||
|
||||
let _bob_swarm_fut = tokio::spawn(async move { bob_swarm_driver.run().await });
|
||||
|
||||
try_join(alice_swap_fut, bob_swap_fut).await.unwrap();
|
||||
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
|
||||
|
||||
let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
|
||||
bob_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
btc_alice_final,
|
||||
btc_alice + btc_to_swap - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||
);
|
||||
assert!(btc_bob_final <= btc_bob - btc_to_swap);
|
||||
|
||||
assert!(xmr_alice_final <= xmr_alice - xmr_to_swap);
|
||||
assert_eq!(xmr_bob_final, xmr_bob + xmr_to_swap);
|
||||
}
|
||||
|
||||
/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice
|
||||
/// the encsig and fail to refund or redeem. Alice punishes.
|
||||
#[tokio::test]
|
||||
async fn alice_punishes_if_bob_never_acts_after_fund() {
|
||||
let _guard = init_tracing();
|
||||
|
||||
let cli = Cli::default();
|
||||
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
||||
let _ = bitcoind.init(5).await;
|
||||
let (monero, _container) =
|
||||
Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
||||
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let bob_btc_starting_balance = btc_to_swap * 10;
|
||||
let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0);
|
||||
|
||||
let alice_btc_starting_balance = bitcoin::Amount::ZERO;
|
||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||
|
||||
// todo: This should not be hardcoded
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9877"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
|
||||
let config = Config::regtest();
|
||||
|
||||
let (
|
||||
alice_state,
|
||||
mut alice_swarm,
|
||||
alice_swarm_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
alice_peer_id,
|
||||
) = init_alice(
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
alice_btc_starting_balance,
|
||||
xmr_to_swap,
|
||||
alice_xmr_starting_balance,
|
||||
alice_multiaddr.clone(),
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (bob_state, bob_swarm_driver, bob_swarm_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) =
|
||||
init_bob(
|
||||
alice_multiaddr,
|
||||
alice_peer_id,
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
bob_btc_starting_balance,
|
||||
xmr_to_swap,
|
||||
bob_xmr_starting_balance,
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let bob_btc_locked_fut = bob::swap::run_until(
|
||||
bob_state,
|
||||
bob::swap::is_btc_locked,
|
||||
bob_swarm_handle,
|
||||
bob_db,
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
OsRng,
|
||||
Uuid::new_v4(),
|
||||
);
|
||||
|
||||
let _bob_swarm_fut = tokio::spawn(async move { bob_swarm_driver.run().await });
|
||||
|
||||
let alice_fut = alice::swap::swap(
|
||||
alice_state,
|
||||
alice_swarm_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
Config::regtest(),
|
||||
);
|
||||
|
||||
let _alice_swarm_fut = tokio::spawn(async move { alice_swarm.run().await });
|
||||
|
||||
// Wait until alice has locked xmr and bob has locked btc
|
||||
let ((alice_state, _), bob_state) = try_join(alice_fut, bob_btc_locked_fut).await.unwrap();
|
||||
|
||||
assert!(matches!(alice_state, AliceState::Punished));
|
||||
let bob_state3 = if let BobState::BtcLocked(state3, ..) = bob_state {
|
||||
state3
|
||||
} else {
|
||||
panic!("Bob in unexpected state");
|
||||
};
|
||||
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
|
||||
|
||||
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
|
||||
// to TX_FEE
|
||||
let lock_tx_bitcoin_fee = bob_btc_wallet
|
||||
.transaction_fee(bob_state3.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
btc_alice_final,
|
||||
alice_btc_starting_balance + btc_to_swap - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
btc_bob_final,
|
||||
bob_btc_starting_balance - btc_to_swap - lock_tx_bitcoin_fee
|
||||
);
|
||||
}
|
||||
|
||||
// Bob locks btc and Alice locks xmr. Alice fails to act so Bob refunds. Alice
|
||||
// then also refunds.
|
||||
#[tokio::test]
|
||||
async fn both_refund() {
|
||||
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("swap=info,xmr_btc=info")
|
||||
.with_ansi(false)
|
||||
.set_default();
|
||||
|
||||
let cli = Cli::default();
|
||||
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
||||
let _ = bitcoind.init(5).await;
|
||||
let (monero, _container) =
|
||||
Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
||||
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let bob_btc_starting_balance = btc_to_swap * 10;
|
||||
let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0);
|
||||
|
||||
let alice_btc_starting_balance = bitcoin::Amount::ZERO;
|
||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||
|
||||
// todo: This should not be hardcoded
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9879"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
|
||||
let (
|
||||
alice_state,
|
||||
mut alice_swarm_driver,
|
||||
alice_swarm_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
alice_peer_id,
|
||||
) = init_alice(
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
alice_btc_starting_balance,
|
||||
xmr_to_swap,
|
||||
alice_xmr_starting_balance,
|
||||
alice_multiaddr.clone(),
|
||||
Config::regtest(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (bob_state, bob_swarm_driver, bob_swarm_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) =
|
||||
init_bob(
|
||||
alice_multiaddr,
|
||||
alice_peer_id,
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
bob_btc_starting_balance,
|
||||
xmr_to_swap,
|
||||
bob_xmr_starting_balance,
|
||||
Config::regtest(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let bob_fut = bob::swap::swap(
|
||||
bob_state,
|
||||
bob_swarm_handle,
|
||||
bob_db,
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
OsRng,
|
||||
Uuid::new_v4(),
|
||||
);
|
||||
|
||||
tokio::spawn(async move { bob_swarm_driver.run().await });
|
||||
|
||||
let alice_xmr_locked_fut = alice::swap::run_until(
|
||||
alice_state,
|
||||
alice::swap::is_xmr_locked,
|
||||
alice_swarm_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
Config::regtest(),
|
||||
);
|
||||
|
||||
tokio::spawn(async move { alice_swarm_driver.run().await });
|
||||
|
||||
// Wait until alice has locked xmr and bob has locked btc
|
||||
let (bob_state, (alice_state, alice_swarm_handle)) =
|
||||
try_join(bob_fut, alice_xmr_locked_fut).await.unwrap();
|
||||
|
||||
let bob_state4 = if let BobState::BtcRefunded(state4) = bob_state {
|
||||
state4
|
||||
} else {
|
||||
panic!("Bob in unexpected state");
|
||||
};
|
||||
|
||||
let (alice_state, _) = alice::swap::swap(
|
||||
alice_state,
|
||||
alice_swarm_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
Config::regtest(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(alice_state, AliceState::XmrRefunded));
|
||||
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
|
||||
|
||||
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
|
||||
// to TX_FEE
|
||||
let lock_tx_bitcoin_fee = bob_btc_wallet
|
||||
.transaction_fee(bob_state4.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(btc_alice_final, alice_btc_starting_balance);
|
||||
|
||||
// Alice or Bob could publish TxCancel. This means Bob could pay tx fees for
|
||||
// TxCancel and TxRefund or only TxRefund
|
||||
let btc_bob_final_alice_submitted_cancel = btc_bob_final
|
||||
== bob_btc_starting_balance
|
||||
- lock_tx_bitcoin_fee
|
||||
- bitcoin::Amount::from_sat(bitcoin::TX_FEE);
|
||||
|
||||
let btc_bob_final_bob_submitted_cancel = btc_bob_final
|
||||
== bob_btc_starting_balance
|
||||
- lock_tx_bitcoin_fee
|
||||
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE);
|
||||
assert!(btc_bob_final_alice_submitted_cancel || btc_bob_final_bob_submitted_cancel);
|
||||
|
||||
alice_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
assert_eq!(xmr_alice_final, xmr_to_swap);
|
||||
|
||||
bob_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
assert_eq!(xmr_bob_final, bob_xmr_starting_balance);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn init_alice(
|
||||
bitcoind: &Bitcoind<'_>,
|
||||
monero: &Monero,
|
||||
btc_to_swap: bitcoin::Amount,
|
||||
_btc_starting_balance: bitcoin::Amount,
|
||||
xmr_to_swap: xmr_btc::monero::Amount,
|
||||
xmr_starting_balance: xmr_btc::monero::Amount,
|
||||
listen: Multiaddr,
|
||||
config: Config,
|
||||
) -> (
|
||||
AliceState,
|
||||
alice::event_loop::EventLoop,
|
||||
alice::event_loop::EventLoopHandle,
|
||||
Arc<swap::bitcoin::Wallet>,
|
||||
Arc<swap::monero::Wallet>,
|
||||
PeerId,
|
||||
) {
|
||||
monero
|
||||
.init(vec![("alice", xmr_starting_balance.as_piconero())])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_xmr_wallet = Arc::new(swap::monero::Wallet(
|
||||
monero.wallet("alice").unwrap().client(),
|
||||
));
|
||||
|
||||
let alice_btc_wallet = Arc::new(
|
||||
swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone(), config.bitcoin_network)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let amounts = SwapAmounts {
|
||||
btc: btc_to_swap,
|
||||
xmr: xmr_to_swap,
|
||||
};
|
||||
|
||||
let (alice_state, alice_behaviour) = {
|
||||
let rng = &mut OsRng;
|
||||
let a = bitcoin::SecretKey::new_random(rng);
|
||||
let s_a = cross_curve_dleq::Scalar::random(rng);
|
||||
let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng);
|
||||
let redeem_address = alice_btc_wallet.as_ref().new_address().await.unwrap();
|
||||
let punish_address = redeem_address.clone();
|
||||
let state0 = State0::new(
|
||||
a,
|
||||
s_a,
|
||||
v_a,
|
||||
amounts.btc,
|
||||
amounts.xmr,
|
||||
config.bitcoin_refund_timelock,
|
||||
config.bitcoin_punish_timelock,
|
||||
redeem_address,
|
||||
punish_address,
|
||||
);
|
||||
|
||||
(
|
||||
AliceState::Started {
|
||||
amounts,
|
||||
state0: state0.clone(),
|
||||
},
|
||||
alice::Behaviour::new(state0),
|
||||
)
|
||||
};
|
||||
|
||||
let alice_peer_id = alice_behaviour.peer_id();
|
||||
let alice_transport = build(alice_behaviour.identity()).unwrap();
|
||||
|
||||
let (swarm_driver, handle) =
|
||||
alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen).unwrap();
|
||||
|
||||
(
|
||||
alice_state,
|
||||
swarm_driver,
|
||||
handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
alice_peer_id,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn init_bob(
|
||||
alice_multiaddr: Multiaddr,
|
||||
alice_peer_id: PeerId,
|
||||
bitcoind: &Bitcoind<'_>,
|
||||
monero: &Monero,
|
||||
btc_to_swap: bitcoin::Amount,
|
||||
btc_starting_balance: bitcoin::Amount,
|
||||
xmr_to_swap: xmr_btc::monero::Amount,
|
||||
xmr_stating_balance: xmr_btc::monero::Amount,
|
||||
config: Config,
|
||||
) -> (
|
||||
BobState,
|
||||
bob::event_loop::EventLoop,
|
||||
bob::event_loop::EventLoopHandle,
|
||||
Arc<swap::bitcoin::Wallet>,
|
||||
Arc<swap::monero::Wallet>,
|
||||
Database,
|
||||
) {
|
||||
let bob_btc_wallet = Arc::new(
|
||||
swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone(), config.bitcoin_network)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
bitcoind
|
||||
.mint(
|
||||
bob_btc_wallet.inner.new_address().await.unwrap(),
|
||||
btc_starting_balance,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
monero
|
||||
.init(vec![("bob", xmr_stating_balance.as_piconero())])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client()));
|
||||
|
||||
let amounts = SwapAmounts {
|
||||
btc: btc_to_swap,
|
||||
xmr: xmr_to_swap,
|
||||
};
|
||||
|
||||
let bob_db_dir = tempdir().unwrap();
|
||||
let bob_db = Database::open(bob_db_dir.path()).unwrap();
|
||||
let bob_behaviour = bob::Behaviour::default();
|
||||
let bob_transport = build(bob_behaviour.identity()).unwrap();
|
||||
|
||||
let refund_address = bob_btc_wallet.new_address().await.unwrap();
|
||||
let state0 = xmr_btc::bob::State0::new(
|
||||
&mut OsRng,
|
||||
btc_to_swap,
|
||||
xmr_to_swap,
|
||||
config.bitcoin_refund_timelock,
|
||||
config.bitcoin_punish_timelock,
|
||||
refund_address,
|
||||
);
|
||||
let bob_state = BobState::Started {
|
||||
state0,
|
||||
amounts,
|
||||
peer_id: alice_peer_id,
|
||||
addr: alice_multiaddr,
|
||||
};
|
||||
|
||||
let (swarm_driver, swarm_handle) =
|
||||
bob::event_loop::EventLoop::new(bob_transport, bob_behaviour).unwrap();
|
||||
|
||||
(
|
||||
bob_state,
|
||||
swarm_driver,
|
||||
swarm_handle,
|
||||
bob_btc_wallet,
|
||||
bob_xmr_wallet,
|
||||
bob_db,
|
||||
)
|
||||
}
|
||||
|
||||
/// Utility function to initialize logging in the test environment.
|
||||
/// Note that you have to keep the `_guard` in scope after calling in test:
|
||||
///
|
||||
/// ```rust
|
||||
/// let _guard = init_tracing();
|
||||
/// ```
|
||||
fn init_tracing() -> DefaultGuard {
|
||||
// converts all log records into tracing events
|
||||
// Note: Make sure to initialize without unwrapping, otherwise this causes
|
||||
// trouble when running multiple tests.
|
||||
let _ = LogTracer::init();
|
||||
|
||||
let global_filter = tracing::Level::WARN;
|
||||
let swap_filter = tracing::Level::DEBUG;
|
||||
let xmr_btc_filter = tracing::Level::DEBUG;
|
||||
let monero_harness_filter = tracing::Level::INFO;
|
||||
let bitcoin_harness_filter = tracing::Level::INFO;
|
||||
|
||||
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(format!(
|
||||
"{},swap={},xmr-btc={},monero_harness={},bitcoin_harness={}",
|
||||
global_filter,
|
||||
swap_filter,
|
||||
xmr_btc_filter,
|
||||
monero_harness_filter,
|
||||
bitcoin_harness_filter,
|
||||
))
|
||||
.set_default()
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
use crate::testutils::{init_alice, init_bob};
|
||||
use futures::future::try_join;
|
||||
use libp2p::Multiaddr;
|
||||
use rand::rngs::OsRng;
|
||||
use swap::{alice, bob};
|
||||
use testcontainers::clients::Cli;
|
||||
use testutils::init_tracing;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::{bitcoin, config::Config};
|
||||
|
||||
pub mod testutils;
|
||||
|
||||
/// Run the following tests with RUST_MIN_STACK=10000000
|
||||
|
||||
#[tokio::test]
|
||||
async fn happy_path() {
|
||||
let _guard = init_tracing();
|
||||
|
||||
let cli = Cli::default();
|
||||
let (
|
||||
monero,
|
||||
testutils::Containers {
|
||||
bitcoind,
|
||||
monerods: _monerods,
|
||||
},
|
||||
) = testutils::init_containers(&cli).await;
|
||||
|
||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
||||
let btc_alice = bitcoin::Amount::ZERO;
|
||||
let btc_bob = btc_to_swap * 10;
|
||||
|
||||
// this xmr value matches the logic of alice::calculate_amounts i.e. btc *
|
||||
// 10_000 * 100
|
||||
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
|
||||
let xmr_alice = xmr_to_swap * 10;
|
||||
let xmr_bob = xmr_btc::monero::Amount::ZERO;
|
||||
|
||||
// todo: This should not be hardcoded
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
|
||||
let config = Config::regtest();
|
||||
|
||||
let (
|
||||
alice_state,
|
||||
mut alice_event_loop,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
alice_db,
|
||||
) = init_alice(
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
xmr_to_swap,
|
||||
xmr_alice,
|
||||
alice_multiaddr.clone(),
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) =
|
||||
init_bob(
|
||||
alice_multiaddr,
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
btc_bob,
|
||||
xmr_to_swap,
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let alice_swap_fut = alice::swap::swap(
|
||||
alice_state,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
config,
|
||||
Uuid::new_v4(),
|
||||
alice_db,
|
||||
);
|
||||
|
||||
let _alice_swarm_fut = tokio::spawn(async move { alice_event_loop.run().await });
|
||||
|
||||
let bob_swap_fut = bob::swap::swap(
|
||||
bob_state,
|
||||
bob_event_loop_handle,
|
||||
bob_db,
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
OsRng,
|
||||
Uuid::new_v4(),
|
||||
);
|
||||
|
||||
let _bob_swarm_fut = tokio::spawn(async move { bob_event_loop.run().await });
|
||||
|
||||
try_join(alice_swap_fut, bob_swap_fut).await.unwrap();
|
||||
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
|
||||
|
||||
let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
|
||||
bob_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
btc_alice_final,
|
||||
btc_alice + btc_to_swap - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||
);
|
||||
assert!(btc_bob_final <= btc_bob - btc_to_swap);
|
||||
|
||||
assert!(xmr_alice_final <= xmr_alice - xmr_to_swap);
|
||||
assert_eq!(xmr_bob_final, xmr_bob + xmr_to_swap);
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
use libp2p::Multiaddr;
|
||||
use rand::rngs::OsRng;
|
||||
use swap::{alice, alice::swap::AliceState, bitcoin, bob, storage::Database};
|
||||
use tempfile::tempdir;
|
||||
use testcontainers::clients::Cli;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::config::Config;
|
||||
|
||||
pub mod testutils;
|
||||
|
||||
use crate::testutils::{init_alice, init_bob};
|
||||
use testutils::init_tracing;
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_alice_restarts_after_encsig_is_learned_resume_swap() {
|
||||
let _guard = init_tracing();
|
||||
|
||||
let cli = Cli::default();
|
||||
let (
|
||||
monero,
|
||||
testutils::Containers {
|
||||
bitcoind,
|
||||
monerods: _monerods,
|
||||
},
|
||||
) = testutils::init_containers(&cli).await;
|
||||
|
||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
||||
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let bob_btc_starting_balance = btc_to_swap * 10;
|
||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9877"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
|
||||
let config = Config::regtest();
|
||||
|
||||
let (
|
||||
start_state,
|
||||
mut alice_event_loop,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
_,
|
||||
) = init_alice(
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
xmr_to_swap,
|
||||
alice_xmr_starting_balance,
|
||||
alice_multiaddr.clone(),
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) =
|
||||
init_bob(
|
||||
alice_multiaddr.clone(),
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
bob_btc_starting_balance,
|
||||
xmr_to_swap,
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
// TODO: we are making a clone of Bob's wallets here to keep them in scope after
|
||||
// Bob's wallets are moved into an async task.
|
||||
let bob_btc_wallet_clone = bob_btc_wallet.clone();
|
||||
let bob_xmr_wallet_clone = bob_xmr_wallet.clone();
|
||||
|
||||
let _ = tokio::spawn(async move {
|
||||
bob::swap::swap(
|
||||
bob_state,
|
||||
bob_event_loop_handle,
|
||||
bob_db,
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
OsRng,
|
||||
Uuid::new_v4(),
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
let _bob_swarm_fut = tokio::spawn(async move { bob_event_loop.run().await });
|
||||
|
||||
let alice_db_datadir = tempdir().unwrap();
|
||||
let alice_db = Database::open(alice_db_datadir.path()).unwrap();
|
||||
|
||||
let _alice_swarm_fut = tokio::spawn(async move { alice_event_loop.run().await });
|
||||
|
||||
let alice_swap_id = Uuid::new_v4();
|
||||
|
||||
let alice_state = alice::swap::run_until(
|
||||
start_state,
|
||||
alice::swap::is_encsig_learned,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
config,
|
||||
alice_swap_id,
|
||||
alice_db,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(alice_state, AliceState::EncSignLearned {..}));
|
||||
|
||||
let alice_db = Database::open(alice_db_datadir.path()).unwrap();
|
||||
let state_before_restart = alice_db.get_state(alice_swap_id).unwrap();
|
||||
|
||||
if let swap::state::Swap::Alice(state) = state_before_restart.clone() {
|
||||
assert!(matches!(state, swap::state::Alice::EncSignLearned {..}));
|
||||
}
|
||||
|
||||
let (mut event_loop_after_restart, event_loop_handle_after_restart) =
|
||||
testutils::init_alice_event_loop(alice_multiaddr);
|
||||
let _alice_swarm_fut = tokio::spawn(async move { event_loop_after_restart.run().await });
|
||||
|
||||
let alice_state = alice::swap::resume_from_database(
|
||||
event_loop_handle_after_restart,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
config,
|
||||
alice_swap_id,
|
||||
alice_db,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(alice_state, AliceState::BtcRedeemed {..}));
|
||||
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet_clone.as_ref().balance().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
btc_alice_final,
|
||||
btc_to_swap - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||
);
|
||||
assert!(btc_bob_final <= bob_btc_starting_balance - btc_to_swap);
|
||||
|
||||
let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
bob_xmr_wallet_clone.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_bob_final = bob_xmr_wallet_clone.as_ref().get_balance().await.unwrap();
|
||||
|
||||
assert!(xmr_alice_final <= alice_xmr_starting_balance - xmr_to_swap);
|
||||
assert_eq!(xmr_bob_final, xmr_to_swap);
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
use libp2p::Multiaddr;
|
||||
use rand::rngs::OsRng;
|
||||
use swap::{alice, bitcoin, bob, storage::Database};
|
||||
use tempfile::tempdir;
|
||||
use testcontainers::clients::Cli;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::config::Config;
|
||||
|
||||
pub mod testutils;
|
||||
|
||||
use crate::testutils::{init_alice, init_bob};
|
||||
use swap::bob::swap::BobState;
|
||||
use testutils::init_tracing;
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_bob_restarts_after_encsig_is_sent_resume_swap() {
|
||||
let _guard = init_tracing();
|
||||
|
||||
let cli = Cli::default();
|
||||
let (
|
||||
monero,
|
||||
testutils::Containers {
|
||||
bitcoind,
|
||||
monerods: _monerods,
|
||||
},
|
||||
) = testutils::init_containers(&cli).await;
|
||||
|
||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
||||
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let bob_btc_starting_balance = btc_to_swap * 10;
|
||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9877"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
|
||||
let config = Config::regtest();
|
||||
|
||||
let (
|
||||
alice_state,
|
||||
mut alice_event_loop,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
alice_db,
|
||||
) = init_alice(
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
xmr_to_swap,
|
||||
alice_xmr_starting_balance,
|
||||
alice_multiaddr.clone(),
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, _) =
|
||||
init_bob(
|
||||
alice_multiaddr.clone(),
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
bob_btc_starting_balance,
|
||||
xmr_to_swap,
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
// TODO: we are making a clone of Alices's wallets here to keep them in scope
|
||||
// after Alices's wallets are moved into an async task.
|
||||
let alice_btc_wallet_clone = alice_btc_wallet.clone();
|
||||
let alice_xmr_wallet_clone = alice_xmr_wallet.clone();
|
||||
|
||||
// TODO: we are making a clone of Bob's wallets here to keep them in scope after
|
||||
// Bob's wallets are moved into an async task.
|
||||
let bob_btc_wallet_clone = bob_btc_wallet.clone();
|
||||
let bob_xmr_wallet_clone = bob_xmr_wallet.clone();
|
||||
|
||||
let _ = tokio::spawn(async move {
|
||||
alice::swap::swap(
|
||||
alice_state,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
config,
|
||||
Uuid::new_v4(),
|
||||
alice_db,
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
let _alice_swarm_fut = tokio::spawn(async move { alice_event_loop.run().await });
|
||||
|
||||
let _bob_swarm_fut = tokio::spawn(async move { bob_event_loop.run().await });
|
||||
|
||||
let bob_swap_id = Uuid::new_v4();
|
||||
let bob_db_datadir = tempdir().unwrap();
|
||||
let bob_db = Database::open(bob_db_datadir.path()).unwrap();
|
||||
|
||||
let bob_state = bob::swap::run_until(
|
||||
bob_state,
|
||||
bob::swap::is_encsig_sent,
|
||||
bob_event_loop_handle,
|
||||
bob_db,
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
OsRng,
|
||||
bob_swap_id,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(bob_state, BobState::EncSigSent {..}));
|
||||
|
||||
let bob_db = Database::open(bob_db_datadir.path()).unwrap();
|
||||
let state_before_restart = bob_db.get_state(bob_swap_id).unwrap();
|
||||
|
||||
if let swap::state::Swap::Bob(state) = state_before_restart.clone() {
|
||||
assert!(matches!(state, swap::state::Bob::EncSigSent {..}));
|
||||
}
|
||||
|
||||
let (event_loop_after_restart, event_loop_handle_after_restart) =
|
||||
testutils::init_bob_event_loop();
|
||||
let _alice_swarm_fut = tokio::spawn(async move { event_loop_after_restart.run().await });
|
||||
|
||||
let alice_state = bob::swap::resume_from_database(
|
||||
event_loop_handle_after_restart,
|
||||
bob_db,
|
||||
bob_btc_wallet,
|
||||
bob_xmr_wallet,
|
||||
OsRng,
|
||||
bob_swap_id,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(alice_state, BobState::XmrRedeemed {..}));
|
||||
|
||||
let btc_alice_final = alice_btc_wallet_clone.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet_clone.as_ref().balance().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
btc_alice_final,
|
||||
btc_to_swap - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||
);
|
||||
assert!(btc_bob_final <= bob_btc_starting_balance - btc_to_swap);
|
||||
|
||||
let xmr_alice_final = alice_xmr_wallet_clone.as_ref().get_balance().await.unwrap();
|
||||
bob_xmr_wallet_clone.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_bob_final = bob_xmr_wallet_clone.as_ref().get_balance().await.unwrap();
|
||||
|
||||
assert!(xmr_alice_final <= alice_xmr_starting_balance - xmr_to_swap);
|
||||
assert_eq!(xmr_bob_final, xmr_to_swap);
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
use crate::testutils::{init_alice, init_bob};
|
||||
use futures::future::try_join;
|
||||
use libp2p::Multiaddr;
|
||||
use rand::rngs::OsRng;
|
||||
use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState};
|
||||
use testcontainers::clients::Cli;
|
||||
use testutils::init_tracing;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::{bitcoin, config::Config};
|
||||
|
||||
pub mod testutils;
|
||||
|
||||
/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice
|
||||
/// the encsig and fail to refund or redeem. Alice punishes.
|
||||
#[tokio::test]
|
||||
async fn alice_punishes_if_bob_never_acts_after_fund() {
|
||||
let _guard = init_tracing();
|
||||
|
||||
let cli = Cli::default();
|
||||
let (
|
||||
monero,
|
||||
testutils::Containers {
|
||||
bitcoind,
|
||||
monerods: _monerods,
|
||||
},
|
||||
) = testutils::init_containers(&cli).await;
|
||||
|
||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
||||
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let bob_btc_starting_balance = btc_to_swap * 10;
|
||||
|
||||
let alice_btc_starting_balance = bitcoin::Amount::ZERO;
|
||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||
|
||||
// todo: This should not be hardcoded
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9877"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
|
||||
let config = Config::regtest();
|
||||
|
||||
let (
|
||||
alice_state,
|
||||
mut alice_event_loop,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
alice_db,
|
||||
) = init_alice(
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
xmr_to_swap,
|
||||
alice_xmr_starting_balance,
|
||||
alice_multiaddr.clone(),
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) =
|
||||
init_bob(
|
||||
alice_multiaddr,
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
bob_btc_starting_balance,
|
||||
xmr_to_swap,
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let bob_btc_locked_fut = bob::swap::run_until(
|
||||
bob_state,
|
||||
bob::swap::is_btc_locked,
|
||||
bob_event_loop_handle,
|
||||
bob_db,
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
OsRng,
|
||||
Uuid::new_v4(),
|
||||
);
|
||||
|
||||
let _bob_swarm_fut = tokio::spawn(async move { bob_event_loop.run().await });
|
||||
|
||||
let alice_fut = alice::swap::swap(
|
||||
alice_state,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
Config::regtest(),
|
||||
Uuid::new_v4(),
|
||||
alice_db,
|
||||
);
|
||||
|
||||
let _alice_swarm_fut = tokio::spawn(async move { alice_event_loop.run().await });
|
||||
|
||||
// Wait until alice has locked xmr and bob has locked btc
|
||||
let (alice_state, bob_state) = try_join(alice_fut, bob_btc_locked_fut).await.unwrap();
|
||||
|
||||
assert!(matches!(alice_state, AliceState::Punished));
|
||||
let bob_state3 = if let BobState::BtcLocked(state3, ..) = bob_state {
|
||||
state3
|
||||
} else {
|
||||
panic!("Bob in unexpected state");
|
||||
};
|
||||
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
|
||||
|
||||
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
|
||||
// to TX_FEE
|
||||
let lock_tx_bitcoin_fee = bob_btc_wallet
|
||||
.transaction_fee(bob_state3.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
btc_alice_final,
|
||||
alice_btc_starting_balance + btc_to_swap - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
btc_bob_final,
|
||||
bob_btc_starting_balance - btc_to_swap - lock_tx_bitcoin_fee
|
||||
);
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
use crate::testutils::{init_alice, init_bob};
|
||||
use futures::future::try_join;
|
||||
use libp2p::Multiaddr;
|
||||
use rand::rngs::OsRng;
|
||||
use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState, storage::Database};
|
||||
use tempfile::tempdir;
|
||||
use testcontainers::clients::Cli;
|
||||
use testutils::init_tracing;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::{bitcoin, config::Config};
|
||||
|
||||
pub mod testutils;
|
||||
|
||||
// Bob locks btc and Alice locks xmr. Alice fails to act so Bob refunds. Alice
|
||||
// then also refunds.
|
||||
#[tokio::test]
|
||||
async fn both_refund() {
|
||||
let _guard = init_tracing();
|
||||
|
||||
let cli = Cli::default();
|
||||
let (
|
||||
monero,
|
||||
testutils::Containers {
|
||||
bitcoind,
|
||||
monerods: _monerods,
|
||||
},
|
||||
) = testutils::init_containers(&cli).await;
|
||||
|
||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
||||
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let bob_btc_starting_balance = btc_to_swap * 10;
|
||||
let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0);
|
||||
|
||||
let alice_btc_starting_balance = bitcoin::Amount::ZERO;
|
||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||
|
||||
// todo: This should not be hardcoded
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9879"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
|
||||
let (
|
||||
alice_state,
|
||||
mut alice_event_loop,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
_,
|
||||
) = init_alice(
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
xmr_to_swap,
|
||||
alice_xmr_starting_balance,
|
||||
alice_multiaddr.clone(),
|
||||
Config::regtest(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (bob_state, bob_event_loop, bob_event_loop_handle, bob_btc_wallet, bob_xmr_wallet, bob_db) =
|
||||
init_bob(
|
||||
alice_multiaddr.clone(),
|
||||
&bitcoind,
|
||||
&monero,
|
||||
btc_to_swap,
|
||||
bob_btc_starting_balance,
|
||||
xmr_to_swap,
|
||||
Config::regtest(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let bob_fut = bob::swap::swap(
|
||||
bob_state,
|
||||
bob_event_loop_handle,
|
||||
bob_db,
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
OsRng,
|
||||
Uuid::new_v4(),
|
||||
);
|
||||
|
||||
tokio::spawn(async move { bob_event_loop.run().await });
|
||||
|
||||
let alice_swap_id = Uuid::new_v4();
|
||||
let alice_db_datadir = tempdir().unwrap();
|
||||
let alice_db = Database::open(alice_db_datadir.path()).unwrap();
|
||||
|
||||
let alice_xmr_locked_fut = alice::swap::run_until(
|
||||
alice_state,
|
||||
alice::swap::is_xmr_locked,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
Config::regtest(),
|
||||
alice_swap_id,
|
||||
alice_db,
|
||||
);
|
||||
|
||||
tokio::spawn(async move { alice_event_loop.run().await });
|
||||
|
||||
// Wait until alice has locked xmr and bob has locked btc
|
||||
let (bob_state, alice_state) = try_join(bob_fut, alice_xmr_locked_fut).await.unwrap();
|
||||
|
||||
let bob_state4 = if let BobState::BtcRefunded(state4) = bob_state {
|
||||
state4
|
||||
} else {
|
||||
panic!("Bob in unexpected state");
|
||||
};
|
||||
|
||||
let alice_db = Database::open(alice_db_datadir.path()).unwrap();
|
||||
let (mut alice_event_loop, alice_event_loop_handle) =
|
||||
testutils::init_alice_event_loop(alice_multiaddr);
|
||||
|
||||
let alice_state = alice::swap::swap(
|
||||
alice_state,
|
||||
alice_event_loop_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
Config::regtest(),
|
||||
alice_swap_id,
|
||||
alice_db,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::spawn(async move { alice_event_loop.run().await });
|
||||
|
||||
assert!(matches!(alice_state, AliceState::XmrRefunded));
|
||||
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
|
||||
|
||||
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
|
||||
// to TX_FEE
|
||||
let lock_tx_bitcoin_fee = bob_btc_wallet
|
||||
.transaction_fee(bob_state4.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(btc_alice_final, alice_btc_starting_balance);
|
||||
|
||||
// Alice or Bob could publish TxCancel. This means Bob could pay tx fees for
|
||||
// TxCancel and TxRefund or only TxRefund
|
||||
let btc_bob_final_alice_submitted_cancel = btc_bob_final
|
||||
== bob_btc_starting_balance
|
||||
- lock_tx_bitcoin_fee
|
||||
- bitcoin::Amount::from_sat(bitcoin::TX_FEE);
|
||||
|
||||
let btc_bob_final_bob_submitted_cancel = btc_bob_final
|
||||
== bob_btc_starting_balance
|
||||
- lock_tx_bitcoin_fee
|
||||
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE);
|
||||
assert!(btc_bob_final_alice_submitted_cancel || btc_bob_final_bob_submitted_cancel);
|
||||
|
||||
alice_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
assert_eq!(xmr_alice_final, xmr_to_swap);
|
||||
|
||||
bob_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
assert_eq!(xmr_bob_final, bob_xmr_starting_balance);
|
||||
}
|
@ -0,0 +1,279 @@
|
||||
use bitcoin_harness::Bitcoind;
|
||||
use libp2p::core::Multiaddr;
|
||||
use monero_harness::{image, Monero};
|
||||
use rand::rngs::OsRng;
|
||||
use std::sync::Arc;
|
||||
use swap::{
|
||||
alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, monero,
|
||||
network::transport::build, storage::Database, SwapAmounts,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
use testcontainers::{clients::Cli, Container};
|
||||
use tracing_core::dispatcher::DefaultGuard;
|
||||
use tracing_log::LogTracer;
|
||||
use xmr_btc::{alice::State0, config::Config, cross_curve_dleq};
|
||||
|
||||
pub async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
|
||||
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
||||
let _ = bitcoind.init(5).await;
|
||||
let (monero, monerods) = Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(monero, Containers { bitcoind, monerods })
|
||||
}
|
||||
|
||||
pub async fn init_wallets(
|
||||
name: &str,
|
||||
bitcoind: &Bitcoind<'_>,
|
||||
monero: &Monero,
|
||||
btc_starting_balance: Option<xmr_btc::bitcoin::Amount>,
|
||||
xmr_starting_balance: Option<xmr_btc::monero::Amount>,
|
||||
config: Config,
|
||||
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
|
||||
match xmr_starting_balance {
|
||||
Some(amount) => {
|
||||
monero
|
||||
.init(vec![(name, amount.as_piconero())])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
None => {
|
||||
monero
|
||||
.init(vec![(name, monero::Amount::ZERO.as_piconero())])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
let xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet(name).unwrap().client()));
|
||||
|
||||
let btc_wallet = Arc::new(
|
||||
swap::bitcoin::Wallet::new(name, bitcoind.node_url.clone(), config.bitcoin_network)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
if let Some(amount) = btc_starting_balance {
|
||||
bitcoind
|
||||
.mint(btc_wallet.inner.new_address().await.unwrap(), amount)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
(btc_wallet, xmr_wallet)
|
||||
}
|
||||
|
||||
pub async fn init_alice_state(
|
||||
btc_to_swap: bitcoin::Amount,
|
||||
xmr_to_swap: monero::Amount,
|
||||
alice_btc_wallet: Arc<bitcoin::Wallet>,
|
||||
config: Config,
|
||||
) -> AliceState {
|
||||
let rng = &mut OsRng;
|
||||
|
||||
let amounts = SwapAmounts {
|
||||
btc: btc_to_swap,
|
||||
xmr: xmr_to_swap,
|
||||
};
|
||||
|
||||
let a = crate::bitcoin::SecretKey::new_random(rng);
|
||||
let s_a = cross_curve_dleq::Scalar::random(rng);
|
||||
let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng);
|
||||
let redeem_address = alice_btc_wallet.as_ref().new_address().await.unwrap();
|
||||
let punish_address = redeem_address.clone();
|
||||
let state0 = State0::new(
|
||||
a,
|
||||
s_a,
|
||||
v_a,
|
||||
amounts.btc,
|
||||
amounts.xmr,
|
||||
config.bitcoin_refund_timelock,
|
||||
config.bitcoin_punish_timelock,
|
||||
redeem_address,
|
||||
punish_address,
|
||||
);
|
||||
|
||||
AliceState::Started { amounts, state0 }
|
||||
}
|
||||
|
||||
pub fn init_alice_event_loop(
|
||||
listen: Multiaddr,
|
||||
) -> (
|
||||
alice::event_loop::EventLoop,
|
||||
alice::event_loop::EventLoopHandle,
|
||||
) {
|
||||
let alice_behaviour = alice::Behaviour::default();
|
||||
let alice_transport = build(alice_behaviour.identity()).unwrap();
|
||||
|
||||
alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen).unwrap()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn init_alice(
|
||||
bitcoind: &Bitcoind<'_>,
|
||||
monero: &Monero,
|
||||
btc_to_swap: bitcoin::Amount,
|
||||
xmr_to_swap: monero::Amount,
|
||||
xmr_starting_balance: xmr_btc::monero::Amount,
|
||||
listen: Multiaddr,
|
||||
config: Config,
|
||||
) -> (
|
||||
AliceState,
|
||||
alice::event_loop::EventLoop,
|
||||
alice::event_loop::EventLoopHandle,
|
||||
Arc<swap::bitcoin::Wallet>,
|
||||
Arc<swap::monero::Wallet>,
|
||||
Database,
|
||||
) {
|
||||
let (alice_btc_wallet, alice_xmr_wallet) = init_wallets(
|
||||
"alice",
|
||||
bitcoind,
|
||||
monero,
|
||||
None,
|
||||
Some(xmr_starting_balance),
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let alice_start_state =
|
||||
init_alice_state(btc_to_swap, xmr_to_swap, alice_btc_wallet.clone(), config).await;
|
||||
|
||||
let (event_loop, event_loop_handle) = init_alice_event_loop(listen);
|
||||
|
||||
let alice_db_datadir = tempdir().unwrap();
|
||||
let alice_db = Database::open(alice_db_datadir.path()).unwrap();
|
||||
|
||||
(
|
||||
alice_start_state,
|
||||
event_loop,
|
||||
event_loop_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
alice_db,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn init_bob_state(
|
||||
alice_multiaddr: Multiaddr,
|
||||
btc_to_swap: bitcoin::Amount,
|
||||
xmr_to_swap: xmr_btc::monero::Amount,
|
||||
bob_btc_wallet: Arc<bitcoin::Wallet>,
|
||||
config: Config,
|
||||
) -> BobState {
|
||||
let amounts = SwapAmounts {
|
||||
btc: btc_to_swap,
|
||||
xmr: xmr_to_swap,
|
||||
};
|
||||
|
||||
let refund_address = bob_btc_wallet.new_address().await.unwrap();
|
||||
let state0 = xmr_btc::bob::State0::new(
|
||||
&mut OsRng,
|
||||
btc_to_swap,
|
||||
xmr_to_swap,
|
||||
config.bitcoin_refund_timelock,
|
||||
config.bitcoin_punish_timelock,
|
||||
refund_address,
|
||||
);
|
||||
|
||||
BobState::Started {
|
||||
state0,
|
||||
amounts,
|
||||
addr: alice_multiaddr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_bob_event_loop() -> (bob::event_loop::EventLoop, bob::event_loop::EventLoopHandle) {
|
||||
let bob_behaviour = bob::Behaviour::default();
|
||||
let bob_transport = build(bob_behaviour.identity()).unwrap();
|
||||
bob::event_loop::EventLoop::new(bob_transport, bob_behaviour).unwrap()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn init_bob(
|
||||
alice_multiaddr: Multiaddr,
|
||||
bitcoind: &Bitcoind<'_>,
|
||||
monero: &Monero,
|
||||
btc_to_swap: bitcoin::Amount,
|
||||
btc_starting_balance: bitcoin::Amount,
|
||||
xmr_to_swap: xmr_btc::monero::Amount,
|
||||
config: Config,
|
||||
) -> (
|
||||
BobState,
|
||||
bob::event_loop::EventLoop,
|
||||
bob::event_loop::EventLoopHandle,
|
||||
Arc<swap::bitcoin::Wallet>,
|
||||
Arc<swap::monero::Wallet>,
|
||||
Database,
|
||||
) {
|
||||
let (bob_btc_wallet, bob_xmr_wallet) = init_wallets(
|
||||
"bob",
|
||||
bitcoind,
|
||||
monero,
|
||||
Some(btc_starting_balance),
|
||||
None,
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let bob_state = init_bob_state(
|
||||
alice_multiaddr,
|
||||
btc_to_swap,
|
||||
xmr_to_swap,
|
||||
bob_btc_wallet.clone(),
|
||||
config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (event_loop, event_loop_handle) = init_bob_event_loop();
|
||||
|
||||
let bob_db_dir = tempdir().unwrap();
|
||||
let bob_db = Database::open(bob_db_dir.path()).unwrap();
|
||||
|
||||
(
|
||||
bob_state,
|
||||
event_loop,
|
||||
event_loop_handle,
|
||||
bob_btc_wallet,
|
||||
bob_xmr_wallet,
|
||||
bob_db,
|
||||
)
|
||||
}
|
||||
|
||||
// This is just to keep the containers alive
|
||||
#[allow(dead_code)]
|
||||
pub struct Containers<'a> {
|
||||
pub bitcoind: Bitcoind<'a>,
|
||||
pub monerods: Vec<Container<'a, Cli, image::Monero>>,
|
||||
}
|
||||
|
||||
/// Utility function to initialize logging in the test environment.
|
||||
/// Note that you have to keep the `_guard` in scope after calling in test:
|
||||
///
|
||||
/// ```rust
|
||||
/// let _guard = init_tracing();
|
||||
/// ```
|
||||
pub fn init_tracing() -> DefaultGuard {
|
||||
// converts all log records into tracing events
|
||||
// Note: Make sure to initialize without unwrapping, otherwise this causes
|
||||
// trouble when running multiple tests.
|
||||
let _ = LogTracer::init();
|
||||
|
||||
let global_filter = tracing::Level::WARN;
|
||||
let swap_filter = tracing::Level::DEBUG;
|
||||
let xmr_btc_filter = tracing::Level::DEBUG;
|
||||
let monero_harness_filter = tracing::Level::INFO;
|
||||
let bitcoin_harness_filter = tracing::Level::INFO;
|
||||
|
||||
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(format!(
|
||||
"{},swap={},xmr-btc={},monero_harness={},bitcoin_harness={}",
|
||||
global_filter,
|
||||
swap_filter,
|
||||
xmr_btc_filter,
|
||||
monero_harness_filter,
|
||||
bitcoin_harness_filter,
|
||||
))
|
||||
.set_default()
|
||||
}
|
Loading…
Reference in New Issue