Skip to content

Commit

Permalink
Merge pull request #2062 from get10101/feat/show-channel-tx-fees
Browse files Browse the repository at this point in the history
Display more fee information when opening a DLC channel
  • Loading branch information
luckysori authored Mar 1, 2024
2 parents ce0841e + 0d8c36c commit e48d93e
Show file tree
Hide file tree
Showing 16 changed files with 316 additions and 73 deletions.
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ resolver = "2"
# We are using our own fork of `rust-dlc` at least until we can drop all the LN-DLC features. Also,
# `p2pderivatives/rust-dlc#master` is missing certain patches that can only be found in the LN-DLC
# branch.
dlc-manager = { git = "https://github.com/get10101/rust-dlc", rev = "bc31c6167e304d7886c328da91e8c17dad1afce5" }
dlc-messages = { git = "https://github.com/get10101/rust-dlc", rev = "bc31c6167e304d7886c328da91e8c17dad1afce5" }
dlc = { git = "https://github.com/get10101/rust-dlc", rev = "bc31c6167e304d7886c328da91e8c17dad1afce5" }
p2pd-oracle-client = { git = "https://github.com/get10101/rust-dlc", rev = "bc31c6167e304d7886c328da91e8c17dad1afce5" }
dlc-trie = { git = "https://github.com/get10101/rust-dlc", rev = "bc31c6167e304d7886c328da91e8c17dad1afce5" }
dlc-manager = { git = "https://github.com/get10101/rust-dlc", rev = "1ab4bf5" }
dlc-messages = { git = "https://github.com/get10101/rust-dlc", rev = "1ab4bf5" }
dlc = { git = "https://github.com/get10101/rust-dlc", rev = "1ab4bf5" }
p2pd-oracle-client = { git = "https://github.com/get10101/rust-dlc", rev = "1ab4bf5" }
dlc-trie = { git = "https://github.com/get10101/rust-dlc", rev = "1ab4bf5" }

# We should usually track the `p2pderivatives/split-tx-experiment[-10101]` branch. For now we depend
# on a special fork which removes a panic in `rust-lightning`.
Expand Down
4 changes: 2 additions & 2 deletions coordinator/src/collaborative_revert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use diesel::r2d2::ConnectionManager;
use diesel::r2d2::Pool;
use diesel::r2d2::PooledConnection;
use diesel::PgConnection;
use dlc::util::weight_to_fee;
use dlc::util::tx_weight_to_fee;
use dlc_manager::channel::ClosedChannel;
use dlc_manager::DlcChannelId;
use dlc_manager::Signer;
Expand Down Expand Up @@ -86,7 +86,7 @@ pub async fn propose_collaborative_revert(
.checked_sub(trader_amount_sats)
.context("Could not substract trader amount from total value without overflow")?;

let fee = weight_to_fee(COLLABORATIVE_REVERT_TX_WEIGHT, fee_rate_sats_vb)
let fee = tx_weight_to_fee(COLLABORATIVE_REVERT_TX_WEIGHT, fee_rate_sats_vb)
.context("Could not calculate fee")?;

let fee_half = fee.checked_div(2).context("Could not divide fee")?;
Expand Down
28 changes: 5 additions & 23 deletions crates/ln-dlc-node/src/dlc_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ use bdk_coin_select::Target;
use bitcoin::secp256k1::KeyPair;
use bitcoin::Network;
use bitcoin::TxIn;
use bitcoin::VarInt;
use lightning::chain::chaininterface::ConfirmationTarget;
use ln_dlc_storage::DlcStorageProvider;
use ln_dlc_storage::WalletStorage;
use std::sync::Arc;
Expand Down Expand Up @@ -180,16 +178,12 @@ impl<D: BdkStorage, S: TenTenOneStorage, N> dlc_manager::Wallet for DlcWallet<D,
&self,
amount: u64,
fee_rate: Option<u64>,
base_weight_wu: u64,
lock_utxos: bool,
) -> Result<Vec<dlc_manager::Utxo>, dlc_manager::error::Error> {
let network = self.on_chain_wallet.network();

let fee_rate = fee_rate.map(|fee_rate| fee_rate as f32).unwrap_or_else(|| {
self.on_chain_wallet
.fee_rate_estimator
.get(ConfirmationTarget::Normal)
.as_sat_per_vb()
});
let fee_rate = fee_rate.expect("always set by rust-dlc");

// Get temporarily reserved UTXOs from in-memory storage.
let mut reserved_outpoints = self.on_chain_wallet.locked_utxos.lock();
Expand All @@ -211,15 +205,7 @@ impl<D: BdkStorage, S: TenTenOneStorage, N> dlc_manager::Wallet for DlcWallet<D,
..Default::default()
};

// Inspired by `rust-bitcoin:0.30.2`.
let segwit_weight = {
let legacy_weight = {
let script_sig_size = tx_in.script_sig.len();
(36 + VarInt(script_sig_size as u64).len() + script_sig_size + 4) * 4
};

legacy_weight + tx_in.witness.serialized_len()
};
let segwit_weight = tx_in.segwit_weight();

