Skip to content

Commit 02f66b8

Browse files
committed
Remove a clone of all the non-dust htlcs when rebuilding a commitment tx
We leverage the `Borrow` trait to allow passing both exclusive and shared references to `CommitmentTransaction::internal_build_outputs`. This allows us to avoid cloning all the htlcs when a `CommitmentTransaction::verify` call recreates the `BuiltCommitmentTransaction`. The iterator trait used in `CommitmentTransaction::internal_build_outputs` allows us to handle both collections of pointers, and a pointer to a collection without requiring any memory allocations. We also add extensive documentation of the inner mechanics of `CommitmentTransaction`.
1 parent f08af5f commit 02f66b8

File tree

1 file changed

+64
-34
lines changed

1 file changed

+64
-34
lines changed

lightning/src/ln/chan_utils.rs

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use bitcoin::secp256k1::{Secp256k1, ecdsa::Signature, Message};
4040
use bitcoin::{secp256k1, Sequence, Witness};
4141

4242
use crate::io;
43+
use core::borrow::Borrow;
4344
use core::cmp;
4445
use crate::util::transaction_utils::sort_outputs;
4546
use crate::ln::channel::{INITIAL_COMMITMENT_NUMBER, ANCHOR_OUTPUT_VALUE_SATOSHI};
@@ -1463,7 +1464,7 @@ impl CommitmentTransaction {
14631464
let keys = TxCreationKeys::from_channel_static_keys(per_commitment_point, channel_parameters.broadcaster_pubkeys(), channel_parameters.countersignatory_pubkeys(), secp_ctx);
14641465

14651466
// Sort outputs and populate output indices
1466-
let (outputs, nondust_htlcs) = Self::internal_build_outputs(&keys, to_broadcaster_value_sat, to_countersignatory_value_sat, nondust_htlcs, channel_parameters).unwrap();
1467+
let (outputs, nondust_htlcs) = Self::build_outputs_and_htlcs(&keys, to_broadcaster_value_sat, to_countersignatory_value_sat, nondust_htlcs, channel_parameters).unwrap();
14671468

14681469
let (obscured_commitment_transaction_number, txins) = Self::internal_build_inputs(commitment_number, channel_parameters);
14691470
let transaction = Self::make_transaction(obscured_commitment_transaction_number, txins, outputs);
@@ -1492,11 +1493,17 @@ impl CommitmentTransaction {
14921493
self
14931494
}
14941495

1496+
// This function
1497+
// - Builds the inputs of the commitment transaction.
1498+
// - Calls `internal_build_outputs` to get a `Vec<(TxOut, Option<&HTLCOutputInCommitment>)>` sorted according to the LN specification,
1499+
// and as they'd appear on the commitment transaction.
1500+
// - Maps this vector to a `Vec<TxOut>`, keeping the original order of the outputs.
1501+
// - Then uses the vectors of inputs and outputs to build the commitment transaction.
14951502
fn internal_rebuild_transaction(&self, keys: &TxCreationKeys, channel_parameters: &DirectedChannelTransactionParameters) -> Result<BuiltCommitmentTransaction, ()> {
14961503
let (obscured_commitment_transaction_number, txins) = Self::internal_build_inputs(self.commitment_number, channel_parameters);
14971504

1498-
let mut nondust_htlcs = self.htlcs.clone();
1499-
let (outputs, _) = Self::internal_build_outputs(keys, self.to_broadcaster_value_sat, self.to_countersignatory_value_sat, nondust_htlcs.iter_mut().collect(), channel_parameters)?;
1505+
let txout_htlc_pairs = Self::internal_build_outputs(keys, self.to_broadcaster_value_sat, self.to_countersignatory_value_sat, self.htlcs().iter(), channel_parameters)?;
1506+
let outputs = txout_htlc_pairs.into_iter().map(|(output, _)| output).collect();
15001507

15011508
let transaction = Self::make_transaction(obscured_commitment_transaction_number, txins, outputs);
15021509
let txid = transaction.compute_txid();
@@ -1516,17 +1523,58 @@ impl CommitmentTransaction {
15161523
}
15171524
}
15181525

1519-
// This is used in two cases:
1520-
// - initial sorting of outputs / HTLCs in the constructor, in which case T is auxiliary data the
1521-
// caller needs to have sorted together with the HTLCs so it can keep track of the output index
1522-
// - building of a bitcoin transaction during a verify() call, in which case T is just ()
1523-
fn internal_build_outputs(keys: &TxCreationKeys, to_broadcaster_value_sat: Amount, to_countersignatory_value_sat: Amount, nondust_htlcs: Vec<&mut HTLCOutputInCommitment>, channel_parameters: &DirectedChannelTransactionParameters) -> Result<(Vec<TxOut>, Vec<HTLCOutputInCommitment>), ()> {
1526+
// This function
1527+
// - Calls `internal_build_outputs` to get a `Vec<(TxOut, Option<&mut HTLCOutputInCommitment>)>` sorted according to the LN specification,
1528+
// and as they'd appear on the commitment transaction.
1529+
// - Then iterates over this vector of tuples:
1530+
// - populates *all* the output indices of the exclusive references in `nondust_htlcs`,
1531+
// - creates a vector of `HTLCOutputInCommitment` sorted by their output index,
1532+
// - creates a vector of `TxOut`, sorted according to the LN Spec, see `sort_outputs`.
1533+
// - Returns the two vectors: the vector of `TxOut`, and the vector of `HTLCOutputInCommitment`:
1534+
// - `Vec<TxOut>` gets moved to the `Transaction` cached by `CommitmentTransaction`,
1535+
// - `Vec<HTLCOutputInCommitment>` gets moved to and cached by `CommitmentTransaction`.
1536+
fn build_outputs_and_htlcs(keys: &TxCreationKeys, to_broadcaster_value_sat: Amount, to_countersignatory_value_sat: Amount, nondust_htlcs: Vec<&mut HTLCOutputInCommitment>, channel_parameters: &DirectedChannelTransactionParameters) -> Result<(Vec<TxOut>, Vec<HTLCOutputInCommitment>), ()> {
1537+
let mut htlcs: Vec<HTLCOutputInCommitment> = Vec::with_capacity(nondust_htlcs.len());
1538+
let mut txout_htlc_pairs: Vec<(TxOut, Option<&mut HTLCOutputInCommitment>)> = Self::internal_build_outputs(keys, to_broadcaster_value_sat, to_countersignatory_value_sat, nondust_htlcs.into_iter(), channel_parameters)?;
1539+
let mut outputs: Vec<TxOut> = Vec::with_capacity(txout_htlc_pairs.len());
1540+
for (idx, out) in txout_htlc_pairs.drain(..).enumerate() {
1541+
if let Some(htlc) = out.1 {
1542+
// Remember `htlc` is a `&mut HTLCOutputInCommitment`, so this populates the HTLC exclusive references passed to `CommitmentTransaction::new`
1543+
htlc.transaction_output_index = Some(idx as u32);
1544+
// We also clone the htlcs to cache them in `CommitmentTransaction`
1545+
htlcs.push(htlc.clone());
1546+
}
1547+
outputs.push(out.0);
1548+
}
1549+
Ok((outputs, htlcs))
1550+
}
1551+
1552+
// This function
1553+
// - Takes an iterator that yields objects of type `T` that can yield a `&HTLCOutputInCommitment`; `T: Borrow<HTLCOutputInCommitment>`.
1554+
// For our use-cases, these are `&mut HTLCOutputInCommitment` (used by `CommitmentTransaction::new`), and `&HTLCOutputInCommitment` (used by `CommitmentTransaction::internal_rebuild_transaction`).
1555+
// - Builds a `Vec<(TxOut, Option<T>)>`, containing all the outputs of the commitment transaction.
1556+
// For the `Option` in the tuple, an HTLC output sets the option to `Some(T)`, and all the other outputs set the option to `None`.
1557+
// - Passes this vector to `sort_outputs` to sort all the tuples in the vector according to the LN specification.
1558+
// - Then returns this sorted vector to the caller.
1559+
fn internal_build_outputs<T: Borrow<HTLCOutputInCommitment>>(keys: &TxCreationKeys, to_broadcaster_value_sat: Amount, to_countersignatory_value_sat: Amount, nondust_htlcs: impl Iterator<Item = T>, channel_parameters: &DirectedChannelTransactionParameters) -> Result<Vec<(TxOut, Option<T>)>, ()> {
15241560
let countersignatory_pubkeys = channel_parameters.countersignatory_pubkeys();
15251561
let broadcaster_funding_key = &channel_parameters.broadcaster_pubkeys().funding_pubkey;
15261562
let countersignatory_funding_key = &countersignatory_pubkeys.funding_pubkey;
15271563
let contest_delay = channel_parameters.contest_delay();
15281564

1529-
let mut txouts: Vec<(TxOut, Option<&mut HTLCOutputInCommitment>)> = Vec::new();
1565+
// Assumes the max number of outputs on the transaction is equal to n_htlcs + to_local + to_remote + local_anchor + remote_anchor
1566+
let mut txouts: Vec<(TxOut, Option<T>)> = Vec::with_capacity(nondust_htlcs.size_hint().0 + 4);
1567+
1568+
for htlc in nondust_htlcs {
1569+
let script = get_htlc_redeemscript(htlc.borrow(), &channel_parameters.channel_type_features(), &keys);
1570+
let txout = TxOut {
1571+
script_pubkey: script.to_p2wsh(),
1572+
value: htlc.borrow().to_bitcoin_amount(),
1573+
};
1574+
txouts.push((txout, Some(htlc)));
1575+
}
1576+
// We avoid relying on `ExactSizeIterator` here
1577+
let nondust_htlcs_is_empty = txouts.is_empty();
15301578

15311579
if to_countersignatory_value_sat > Amount::ZERO {
15321580
let script = if channel_parameters.channel_type_features().supports_anchors_zero_fee_htlc_tx() {
@@ -1559,7 +1607,7 @@ impl CommitmentTransaction {
15591607
}
15601608

15611609
if channel_parameters.channel_type_features().supports_anchors_zero_fee_htlc_tx() {
1562-
if to_broadcaster_value_sat > Amount::ZERO || !nondust_htlcs.is_empty() {
1610+
if to_broadcaster_value_sat > Amount::ZERO || !nondust_htlcs_is_empty {
15631611
let anchor_script = get_anchor_redeemscript(broadcaster_funding_key);
15641612
txouts.push((
15651613
TxOut {
@@ -1570,7 +1618,7 @@ impl CommitmentTransaction {
15701618
));
15711619
}
15721620

1573-
if to_countersignatory_value_sat > Amount::ZERO || !nondust_htlcs.is_empty() {
1621+
if to_countersignatory_value_sat > Amount::ZERO || !nondust_htlcs_is_empty {
15741622
let anchor_script = get_anchor_redeemscript(countersignatory_funding_key);
15751623
txouts.push((
15761624
TxOut {
@@ -1582,41 +1630,23 @@ impl CommitmentTransaction {
15821630
}
15831631
}
15841632

1585-
let mut htlcs = Vec::with_capacity(nondust_htlcs.len());
1586-
for htlc in nondust_htlcs {
1587-
let script = get_htlc_redeemscript(&htlc, &channel_parameters.channel_type_features(), &keys);
1588-
let txout = TxOut {
1589-
script_pubkey: script.to_p2wsh(),
1590-
value: htlc.to_bitcoin_amount(),
1591-
};
1592-
txouts.push((txout, Some(htlc)));
1593-
}
1594-
15951633
// Sort output in BIP-69 order (amount, scriptPubkey). Tie-breaks based on HTLC
15961634
// CLTV expiration height.
15971635
sort_outputs(&mut txouts, |a, b| {
1598-
if let &Some(ref a_htlcout) = a {
1599-
if let &Some(ref b_htlcout) = b {
1600-
a_htlcout.cltv_expiry.cmp(&b_htlcout.cltv_expiry)
1636+
if let Some(a_htlcout) = a {
1637+
if let Some(b_htlcout) = b {
1638+
a_htlcout.borrow().cltv_expiry.cmp(&b_htlcout.borrow().cltv_expiry)
16011639
// Note that due to hash collisions, we have to have a fallback comparison
16021640
// here for fuzzing mode (otherwise at least chanmon_fail_consistency
16031641
// may fail)!
1604-
.then(a_htlcout.payment_hash.0.cmp(&b_htlcout.payment_hash.0))
1642+
.then(a_htlcout.borrow().payment_hash.0.cmp(&b_htlcout.borrow().payment_hash.0))
16051643
// For non-HTLC outputs, if they're copying our SPK we don't really care if we
16061644
// close the channel due to mismatches - they're doing something dumb:
16071645
} else { cmp::Ordering::Equal }
16081646
} else { cmp::Ordering::Equal }
16091647
});
16101648

1611-
let mut outputs = Vec::with_capacity(txouts.len());
1612-
for (idx, out) in txouts.drain(..).enumerate() {
1613-
if let Some(htlc) = out.1 {
1614-
htlc.transaction_output_index = Some(idx as u32);
1615-
htlcs.push(htlc.clone());
1616-
}
1617-
outputs.push(out.0);
1618-
}
1619-
Ok((outputs, htlcs))
1649+
Ok(txouts)
16201650
}
16211651

16221652
fn internal_build_inputs(commitment_number: u64, channel_parameters: &DirectedChannelTransactionParameters) -> (u64, Vec<TxIn>) {

0 commit comments

Comments
 (0)