Skip to content

Commit

Permalink
Calculate expected DLC funding fees correctly
Browse files Browse the repository at this point in the history
Inspired by this patch[^1] in `master`.

Since in the previous patch the `FUND_TX_BASE_WEIGHT` is passed
explicit to the implementer of `get_utxos_for_amount`, we can exclude
that from the target `amount` that we pass to `get_utxos_for_amount`.

Besides the party's collateral and all the fees related to opening a
_channel_, the only thing other that we need to take into account is
the transaction fee for the CET or the refund transaction.

The base weight can be split in half between the two parties, and each
party then has to pay for their own (possible) output on the CET or
refund transaction.

When constructing the fund transaction itself, we were also forgetting
to add the `8 + 1` bytes of each CET or refund transaction party
outputs.

[^1]: p2pderivatives@c4a0f88.
  • Loading branch information
luckysori committed Feb 22, 2024
1 parent 1fb8462 commit d9d8426
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 21 deletions.
26 changes: 13 additions & 13 deletions dlc-manager/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::ops::Deref;

use bitcoin::{consensus::Encodable, Txid};
use dlc::{PartyParams, TxInputInfo, FUND_TX_BASE_WEIGHT};
use dlc::{util::{cet_or_refund_base_fee, dlc_payout_spk_fee}, PartyParams, TxInputInfo, FUND_TX_BASE_WEIGHT};
use dlc_messages::{
oracle_msgs::{OracleAnnouncement, OracleAttestation},
FundingInput,
Expand All @@ -18,9 +18,6 @@ use crate::{
Blockchain, Wallet,
};

const APPROXIMATE_CET_VBYTES: u64 = 190;
const APPROXIMATE_CLOSING_VBYTES: u64 = 168;

macro_rules! get_object_in_state {
($manager: expr, $id: expr, $state: ident, $peer_id: expr, $object_type: ident, $get_call: ident) => {{
let object = $manager.get_store().$get_call($id)?;
Expand Down Expand Up @@ -51,10 +48,6 @@ macro_rules! get_object_in_state {

pub(crate) use get_object_in_state;

pub fn get_common_fee(fee_rate: u64) -> u64 {
(APPROXIMATE_CET_VBYTES + APPROXIMATE_CLOSING_VBYTES) * fee_rate
}

#[cfg(not(feature = "fuzztarget"))]
pub(crate) fn get_new_serial_id() -> u64 {
thread_rng().next_u64()
Expand Down Expand Up @@ -123,16 +116,22 @@ where
let mut total_input = 0;

if needs_utxo {
let payout_spk_fee = dlc_payout_spk_fee(&payout_spk, fee_rate);

// The extra fee is split evenly between both parties. For simplicity, we allow overshooting
// by 1 sat during coin selection
let extra_fee_half = extra_fee.div_ceil(2);

let appr_required_amount = own_collateral + get_half_common_fee(fee_rate) + extra_fee_half;
let appr_required_amount = own_collateral
+ get_half_cet_or_refund_fee(fee_rate)?
+ payout_spk_fee
+ extra_fee_half;

let utxos = wallet.get_utxos_for_amount(
appr_required_amount,
Some(fee_rate),
(FUND_TX_BASE_WEIGHT / 2) as u64,
true
true,
)?;
for utxo in utxos {
let prev_tx = blockchain.get_transaction(&utxo.outpoint.txid)?;
Expand Down Expand Up @@ -188,9 +187,10 @@ where
})
}

fn get_half_common_fee(fee_rate: u64) -> u64 {
let common_fee = get_common_fee(fee_rate);
(common_fee as f64 / 2_f64).ceil() as u64
fn get_half_cet_or_refund_fee(fee_rate: u64) -> Result<u64, Error> {
let common_fee = cet_or_refund_base_fee(fee_rate)?;

Ok((common_fee as f64 / 2_f64).ceil() as u64)
}

pub(crate) fn get_range_info_and_oracle_sigs(
Expand Down
16 changes: 8 additions & 8 deletions dlc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ use secp256k1_zkp::{
use serde::{Deserialize, Serialize};
use std::fmt;

use crate::util::dlc_payout_spk_fee;

pub mod channel;
pub mod secp_utils;
pub mod util;
Expand Down Expand Up @@ -301,14 +303,12 @@ impl PartyParams {
// among parties independently of output types
let this_party_cet_base_weight = CET_BASE_WEIGHT / 2;

// size of the payout script pubkey scaled by 4 from vBytes to weight units
let output_spk_weight = self
.payout_script_pubkey
.len()
.checked_mul(4)
.ok_or(Error::InvalidArgument("failed to multiply 4 to payout script pubkey length".to_string()))?;
let total_cet_weight = checked_add!(this_party_cet_base_weight, output_spk_weight)?;
let cet_or_refund_fee = util::tx_weight_to_fee(total_cet_weight, fee_rate_per_vb)?;
let output_spk_fee = dlc_payout_spk_fee(
&self.payout_script_pubkey,
fee_rate_per_vb,
);
let cet_or_refund_base_fee = util::weight_to_fee(this_party_cet_base_weight, fee_rate_per_vb)?;
let cet_or_refund_fee = checked_add!(cet_or_refund_base_fee, output_spk_fee)?;
let required_input_funds =
checked_add!(self.collateral, fund_fee_without_change, cet_or_refund_fee, extra_fee)?;
if self.input_amount < required_input_funds {
Expand Down
22 changes: 22 additions & 0 deletions dlc/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,35 @@ pub fn weight_to_fee(weight: usize, fee_rate: u64) -> Result<u64, Error> {
Ok(fee)
}

/// Calculate the base transaction fee for a CET or refund transaction, for the given fee rate.
pub fn cet_or_refund_base_fee(fee_rate: u64) -> Result<u64, Error> {
let base_weight = crate::CET_BASE_WEIGHT;
tx_weight_to_fee(base_weight, fee_rate)
}

/// Calculate the extra transaction fees that need to be reserved when opening a DLC channel.
///
/// These fees apply to the entire channel and will need to be divided between the two parties.
pub fn dlc_channel_extra_fee(fee_rate: u64) -> Result<u64, Error> {
tx_weight_to_fee(BUFFER_TX_WEIGHT + CET_EXTRA_WEIGHT, fee_rate)
}

/// Calculate the fraction of a transaction fee that must be included to pay for the given payout
/// output script pubkey.
///
/// Payout outputs are included in CETs and refund transactions.
pub fn dlc_payout_spk_fee(payout_spk: &Script, fee_rate_sats_per_vb: u64) -> u64 {
// Numbers come from
// https://github.com/discreetlogcontracts/dlcspecs/blob/master/Transactions.md#expected-weight-of-the-contract-execution-or-refund-transaction.

let value_vb = 8;
let var_int_vb = 1;

let payout_spk_vb = payout_spk.len() as u64;

(value_vb + var_int_vb + payout_spk_vb) * fee_rate_sats_per_vb
}

fn get_pkh_script_pubkey_from_sk<C: Signing>(secp: &Secp256k1<C>, sk: &SecretKey) -> Script {
use bitcoin::hashes::*;
let pk = bitcoin::PublicKey {
Expand Down

0 comments on commit d9d8426

Please sign in to comment.