Skip to content

Commit

Permalink
Add Weight and FeeRate newtypes
Browse files Browse the repository at this point in the history
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 rust-bitcoin#630
  • Loading branch information
Kixunil committed Feb 7, 2023
1 parent 69688b6 commit 28d488f
Show file tree
Hide file tree
Showing 7 changed files with 399 additions and 23 deletions.
2 changes: 1 addition & 1 deletion bitcoin/fuzz/fuzz_targets/deserialize_transaction.rs
Expand Up @@ -8,7 +8,7 @@ fn do_test(data: &[u8]) {
let ser = bitcoin::consensus::encode::serialize(&tx);
assert_eq!(&ser[..], data);
let len = ser.len();
let calculated_weight = tx.weight();
let calculated_weight = tx.weight().to_wu() as usize;
for input in &mut tx.input {
input.witness = bitcoin::blockdata::witness::Witness::default();
}
Expand Down
12 changes: 6 additions & 6 deletions bitcoin/src/blockdata/block.rs
Expand Up @@ -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;

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());

Expand Down
172 changes: 172 additions & 0 deletions bitcoin/src/blockdata/fee_rate.rs
@@ -0,0 +1,172 @@
//! 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`](Self::MIN), may better express intent in some contexts.
pub const ZERO: FeeRate = FeeRate(0);

/// Minimum possible value (0 sat/kwu).
///
/// Equivalent to [`ZERO`](Self::ZERO), may better express intent in some contexts.
pub const MIN: FeeRate = FeeRate(u64::min_value());

/// Maximum possible value.
pub const MAX: FeeRate = FeeRate(u64::max_value());

/// 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<Self> {
// 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_ceil(self) -> u64 {
(self.0 + (1000 / 4 - 1)) / (1000 / 4)
}

/// Checked multiplication.
///
/// Computes `self * rhs` returning `None` if overflow occurred.
pub fn checked_mul(self, rhs: u64) -> Option<Self> {
self.0.checked_mul(rhs).map(Self)
}

/// Checked division.
///
/// Computes `self / rhs` returning `None` if `rhs == 0`.
pub fn checked_div(self, rhs: u64) -> Option<Self> {
self.0.checked_div(rhs).map(Self)
}
}

/// 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<FeeRate> for u64 {
fn from(value: FeeRate) -> Self {
value.to_sat_per_kwu()
}
}

impl Mul<u64> for FeeRate {
type Output = FeeRate;

fn mul(self, rhs: u64) -> Self::Output {
FeeRate(self.0 * rhs)
}
}

impl Mul<FeeRate> 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<FeeRate> 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<Weight> for FeeRate {
type Output = Amount;

fn mul(self, rhs: Weight) -> Self::Output {
rhs * self
}
}

impl MulAssign<u64> for FeeRate {
fn mul_assign(&mut self, rhs: u64) {
self.0 *= rhs
}
}

impl Div<u64> for FeeRate {
type Output = FeeRate;

fn div(self, rhs: u64) -> Self::Output {
FeeRate(self.0 / rhs)
}
}

impl DivAssign<u64> for FeeRate {
fn div_assign(&mut self, rhs: u64) {
self.0 /= rhs
}
}

impl Div<Weight> 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);
5 changes: 5 additions & 0 deletions bitcoin/src/blockdata/mod.rs
Expand Up @@ -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;
31 changes: 16 additions & 15 deletions bitcoin/src/blockdata/transaction.rs
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
Expand All @@ -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_ceil() as usize
}

/// Returns the size of this transaction excluding the witness data.
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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![],
Expand All @@ -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);
}
}
}
Expand Down

0 comments on commit 28d488f

Please sign in to comment.