Skip to content

Commit 70cf451

Browse files
committed
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
1 parent 69688b6 commit 70cf451

7 files changed

Lines changed: 363 additions & 23 deletions

File tree

bitcoin/fuzz/fuzz_targets/deserialize_transaction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ fn do_test(data: &[u8]) {
88
let ser = bitcoin::consensus::encode::serialize(&tx);
99
assert_eq!(&ser[..], data);
1010
let len = ser.len();
11-
let calculated_weight = tx.weight();
11+
let calculated_weight = tx.weight().to_wu() as usize;
1212
for input in &mut tx.input {
1313
input.witness = bitcoin::blockdata::witness::Witness::default();
1414
}

bitcoin/src/blockdata/block.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ use crate::hashes::{Hash, HashEngine};
1919
use crate::hash_types::{Wtxid, TxMerkleNode, WitnessMerkleNode, WitnessCommitment};
2020
use crate::consensus::{encode, Encodable, Decodable};
2121
use crate::blockdata::transaction::Transaction;
22-
use crate::blockdata::constants::WITNESS_SCALE_FACTOR;
2322
use crate::blockdata::script;
2423
use crate::pow::{CompactTarget, Target, Work};
2524
use crate::VarInt;
2625
use crate::internal_macros::impl_consensus_encoding;
2726
use crate::io;
27+
use super::Weight;
2828

2929
pub use crate::hash_types::BlockHash;
3030

@@ -302,9 +302,9 @@ impl Block {
302302
}
303303

304304
/// Returns the weight of the block.
305-
pub fn weight(&self) -> usize {
306-
let base_weight = WITNESS_SCALE_FACTOR * self.base_size();
307-
let txs_weight: usize = self.txdata.iter().map(Transaction::weight).sum();
305+
pub fn weight(&self) -> Weight {
306+
let base_weight = Weight::from_non_witness_data_size(self.base_size() as u64);
307+
let txs_weight: Weight = self.txdata.iter().map(Transaction::weight).sum();
308308
base_weight + txs_weight
309309
}
310310

@@ -470,7 +470,7 @@ mod tests {
470470

471471
assert_eq!(real_decode.size(), some_block.len());
472472
assert_eq!(real_decode.strippedsize(), some_block.len());
473-
assert_eq!(real_decode.weight(), some_block.len() * 4);
473+
assert_eq!(real_decode.weight(), Weight::from_non_witness_data_size(some_block.len() as u64));
474474

475475
// should be also ok for a non-witness block as commitment is optional in that case
476476
assert!(real_decode.check_witness_commitment());
@@ -505,7 +505,7 @@ mod tests {
505505

506506
assert_eq!(real_decode.size(), segwit_block.len());
507507
assert_eq!(real_decode.strippedsize(), 4283);
508-
assert_eq!(real_decode.weight(), 17168);
508+
assert_eq!(real_decode.weight(), Weight::from_wu(17168));
509509

510510
assert!(real_decode.check_witness_commitment());
511511

bitcoin/src/blockdata/fee_rate.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//! Implements `FeeRate` and assoctiated features.
2+
3+
use core::fmt;
4+
use core::ops::{Mul, Div};
5+
use crate::Amount;
6+
use super::Weight;
7+
8+
/// Represents fee rate.
9+
///
10+
/// This is an integer newtype representing fee rate in `sat/kwu`. It provides protection against mixing
11+
/// up the types as well as basic formatting features.
12+
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
13+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14+
#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
15+
#[cfg_attr(feature = "serde", serde(transparent))]
16+
pub struct FeeRate(u64);
17+
18+
impl FeeRate {
19+
/// 0 sat/kwu.
20+
///
21+
/// Equivalent to [`MIN`](Self::MIN), may better express intent in some contexts.
22+
pub const ZERO: FeeRate = FeeRate(0);
23+
24+
/// Minimum possible value (0 sat/kwu).
25+
///
26+
/// Equivalent to [`ZERO`](Self::ZERO), may better express intent in some contexts.
27+
pub const MIN: FeeRate = FeeRate(u64::min_value());
28+
29+
/// Maximum possible value.
30+
pub const MAX: FeeRate = FeeRate(u64::max_value());
31+
32+
/// Minimum fee rate required to broadcast a transaction.
33+
///
34+
/// The value matches the default Bitcoin Core policy at the time of library release.
35+
pub const BROADCAST_MIN: FeeRate = FeeRate::from_sat_per_vb_unchecked(1);
36+
37+
/// Fee rate used to compute dust amount.
38+
pub const DUST: FeeRate = FeeRate::from_sat_per_vb_unchecked(3);
39+
40+
/// Constructs `FeeRate` from satoshis per 1000 weight units.
41+
pub const fn from_sat_per_kwu(sat_kwu: u64) -> Self {
42+
FeeRate(sat_kwu)
43+
}
44+
45+
/// Constructs `FeeRate` from satoshis per virtual bytes.
46+
///
47+
/// # Errors
48+
///
49+
/// Returns `None` on arithmetic overflow.
50+
pub fn from_sat_per_vb(sat_vb: u64) -> Option<Self> {
51+
// 1 vb == 4 wu
52+
// 1 sat/vb == 1/4 sat/wu
53+
// sat_vb sat/vb * 1000 / 4 == sat/kwu
54+
Some(FeeRate(sat_vb.checked_mul(1000 / 4)?))
55+
}
56+
57+
/// Constructs `FeeRate` from satoshis per virtual bytes without overflow check.
58+
pub const fn from_sat_per_vb_unchecked(sat_vb: u64) -> Self {
59+
FeeRate(sat_vb * (1000 / 4))
60+
}
61+
62+
/// Returns raw fee rate.
63+
///
64+
/// Can be used instead of `into()` to avoid inference issues.
65+
pub const fn to_sat_per_kwu(self) -> u64 {
66+
self.0
67+
}
68+
69+
/// Converts to sat/vB rounding down.
70+
pub const fn to_sat_per_vb_floor(self) -> u64 {
71+
self.0 / (1000 / 4)
72+
}
73+
74+
/// Converts to sat/vB rounding up.
75+
pub const fn to_sat_per_vb_ceil(self) -> u64 {
76+
(self.0 + (1000 / 4 - 1)) / (1000 / 4)
77+
}
78+
79+
/// Checked multiplication.
80+
///
81+
/// Computes `self * rhs` returning `None` if overflow occurred.
82+
pub fn checked_mul(self, rhs: u64) -> Option<Self> {
83+
self.0.checked_mul(rhs).map(Self)
84+
}
85+
86+
/// Checked division.
87+
///
88+
/// Computes `self / rhs` returning `None` if `rhs == 0`.
89+
pub fn checked_div(self, rhs: u64) -> Option<Self> {
90+
self.0.checked_div(rhs).map(Self)
91+
}
92+
}
93+
94+
/// Alternative will display the unit.
95+
impl fmt::Display for FeeRate {
96+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97+
if f.alternate() {
98+
write!(f, "{} sat/kwu", self.0)
99+
} else {
100+
fmt::Display::fmt(&self.0, f)
101+
}
102+
}
103+
}
104+
105+
impl From<FeeRate> for u64 {
106+
fn from(value: FeeRate) -> Self {
107+
value.to_sat_per_kwu()
108+
}
109+
}
110+
111+
/// Computes ceiling so that fee computation is conservative.
112+
impl Mul<FeeRate> for Weight {
113+
type Output = Amount;
114+
115+
fn mul(self, rhs: FeeRate) -> Self::Output {
116+
Amount::from_sat((rhs.to_sat_per_kwu() * self.to_wu() + 999) / 1000)
117+
}
118+
}
119+
120+
impl Mul<Weight> for FeeRate {
121+
type Output = Amount;
122+
123+
fn mul(self, rhs: Weight) -> Self::Output {
124+
rhs * self
125+
}
126+
}
127+
128+
impl Div<Weight> for Amount {
129+
type Output = FeeRate;
130+
131+
fn div(self, rhs: Weight) -> Self::Output {
132+
FeeRate(self.to_sat() * 1000 / rhs.to_wu())
133+
}
134+
}
135+
136+
crate::parse::impl_parse_str_through_int!(FeeRate);

bitcoin/src/blockdata/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ pub mod script;
1414
pub mod transaction;
1515
pub mod block;
1616
pub mod witness;
17+
pub mod weight;
18+
pub mod fee_rate;
19+
20+
pub use weight::Weight;
21+
pub use fee_rate::FeeRate;

bitcoin/src/blockdata/transaction.rs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use crate::hash_types::{Sighash, Txid, Wtxid};
3434
use crate::VarInt;
3535
use crate::internal_macros::impl_consensus_encoding;
3636
use crate::parse::impl_parse_str_through_int;
37+
use super::Weight;
3738

3839
#[cfg(doc)]
3940
use crate::sighash::{EcdsaSighashType, SchnorrSighashType};
@@ -839,8 +840,8 @@ impl Transaction {
839840
/// API. The unsigned transaction encoded within PSBT is always a non-segwit transaction
840841
/// and can therefore avoid this ambiguity.
841842
#[inline]
842-
pub fn weight(&self) -> usize {
843-
self.scaled_size(WITNESS_SCALE_FACTOR)
843+
pub fn weight(&self) -> Weight {
844+
Weight::from_wu(self.scaled_size(WITNESS_SCALE_FACTOR) as u64)
844845
}
845846

846847
/// Returns the regular byte-wise consensus-serialized size of this transaction.
@@ -860,8 +861,8 @@ impl Transaction {
860861
/// [`policy`]: ../policy/mod.rs.html
861862
#[inline]
862863
pub fn vsize(&self) -> usize {
863-
let weight = self.weight();
864-
(weight + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR
864+
// No overflow because it's computed from data in memory
865+
self.weight().to_vbytes_ceil() as usize
865866
}
866867

867868
/// Returns the size of this transaction excluding the witness data.
@@ -1259,7 +1260,7 @@ mod tests {
12591260
"a6eab3c14ab5272a58a5ba91505ba1a4b6d7a3a9fcbd187b6cd99a7b6d548cb7".to_string());
12601261
assert_eq!(format!("{:x}", realtx.wtxid()),
12611262
"a6eab3c14ab5272a58a5ba91505ba1a4b6d7a3a9fcbd187b6cd99a7b6d548cb7".to_string());
1262-
assert_eq!(realtx.weight(), tx_bytes.len()*WITNESS_SCALE_FACTOR);
1263+
assert_eq!(realtx.weight().to_wu() as usize, tx_bytes.len()*WITNESS_SCALE_FACTOR);
12631264
assert_eq!(realtx.size(), tx_bytes.len());
12641265
assert_eq!(realtx.vsize(), tx_bytes.len());
12651266
assert_eq!(realtx.strippedsize(), tx_bytes.len());
@@ -1293,7 +1294,7 @@ mod tests {
12931294
"f5864806e3565c34d1b41e716f72609d00b55ea5eac5b924c9719a842ef42206".to_string());
12941295
assert_eq!(format!("{:x}", realtx.wtxid()),
12951296
"80b7d8a82d5d5bf92905b06f2014dd699e03837ca172e3a59d51426ebbe3e7f5".to_string());
1296-
const EXPECTED_WEIGHT: usize = 442;
1297+
const EXPECTED_WEIGHT: Weight = Weight::from_wu(442);
12971298
assert_eq!(realtx.weight(), EXPECTED_WEIGHT);
12981299
assert_eq!(realtx.size(), tx_bytes.len());
12991300
assert_eq!(realtx.vsize(), 111);
@@ -1302,12 +1303,12 @@ mod tests {
13021303
// weight = WITNESS_SCALE_FACTOR * stripped_size + witness_size
13031304
// then,
13041305
// stripped_size = (weight - size) / (WITNESS_SCALE_FACTOR - 1)
1305-
let expected_strippedsize = (EXPECTED_WEIGHT - tx_bytes.len()) / (WITNESS_SCALE_FACTOR - 1);
1306+
let expected_strippedsize = (EXPECTED_WEIGHT.to_wu() as usize - tx_bytes.len()) / (WITNESS_SCALE_FACTOR - 1);
13061307
assert_eq!(realtx.strippedsize(), expected_strippedsize);
13071308
// Construct a transaction without the witness data.
13081309
let mut tx_without_witness = realtx;
13091310
tx_without_witness.input.iter_mut().for_each(|input| input.witness.clear());
1310-
assert_eq!(tx_without_witness.weight(), expected_strippedsize*WITNESS_SCALE_FACTOR);
1311+
assert_eq!(tx_without_witness.weight().to_wu() as usize, expected_strippedsize*WITNESS_SCALE_FACTOR);
13111312
assert_eq!(tx_without_witness.size(), expected_strippedsize);
13121313
assert_eq!(tx_without_witness.vsize(), expected_strippedsize);
13131314
assert_eq!(tx_without_witness.strippedsize(), expected_strippedsize);
@@ -1412,7 +1413,7 @@ mod tests {
14121413

14131414
assert_eq!(format!("{:x}", tx.wtxid()), "d6ac4a5e61657c4c604dcde855a1db74ec6b3e54f32695d72c5e11c7761ea1b4");
14141415
assert_eq!(format!("{:x}", tx.txid()), "9652aa62b0e748caeec40c4cb7bc17c6792435cc3dfe447dd1ca24f912a1c6ec");
1415-
assert_eq!(tx.weight(), 2718);
1416+
assert_eq!(tx.weight(), Weight::from_wu(2718));
14161417

14171418
// non-segwit tx from my mempool
14181419
let tx_bytes = hex!(
@@ -1444,7 +1445,7 @@ mod tests {
14441445
fn test_segwit_tx_decode() {
14451446
let tx_bytes = hex!("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff3603da1b0e00045503bd5704c7dd8a0d0ced13bb5785010800000000000a636b706f6f6c122f4e696e6a61506f6f6c2f5345475749542fffffffff02b4e5a212000000001976a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac0000000000000000266a24aa21a9edf91c46b49eb8a29089980f02ee6b57e7d63d33b18b4fddac2bcd7db2a39837040120000000000000000000000000000000000000000000000000000000000000000000000000");
14461447
let tx: Transaction = deserialize(&tx_bytes).unwrap();
1447-
assert_eq!(tx.weight(), 780);
1448+
assert_eq!(tx.weight(), Weight::from_wu(780));
14481449
serde_round_trip!(tx);
14491450

14501451
let consensus_encoded = serialize(&tx);
@@ -1603,7 +1604,7 @@ mod tests {
16031604
(false, "0100000001c336895d9fa674f8b1e294fd006b1ac8266939161600e04788c515089991b50a030000006a47304402204213769e823984b31dcb7104f2c99279e74249eacd4246dabcf2575f85b365aa02200c3ee89c84344ae326b637101a92448664a8d39a009c8ad5d147c752cbe112970121028b1b44b4903c9103c07d5a23e3c7cf7aeb0ba45ddbd2cfdce469ab197381f195fdffffff040000000000000000536a4c5058325bb7b7251cf9e36cac35d691bd37431eeea426d42cbdecca4db20794f9a4030e6cb5211fabf887642bcad98c9994430facb712da8ae5e12c9ae5ff314127d33665000bb26c0067000bb0bf00322a50c300000000000017a9145ca04fdc0a6d2f4e3f67cfeb97e438bb6287725f8750c30000000000001976a91423086a767de0143523e818d4273ddfe6d9e4bbcc88acc8465003000000001976a914c95cbacc416f757c65c942f9b6b8a20038b9b12988ac00000000"),
16041605
];
16051606

1606-
let empty_transaction_size = Transaction {
1607+
let empty_transaction_weight = Transaction {
16071608
version: 0,
16081609
lock_time: absolute::LockTime::ZERO,
16091610
input: vec![],
@@ -1620,12 +1621,12 @@ mod tests {
16201621
let tx: Transaction = deserialize(Vec::from_hex(tx).unwrap().as_slice()).unwrap();
16211622
// The empty tx size doesn't include the segwit marker (`0001`), so, in case of segwit txs,
16221623
// we have to manually add it ourselves
1623-
let segwit_marker_size = if *is_segwit { 2 } else { 0 };
1624-
let calculated_size = empty_transaction_size
1625-
+ segwit_marker_size
1624+
let segwit_marker_weight = if *is_segwit { 2 } else { 0 };
1625+
let calculated_size = empty_transaction_weight.to_wu() as usize
1626+
+ segwit_marker_weight
16261627
+ tx.input.iter().fold(0, |sum, i| sum + txin_weight(i))
16271628
+ tx.output.iter().fold(0, |sum, o| sum + o.weight());
1628-
assert_eq!(calculated_size, tx.weight());
1629+
assert_eq!(calculated_size, tx.weight().to_wu() as usize);
16291630
}
16301631
}
16311632
}

0 commit comments

Comments
 (0)