From 40efe05192516db9fbdaabb4b68afd7d3112f732 Mon Sep 17 00:00:00 2001 From: Martin Habovstiak Date: Mon, 6 Feb 2023 21:01:48 +0100 Subject: [PATCH] Add `Weight` and `FeeRate` newtypes Use of general-purpose integers is often error-prone and annoying. We're working towards improving it by introducing newtypes. This adds newtypes for weight and fee rate to make fee computation easier and more readable. Note however that this dosn't change the type for individual parts of the transaction since computing the total weight is not as simple as summing them up and we want to avoid such confusion. Part of #630 --- bitcoin/src/blockdata/block.rs | 12 +- bitcoin/src/blockdata/fee_rate.rs | 158 +++++++++++++++++++++++++++ bitcoin/src/blockdata/mod.rs | 5 + bitcoin/src/blockdata/transaction.rs | 31 +++--- bitcoin/src/blockdata/weight.rs | 156 ++++++++++++++++++++++++++ 5 files changed, 341 insertions(+), 21 deletions(-) create mode 100644 bitcoin/src/blockdata/fee_rate.rs create mode 100644 bitcoin/src/blockdata/weight.rs diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs index 120b689624..947598afad 100644 --- a/bitcoin/src/blockdata/block.rs +++ b/bitcoin/src/blockdata/block.rs @@ -19,12 +19,12 @@ use crate::hashes::{Hash, HashEngine}; use crate::hash_types::{Wtxid, TxMerkleNode, WitnessMerkleNode, WitnessCommitment}; use crate::consensus::{encode, Encodable, Decodable}; use crate::blockdata::transaction::Transaction; -use crate::blockdata::constants::WITNESS_SCALE_FACTOR; use crate::blockdata::script; use crate::pow::{CompactTarget, Target, Work}; use crate::VarInt; use crate::internal_macros::impl_consensus_encoding; use crate::io; +use super::Weight; pub use crate::hash_types::BlockHash; @@ -302,9 +302,9 @@ impl Block { } /// Returns the weight of the block. - pub fn weight(&self) -> usize { - let base_weight = WITNESS_SCALE_FACTOR * self.base_size(); - let txs_weight: usize = self.txdata.iter().map(Transaction::weight).sum(); + pub fn weight(&self) -> Weight { + let base_weight = Weight::from_non_witness_data_size(self.base_size() as u64); + let txs_weight: Weight = self.txdata.iter().map(Transaction::weight).sum(); base_weight + txs_weight } @@ -470,7 +470,7 @@ mod tests { assert_eq!(real_decode.size(), some_block.len()); assert_eq!(real_decode.strippedsize(), some_block.len()); - assert_eq!(real_decode.weight(), some_block.len() * 4); + assert_eq!(real_decode.weight(), Weight::from_non_witness_data_size(some_block.len() as u64)); // should be also ok for a non-witness block as commitment is optional in that case assert!(real_decode.check_witness_commitment()); @@ -505,7 +505,7 @@ mod tests { assert_eq!(real_decode.size(), segwit_block.len()); assert_eq!(real_decode.strippedsize(), 4283); - assert_eq!(real_decode.weight(), 17168); + assert_eq!(real_decode.weight(), Weight::from_wu(17168)); assert!(real_decode.check_witness_commitment()); diff --git a/bitcoin/src/blockdata/fee_rate.rs b/bitcoin/src/blockdata/fee_rate.rs new file mode 100644 index 0000000000..28650ed3bc --- /dev/null +++ b/bitcoin/src/blockdata/fee_rate.rs @@ -0,0 +1,158 @@ +//! Implements `FeeRate` and assoctiated features. + +use core::fmt; +use core::ops::{Mul, MulAssign, Div, DivAssign}; +use crate::Amount; +use super::Weight; + +/// Represents fee rate. +/// +/// This is an integer newtype representing fee rate in `sat/kwu`. It provides protection against mixing +/// up the types as well as basic formatting features. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct FeeRate(u64); + +impl FeeRate { + /// 0 sat/kwu. + /// + /// Equivalent to [`MIN`], may better express intent in some contexts. + pub const ZERO: FeeRate = FeeRate(0); + + /// Minimum possible value (0 sat/kwu). + /// + /// Equivalent to [`ZERO`], may better express intent in some contexts. + pub const MIN: FeeRate = FeeRate(u64::MIN); + + /// Maximum possible value. + pub const MAX: FeeRate = FeeRate(u64::MAX); + + /// Minimum fee rate required to broadcast a transaction. + /// + /// The value matches the default Bitcoin Core policy at the time of library release. + pub const BROADCAST_MIN: FeeRate = FeeRate::from_sat_per_vb_unchecked(1); + + /// Fee rate used to compute dust amount. + pub const DUST: FeeRate = FeeRate::from_sat_per_vb_unchecked(3); + + /// Constructs `FeeRate` from satoshis per 1000 weight units. + pub const fn from_sat_per_kwu(sat_kwu: u64) -> Self { + FeeRate(sat_kwu) + } + + /// Constructs `FeeRate` from satoshis per virtual bytes. + /// + /// # Errors + /// + /// Returns `None` on arithmetic overflow. + pub fn from_sat_per_vb(sat_vb: u64) -> Option { + // 1 vb == 4 wu + // 1 sat/vb == 1/4 sat/wu + // sat_vb sat/vb * 1000 / 4 == sat/kwu + Some(FeeRate(sat_vb.checked_mul(1000 / 4)?)) + } + + /// Constructs `FeeRate` from satoshis per virtual bytes without overflow check. + pub const fn from_sat_per_vb_unchecked(sat_vb: u64) -> Self { + FeeRate(sat_vb * (1000 / 4)) + } + + /// Returns raw fee rate. + /// + /// Can be used instead of `into()` to avoid inference issues. + pub const fn to_sat_per_kwu(self) -> u64 { + self.0 + } + + /// Converts to sat/vB rounding down. + pub const fn to_sat_per_vb_floor(self) -> u64 { + self.0 / (1000 / 4) + } + + /// Converts to sat/vB rounding up. + pub const fn to_sat_per_vb_ceiling(self) -> u64 { + (self.0 + (1000 / 4 - 1)) / (1000 / 4) + } +} + +/// Alternative will display the unit. +impl fmt::Display for FeeRate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "{} sat/kwu", self.0) + } else { + fmt::Display::fmt(&self.0, f) + } + } +} + +impl From for u64 { + fn from(value: FeeRate) -> Self { + value.to_sat_per_kwu() + } +} + +impl Mul for FeeRate { + type Output = FeeRate; + + fn mul(self, rhs: u64) -> Self::Output { + FeeRate(self.0 * rhs) + } +} + +impl Mul for u64 { + type Output = FeeRate; + + fn mul(self, rhs: FeeRate) -> Self::Output { + FeeRate(self * rhs.0) + } +} + +/// Computes ceiling so that fee computation is conservative. +impl Mul for Weight { + type Output = Amount; + + fn mul(self, rhs: FeeRate) -> Self::Output { + Amount::from_sat((rhs.to_sat_per_kwu() * self.to_wu() + 999) / 1000) + } +} + +impl Mul for FeeRate { + type Output = Amount; + + fn mul(self, rhs: Weight) -> Self::Output { + rhs * self + } +} + +impl MulAssign for FeeRate { + fn mul_assign(&mut self, rhs: u64) { + self.0 *= rhs + } +} + +impl Div for FeeRate { + type Output = FeeRate; + + fn div(self, rhs: u64) -> Self::Output { + FeeRate(self.0 / rhs) + } +} + +impl DivAssign for FeeRate { + fn div_assign(&mut self, rhs: u64) { + self.0 /= rhs + } +} + +impl Div for Amount { + type Output = FeeRate; + + fn div(self, rhs: Weight) -> Self::Output { + FeeRate(self.to_sat() * 1000 / rhs.to_wu()) + } +} + +crate::parse::impl_parse_str_through_int!(FeeRate); diff --git a/bitcoin/src/blockdata/mod.rs b/bitcoin/src/blockdata/mod.rs index 2edb7883ce..ade1bf3417 100644 --- a/bitcoin/src/blockdata/mod.rs +++ b/bitcoin/src/blockdata/mod.rs @@ -14,3 +14,8 @@ pub mod script; pub mod transaction; pub mod block; pub mod witness; +pub mod weight; +pub mod fee_rate; + +pub use weight::Weight; +pub use fee_rate::FeeRate; diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs index 620c41d3fb..30fa08a57a 100644 --- a/bitcoin/src/blockdata/transaction.rs +++ b/bitcoin/src/blockdata/transaction.rs @@ -34,6 +34,7 @@ use crate::hash_types::{Sighash, Txid, Wtxid}; use crate::VarInt; use crate::internal_macros::impl_consensus_encoding; use crate::parse::impl_parse_str_through_int; +use super::Weight; #[cfg(doc)] use crate::sighash::{EcdsaSighashType, SchnorrSighashType}; @@ -839,8 +840,8 @@ impl Transaction { /// API. The unsigned transaction encoded within PSBT is always a non-segwit transaction /// and can therefore avoid this ambiguity. #[inline] - pub fn weight(&self) -> usize { - self.scaled_size(WITNESS_SCALE_FACTOR) + pub fn weight(&self) -> Weight { + Weight::from_wu(self.scaled_size(WITNESS_SCALE_FACTOR) as u64) } /// Returns the regular byte-wise consensus-serialized size of this transaction. @@ -860,8 +861,8 @@ impl Transaction { /// [`policy`]: ../policy/mod.rs.html #[inline] pub fn vsize(&self) -> usize { - let weight = self.weight(); - (weight + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR + // No overflow because it's computed from data in memory + self.weight().to_vbytes_ceiling() as usize } /// Returns the size of this transaction excluding the witness data. @@ -1259,7 +1260,7 @@ mod tests { "a6eab3c14ab5272a58a5ba91505ba1a4b6d7a3a9fcbd187b6cd99a7b6d548cb7".to_string()); assert_eq!(format!("{:x}", realtx.wtxid()), "a6eab3c14ab5272a58a5ba91505ba1a4b6d7a3a9fcbd187b6cd99a7b6d548cb7".to_string()); - assert_eq!(realtx.weight(), tx_bytes.len()*WITNESS_SCALE_FACTOR); + assert_eq!(realtx.weight().to_wu() as usize, tx_bytes.len()*WITNESS_SCALE_FACTOR); assert_eq!(realtx.size(), tx_bytes.len()); assert_eq!(realtx.vsize(), tx_bytes.len()); assert_eq!(realtx.strippedsize(), tx_bytes.len()); @@ -1293,7 +1294,7 @@ mod tests { "f5864806e3565c34d1b41e716f72609d00b55ea5eac5b924c9719a842ef42206".to_string()); assert_eq!(format!("{:x}", realtx.wtxid()), "80b7d8a82d5d5bf92905b06f2014dd699e03837ca172e3a59d51426ebbe3e7f5".to_string()); - const EXPECTED_WEIGHT: usize = 442; + const EXPECTED_WEIGHT: Weight = Weight::from_wu(442); assert_eq!(realtx.weight(), EXPECTED_WEIGHT); assert_eq!(realtx.size(), tx_bytes.len()); assert_eq!(realtx.vsize(), 111); @@ -1302,12 +1303,12 @@ mod tests { // weight = WITNESS_SCALE_FACTOR * stripped_size + witness_size // then, // stripped_size = (weight - size) / (WITNESS_SCALE_FACTOR - 1) - let expected_strippedsize = (EXPECTED_WEIGHT - tx_bytes.len()) / (WITNESS_SCALE_FACTOR - 1); + let expected_strippedsize = (EXPECTED_WEIGHT.to_wu() as usize - tx_bytes.len()) / (WITNESS_SCALE_FACTOR - 1); assert_eq!(realtx.strippedsize(), expected_strippedsize); // Construct a transaction without the witness data. let mut tx_without_witness = realtx; tx_without_witness.input.iter_mut().for_each(|input| input.witness.clear()); - assert_eq!(tx_without_witness.weight(), expected_strippedsize*WITNESS_SCALE_FACTOR); + assert_eq!(tx_without_witness.weight().to_wu() as usize, expected_strippedsize*WITNESS_SCALE_FACTOR); assert_eq!(tx_without_witness.size(), expected_strippedsize); assert_eq!(tx_without_witness.vsize(), expected_strippedsize); assert_eq!(tx_without_witness.strippedsize(), expected_strippedsize); @@ -1412,7 +1413,7 @@ mod tests { assert_eq!(format!("{:x}", tx.wtxid()), "d6ac4a5e61657c4c604dcde855a1db74ec6b3e54f32695d72c5e11c7761ea1b4"); assert_eq!(format!("{:x}", tx.txid()), "9652aa62b0e748caeec40c4cb7bc17c6792435cc3dfe447dd1ca24f912a1c6ec"); - assert_eq!(tx.weight(), 2718); + assert_eq!(tx.weight(), Weight::from_wu(2718)); // non-segwit tx from my mempool let tx_bytes = hex!( @@ -1444,7 +1445,7 @@ mod tests { fn test_segwit_tx_decode() { let tx_bytes = hex!("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff3603da1b0e00045503bd5704c7dd8a0d0ced13bb5785010800000000000a636b706f6f6c122f4e696e6a61506f6f6c2f5345475749542fffffffff02b4e5a212000000001976a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac0000000000000000266a24aa21a9edf91c46b49eb8a29089980f02ee6b57e7d63d33b18b4fddac2bcd7db2a39837040120000000000000000000000000000000000000000000000000000000000000000000000000"); let tx: Transaction = deserialize(&tx_bytes).unwrap(); - assert_eq!(tx.weight(), 780); + assert_eq!(tx.weight(), Weight::from_wu(780)); serde_round_trip!(tx); let consensus_encoded = serialize(&tx); @@ -1603,7 +1604,7 @@ mod tests { (false, "0100000001c336895d9fa674f8b1e294fd006b1ac8266939161600e04788c515089991b50a030000006a47304402204213769e823984b31dcb7104f2c99279e74249eacd4246dabcf2575f85b365aa02200c3ee89c84344ae326b637101a92448664a8d39a009c8ad5d147c752cbe112970121028b1b44b4903c9103c07d5a23e3c7cf7aeb0ba45ddbd2cfdce469ab197381f195fdffffff040000000000000000536a4c5058325bb7b7251cf9e36cac35d691bd37431eeea426d42cbdecca4db20794f9a4030e6cb5211fabf887642bcad98c9994430facb712da8ae5e12c9ae5ff314127d33665000bb26c0067000bb0bf00322a50c300000000000017a9145ca04fdc0a6d2f4e3f67cfeb97e438bb6287725f8750c30000000000001976a91423086a767de0143523e818d4273ddfe6d9e4bbcc88acc8465003000000001976a914c95cbacc416f757c65c942f9b6b8a20038b9b12988ac00000000"), ]; - let empty_transaction_size = Transaction { + let empty_transaction_weight = Transaction { version: 0, lock_time: absolute::LockTime::ZERO, input: vec![], @@ -1620,12 +1621,12 @@ mod tests { let tx: Transaction = deserialize(Vec::from_hex(tx).unwrap().as_slice()).unwrap(); // The empty tx size doesn't include the segwit marker (`0001`), so, in case of segwit txs, // we have to manually add it ourselves - let segwit_marker_size = if *is_segwit { 2 } else { 0 }; - let calculated_size = empty_transaction_size - + segwit_marker_size + let segwit_marker_weight = if *is_segwit { 2 } else { 0 }; + let calculated_size = empty_transaction_weight.to_wu() as usize + + segwit_marker_weight + tx.input.iter().fold(0, |sum, i| sum + txin_weight(i)) + tx.output.iter().fold(0, |sum, o| sum + o.weight()); - assert_eq!(calculated_size, tx.weight()); + assert_eq!(calculated_size, tx.weight().to_wu() as usize); } } } diff --git a/bitcoin/src/blockdata/weight.rs b/bitcoin/src/blockdata/weight.rs new file mode 100644 index 0000000000..2b12698d46 --- /dev/null +++ b/bitcoin/src/blockdata/weight.rs @@ -0,0 +1,156 @@ +//! Implements `Weight` and associated features. + +use core::fmt; +use core::ops::{Add, AddAssign, Sub, SubAssign, Mul, MulAssign, Div, DivAssign}; + +/// Represents block weight - the weight of a transaction or block. +/// +/// This is an integer newtype representing weigth in `wu`. It provides protection against mixing +/// up the types as well as basic formatting features. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct Weight(u64); + +impl Weight { + /// 0 wu. + /// + /// Equivalent to [`MIN`], may better express intent in some contexts. + pub const ZERO: Weight = Weight(0); + + /// Minimum possible value (0 wu). + /// + /// Equivalent to [`ZERO`], may better express intent in some contexts. + pub const MIN: Weight = Weight(u64::MIN); + + /// Maximum possible value. + pub const MAX: Weight = Weight(u64::MAX); + + /// Directly constructs `Weight` from weight units. + pub const fn from_wu(wu: u64) -> Self { + Weight(wu) + } + + /// Constructs `Weight` from witness size. + pub const fn from_witness_data_size(witness_size: u64) -> Self { + Weight(witness_size) + } + + /// Constructs `Weight` from non-witness size. + pub const fn from_non_witness_data_size(non_witness_size: u64) -> Self { + Weight(non_witness_size * 4) + } + + /// Returns raw weight units. + /// + /// Can be used instead of `into()` to avoid inference issues. + pub const fn to_wu(self) -> u64 { + self.0 + } + + /// Converts to vB rounding down. + pub const fn to_vbytes_floor(self) -> u64 { + self.0 / 4 + } + + /// Converts to vB rounding up. + pub const fn to_vbytes_ceiling(self) -> u64 { + (self.0 + 3) / 4 + } +} + +/// Alternative will display the unit. +impl fmt::Display for Weight { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "{} wu", self.0) + } else { + fmt::Display::fmt(&self.0, f) + } + } +} + +impl From for u64 { + fn from(value: Weight) -> Self { + value.to_wu() + } +} + +impl Add for Weight { + type Output = Weight; + + fn add(self, rhs: Weight) -> Self::Output { + Weight(self.0 + rhs.0) + } +} + +impl AddAssign for Weight { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0 + } +} + +impl Sub for Weight { + type Output = Weight; + + fn sub(self, rhs: Weight) -> Self::Output { + Weight(self.0 - rhs.0) + } +} + +impl SubAssign for Weight { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0 + } +} + +impl Mul for Weight { + type Output = Weight; + + fn mul(self, rhs: u64) -> Self::Output { + Weight(self.0 * rhs) + } +} + +impl Mul for u64 { + type Output = Weight; + + fn mul(self, rhs: Weight) -> Self::Output { + Weight(self * rhs.0) + } +} + +impl MulAssign for Weight { + fn mul_assign(&mut self, rhs: u64) { + self.0 *= rhs + } +} + +impl Div for Weight { + type Output = Weight; + + fn div(self, rhs: u64) -> Self::Output { + Weight(self.0 / rhs) + } +} + +impl DivAssign for Weight { + fn div_assign(&mut self, rhs: u64) { + self.0 /= rhs + } +} + +impl core::iter::Sum for Weight { + fn sum(iter: I) -> Self where I: Iterator { + Weight(iter.map(Weight::to_wu).sum()) + } +} + +impl<'a> core::iter::Sum<&'a Weight> for Weight { + fn sum(iter: I) -> Self where I: Iterator { + iter.cloned().sum() + } +} + +crate::parse::impl_parse_str_through_int!(Weight);