Skip to content

Commit 39cb0a3

Browse files
committed
Add TxBuilder::build_commitment_transaction
When the `TxBuilder` API is made public in the future, this trait method will let implementers build custom commitment transactions as part of the lightning state machine.
1 parent 07b7940 commit 39cb0a3

File tree

2 files changed

+155
-58
lines changed

2 files changed

+155
-58
lines changed

lightning/src/ln/channel.rs

Lines changed: 28 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,13 +1130,14 @@ struct CommitmentData<'a> {
11301130
}
11311131

11321132
/// A struct gathering stats on a commitment transaction, either local or remote.
1133-
struct CommitmentStats {
1133+
#[derive(Debug, PartialEq)]
1134+
pub(crate) struct CommitmentStats {
11341135
/// The total fee included in the commitment transaction
1135-
commit_tx_fee_sat: u64,
1136+
pub commit_tx_fee_sat: u64,
11361137
/// The local balance before fees *not* considering dust limits
1137-
local_balance_before_fee_msat: u64,
1138+
pub local_balance_before_fee_msat: u64,
11381139
/// The remote balance before fees *not* considering dust limits
1139-
remote_balance_before_fee_msat: u64,
1140+
pub remote_balance_before_fee_msat: u64,
11401141
}
11411142

11421143
/// Used when calculating whether we or the remote can afford an additional HTLC.
@@ -4533,16 +4534,10 @@ where
45334534
let broadcaster_dust_limit_sat = if local { self.holder_dust_limit_satoshis } else { self.counterparty_dust_limit_satoshis };
45344535
let feerate_per_kw = self.get_commitment_feerate(funding, generated_by_local);
45354536

4536-
let stats = self.build_commitment_stats(funding, local, generated_by_local, None, None);
4537-
let CommitmentStats {
4538-
commit_tx_fee_sat,
4539-
local_balance_before_fee_msat,
4540-
remote_balance_before_fee_msat
4541-
} = stats;
4542-
45434537
let num_htlcs = self.pending_inbound_htlcs.len() + self.pending_outbound_htlcs.len();
45444538
let mut htlcs_included: Vec<(HTLCOutputInCommitment, Option<&HTLCSource>)> = Vec::with_capacity(num_htlcs);
4545-
let mut nondust_htlcs: Vec<HTLCOutputInCommitment> = Vec::with_capacity(num_htlcs);
4539+
let mut value_to_self_claimed_msat = 0;
4540+
let mut value_to_remote_claimed_msat = 0;
45464541

45474542
log_trace!(logger, "Building commitment transaction number {} (really {} xor {}) for channel {} for {}, generated by {} with fee {}...",
45484543
commitment_number, (INITIAL_COMMITMENT_NUMBER - commitment_number),
@@ -4557,21 +4552,15 @@ where
45574552
amount_msat: $htlc.amount_msat,
45584553
cltv_expiry: $htlc.cltv_expiry,
45594554
payment_hash: $htlc.payment_hash,
4560-
transaction_output_index: None
4555+
transaction_output_index: None,
45614556
}
45624557
}
45634558
}
45644559

45654560
macro_rules! add_htlc_output {
45664561
($htlc: expr, $outbound: expr, $source: expr) => {
4567-
let htlc_in_tx = get_htlc_in_commitment!($htlc, $outbound == local);
4568-
htlcs_included.push((htlc_in_tx.clone(), $source));
4569-
if $htlc.is_dust(local, feerate_per_kw, broadcaster_dust_limit_sat, funding.get_channel_type()) {
4570-
log_trace!(logger, " ...including {} {} dust HTLC {} (hash {}) with value {} due to dust limit", if $outbound { "outbound" } else { "inbound" }, $htlc.state, $htlc.htlc_id, $htlc.payment_hash, $htlc.amount_msat);
4571-
} else {
4572-
log_trace!(logger, " ...including {} {} HTLC {} (hash {}) with value {}", if $outbound { "outbound" } else { "inbound" }, $htlc.state, $htlc.htlc_id, $htlc.payment_hash, $htlc.amount_msat);
4573-
nondust_htlcs.push(htlc_in_tx);
4574-
}
4562+
let htlc = get_htlc_in_commitment!($htlc, $outbound == local);
4563+
htlcs_included.push((htlc, $source));
45754564
}
45764565
}
45774566