// The 10101 wallet always generates SegWit addresses.
//
Expand All @@ -230,17 +216,13 @@ impl<D: BdkStorage, S: TenTenOneStorage, N> dlc_manager::Wallet for DlcWallet<D,
})
.collect::<Vec<_>>();

// This is a standard base weight (without inputs or change outputs) for on-chain DLCs. We
// assume that this value is still correct for DLC channels.
let funding_tx_base_weight = 212;

let target = Target {
feerate: bdk_coin_select::FeeRate::from_sat_per_vb(fee_rate),
feerate: bdk_coin_select::FeeRate::from_sat_per_vb(fee_rate as f32),
min_fee: 0,
value: amount,
};

let mut coin_selector = CoinSelector::new(&candidates, funding_tx_base_weight);
let mut coin_selector = CoinSelector::new(&candidates, base_weight_wu as u32);

let dust_limit = 0;
let long_term_feerate = bdk_coin_select::FeeRate::default_min_relay_fee();
Expand Down
65 changes: 65 additions & 0 deletions crates/ln-dlc-node/src/node/dlc_channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -818,3 +818,68 @@ pub fn send_dlc_message<D: BdkStorage, S: TenTenOneStorage + 'static, N: LnDlcSt
// enqueued message ASAP.
peer_manager.process_events();
}

/// Give an estimate for the fee reserve of a DLC channel, given a fee rate.
///
/// Limitations:
///
/// - `rust-dlc` assumes that both parties will use P2WPKH script pubkeys for their CET outputs. If
/// they don't then the reserved fee might be slightly over or under the target fee rate.
///
/// - Rounding errors can cause very slight differences between what we estimate here and what
/// `rust-dlc` will end up reserving.
pub fn estimated_dlc_channel_fee_reserve(fee_rate_sats_per_vb: f64) -> Amount {
let buffer_weight_wu = dlc::channel::BUFFER_TX_WEIGHT;

let cet_or_refund_weight_wu = {
let cet_or_refund_base_weight_wu = dlc::CET_BASE_WEIGHT;
// Because the CET spends from a buffer transaction, compared to a regular DLC that spends
// directly from the funding transaction.
let cet_or_refund_extra_weight_wu = dlc::channel::CET_EXTRA_WEIGHT;

// This is the standard length of a P2WPKH script pubkey.
let cet_or_refund_output_spk_bytes = 22;

// Value = 8 bytes; var_int = 1 byte.
let cet_or_refund_output_weight_wu = (8 + 1 + cet_or_refund_output_spk_bytes) * 4;

cet_or_refund_base_weight_wu
+ cet_or_refund_extra_weight_wu
// 1 output per party.
+ (2 * cet_or_refund_output_weight_wu)
};

let total_weight_vb = (buffer_weight_wu + cet_or_refund_weight_wu) as f64 / 4.0;

let total_fee_reserve = total_weight_vb * fee_rate_sats_per_vb;
let total_fee_reserve = total_fee_reserve.ceil() as u64;

Amount::from_sat(total_fee_reserve)
}

