xmr-btc-swap/xmr-btc/tests/e2e.rs
rishflab 39afb4196b Save and recover protocol state from disk
NOTE: This implementation saves secrets to disk! It is not
secure.

The storage API allows the caller to atomically record the state
of the protocol. The user can retrieve this recorded state and
re-commence the protocol from that point. The state is recorded
using a hard coded key, causing it to overwrite the previously
recorded state. This limitation means that this recovery
mechanism should not be used in a program that simultaneously
manages the execution of multiple swaps.

An e2e test was added to show how to save, recover and resume
protocol execution. This logic could also be integrated into the
run_until functions to automate saving but was not included at
this stage as protocol execution is currently under development.

Serialisation and deserialisation was implemented on the states
to allow the to be stored using the database. Currently the
secret's are also being stored to disk but should be recovered
from a seed or wallets.
2020-10-21 18:39:45 +11:00

529 lines
16 KiB
Rust

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};
mod harness;
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<'a>,
bitcoind: &Bitcoind<'a>,
) -> (
alice::State0,
bob::State0,
AliceNode<'a>,
BobNode<'a>,
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::AliceWallet(&monero);
let bob_monero_wallet = wallet::monero::BobWallet(&monero);
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_alice().await.unwrap();
let bob_initial_xmr_balance = bob.monero_wallet.0.get_balance_bob().await.unwrap();
let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap();
let punish_address = redeem_address.clone();
let refund_address = bob.bitcoin_wallet.new_address().await.unwrap();
let alice_state0 = alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
RELATIVE_REFUND_TIMELOCK,
RELATIVE_PUNISH_TIMELOCK,
redeem_address.clone(),
punish_address.clone(),
);
let bob_state0 = bob::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
RELATIVE_REFUND_TIMELOCK,
RELATIVE_PUNISH_TIMELOCK,
refund_address,
);
let initial_balances = InitialBalances {
alice_xmr: alice_initial_xmr_balance,
alice_btc: alice_initial_btc_balance,
bob_xmr: bob_initial_xmr_balance,
bob_btc: bob_initial_btc_balance,
};
(
alice_state0,
bob_state0,
alice,
bob,
initial_balances,
swap_amounts,
)
}
mod tests {
// NOTE: For some reason running these tests overflows the stack. In order to
// mitigate this run them with:
//
// RUST_MIN_STACK=100000000 cargo test
use crate::{
harness,
harness::node::{run_alice_until, run_bob_until},
init_bitcoind, init_test, ALICE_TEST_DB_FOLDER, BOB_TEST_DB_FOLDER,
};
use futures::future;
use monero_harness::Monero;
use rand::rngs::OsRng;
use std::{convert::TryInto, path::Path};
use testcontainers::clients::Cli;
use tracing_subscriber::util::SubscriberInitExt;
use xmr_btc::{
alice, bitcoin,
bitcoin::{Amount, TX_FEE},
bob,
};
#[tokio::test]
async fn happy_path() {
let _guard = tracing_subscriber::fmt()
.with_env_filter("info")
.set_default();
let cli = Cli::default();
let monero = 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).await;
let (alice_state, bob_state) = future::try_join(
run_alice_until(
&mut alice_node,
alice_state0.into(),
harness::alice::is_state6,
&mut OsRng,
),
run_bob_until(
&mut bob_node,
bob_state0.into(),
harness::bob::is_state5,
&mut OsRng,
),
)
.await
.unwrap();
let alice_state6: alice::State6 = alice_state.try_into().unwrap();
let bob_state5: bob::State5 = bob_state.try_into().unwrap();
let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_node.bitcoin_wallet.balance().await.unwrap();
let lock_tx_bitcoin_fee = bob_node
.bitcoin_wallet
.transaction_fee(bob_state5.tx_lock_id())
.await
.unwrap();
let alice_final_xmr_balance = alice_node
.monero_wallet
.0
.get_balance_alice()
.await
.unwrap();
bob_node
.monero_wallet
.0
.wait_for_bob_wallet_block_height()
.await
.unwrap();
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap();
assert_eq!(
alice_final_btc_balance,
initial_balances.alice_btc + swap_amounts.btc
- bitcoin::Amount::from_sat(bitcoin::TX_FEE)
);
assert_eq!(
bob_final_btc_balance,
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
);
assert_eq!(
alice_final_xmr_balance,
initial_balances.alice_xmr
- u64::from(swap_amounts.xmr)
- u64::from(alice_state6.lock_xmr_fee())
);
assert_eq!(
bob_final_xmr_balance,
initial_balances.bob_xmr + u64::from(swap_amounts.xmr)
);
}
#[tokio::test]
async fn both_refund() {
let _guard = tracing_subscriber::fmt()
.with_env_filter("info")
.set_default();
let cli = Cli::default();
let monero = 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).await;
let (alice_state, bob_state) = future::try_join(
run_alice_until(
&mut alice_node,
alice_state0.into(),
harness::alice::is_state5,
&mut OsRng,
),
run_bob_until(
&mut bob_node,
bob_state0.into(),
harness::bob::is_state3,
&mut OsRng,
),
)
.await
.unwrap();
let alice_state5: alice::State5 = alice_state.try_into().unwrap();
let bob_state3: bob::State3 = bob_state.try_into().unwrap();
bob_state3
.refund_btc(&bob_node.bitcoin_wallet)
.await
.unwrap();
alice_state5
.refund_xmr(&alice_node.bitcoin_wallet, &alice_node.monero_wallet)
.await
.unwrap();
let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_node.bitcoin_wallet.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_node
.bitcoin_wallet
.transaction_fee(bob_state3.tx_lock_id())
.await
.unwrap();
alice_node
.monero_wallet
.0
.wait_for_alice_wallet_block_height()
.await
.unwrap();
let alice_final_xmr_balance = alice_node
.monero_wallet
.0
.get_balance_alice()
.await
.unwrap();
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap();
assert_eq!(alice_final_btc_balance, initial_balances.alice_btc);
assert_eq!(
bob_final_btc_balance,
// The 2 * TX_FEE corresponds to tx_refund and tx_cancel.
initial_balances.bob_btc - Amount::from_sat(2 * TX_FEE) - lock_tx_bitcoin_fee
);
// 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!(bob_final_xmr_balance, initial_balances.bob_xmr);
}
#[tokio::test]
async fn alice_punishes() {
let _guard = tracing_subscriber::fmt()
.with_env_filter("info")
.set_default();
let cli = Cli::default();
let monero = 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).await;
let (alice_state, bob_state) = future::try_join(
run_alice_until(
&mut alice_node,
alice_state0.into(),
harness::alice::is_state4,
&mut OsRng,
),
run_bob_until(
&mut bob_node,
bob_state0.into(),
harness::bob::is_state3,
&mut OsRng,
),
)
.await
.unwrap();
let alice_state4: alice::State4 = alice_state.try_into().unwrap();
let bob_state3: bob::State3 = bob_state.try_into().unwrap();
alice_state4
.punish(&alice_node.bitcoin_wallet)
.await
.unwrap();
let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_node.bitcoin_wallet.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_node
.bitcoin_wallet
.transaction_fee(bob_state3.tx_lock_id())
.await
.unwrap();
assert_eq!(
alice_final_btc_balance,
initial_balances.alice_btc + swap_amounts.btc - Amount::from_sat(2 * TX_FEE)
);
assert_eq!(
bob_final_btc_balance,
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
);
}
#[tokio::test]
async fn recover_protocol_state_from_db() {
let _guard = tracing_subscriber::fmt()
.with_env_filter("info")
.set_default();
let cli = Cli::default();
let monero = Monero::new(&cli);
let bitcoind = init_bitcoind(&cli).await;
let alice_db = harness::storage::Database::open(Path::new(ALICE_TEST_DB_FOLDER)).unwrap();
let bob_db = harness::storage::Database::open(Path::new(BOB_TEST_DB_FOLDER)).unwrap();
let (
alice_state0,
bob_state0,
mut alice_node,
mut bob_node,
initial_balances,
swap_amounts,
) = init_test(&monero, &bitcoind).await;
{
let (alice_state, bob_state) = future::try_join(
run_alice_until(
&mut alice_node,
alice_state0.into(),
harness::alice::is_state5,
&mut OsRng,
),
run_bob_until(
&mut bob_node,
bob_state0.into(),
harness::bob::is_state3,
&mut OsRng,
),
)
.await
.unwrap();
let alice_state5: alice::State5 = alice_state.try_into().unwrap();
let bob_state3: bob::State3 = bob_state.try_into().unwrap();
// save state to db
alice_db.insert_latest_state(&alice_state5).await.unwrap();
bob_db.insert_latest_state(&bob_state3).await.unwrap();
};
let (alice_state6, bob_state5) = {
// recover state from db
let alice_state5: alice::State5 = alice_db.get_latest_state().unwrap();
let bob_state3: bob::State3 = bob_db.get_latest_state().unwrap();
let (alice_state, bob_state) = future::try_join(
run_alice_until(
&mut alice_node,
alice_state5.into(),
harness::alice::is_state6,
&mut OsRng,
),
run_bob_until(
&mut bob_node,
bob_state3.into(),
harness::bob::is_state5,
&mut OsRng,
),
)
.await
.unwrap();
let alice_state: alice::State6 = alice_state.try_into().unwrap();
let bob_state: bob::State5 = bob_state.try_into().unwrap();
(alice_state, bob_state)
};
let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_node.bitcoin_wallet.balance().await.unwrap();
let lock_tx_bitcoin_fee = bob_node
.bitcoin_wallet
.transaction_fee(bob_state5.tx_lock_id())
.await
.unwrap();
let alice_final_xmr_balance = alice_node
.monero_wallet
.0
.get_balance_alice()
.await
.unwrap();
bob_node
.monero_wallet
.0
.wait_for_bob_wallet_block_height()
.await
.unwrap();
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap();
assert_eq!(
alice_final_btc_balance,
initial_balances.alice_btc + swap_amounts.btc
- bitcoin::Amount::from_sat(bitcoin::TX_FEE)
);
assert_eq!(
bob_final_btc_balance,
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
);
assert_eq!(
alice_final_xmr_balance,
initial_balances.alice_xmr
- u64::from(swap_amounts.xmr)
- u64::from(alice_state6.lock_xmr_fee())
);
assert_eq!(
bob_final_xmr_balance,
initial_balances.bob_xmr + u64::from(swap_amounts.xmr)
);
}
}