diff --git a/src/types.rs b/src/types.rs index ddd498aa..3e4d8edf 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,6 +10,7 @@ // licenses. use std::convert::AsRef; +use std::ops::Sub; use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut}; use bitcoin::{hash_types::Txid, util::psbt}; @@ -65,10 +66,31 @@ impl FeeRate { FeeRate(1.0) } + /// Calculate fee rate from `fee` and weight units (`wu`). + pub fn from_wu(fee: u64, wu: usize) -> FeeRate { + Self::from_vb(fee, wu.vbytes()) + } + + /// Calculate fee rate from `fee` and `vbytes`. + pub fn from_vb(fee: u64, vbytes: usize) -> FeeRate { + let rate = fee as f32 / vbytes as f32; + Self::from_sat_per_vb(rate) + } + /// Return the value as satoshi/vbyte pub fn as_sat_vb(&self) -> f32 { self.0 } + + /// Calculate absolute fee in Satoshis using size in weight units. + pub fn fee_wu(&self, wu: usize) -> u64 { + self.fee_vb(wu.vbytes()) + } + + /// Calculate absolute fee in Satoshis using size in virtual bytes. + pub fn fee_vb(&self, vbytes: usize) -> u64 { + (self.as_sat_vb() * vbytes as f32).ceil() as u64 + } } impl std::default::Default for FeeRate { @@ -77,6 +99,27 @@ impl std::default::Default for FeeRate { } } +impl Sub for FeeRate { + type Output = Self; + + fn sub(self, other: FeeRate) -> Self::Output { + FeeRate(self.0 - other.0) + } +} + +/// Trait implemented by types that can be used to measure weight units. +pub trait Vbytes { + /// Convert weight units to virtual bytes. + fn vbytes(self) -> usize; +} + +impl Vbytes for usize { + fn vbytes(self) -> usize { + // ref: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations + (self as f32 / 4.0).ceil() as usize + } +} + /// An unspent output owned by a [`Wallet`]. /// /// [`Wallet`]: crate::Wallet diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 978f3d16..1af50e6e 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -26,7 +26,7 @@ //! ``` //! # use std::str::FromStr; //! # use bitcoin::*; -//! # use bdk::wallet::coin_selection::*; +//! # use bdk::wallet::{self, coin_selection::*}; //! # use bdk::database::Database; //! # use bdk::*; //! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4; @@ -41,7 +41,7 @@ //! optional_utxos: Vec, //! fee_rate: FeeRate, //! amount_needed: u64, -//! fee_amount: f32, +//! fee_amount: u64, //! ) -> Result { //! let mut selected_amount = 0; //! let mut additional_weight = 0; @@ -57,9 +57,8 @@ //! }, //! ) //! .collect::>(); -//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0; -//! let amount_needed_with_fees = -//! (fee_amount + additional_fees).ceil() as u64 + amount_needed; +//! let additional_fees = fee_rate.fee_wu(additional_weight); +//! let amount_needed_with_fees = (fee_amount + additional_fees) + amount_needed; //! if amount_needed_with_fees > selected_amount { //! return Err(bdk::Error::InsufficientFunds { //! needed: amount_needed_with_fees, @@ -90,7 +89,6 @@ //! ``` use crate::types::FeeRate; -use crate::wallet::Vbytes; use crate::{database::Database, WeightedUtxo}; use crate::{error::Error, Utxo}; @@ -118,7 +116,7 @@ pub struct CoinSelectionResult { /// List of outputs selected for use as inputs pub selected: Vec, /// Total fee amount in satoshi - pub fee_amount: f32, + pub fee_amount: u64, } impl CoinSelectionResult { @@ -165,7 +163,7 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug { optional_utxos: Vec, fee_rate: FeeRate, amount_needed: u64, - fee_amount: f32, + fee_amount: u64, ) -> Result; } @@ -184,10 +182,8 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { mut optional_utxos: Vec, fee_rate: FeeRate, amount_needed: u64, - mut fee_amount: f32, + mut fee_amount: u64, ) -> Result { - let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0; - log::debug!( "amount_needed = `{}`, fee_amount = `{}`, fee_rate = `{:?}`", amount_needed, @@ -212,9 +208,9 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { .scan( (&mut selected_amount, &mut fee_amount), |(selected_amount, fee_amount), (must_use, weighted_utxo)| { - if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) { + if must_use || **selected_amount < amount_needed + **fee_amount { **fee_amount += - calc_fee_bytes(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight); + fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight); **selected_amount += weighted_utxo.utxo.txout().value; log::debug!( @@ -231,7 +227,7 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { ) .collect::>(); - let amount_needed_with_fees = amount_needed + (fee_amount.ceil() as u64); + let amount_needed_with_fees = amount_needed + fee_amount; if selected_amount < amount_needed_with_fees { return Err(Error::InsufficientFunds { needed: amount_needed_with_fees, @@ -251,16 +247,15 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { struct OutputGroup { weighted_utxo: WeightedUtxo, // Amount of fees for spending a certain utxo, calculated using a certain FeeRate - fee: f32, + fee: u64, // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it effective_value: i64, } impl OutputGroup { fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self { - let fee = - (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight).vbytes() * fee_rate.as_sat_vb(); - let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64; + let fee = fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight); + let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64; OutputGroup { weighted_utxo, fee, @@ -303,7 +298,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { optional_utxos: Vec, fee_rate: FeeRate, amount_needed: u64, - fee_amount: f32, + fee_amount: u64, ) -> Result { // Mapping every (UTXO, usize) to an output group let required_utxos: Vec = required_utxos @@ -325,7 +320,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { .iter() .fold(0, |acc, x| acc + x.effective_value); - let actual_target = fee_amount.ceil() as u64 + amount_needed; + let actual_target = fee_amount + amount_needed; let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb(); let expected = (curr_available_value + curr_value) @@ -378,7 +373,7 @@ impl BranchAndBoundCoinSelection { mut curr_value: i64, mut curr_available_value: i64, actual_target: i64, - fee_amount: f32, + fee_amount: u64, cost_of_change: f32, ) -> Result { // current_selection[i] will contain true if we are using optional_utxos[i], @@ -486,7 +481,7 @@ impl BranchAndBoundCoinSelection { mut optional_utxos: Vec, curr_value: i64, actual_target: i64, - fee_amount: f32, + fee_amount: u64, ) -> CoinSelectionResult { #[cfg(not(test))] optional_utxos.shuffle(&mut thread_rng()); @@ -515,10 +510,10 @@ impl BranchAndBoundCoinSelection { fn calculate_cs_result( mut selected_utxos: Vec, mut required_utxos: Vec, - mut fee_amount: f32, + mut fee_amount: u64, ) -> CoinSelectionResult { selected_utxos.append(&mut required_utxos); - fee_amount += selected_utxos.iter().map(|u| u.fee).sum::(); + fee_amount += selected_utxos.iter().map(|u| u.fee).sum::(); let selected = selected_utxos .into_iter() .map(|u| u.weighted_utxo.utxo) @@ -540,6 +535,7 @@ mod test { use super::*; use crate::database::MemoryDatabase; use crate::types::*; + use crate::wallet::Vbytes; use rand::rngs::StdRng; use rand::seq::SliceRandom; @@ -547,7 +543,7 @@ mod test { const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2; - const FEE_AMOUNT: f32 = 50.0; + const FEE_AMOUNT: u64 = 50; fn get_test_utxos() -> Vec { vec![ @@ -656,13 +652,13 @@ mod test { vec![], FeeRate::from_sat_per_vb(1.0), 250_000, - 50.0, + FEE_AMOUNT, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_010); - assert!((result.fee_amount - 254.0).abs() < f32::EPSILON); + assert_eq!(result.fee_amount, 254) } #[test] @@ -677,13 +673,13 @@ mod test { vec![], FeeRate::from_sat_per_vb(1.0), 20_000, - 50.0, + FEE_AMOUNT, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_010); - assert!((result.fee_amount - 254.0).abs() < f32::EPSILON); + assert_eq!(result.fee_amount, 254); } #[test] @@ -698,13 +694,13 @@ mod test { utxos, FeeRate::from_sat_per_vb(1.0), 20_000, - 50.0, + FEE_AMOUNT, ) .unwrap(); assert_eq!(result.selected.len(), 1); assert_eq!(result.selected_amount(), 200_000); - assert!((result.fee_amount - 118.0).abs() < f32::EPSILON); + assert_eq!(result.fee_amount, 118); } #[test] @@ -720,7 +716,7 @@ mod test { utxos, FeeRate::from_sat_per_vb(1.0), 500_000, - 50.0, + FEE_AMOUNT, ) .unwrap(); } @@ -738,7 +734,7 @@ mod test { utxos, FeeRate::from_sat_per_vb(1000.0), 250_000, - 50.0, + FEE_AMOUNT, ) .unwrap(); } @@ -758,13 +754,13 @@ mod test { utxos, FeeRate::from_sat_per_vb(1.0), 250_000, - 50.0, + FEE_AMOUNT, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_000); - assert!((result.fee_amount - 254.0).abs() < f32::EPSILON); + assert_eq!(result.fee_amount, 254); } #[test] @@ -785,7 +781,7 @@ mod test { assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_010); - assert!((result.fee_amount - 254.0).abs() < f32::EPSILON); + assert_eq!(result.fee_amount, 254); } #[test] @@ -806,7 +802,7 @@ mod test { assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300010); - assert!((result.fee_amount - 254.0).abs() < f32::EPSILON); + assert_eq!(result.fee_amount, 254); } #[test] @@ -822,7 +818,7 @@ mod test { utxos, FeeRate::from_sat_per_vb(1.0), 500_000, - 50.0, + FEE_AMOUNT, ) .unwrap(); } @@ -840,7 +836,7 @@ mod test { utxos, FeeRate::from_sat_per_vb(1000.0), 250_000, - 50.0, + FEE_AMOUNT, ) .unwrap(); } @@ -857,7 +853,7 @@ mod test { utxos, FeeRate::from_sat_per_vb(1.0), 99932, // first utxo's effective value - 0.0, + 0, ) .unwrap(); @@ -865,7 +861,7 @@ mod test { assert_eq!(result.selected_amount(), 100_000); let input_size = (TXIN_BASE_WEIGHT + P2WPKH_WITNESS_SIZE).vbytes(); let epsilon = 0.5; - assert!((1.0 - (result.fee_amount / input_size)).abs() < epsilon); + assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < epsilon); } #[test] @@ -884,7 +880,7 @@ mod test { optional_utxos, FeeRate::from_sat_per_vb(0.0), target_amount, - 0.0, + 0, ) .unwrap(); assert_eq!(result.selected_amount(), target_amount); @@ -911,7 +907,7 @@ mod test { 0, curr_available_value, 20_000, - 50.0, + FEE_AMOUNT, cost_of_change, ) .unwrap(); @@ -938,7 +934,7 @@ mod test { 0, curr_available_value, 20_000, - 50.0, + FEE_AMOUNT, cost_of_change, ) .unwrap(); @@ -950,7 +946,6 @@ mod test { let fee_rate = FeeRate::from_sat_per_vb(1.0); let size_of_change = 31; let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb(); - let fee_amount = 50.0; let utxos: Vec<_> = generate_same_value_utxos(50_000, 10) .into_iter() @@ -972,12 +967,12 @@ mod test { curr_value, curr_available_value, target_amount, - fee_amount, + FEE_AMOUNT, cost_of_change, ) .unwrap(); - assert!((result.fee_amount - 186.0).abs() < f32::EPSILON); assert_eq!(result.selected_amount(), 100_000); + assert_eq!(result.fee_amount, 186); } // TODO: bnb() function should be optimized, and this test should be done with more utxos @@ -1009,7 +1004,7 @@ mod test { curr_value, curr_available_value, target_amount, - 0.0, + 0, 0.0, ) .unwrap(); @@ -1035,12 +1030,10 @@ mod test { utxos, 0, target_amount as i64, - 50.0, + FEE_AMOUNT, ); assert!(result.selected_amount() > target_amount); - assert!( - (result.fee_amount - (50.0 + result.selected.len() as f32 * 68.0)).abs() < f32::EPSILON - ); + assert_eq!(result.fee_amount, (50 + result.selected.len() * 68) as u64); } } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 1f29239f..f507d0e2 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -543,7 +543,7 @@ where }); } } - (FeeRate::from_sat_per_vb(0.0), *fee as f32) + (FeeRate::from_sat_per_vb(0.0), *fee) } FeePolicy::FeeRate(rate) => { if let Some(previous_fee) = params.bumping_fee { @@ -554,7 +554,7 @@ where }); } } - (*rate, 0.0) + (*rate, 0) } }; @@ -573,8 +573,7 @@ where let mut outgoing: u64 = 0; let mut received: u64 = 0; - let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0; - fee_amount += calc_fee_bytes(tx.get_weight()); + fee_amount += fee_rate.fee_wu(tx.get_weight()); let recipients = params.recipients.iter().map(|(r, v)| (r, *v)); @@ -591,7 +590,7 @@ where script_pubkey: script_pubkey.clone(), value, }; - fee_amount += calc_fee_bytes(serialize(&new_out).len() * 4); + fee_amount += fee_rate.fee_vb(serialize(&new_out).len()); tx.output.push(new_out); @@ -649,9 +648,8 @@ where } }; - fee_amount += calc_fee_bytes(serialize(&drain_output).len() * 4); + fee_amount += fee_rate.fee_vb(serialize(&drain_output).len()); - let mut fee_amount = fee_amount.ceil() as u64; let drain_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount); if tx.output.is_empty() { @@ -754,8 +752,10 @@ where return Err(Error::IrreplaceableTransaction); } - let vbytes = tx.get_weight().vbytes(); - let feerate = details.fee.ok_or(Error::FeeRateUnavailable)? as f32 / vbytes; + let feerate = FeeRate::from_wu( + details.fee.ok_or(Error::FeeRateUnavailable)?, + tx.get_weight(), + ); // remove the inputs from the tx and process them let original_txin = tx.input.drain(..).collect::>(); @@ -832,7 +832,7 @@ where utxos: original_utxos, bumping_fee: Some(tx_builder::PreviousFee { absolute: details.fee.ok_or(Error::FeeRateUnavailable)?, - rate: feerate, + rate: feerate.as_sat_vb(), }), ..Default::default() }; @@ -1548,18 +1548,6 @@ where } } -/// Trait implemented by types that can be used to measure weight units. -pub trait Vbytes { - /// Convert weight units to virtual bytes. - fn vbytes(self) -> f32; -} - -impl Vbytes for usize { - fn vbytes(self) -> f32 { - self as f32 / 4.0 - } -} - #[cfg(test)] pub(crate) mod test { use std::str::FromStr; @@ -1746,13 +1734,13 @@ pub(crate) mod test { dust_change = true; )* - let tx_fee_rate = $fees as f32 / (tx.get_weight().vbytes()); - let fee_rate = $fee_rate.as_sat_vb(); + let tx_fee_rate = FeeRate::from_wu($fees, tx.get_weight()); + let fee_rate = $fee_rate; if !dust_change { - assert!((tx_fee_rate - fee_rate).abs() < 0.5, "Expected fee rate of {}, the tx has {}", fee_rate, tx_fee_rate); + assert!((tx_fee_rate - fee_rate).as_sat_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate); } else { - assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {}, the tx has {}", fee_rate, tx_fee_rate); + assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate); } }); }