/// Give an estimate for the fee paid to publish a DLC channel funding transaction, given a fee
/// rate.
///
/// This estimate is based on a funding transaction spending _two_ P2WPKH inputs (one per party) and
/// including _two_ P2WPKH change outputs (also one per party).
///
/// Values taken from
/// https://github.com/discreetlogcontracts/dlcspecs/blob/master/Transactions.md#fees.
pub fn estimated_funding_transaction_fee(fee_rate_sats_per_vb: f64) -> Amount {
let base_weight_wu = dlc::FUND_TX_BASE_WEIGHT;

let input_script_pubkey_length = 22;
let max_witness_length = 108;
let input_weight_wu = 164 + (4 * input_script_pubkey_length) + max_witness_length;

let output_script_pubkey_length = 22;
let output_weight_wu = 36 + (4 * output_script_pubkey_length);

let total_weight_wu = base_weight_wu + (input_weight_wu * 2) + (output_weight_wu * 2);
let total_weight_vb = total_weight_wu as f64 / 4.0;

let fee = total_weight_vb * fee_rate_sats_per_vb;
let fee = fee.ceil() as u64;

Amount::from_sat(fee)
}
88 changes: 75 additions & 13 deletions crates/ln-dlc-node/src/tests/dlc_channel.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::bitcoin_conversion::to_secp_pk_29;
use crate::node::dlc_channel::estimated_dlc_channel_fee_reserve;
use crate::node::InMemoryStore;
use crate::node::Node;
use crate::node::RunningNode;
Expand Down Expand Up @@ -260,39 +261,100 @@ async fn can_open_and_force_close_channel() {

#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn can_open_channel_with_min_inputs() {
async fn funding_transaction_pays_expected_fees() {
init_tracing();

// Arrange

let app_dlc_collateral = Amount::from_sat(10_000);
let coordinator_dlc_collateral = Amount::from_sat(10_000);

let fee_rate_sats_per_vb = 2;

// Give enough funds to app and coordinator so that each party can have their own change output.
// This is not currently enforced by `rust-dlc`, but it will be in the near future:
// https://github.com/p2pderivatives/rust-dlc/pull/152.
let (app, _running_app) = start_and_fund_app(app_dlc_collateral * 2, 1).await;
let (coordinator, _running_coordinator) =
start_and_fund_coordinator(app_dlc_collateral * 2, 1).await;

// Act

let (app_signed_channel, _) = open_channel_and_position(
app.clone(),
coordinator.clone(),
app_dlc_collateral,
coordinator_dlc_collateral,
Some(fee_rate_sats_per_vb),
)
.await;

// Assert

let fund_tx_outputs_amount = app_signed_channel
.fund_tx
.output
.iter()
.fold(Amount::ZERO, |acc, output| {
acc + Amount::from_sat(output.value)
});

let fund_tx_inputs_amount = Amount::from_sat(
app_signed_channel.own_params.input_amount + app_signed_channel.counter_params.input_amount,
);

let fund_tx_fee = fund_tx_inputs_amount - fund_tx_outputs_amount;

let fund_tx_weight_wu = app_signed_channel.fund_tx.weight();
let fund_tx_weight_vb = (fund_tx_weight_wu / 4) as u64;

let fund_tx_fee_rate_sats_per_vb = fund_tx_fee.to_sat() / fund_tx_weight_vb;

assert_eq!(fund_tx_fee_rate_sats_per_vb, fee_rate_sats_per_vb);
}

#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn dlc_channel_includes_expected_fee_reserve() {
init_tracing();

let app_dlc_collateral = Amount::from_sat(10_000);
let coordinator_dlc_collateral = Amount::from_sat(10_000);

// We must fix the fee rate so that we can predict how many sats `rust-dlc` will allocate
// for transaction fees.
let fee_rate_sats_per_vbyte = 2;
let expected_fund_tx_fee = 252 * fee_rate_sats_per_vbyte;
let fee_rate_sats_per_vb = 2;

// This also depends on the fee rate, but the formula is a bit more involved.
let fee_reserve = 880;
let total_fee_reserve = estimated_dlc_channel_fee_reserve(fee_rate_sats_per_vb as f64);

// Fee costs are evenly split.
let fee_cost_per_party = (expected_fund_tx_fee + fee_reserve) / 2;
let fee_cost_per_party = Amount::from_sat(fee_cost_per_party);
let expected_fund_output_amount =
app_dlc_collateral + coordinator_dlc_collateral + total_fee_reserve;

let (app, _running_app) = start_and_fund_app(app_dlc_collateral + fee_cost_per_party, 1).await;
let (app, _running_app) = start_and_fund_app(app_dlc_collateral * 2, 1).await;
let (coordinator, _running_coordinator) =
start_and_fund_coordinator(coordinator_dlc_collateral + fee_cost_per_party, 1).await;
start_and_fund_coordinator(coordinator_dlc_collateral * 2, 1).await;

let (app_signed_channel, _) = open_channel_and_position(
app.clone(),
coordinator.clone(),
app_dlc_collateral,
coordinator_dlc_collateral,
Some(fee_rate_sats_per_vbyte),
Some(fee_rate_sats_per_vb),
)
.await;

// No change output means that the inputs were spent in full by the fund output.
assert!(app_signed_channel.fund_tx.output.len() == 1);
let fund_output_vout = app_signed_channel.fund_output_index;
let fund_output_amount = &app_signed_channel.fund_tx.output[fund_output_vout].value;

// We cannot easily assert equality because both `rust-dlc` and us have to round in several
// spots.
let epsilon = *fund_output_amount as i64 - expected_fund_output_amount.to_sat() as i64;

assert!(
epsilon.abs() < 5,
"Error out of bounds: actual {fund_output_amount} != {}",
expected_fund_output_amount.to_sat()
);
}

async fn start_and_fund_app(
Expand Down
9 changes: 9 additions & 0 deletions mobile/lib/common/dlc_channel_service.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:get_10101/common/domain/dlc_channel.dart';
import 'package:get_10101/common/domain/model.dart';
import 'package:get_10101/ffi.dart' as rust;

class DlcChannelService {
Expand All @@ -13,4 +14,12 @@ class DlcChannelService {
Future<void> deleteDlcChannel(String dlcChannelId) async {
await rust.api.deleteDlcChannel(dlcChannelId: dlcChannelId);
}

Amount getEstimatedChannelFeeReserve() {
return Amount(rust.api.getEstimatedChannelFeeReserve());
}

Amount getEstimatedFundingTxFee() {
return Amount(rust.api.getEstimatedFundingTxFee());
}
}
Loading

0 comments on commit e48d93e

Please sign in to comment.