Add relative and absolute max transaction fee.

This commit is contained in:
Philipp Hoenisch 2021-05-04 11:34:00 +10:00
parent dc6ab0fa52
commit dc8dd5af28
No known key found for this signature in database
GPG Key ID: E5F8E74C672BC666
5 changed files with 168 additions and 46 deletions

View File

@ -325,8 +325,14 @@ mod tests {
let btc_amount = Amount::from_sat(500_000); let btc_amount = Amount::from_sat(500_000);
let xmr_amount = crate::monero::Amount::from_piconero(10000); let xmr_amount = crate::monero::Amount::from_piconero(10000);
let tx_redeem_fee = alice_wallet.estimate_fee(TxRedeem::weight()).await.unwrap(); let tx_redeem_fee = alice_wallet
let tx_punish_fee = alice_wallet.estimate_fee(TxPunish::weight()).await.unwrap(); .estimate_fee(TxRedeem::weight(), btc_amount)
.await
.unwrap();
let tx_punish_fee = alice_wallet
.estimate_fee(TxPunish::weight(), btc_amount)
.await
.unwrap();
let redeem_address = alice_wallet.new_address().await.unwrap(); let redeem_address = alice_wallet.new_address().await.unwrap();
let punish_address = alice_wallet.new_address().await.unwrap(); let punish_address = alice_wallet.new_address().await.unwrap();

View File

@ -22,7 +22,12 @@ use std::time::{Duration, Instant};
use tokio::sync::{watch, Mutex}; use tokio::sync::{watch, Mutex};
const SLED_TREE_NAME: &str = "default_tree"; const SLED_TREE_NAME: &str = "default_tree";
const MAX_TX_FEE: u64 = 100_000;
// TODO make these values configurable
/// Assuming we add a spread of 3% we don't want to pay more than 3% of the
/// amount for tx fees.
const MAX_RELATIVE_TX_FEE: f64 = 0.03;
const MAX_ABSOLUTE_TX_FEE: u64 = 100_000;
pub struct Wallet<B = ElectrumBlockchain, D = bdk::sled::Tree, C = Client> { pub struct Wallet<B = ElectrumBlockchain, D = bdk::sled::Tree, C = Client> {
client: Arc<Mutex<C>>, client: Arc<Mutex<C>>,
@ -322,46 +327,81 @@ where
} }
/// Estimate total tx fee based for a pre-defined target block based on the /// Estimate total tx fee based for a pre-defined target block based on the
/// transaction weight /// transaction weight. The max fee cannot be more than MAX_PERCENTAGE_FEE
pub async fn estimate_fee(&self, weight: usize) -> Result<bitcoin::Amount> { /// of amount
pub async fn estimate_fee(
&self,
weight: usize,
transfer_amount: bitcoin::Amount,
) -> Result<bitcoin::Amount> {
let client = self.client.lock().await; let client = self.client.lock().await;
let fee_rate = client.estimate_feerate(self.target_block)?; let fee_rate = client.estimate_feerate(self.target_block)?;
let min_relay_fee = client.min_relay_fee()?; let min_relay_fee = client.min_relay_fee()?;
tracing::debug!("Min relay fee: {}", min_relay_fee); tracing::debug!("Min relay fee: {}", min_relay_fee);
// Doing some heavy math here :)
// `usize` is 32 or 64 bits wide, but `f32`'s mantissa is only 23 bits wide.
// This fine because such a big transaction cannot exist and there are also no
// negative fees.
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let sats_per_vbyte = ((weight as f32) / 4.0 * fee_rate.as_sat_vb()) as u64;
tracing::debug!(
"Estimated fee for weight: {} for fee_rate: {:?} is in total: {}",
weight,
fee_rate,
sats_per_vbyte
);
if sats_per_vbyte < min_relay_fee.as_sat() { Ok(estimate_fee(
tracing::warn!( weight,
"Estimated fee of {} is smaller than the min relay fee, defaulting to min relay fee {}", transfer_amount,
sats_per_vbyte, fee_rate,
min_relay_fee.as_sat() min_relay_fee,
); ))
Ok(min_relay_fee) }
} else if sats_per_vbyte > MAX_TX_FEE { }
tracing::warn!(
"Hard bound of max transaction fees reached. Falling back to: {} sats", fn estimate_fee(
MAX_TX_FEE weight: usize,
); transfer_amount: Amount,
Ok(bitcoin::Amount::from_sat(MAX_TX_FEE)) fee_rate: FeeRate,
} else { min_relay_fee: Amount,
Ok(bitcoin::Amount::from_sat(sats_per_vbyte)) ) -> Amount {
} // Doing some heavy math here :)
// `usize` is 32 or 64 bits wide, but `f32`'s mantissa is only 23 bits wide.
// This fine because such a big transaction cannot exist and there are also no
// negative fees.
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let sats_per_vbyte = ((weight as f32) / 4.0 * fee_rate.as_sat_vb()) as u64;
tracing::debug!(
"Estimated fee for weight: {} for fee_rate: {:?} is in total: {}",
weight,
fee_rate,
sats_per_vbyte
);
// Similar as above: we do not care about fractional fees and have to cast a
// couple of times.
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let max_allowed_fee = (transfer_amount.as_sat() as f64 * MAX_RELATIVE_TX_FEE).ceil() as u64;
if sats_per_vbyte < min_relay_fee.as_sat() {
tracing::warn!(
"Estimated fee of {} is smaller than the min relay fee, defaulting to min relay fee {}",
sats_per_vbyte,
min_relay_fee.as_sat()
);
min_relay_fee
} else if sats_per_vbyte > max_allowed_fee && sats_per_vbyte > MAX_ABSOLUTE_TX_FEE {
tracing::warn!(
"Hard bound of transaction fees reached. Falling back to: {} sats",
MAX_ABSOLUTE_TX_FEE
);
bitcoin::Amount::from_sat(MAX_ABSOLUTE_TX_FEE)
} else if sats_per_vbyte > max_allowed_fee {
tracing::warn!(
"Relative bound of transaction fees reached. Falling back to: {} sats",
max_allowed_fee
);
bitcoin::Amount::from_sat(max_allowed_fee)
} else {
bitcoin::Amount::from_sat(sats_per_vbyte)
} }
} }
@ -734,4 +774,76 @@ mod tests {
assert_eq!(confirmed.depth, 0) assert_eq!(confirmed.depth, 0)
} }
#[test]
fn given_one_BTC_and_100k_sats_per_vb_fees_should_not_hit_max() {
// 400 weight = 100 vbyte
let weight = 400;
let amount = bitcoin::Amount::from_sat(100_000_000);
let sat_per_vb = 100.0;
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
let relay_fee = bitcoin::Amount::ONE_SAT;
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee);
// weight / 4.0 * sat_per_vb
let should_fee = bitcoin::Amount::from_sat(10_000);
assert_eq!(is_fee, should_fee);
}
#[test]
fn given_1BTC_and_1_sat_per_vb_fees_and_100ksat_min_relay_fee_should_hit_min() {
// 400 weight = 100 vbyte
let weight = 400;
let amount = bitcoin::Amount::from_sat(100_000_000);
let sat_per_vb = 1.0;
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
let relay_fee = bitcoin::Amount::from_sat(100_000);
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee);
// weight / 4.0 * sat_per_vb would be smaller than relay fee hence we take min
// relay fee
let should_fee = bitcoin::Amount::from_sat(100_000);
assert_eq!(is_fee, should_fee);
}
#[test]
fn given_1mio_sat_and_1k_sats_per_vb_fees_should_hit_relative_max() {
// 400 weight = 100 vbyte
let weight = 400;
let amount = bitcoin::Amount::from_sat(1_000_000);
let sat_per_vb = 1_000.0;
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
let relay_fee = bitcoin::Amount::ONE_SAT;
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee);
// weight / 4.0 * sat_per_vb would be greater than 0.03% hence we take max
// relative fee.
let should_fee = bitcoin::Amount::from_sat(30_000);
assert_eq!(is_fee, should_fee);
}
#[test]
fn given_1BTC_and_4mio_sats_per_vb_fees_should_hit_total_max() {
// even if we send 1BTC we don't want to pay 0.3BTC in fees. This would be
// $1,650 at the moment.
let weight = 400;
let amount = bitcoin::Amount::from_sat(100_000_000);
let sat_per_vb = 4_000_000.0;
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
let relay_fee = bitcoin::Amount::ONE_SAT;
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee);
// weight / 4.0 * sat_per_vb would be greater than 0.03% hence we take total
// max allowed fee.
let should_fee = bitcoin::Amount::from_sat(MAX_ABSOLUTE_TX_FEE);
assert_eq!(is_fee, should_fee);
}
} }

