Apply bitcoin fee per vsize, not per weight unit

This enables more precision.
This commit is contained in:
Luke Parker 2024-07-15 18:35:49 -04:00
parent 8e7e61adbd
commit 9955ef54a5
3 changed files with 38 additions and 36 deletions

View file

@ -61,11 +61,11 @@ pub struct SignableTransaction {
} }
impl SignableTransaction { impl SignableTransaction {
fn calculate_weight( fn calculate_weight_vbytes(
inputs: usize, inputs: usize,
payments: &[(ScriptBuf, u64)], payments: &[(ScriptBuf, u64)],
change: Option<&ScriptBuf>, change: Option<&ScriptBuf>,
) -> u64 { ) -> (u64, u64) {
// Expand this a full transaction in order to use the bitcoin library's weight function // Expand this a full transaction in order to use the bitcoin library's weight function
let mut tx = Transaction { let mut tx = Transaction {
version: Version(2), version: Version(2),
@ -99,7 +99,33 @@ impl SignableTransaction {
// the value is fixed size (so any value could be used here) // the value is fixed size (so any value could be used here)
tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change.clone() }); tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change.clone() });
} }
u64::from(tx.weight())
let weight = tx.weight();
// Now calculate the size in vbytes
/*
"Virtual transaction size" is weight ceildiv 4 per
https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04
/src/policy/policy.cpp#L295-L298
implements this almost as expected, with an additional consideration to signature operations
Signature operations (the second argument of the following call) do not count Taproot
signatures per https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#cite_ref-11-0
We don't risk running afoul of the Taproot signature limit as it allows at least one per
input, which is all we use
*/
(
weight.to_wu(),
u64::try_from(bitcoin::policy::get_virtual_tx_size(
i64::try_from(weight.to_wu()).unwrap(),
0i64,
))
.unwrap(),
)
} }
/// Returns the fee necessary for this transaction to achieve the fee rate specified at /// Returns the fee necessary for this transaction to achieve the fee rate specified at
@ -128,7 +154,7 @@ impl SignableTransaction {
payments: &[(ScriptBuf, u64)], payments: &[(ScriptBuf, u64)],
change: Option<ScriptBuf>, change: Option<ScriptBuf>,
data: Option<Vec<u8>>, data: Option<Vec<u8>>,
fee_per_weight: u64, fee_per_vbyte: u64,
) -> Result<SignableTransaction, TransactionError> { ) -> Result<SignableTransaction, TransactionError> {
if inputs.is_empty() { if inputs.is_empty() {
Err(TransactionError::NoInputs)?; Err(TransactionError::NoInputs)?;
@ -177,34 +203,14 @@ impl SignableTransaction {
}) })
} }
let mut weight = Self::calculate_weight(tx_ins.len(), payments, None); let (mut weight, vbytes) = Self::calculate_weight_vbytes(tx_ins.len(), payments, None);
let mut needed_fee = fee_per_weight * weight;
// "Virtual transaction size" is weight ceildiv 4 per let mut needed_fee = fee_per_vbyte * vbytes;
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/
// src/policy/policy.cpp#L295-L298
// implements this as expected
// Technically, it takes whatever's greater, the weight or the amount of signature operations
// multiplied by DEFAULT_BYTES_PER_SIGOP (20)
// We only use 1 signature per input, and our inputs have a weight exceeding 20
// Accordingly, our inputs' weight will always be greater than the cost of the signature ops
let vsize = weight.div_ceil(4);
debug_assert_eq!(
u64::try_from(bitcoin::policy::get_virtual_tx_size(
weight.try_into().unwrap(),
tx_ins.len().try_into().unwrap()
))
.unwrap(),
vsize
);
// Technically, if there isn't change, this TX may still pay enough of a fee to pass the // Technically, if there isn't change, this TX may still pay enough of a fee to pass the
// minimum fee. Such edge cases aren't worth programming when they go against intent, as the // minimum fee. Such edge cases aren't worth programming when they go against intent, as the
// specified fee rate is too low to be valid // specified fee rate is too low to be valid
// bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE is in sats/kilo-vbyte // bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE is in sats/kilo-vbyte
if needed_fee < ((u64::from(bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE) * vsize) / 1000) { if needed_fee < ((u64::from(bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE) * vbytes) / 1000) {
Err(TransactionError::TooLowFee)?; Err(TransactionError::TooLowFee)?;
} }
@ -214,8 +220,9 @@ impl SignableTransaction {
// If there's a change address, check if there's change to give it // If there's a change address, check if there's change to give it
if let Some(change) = change { if let Some(change) = change {
let weight_with_change = Self::calculate_weight(tx_ins.len(), payments, Some(&change)); let (weight_with_change, vbytes_with_change) =
let fee_with_change = fee_per_weight * weight_with_change; Self::calculate_weight_vbytes(tx_ins.len(), payments, Some(&change));
let fee_with_change = fee_per_vbyte * vbytes_with_change;
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) { if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
if value >= DUST { if value >= DUST {
tx_outs.push(TxOut { value: Amount::from_sat(value), script_pubkey: change }); tx_outs.push(TxOut { value: Amount::from_sat(value), script_pubkey: change });

View file

@ -303,7 +303,7 @@ async_sequential! {
} }
// Make sure the change is correct // Make sure the change is correct
assert_eq!(needed_fee, u64::from(tx.weight()) * FEE); assert_eq!(needed_fee, u64::try_from(tx.vsize()).unwrap() * FEE);
let input_value = output.value() + offset_output.value(); let input_value = output.value() + offset_output.value();
let output_value = tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>(); let output_value = tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>();
assert_eq!(input_value - output_value, needed_fee); assert_eq!(input_value - output_value, needed_fee);

View file

@ -405,7 +405,7 @@ impl Bitcoin {
.to_sat(); .to_sat();
} }
let out = tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>(); let out = tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>();
fees.push((in_value - out) / tx.weight().to_wu()); fees.push((in_value - out) / u64::try_from(tx.vsize()).unwrap());
} }
} }
fees.sort(); fees.sort();
@ -413,11 +413,6 @@ impl Bitcoin {
// The DUST constant documentation notes a relay rule practically enforcing a // The DUST constant documentation notes a relay rule practically enforcing a
// 1000 sat/kilo-vbyte minimum fee. // 1000 sat/kilo-vbyte minimum fee.
//
// 1000 sat/kilo-vbyte is 1000 sat/4-kilo-weight (250 sat/kilo-weight).
// Since bitcoin-serai takes fee per weight, we'd need to pass 0.25 to achieve this fee rate.
// Accordingly, setting 1 is 4x the current relay rule minimum (and should be more than safe).
// TODO: Rewrite to fee_per_vbyte, not fee_per_weight?
Ok(Fee(fee.max(1))) Ok(Fee(fee.max(1)))
} }