Browse Source

Prevent rounding errors when calculating fees

Converting from f64 to u64 can make us lose precision.
Create a new Fee type that encapsulates calculating fees, includding splitting
it between two parties.
refactor/no-log-handler
Mariusz Klochowicz 3 years ago
parent
commit
319e0ba8dd
No known key found for this signature in database GPG Key ID: 470C865699C8D4D
  1. 120
      cfd_protocol/src/protocol/transactions.rs

120
cfd_protocol/src/protocol/transactions.rs

@ -87,7 +87,7 @@ pub(crate) struct CommitTransaction {
amount: Amount, amount: Amount,
sighash: SigHash, sighash: SigHash,
lock_descriptor: Descriptor<PublicKey>, lock_descriptor: Descriptor<PublicKey>,
fee: u64, fee: Fee,
} }
impl CommitTransaction { impl CommitTransaction {
@ -131,9 +131,9 @@ impl CommitTransaction {
input: vec![lock_input], input: vec![lock_input],
output: vec![output], output: vec![output],
}; };
let fee = (Self::SIGNED_VBYTES * SATS_PER_VBYTE as f64) as u64; let fee = Fee::new(Self::SIGNED_VBYTES);
let commit_tx_amount = lock_amount - fee as u64; let commit_tx_amount = lock_amount - fee.as_u64();
inner.output[0].value = commit_tx_amount; inner.output[0].value = commit_tx_amount;
let sighash = SigHashCache::new(&inner).signature_hash( let sighash = SigHashCache::new(&inner).signature_hash(
@ -185,7 +185,7 @@ impl CommitTransaction {
} }
fn fee(&self) -> u64 { fn fee(&self) -> u64 {
self.fee self.fee.as_u64()
} }
} }
@ -223,11 +223,10 @@ impl ContractExecutionTransaction {
..Default::default() ..Default::default()
}; };
let mut fee = Self::SIGNED_VBYTES * SATS_PER_VBYTE; let fee = Fee::new(Self::SIGNED_VBYTES).add(commit_tx.fee() as f64);
fee += commit_tx.fee() as f64;
let output = payout let output = payout
.with_updated_fee( .with_updated_fee(
Amount::from_sat(fee as u64), Amount::from_sat(fee.as_u64()),
maker_address.script_pubkey().dust_value(), maker_address.script_pubkey().dust_value(),
taker_address.script_pubkey().dust_value(), taker_address.script_pubkey().dust_value(),
)? )?
@ -372,15 +371,14 @@ pub fn close_transaction(
// TODO: The fee could take into account the network state in this // TODO: The fee could take into account the network state in this
// case, since this transaction is to be broadcast immediately // case, since this transaction is to be broadcast immediately
// after building and signing it // after building and signing it
let fee = SIGNED_VBYTES * SATS_PER_VBYTE as f64; let (maker_fee, taker_fee) = Fee::new(SIGNED_VBYTES).split();
let fee_per_party = (fee / 2.0) as u64;
let maker_output = TxOut { let maker_output = TxOut {
value: maker_amount.as_sat() - fee_per_party, value: maker_amount.as_sat() - maker_fee,
script_pubkey: maker_address.script_pubkey(), script_pubkey: maker_address.script_pubkey(),
}; };
let taker_output = TxOut { let taker_output = TxOut {
value: taker_amount.as_sat() - fee_per_party, value: taker_amount.as_sat() - taker_fee,
script_pubkey: taker_address.script_pubkey(), script_pubkey: taker_address.script_pubkey(),
}; };
@ -496,3 +494,103 @@ pub fn punish_transaction(
Ok(punish_tx) Ok(punish_tx)
} }
#[derive(Clone, Debug)]
struct Fee {
fee: f64,
}
impl Fee {
fn new(signed_vbytes: f64) -> Self {
let fee = signed_vbytes * SATS_PER_VBYTE;
Self { fee }
}
#[must_use]
fn add(self, number: f64) -> Fee {
Fee {
fee: self.fee + number,
}
}
fn as_u64(&self) -> u64 {
// Ceil to prevent going lower than the min relay fee
self.fee.ceil() as u64
}
fn split(&self) -> (u64, u64) {
let half = self.as_u64() / 2;
(half as u64, self.as_u64() - half)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn test_fee_always_above_min_relay_fee(signed_vbytes in 1.0f64..100_000_000.0f64) {
let fee = Fee::new(signed_vbytes);
let (maker_fee, taker_fee) = fee.split();
prop_assert!(signed_vbytes <= fee.as_u64() as f64);
prop_assert!(signed_vbytes <= (maker_fee + taker_fee) as f64);
}
}
// A bunch of tests illustrating how fees are split
#[test]
fn test_splitting_fee_1_0() {
const SIGNED_VBYTES_TEST: f64 = 1.0;
let fee = Fee::new(SIGNED_VBYTES_TEST);
let (maker_fee, taker_fee) = fee.split();
assert_eq!(fee.as_u64(), 1);
assert_eq!(maker_fee, 0);
assert_eq!(taker_fee, 1);
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST);
}
#[test]
fn test_splitting_fee_2_0() {
const SIGNED_VBYTES_TEST: f64 = 2.0;
let fee = Fee::new(SIGNED_VBYTES_TEST);
let (maker_fee, taker_fee) = fee.split();
assert_eq!(fee.as_u64(), 2);
assert_eq!(maker_fee, 1);
assert_eq!(taker_fee, 1);
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST);
}
#[test]
fn test_splitting_fee_2_1() {
const SIGNED_VBYTES_TEST: f64 = 2.1;
let fee = Fee::new(SIGNED_VBYTES_TEST);
let (maker_fee, taker_fee) = fee.split();
assert_eq!(fee.as_u64(), 3);
assert_eq!(maker_fee, 1);
assert_eq!(taker_fee, 2);
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST);
}
#[test]
fn test_splitting_fee_2_6() {
const SIGNED_VBYTES_TEST: f64 = 2.6;
let fee = Fee::new(SIGNED_VBYTES_TEST);
let (maker_fee, taker_fee) = fee.split();
assert_eq!(fee.as_u64(), 3);
assert_eq!(maker_fee, 1);
assert_eq!(taker_fee, 2);
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST);
}
}

Loading…
Cancel
Save