Improve resilience of balance assertions

Sometimes, a single sync is not enough because we are still waiting
for the block to be mined.

We introduce an abstraction that loops on fetching the latest balance
with a certain timeout for asserting the balance.
This commit is contained in:
Thomas Eizinger 2021-03-26 12:44:40 +11:00
parent a4c70dfe94
commit 0a82ce989b
No known key found for this signature in database
GPG key ID: 651AC83A6C6C8B96

View file

@ -2,13 +2,16 @@ mod bitcoind;
mod electrs; mod electrs;
use crate::testutils; use crate::testutils;
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use bitcoin_harness::{BitcoindRpcApi, Client}; use bitcoin_harness::{BitcoindRpcApi, Client};
use futures::Future; use futures::Future;
use get_port::get_port; use get_port::get_port;
use libp2p::core::Multiaddr; use libp2p::core::Multiaddr;
use libp2p::{PeerId, Swarm}; use libp2p::{PeerId, Swarm};
use monero_harness::{image, Monero}; use monero_harness::{image, Monero};
use std::cmp::Ordering;
use std::fmt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -147,93 +150,83 @@ impl TestContext {
pub async fn assert_alice_redeemed(&mut self, state: AliceState) { pub async fn assert_alice_redeemed(&mut self, state: AliceState) {
assert!(matches!(state, AliceState::BtcRedeemed)); assert!(matches!(state, AliceState::BtcRedeemed));
self.alice_bitcoin_wallet.sync().await.unwrap(); assert_eventual_balance(
self.alice_bitcoin_wallet.as_ref(),
Ordering::Equal,
self.alice_redeemed_btc_balance(),
)
.await
.unwrap();
let btc_balance_after_swap = self.alice_bitcoin_wallet.balance().await.unwrap(); assert_eventual_balance(
assert_eq!( self.alice_monero_wallet.as_ref(),
btc_balance_after_swap, Ordering::Less,
self.alice_starting_balances.btc + self.btc_amount self.alice_redeemed_xmr_balance(),
- bitcoin::Amount::from_sat(bitcoin::TX_FEE) )
); .await
.unwrap();
let xmr_balance_after_swap = self.alice_monero_wallet.get_balance().await.unwrap();
assert!(
xmr_balance_after_swap <= self.alice_starting_balances.xmr - self.xmr_amount,
"{} !< {} - {}",
xmr_balance_after_swap,
self.alice_starting_balances.xmr,
self.xmr_amount
);
} }
pub async fn assert_alice_refunded(&mut self, state: AliceState) { pub async fn assert_alice_refunded(&mut self, state: AliceState) {
assert!(matches!(state, AliceState::XmrRefunded)); assert!(matches!(state, AliceState::XmrRefunded));
self.alice_bitcoin_wallet.sync().await.unwrap(); assert_eventual_balance(
self.alice_bitcoin_wallet.as_ref(),
let btc_balance_after_swap = self.alice_bitcoin_wallet.balance().await.unwrap(); Ordering::Equal,
assert_eq!(btc_balance_after_swap, self.alice_starting_balances.btc); self.alice_refunded_btc_balance(),
)
// Ensure that Alice's balance is refreshed as we use a newly created wallet .await
self.alice_monero_wallet.refresh().await.unwrap(); .unwrap();
let xmr_balance_after_swap = self.alice_monero_wallet.get_balance().await.unwrap();
// Alice pays fees - comparison does not take exact lock fee into account // Alice pays fees - comparison does not take exact lock fee into account
assert!( assert_eventual_balance(
xmr_balance_after_swap > self.alice_starting_balances.xmr - self.xmr_amount, self.alice_monero_wallet.as_ref(),
"{} > {} - {}", Ordering::Greater,
xmr_balance_after_swap, self.alice_refunded_xmr_balance(),
self.alice_starting_balances.xmr, )
self.xmr_amount .await
); .unwrap();
} }
pub async fn assert_alice_punished(&self, state: AliceState) { pub async fn assert_alice_punished(&self, state: AliceState) {
assert!(matches!(state, AliceState::BtcPunished)); assert!(matches!(state, AliceState::BtcPunished));
self.alice_bitcoin_wallet.sync().await.unwrap(); assert_eventual_balance(
self.alice_bitcoin_wallet.as_ref(),
let btc_balance_after_swap = self.alice_bitcoin_wallet.balance().await.unwrap(); Ordering::Equal,
assert_eq!( self.alice_punished_btc_balance(),
btc_balance_after_swap, )
self.alice_starting_balances.btc + self.btc_amount
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
);
let xmr_balance_after_swap = self.alice_monero_wallet.get_balance().await.unwrap();
assert!(xmr_balance_after_swap <= self.alice_starting_balances.xmr - self.xmr_amount);
}
pub async fn assert_bob_redeemed(&self, state: BobState) {
self.bob_bitcoin_wallet.sync().await.unwrap();
let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state {
tx_lock_id
} else {
panic!("Bob in not in xmr redeemed state: {:?}", state);
};
let lock_tx_bitcoin_fee = self
.bob_bitcoin_wallet
.transaction_fee(lock_tx_id)
.await .await
.unwrap(); .unwrap();
let btc_balance_after_swap = self.bob_bitcoin_wallet.balance().await.unwrap(); assert_eventual_balance(
assert_eq!( self.alice_monero_wallet.as_ref(),
btc_balance_after_swap, Ordering::Less,
self.bob_starting_balances.btc - self.btc_amount - lock_tx_bitcoin_fee self.alice_punished_xmr_balance(),
); )
.await
.unwrap();
}
pub async fn assert_bob_redeemed(&self, state: BobState) {
assert_eventual_balance(
self.bob_bitcoin_wallet.as_ref(),
Ordering::Equal,
self.bob_redeemed_btc_balance(state).await.unwrap(),
)
.await
.unwrap();
// unload the generated wallet by opening the original wallet // unload the generated wallet by opening the original wallet
self.bob_monero_wallet.re_open().await.unwrap(); self.bob_monero_wallet.re_open().await.unwrap();
// refresh the original wallet to make sure the balance is caught up
self.bob_monero_wallet.refresh().await.unwrap();
// Ensure that Bob's balance is refreshed as we use a newly created wallet assert_eventual_balance(
self.bob_monero_wallet.refresh().await.unwrap(); self.bob_monero_wallet.as_ref(),
let xmr_balance_after_swap = self.bob_monero_wallet.get_balance().await.unwrap(); Ordering::Greater,
assert!(xmr_balance_after_swap > self.bob_starting_balances.xmr); self.bob_redeemed_xmr_balance(),
)
.await
.unwrap();
} }
pub async fn assert_bob_refunded(&self, state: BobState) { pub async fn assert_bob_refunded(&self, state: BobState) {
@ -266,33 +259,181 @@ impl TestContext {
// Since we cannot be sure who submitted it we have to assert accordingly // Since we cannot be sure who submitted it we have to assert accordingly
assert!(alice_submitted_cancel || bob_submitted_cancel); assert!(alice_submitted_cancel || bob_submitted_cancel);
let xmr_balance_after_swap = self.bob_monero_wallet.get_balance().await.unwrap(); assert_eventual_balance(
assert_eq!(xmr_balance_after_swap, self.bob_starting_balances.xmr); self.bob_monero_wallet.as_ref(),
Ordering::Equal,
self.bob_refunded_xmr_balance(),
)
.await
.unwrap();
} }
pub async fn assert_bob_punished(&self, state: BobState) { pub async fn assert_bob_punished(&self, state: BobState) {
self.bob_bitcoin_wallet.sync().await.unwrap(); assert_eventual_balance(
self.bob_bitcoin_wallet.as_ref(),
Ordering::Equal,
self.bob_punished_btc_balance(state).await.unwrap(),
)
.await
.unwrap();
assert_eventual_balance(
self.bob_monero_wallet.as_ref(),
Ordering::Equal,
self.bob_punished_xmr_balance(),
)
.await
.unwrap();
}
fn alice_redeemed_xmr_balance(&self) -> monero::Amount {
self.alice_starting_balances.xmr - self.xmr_amount
}
fn alice_redeemed_btc_balance(&self) -> bitcoin::Amount {
self.alice_starting_balances.btc + self.btc_amount
- bitcoin::Amount::from_sat(bitcoin::TX_FEE)
}
fn bob_redeemed_xmr_balance(&self) -> monero::Amount {
self.bob_starting_balances.xmr
}
async fn bob_redeemed_btc_balance(&self, state: BobState) -> Result<bitcoin::Amount> {
self.bob_bitcoin_wallet.sync().await?;
let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state {
tx_lock_id
} else {
bail!("Bob in not in xmr redeemed state: {:?}", state);
};
let lock_tx_bitcoin_fee = self.bob_bitcoin_wallet.transaction_fee(lock_tx_id).await?;
Ok(self.bob_starting_balances.btc - self.btc_amount - lock_tx_bitcoin_fee)
}
fn alice_refunded_xmr_balance(&self) -> monero::Amount {
self.alice_starting_balances.xmr - self.xmr_amount
}
fn alice_refunded_btc_balance(&self) -> bitcoin::Amount {
self.alice_starting_balances.btc
}
fn bob_refunded_xmr_balance(&self) -> monero::Amount {
self.bob_starting_balances.xmr
}
fn alice_punished_xmr_balance(&self) -> monero::Amount {
self.alice_starting_balances.xmr - self.xmr_amount
}
fn alice_punished_btc_balance(&self) -> bitcoin::Amount {
self.alice_starting_balances.btc + self.btc_amount
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
}
fn bob_punished_xmr_balance(&self) -> monero::Amount {
self.bob_starting_balances.xmr
}
async fn bob_punished_btc_balance(&self, state: BobState) -> Result<bitcoin::Amount> {
self.bob_bitcoin_wallet.sync().await?;
let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state { let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state {
tx_lock_id tx_lock_id
} else { } else {
panic!("Bob in not in btc punished state: {:?}", state); bail!("Bob in not in btc punished state: {:?}", state);
}; };
let lock_tx_bitcoin_fee = self let lock_tx_bitcoin_fee = self.bob_bitcoin_wallet.transaction_fee(lock_tx_id).await?;
.bob_bitcoin_wallet
.transaction_fee(lock_tx_id)
.await
.unwrap();
let btc_balance_after_swap = self.bob_bitcoin_wallet.balance().await.unwrap(); Ok(self.bob_starting_balances.btc - self.btc_amount - lock_tx_bitcoin_fee)
assert_eq!( }
btc_balance_after_swap, }
self.bob_starting_balances.btc - self.btc_amount - lock_tx_bitcoin_fee
async fn assert_eventual_balance<A: fmt::Display + PartialOrd>(
wallet: &impl Wallet<Amount = A>,
ordering: Ordering,
expected: A,
) -> Result<()> {
let ordering_str = match ordering {
Ordering::Less => "less than",
Ordering::Equal => "equal to",
Ordering::Greater => "greater than",
};
let mut current_balance = wallet.get_balance().await?;
let assertion = async {
while current_balance.partial_cmp(&expected).unwrap() != ordering {
tokio::time::sleep(Duration::from_millis(500)).await;
wallet.refresh().await?;
current_balance = wallet.get_balance().await?;
}
tracing::debug!(
"Assertion successful! Balance {} is {} {}",
current_balance,
ordering_str,
expected
); );
let xmr_balance_after_swap = self.bob_monero_wallet.get_balance().await.unwrap(); Result::<_, anyhow::Error>::Ok(())
assert_eq!(xmr_balance_after_swap, self.bob_starting_balances.xmr); };
let timeout = Duration::from_secs(10);
tokio::time::timeout(timeout, assertion)
.await
.with_context(|| {
format!(
"Expected balance to be {} {} after at most {}s but was {}",
ordering_str,
expected,
timeout.as_secs(),
current_balance
)
})??;
Ok(())
}
#[async_trait]
trait Wallet {
type Amount;
async fn refresh(&self) -> Result<()>;
async fn get_balance(&self) -> Result<Self::Amount>;
}
#[async_trait]
impl Wallet for monero::Wallet {
type Amount = monero::Amount;
async fn refresh(&self) -> Result<()> {
self.refresh().await?;
Ok(())
}
async fn get_balance(&self) -> Result<Self::Amount> {
self.get_balance().await
}
}
#[async_trait]
impl Wallet for bitcoin::Wallet {
type Amount = bitcoin::Amount;
async fn refresh(&self) -> Result<()> {
self.sync().await
}
async fn get_balance(&self) -> Result<Self::Amount> {
self.balance().await
} }
} }