View File

@ -163,10 +163,10 @@ where
} }
// TODO: This should be cleaned up. // TODO: This should be cleaned up.
let tx_redeem_fee = self.bitcoin_wallet let tx_redeem_fee = self.bitcoin_wallet
.estimate_fee(bitcoin::TxRedeem::weight()) .estimate_fee(bitcoin::TxRedeem::weight(), btc)
.await; .await;
let tx_punish_fee = self.bitcoin_wallet let tx_punish_fee = self.bitcoin_wallet
.estimate_fee(bitcoin::TxPunish::weight()) .estimate_fee(bitcoin::TxPunish::weight(), btc)
.await; .await;
let redeem_address = self.bitcoin_wallet.new_address().await; let redeem_address = self.bitcoin_wallet.new_address().await;
let punish_address = self.bitcoin_wallet.new_address().await; let punish_address = self.bitcoin_wallet.new_address().await;

View File

@ -66,8 +66,12 @@ async fn next_state(
Ok(match state { Ok(match state {
BobState::Started { btc_amount } => { BobState::Started { btc_amount } => {
let bitcoin_refund_address = bitcoin_wallet.new_address().await?; let bitcoin_refund_address = bitcoin_wallet.new_address().await?;
let tx_refund_fee = bitcoin_wallet.estimate_fee(TxRefund::weight()).await?; let tx_refund_fee = bitcoin_wallet
let tx_cancel_fee = bitcoin_wallet.estimate_fee(TxCancel::weight()).await?; .estimate_fee(TxRefund::weight(), btc_amount)
.await?;
let tx_cancel_fee = bitcoin_wallet
.estimate_fee(TxCancel::weight(), btc_amount)
.await?;
let state2 = request_price_and_setup( let state2 = request_price_and_setup(
swap_id, swap_id,

View File

@ -634,12 +634,12 @@ impl TestContext {
let cancel_fee = self let cancel_fee = self
.alice_bitcoin_wallet .alice_bitcoin_wallet
.estimate_fee(TxCancel::weight()) .estimate_fee(TxCancel::weight(), self.btc_amount)
.await .await
.expect("To estimate fee correctly"); .expect("To estimate fee correctly");
let refund_fee = self let refund_fee = self
.alice_bitcoin_wallet .alice_bitcoin_wallet
.estimate_fee(TxRefund::weight()) .estimate_fee(TxRefund::weight(), self.btc_amount)
.await .await
.expect("To estimate fee correctly"); .expect("To estimate fee correctly");
@ -682,7 +682,7 @@ impl TestContext {
async fn alice_redeemed_btc_balance(&self) -> bitcoin::Amount { async fn alice_redeemed_btc_balance(&self) -> bitcoin::Amount {
let fee = self let fee = self
.alice_bitcoin_wallet .alice_bitcoin_wallet
.estimate_fee(TxRedeem::weight()) .estimate_fee(TxRedeem::weight(), self.btc_amount)
.await .await
.expect("To estimate fee correctly"); .expect("To estimate fee correctly");
self.alice_starting_balances.btc + self.btc_amount - fee self.alice_starting_balances.btc + self.btc_amount - fee
@ -725,12 +725,12 @@ impl TestContext {
async fn alice_punished_btc_balance(&self) -> bitcoin::Amount { async fn alice_punished_btc_balance(&self) -> bitcoin::Amount {
let cancel_fee = self let cancel_fee = self
.alice_bitcoin_wallet .alice_bitcoin_wallet
.estimate_fee(TxCancel::weight()) .estimate_fee(TxCancel::weight(), self.btc_amount)
.await .await
.expect("To estimate fee correctly"); .expect("To estimate fee correctly");
let punish_fee = self let punish_fee = self
.alice_bitcoin_wallet .alice_bitcoin_wallet
.estimate_fee(TxPunish::weight()) .estimate_fee(TxPunish::weight(), self.btc_amount)
.await .await
.expect("To estimate fee correctly"); .expect("To estimate fee correctly");
self.alice_starting_balances.btc + self.btc_amount - cancel_fee - punish_fee self.alice_starting_balances.btc + self.btc_amount - cancel_fee - punish_fee