mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-03 09:29:46 +00:00
Apply bitcoin fee per vsize, not per weight unit
This enables more precision.
This commit is contained in:
parent
8e7e61adbd
commit
9955ef54a5
3 changed files with 38 additions and 36 deletions
|
@ -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 });
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue