Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display more fee information when opening a DLC channel #2062

Merged
merged 7 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this test really panic? If it panics, why do we need an assert, or is the assert panicking?

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
Loading