@@ -4580,11 +4569,13 @@ where
45804569

45814570
for htlc in self.pending_inbound_htlcs.iter() {
45824571
if htlc.state.included_in_commitment(generated_by_local) {
4572+
log_trace!(logger, " ...including inbound {} HTLC {} (hash {}) with value {}", htlc.state, htlc.htlc_id, htlc.payment_hash, htlc.amount_msat);
45834573
add_htlc_output!(htlc, false, None);
45844574
} else {
45854575
log_trace!(logger, " ...not including inbound HTLC {} (hash {}) with value {} due to state ({})", htlc.htlc_id, htlc.payment_hash, htlc.amount_msat, htlc.state);
45864576
if let Some(preimage) = htlc.state.preimage() {
45874577
inbound_htlc_preimages.push(preimage);
4578+
value_to_self_claimed_msat += htlc.amount_msat;
45884579
}
45894580
}
45904581
};
@@ -4594,53 +4585,35 @@ where
45944585
outbound_htlc_preimages.push(preimage);
45954586
}
45964587
if htlc.state.included_in_commitment(generated_by_local) {
4588+
log_trace!(logger, " ...including outbound {} HTLC {} (hash {}) with value {}", htlc.state, htlc.htlc_id, htlc.payment_hash, htlc.amount_msat);
45974589
add_htlc_output!(htlc, true, Some(&htlc.source));
45984590
} else {
45994591
log_trace!(logger, " ...not including outbound HTLC {} (hash {}) with value {} due to state ({})", htlc.htlc_id, htlc.payment_hash, htlc.amount_msat, htlc.state);
4592+
if htlc.state.preimage().is_some() {
4593+
value_to_remote_claimed_msat += htlc.amount_msat;
4594+
}
46004595
}
46014596
};
46024597

4603-
// We MUST use saturating subs here, as the funder's balance is not guaranteed to be greater
4604-
// than or equal to `commit_tx_fee_sat`.
4598+
// # Panics
46054599
//
4606-
// This is because when the remote party sends an `update_fee` message, we build the new
4607-
// commitment transaction *before* checking whether the remote party's balance is enough to
4608-
// cover the total fee.
4609-
4610-
let (value_to_self, value_to_remote) = if funding.is_outbound() {
4611-
((local_balance_before_fee_msat / 1000).saturating_sub(commit_tx_fee_sat), remote_balance_before_fee_msat / 1000)
4612-
} else {
4613-
(local_balance_before_fee_msat / 1000, (remote_balance_before_fee_msat / 1000).saturating_sub(commit_tx_fee_sat))
4614-
};
4615-
4616-
let mut to_broadcaster_value_sat = if local { value_to_self } else { value_to_remote };
4617-
let mut to_countersignatory_value_sat = if local { value_to_remote } else { value_to_self };
4618-
4619-
if to_broadcaster_value_sat >= broadcaster_dust_limit_sat {
4620-
log_trace!(logger, " ...including {} output with value {}", if local { "to_local" } else { "to_remote" }, to_broadcaster_value_sat);
4621-
} else {
4622-
to_broadcaster_value_sat = 0;
4623-
}
4600+
// After all HTLC claims have been accounted for, the local balance MUST remain greater than or equal to 0.
46244601

4625-
if to_countersignatory_value_sat >= broadcaster_dust_limit_sat {
4626-
log_trace!(logger, " ...including {} output with value {}", if local { "to_remote" } else { "to_local" }, to_countersignatory_value_sat);
4627-
} else {
4628-
to_countersignatory_value_sat = 0;
4629-
}
4602+
let value_to_self_msat = (funding.value_to_self_msat + value_to_self_claimed_msat).checked_sub(value_to_remote_claimed_msat).unwrap();
46304603

4631-
let channel_parameters =
4632-
if local { funding.channel_transaction_parameters.as_holder_broadcastable() }
4633-
else { funding.channel_transaction_parameters.as_counterparty_broadcastable() };
4634-
let tx = CommitmentTransaction::new(
4604+
let (tx, stats) = SpecTxBuilder {}.build_commitment_transaction(
4605+
local,
46354606
commitment_number,
46364607
per_commitment_point,
4637-
to_broadcaster_value_sat,
4638-
to_countersignatory_value_sat,
4639-
feerate_per_kw,
4640-
nondust_htlcs,
4641-
&channel_parameters,
4608+
&funding.channel_transaction_parameters,
46424609
&self.secp_ctx,
4610+
value_to_self_msat,
4611+
htlcs_included.iter().map(|(htlc, _source)| htlc).cloned().collect(),
4612+
feerate_per_kw,
4613+
broadcaster_dust_limit_sat,
4614+
logger,
46434615
);
4616+
debug_assert_eq!(stats, self.build_commitment_stats(funding, local, generated_by_local, None, None));
46444617

46454618
// This populates the HTLC-source table with the indices from the HTLCs in the commitment
46464619
// transaction.

lightning/src/sign/tx_builder.rs

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
//! Defines the `TxBuilder` trait, and the `SpecTxBuilder` type
22
3-
use types::features::ChannelTypeFeatures;
3+
use core::ops::Deref;
44

5-
use crate::ln::chan_utils::commit_tx_fee_sat;
6-
use crate::ln::channel::ANCHOR_OUTPUT_VALUE_SATOSHI;
5+
use bitcoin::secp256k1::{self, PublicKey, Secp256k1};
6+
7+
use crate::ln::chan_utils::{
8+
commit_tx_fee_sat, htlc_success_tx_weight, htlc_timeout_tx_weight,
9+
ChannelTransactionParameters, CommitmentTransaction, HTLCOutputInCommitment,
10+
};
11+
use crate::ln::channel::{CommitmentStats, ANCHOR_OUTPUT_VALUE_SATOSHI};
12+
use crate::prelude::*;
13+
use crate::types::features::ChannelTypeFeatures;
14+
use crate::util::logger::Logger;
715

816
pub(crate) trait TxBuilder {
917
fn commit_tx_fee_sat(
@@ -13,6 +21,14 @@ pub(crate) trait TxBuilder {
1321
&self, is_outbound_from_holder: bool, value_to_self_after_htlcs: u64,
1422
value_to_remote_after_htlcs: u64, channel_type: &ChannelTypeFeatures,
1523
) -> (u64, u64);
24+
fn build_commitment_transaction<L: Deref>(
25+
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
26+
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
27+
value_to_self_msat: u64, htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
28+
broadcaster_dust_limit_sat: u64, logger: &L,
29+
) -> (CommitmentTransaction, CommitmentStats)
30+
where
31+
L::Target: Logger;
1632
}
1733

1834
pub(crate) struct SpecTxBuilder {}
@@ -53,4 +69,112 @@ impl TxBuilder for SpecTxBuilder {
5369

5470
(local_balance_before_fee_msat, remote_balance_before_fee_msat)
5571
}
72+
#[rustfmt::skip]
73+
fn build_commitment_transaction<L: Deref>(
74+
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
75+
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
76+
value_to_self_msat: u64, mut htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
77+
broadcaster_dust_limit_sat: u64, logger: &L,
78+
) -> (CommitmentTransaction, CommitmentStats)
79+
where
80+
L::Target: Logger,
81+
{
82+
let mut local_htlc_total_msat = 0;
83+
let mut remote_htlc_total_msat = 0;
84+
let channel_type = &channel_parameters.channel_type_features;
85+
86+
let is_dust = |offered: bool, amount_msat: u64| -> bool {
87+
let htlc_tx_fee_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() {
88+
0
89+
} else {
90+
let htlc_tx_weight = if offered {
91+
htlc_timeout_tx_weight(channel_type)
92+
} else {
93+
htlc_success_tx_weight(channel_type)
94+
};
95+
// As required by the spec, round down
96+
feerate_per_kw as u64 * htlc_tx_weight / 1000
97+
};
98+
amount_msat / 1000 < broadcaster_dust_limit_sat + htlc_tx_fee_sat
99+
};
100+
101+
// Trim dust htlcs
102+
htlcs_in_tx.retain(|htlc| {
103+
if htlc.offered == local {
104+
// This is an outbound htlc
105+
local_htlc_total_msat += htlc.amount_msat;
106+
} else {
107+
remote_htlc_total_msat += htlc.amount_msat;
108+
}
109+
if is_dust(htlc.offered, htlc.amount_msat) {
110+
log_trace!(logger, " ...trimming {} HTLC with value {}sat, hash {}, due to dust limit {}", if htlc.offered == local { "outbound" } else { "inbound" }, htlc.amount_msat / 1000, htlc.payment_hash, broadcaster_dust_limit_sat);
111+
false
112+
} else {
113+
true
114+
}
115+
});
116+
117+
// # Panics
118+
//
119+
// The value going to each party MUST be 0 or positive, even if all HTLCs pending in the
120+
// commitment clear by failure.
121+
122+
let commit_tx_fee_sat = self.commit_tx_fee_sat(feerate_per_kw, htlcs_in_tx.len(), &channel_parameters.channel_type_features);
123+
let value_to_self_after_htlcs_msat = value_to_self_msat.checked_sub(local_htlc_total_msat).unwrap();
124+
let value_to_remote_after_htlcs_msat =
125+
(channel_parameters.channel_value_satoshis * 1000).checked_sub(value_to_self_msat).unwrap().checked_sub(remote_htlc_total_msat).unwrap();
126+
let (local_balance_before_fee_msat, remote_balance_before_fee_msat) =
127+
self.subtract_non_htlc_outputs(channel_parameters.is_outbound_from_holder, value_to_self_after_htlcs_msat, value_to_remote_after_htlcs_msat, &channel_parameters.channel_type_features);
128+
129+
// We MUST use saturating subs here, as the funder's balance is not guaranteed to be greater
130+
// than or equal to `commit_tx_fee_sat`.
131+
//
132+
// This is because when the remote party sends an `update_fee` message, we build the new
133+
// commitment transaction *before* checking whether the remote party's balance is enough to
134+
// cover the total fee.
135+
136+
let (value_to_self, value_to_remote) = if channel_parameters.is_outbound_from_holder {
137+
((local_balance_before_fee_msat / 1000).saturating_sub(commit_tx_fee_sat), remote_balance_before_fee_msat / 1000)
138+
} else {
139+
(local_balance_before_fee_msat / 1000, (remote_balance_before_fee_msat / 1000).saturating_sub(commit_tx_fee_sat))
140+
};
141+
142+
let mut to_broadcaster_value_sat = if local { value_to_self } else { value_to_remote };
143+
let mut to_countersignatory_value_sat = if local { value_to_remote } else { value_to_self };
144+
145+
if to_broadcaster_value_sat >= broadcaster_dust_limit_sat {
146+
log_trace!(logger, " ...including {} output with value {}", if local { "to_local" } else { "to_remote" }, to_broadcaster_value_sat);
147+
} else {
148+
to_broadcaster_value_sat = 0;
149+
}
150+
151+
if to_countersignatory_value_sat >= broadcaster_dust_limit_sat {
152+
log_trace!(logger, " ...including {} output with value {}", if local { "to_remote" } else { "to_local" }, to_countersignatory_value_sat);
153+
} else {
154+
to_countersignatory_value_sat = 0;
155+
}
156+
157+
let directed_parameters =
158+
if local { channel_parameters.as_holder_broadcastable() }
159+
else { channel_parameters.as_counterparty_broadcastable() };
160+
let tx = CommitmentTransaction::new(
161+
commitment_number,
162+
per_commitment_point,
163+
to_broadcaster_value_sat,
164+
to_countersignatory_value_sat,
165+
feerate_per_kw,
166+
htlcs_in_tx,
167+
&directed_parameters,
168+
secp_ctx,
169+
);
170+
171+
(
172+
tx,
173+
CommitmentStats {
174+
commit_tx_fee_sat,
175+
local_balance_before_fee_msat,
176+
remote_balance_before_fee_msat,
177+
},
178+
)
179+
}
56180
}

0 commit comments

Comments
 (0)