Skip to content

Commit c71334e

Browse files
authored
Merge pull request #4067 from tankyleo/2025-09-p2a-onchain
Sweep P2A outputs on commitment transactions in 0FC channels
2 parents 9d6d08b + ffa60c8 commit c71334e

File tree

9 files changed

+144
-88
lines changed

9 files changed

+144
-88
lines changed

fuzz/src/full_stack.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,7 @@ fn two_peer_forwarding_seed() -> Vec<u8> {
10481048
// our network key
10491049
ext_from_hex("0100000000000000000000000000000000000000000000000000000000000000", &mut test);
10501050
// config
1051-
ext_from_hex("0000000000900000000000000000640001000000000001ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff000100000000", &mut test);
1051+
ext_from_hex("000000000090000000000000000064000100000000000100ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff000100000000", &mut test);
10521052

10531053
// new outbound connection with id 0
10541054
ext_from_hex("00", &mut test);
@@ -1502,7 +1502,7 @@ fn gossip_exchange_seed() -> Vec<u8> {
15021502
// our network key
15031503
ext_from_hex("0100000000000000000000000000000000000000000000000000000000000000", &mut test);
15041504
// config
1505-
ext_from_hex("0000000000900000000000000000640001000000000001ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff000100000000", &mut test);
1505+
ext_from_hex("000000000090000000000000000064000100000000000100ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff000100000000", &mut test);
15061506

15071507
// new outbound connection with id 0
15081508
ext_from_hex("00", &mut test);

lightning/src/chain/channelmonitor.rs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2715,6 +2715,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
27152715
} else {
27162716
let funding = get_confirmed_funding_scope!(self);
27172717
debug_assert!(!funding.channel_type_features().supports_anchors_zero_fee_htlc_tx());
2718+
debug_assert!(!funding.channel_type_features().supports_anchor_zero_fee_commitments());
27182719
BitcoinOutPoint::new(*txid, 0)
27192720
}
27202721
} else {
@@ -3949,10 +3950,18 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
39493950
// new channel updates.
39503951
self.holder_tx_signed = true;
39513952
let mut watch_outputs = Vec::new();
3952-
// We can't broadcast our HTLC transactions while the commitment transaction is
3953-
// unconfirmed. We'll delay doing so until we detect the confirmed commitment in
3954-
// `transactions_confirmed`.
3955-
if !funding.channel_type_features().supports_anchors_zero_fee_htlc_tx() {
3953+
// In CSV anchor channels, we can't broadcast our HTLC transactions while the commitment transaction is
3954+
// unconfirmed.
3955+
// We'll delay doing so until we detect the confirmed commitment in `transactions_confirmed`.
3956+
//
3957+
// TODO: For now in 0FC channels, we also delay broadcasting any HTLC transactions until the commitment
3958+
// transaction gets confirmed. It is nonetheless possible to add HTLC spends to the P2A spend
3959+
// transaction while the commitment transaction is still unconfirmed.
3960+
let zero_fee_htlcs =
3961+
self.channel_type_features().supports_anchors_zero_fee_htlc_tx();
3962+
let zero_fee_commitments =
3963+
self.channel_type_features().supports_anchor_zero_fee_commitments();
3964+
if !zero_fee_htlcs && !zero_fee_commitments {
39563965
// Because we're broadcasting a commitment transaction, we should construct the package
39573966
// assuming it gets confirmed in the next block. Sadly, we have code which considers
39583967
// "not yet confirmed" things as discardable, so we cannot do that here.
@@ -4416,8 +4425,6 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
44164425
counterparty_node_id,
44174426
claim_id,
44184427
package_target_feerate_sat_per_1000_weight,
4419-
commitment_tx,
4420-
commitment_tx_fee_satoshis,
44214428
anchor_descriptor: AnchorDescriptor {
44224429
channel_derivation_parameters: ChannelDerivationParameters {
44234430
keys_id: self.channel_keys_id,
@@ -4428,8 +4435,11 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
44284435
txid: commitment_txid,
44294436
vout: anchor_output_idx,
44304437
},
4438+
value: commitment_tx.output[anchor_output_idx as usize].value,
44314439
},
44324440
pending_htlcs: pending_nondust_htlcs,
4441+
commitment_tx,
4442+
commitment_tx_fee_satoshis,
44334443
}));
44344444
},
44354445
ClaimEvent::BumpHTLC {
@@ -4646,7 +4656,6 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
46464656
if outp.script_pubkey == revokeable_p2wsh {
46474657
let revk_outp = RevokedOutput::build(
46484658
per_commitment_point, per_commitment_key, outp.value,
4649-
funding_spent.channel_type_features().supports_anchors_zero_fee_htlc_tx(),
46504659
funding_spent.channel_parameters.clone(), height,
46514660
);
46524661
let justice_package = PackageTemplate::build_package(
@@ -4859,9 +4868,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
48594868
if input.previous_output.txid == *commitment_txid && input.witness.len() == 5 && tx.output.get(idx).is_some() {
48604869
log_error!(logger, "Got broadcast of revoked counterparty HTLC transaction, spending {}:{}", htlc_txid, idx);
48614870
let revk_outp = RevokedOutput::build(
4862-
per_commitment_point, per_commitment_key, tx.output[idx].value, false,
4863-
funding_spent.channel_parameters.clone(),
4864-
height,
4871+
per_commitment_point, per_commitment_key, tx.output[idx].value,
4872+
self.funding.channel_parameters.clone(), height,
48654873
);
48664874
let justice_package = PackageTemplate::build_package(
48674875
htlc_txid, idx as u32, PackageSolvingData::RevokedOutput(revk_outp),
@@ -5129,9 +5137,22 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
51295137
self.funding.current_holder_commitment_tx.add_holder_sig(&redeem_script, sig)
51305138
};
51315139
let mut holder_transactions = vec![commitment_tx];
5132-
// When anchor outputs are present, the HTLC transactions are only final once the commitment
5133-
// transaction confirms due to the CSV 1 encumberance.
5134-
if self.channel_type_features().supports_anchors_zero_fee_htlc_tx() {
5140+
5141+
if self.channel_type_features().supports_anchors_zero_fee_htlc_tx()
5142+
|| self.channel_type_features().supports_anchor_zero_fee_commitments()
5143+
{
5144+
// HTLC transactions in these channels require external funding before finalized,
5145+
// so we return the commitment transaction alone here.
5146+
//
5147+
// In 0FC channels, we *could* use HTLC transactions to pay for fees on a
5148+
// 0FC commitment transaction to save the fixed transaction overhead
5149+
// (locktime + version), but we would still have to pay for fees using
5150+
// external UTXOs to avoid invalidating the counterparty HTLC signature.
5151+
// This is something we would consider in the future.
5152+
//
5153+
// Furthermore, we can't broadcast a HTLC claim transaction while the
5154+
// anchor claim transaction and its parent are still unconfirmed due to the
5155+
// current single-child restriction on TRUC transactions.
51355156
return holder_transactions;
51365157
}
51375158

lightning/src/chain/onchaintx.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ use crate::chain::package::{PackageSolvingData, PackageTemplate};
3030
use crate::chain::transaction::MaybeSignedTransaction;
3131
use crate::chain::ClaimId;
3232
use crate::ln::chan_utils::{
33-
self, ChannelTransactionParameters, HTLCOutputInCommitment, HolderCommitmentTransaction,
33+
get_keyed_anchor_redeemscript, shared_anchor_script_pubkey, ChannelTransactionParameters,
34+
HTLCOutputInCommitment, HolderCommitmentTransaction,
3435
};
3536
use crate::ln::msgs::DecodeError;
3637
use crate::sign::{ecdsa::EcdsaChannelSigner, EntropySource, HTLCDescriptor, SignerProvider};
@@ -677,7 +678,16 @@ impl<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
677678
let channel_parameters = output.channel_parameters.as_ref()
678679
.unwrap_or(self.channel_parameters());
679680
let funding_pubkey = &channel_parameters.holder_pubkeys.funding_pubkey;
680-
match chan_utils::get_keyed_anchor_output(&tx, funding_pubkey) {
681+
let script_pubkey = if channel_parameters.channel_type_features.supports_anchors_zero_fee_htlc_tx() {
682+
get_keyed_anchor_redeemscript(funding_pubkey).to_p2wsh()
683+
} else {
684+
debug_assert!(channel_parameters.channel_type_features.supports_anchor_zero_fee_commitments());
685+
shared_anchor_script_pubkey()
686+
};
687+
let anchor_output = tx.output.iter().enumerate()
688+
.find(|(_, txout)| txout.script_pubkey == script_pubkey)
689+
.map(|(idx, txout)| (idx as u32, txout));
690+
match anchor_output {
681691
// An anchor output was found, so we should yield a funding event externally.
682692
Some((idx, _)) => {
683693
// TODO: Use a lower confirmation target when both our and the

lightning/src/chain/package.rs

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ pub(crate) struct RevokedOutput {
144144
weight: u64,
145145
amount: Amount,
146146
on_counterparty_tx_csv: u16,
147-
is_counterparty_balance_on_anchors: Option<()>,
148147
channel_parameters: Option<ChannelTransactionParameters>,
149148
// Added in LDK 0.1.4/0.2 and always set since.
150149
outpoint_confirmation_height: Option<u32>,
@@ -154,7 +153,7 @@ impl RevokedOutput {
154153
#[rustfmt::skip]
155154
pub(crate) fn build(
156155
per_commitment_point: PublicKey, per_commitment_key: SecretKey, amount: Amount,
157-
is_counterparty_balance_on_anchors: bool, channel_parameters: ChannelTransactionParameters,
156+
channel_parameters: ChannelTransactionParameters,
158157
outpoint_confirmation_height: u32,
159158
) -> Self {
160159
let directed_params = channel_parameters.as_counterparty_broadcastable();
@@ -170,7 +169,6 @@ impl RevokedOutput {
170169
weight: WEIGHT_REVOKED_OUTPUT,
171170
amount,
172171
on_counterparty_tx_csv,
173-
is_counterparty_balance_on_anchors: if is_counterparty_balance_on_anchors { Some(()) } else { None },
174172
channel_parameters: Some(channel_parameters),
175173
outpoint_confirmation_height: Some(outpoint_confirmation_height),
176174
}
@@ -186,7 +184,9 @@ impl_writeable_tlv_based!(RevokedOutput, {
186184
(8, weight, required),
187185
(10, amount, required),
188186
(12, on_counterparty_tx_csv, required),
189-
(14, is_counterparty_balance_on_anchors, option),
187+
// Unused since 0.1, this setting causes downgrades to before 0.1 to refuse to
188+
// aggregate `RevokedOutput` claims, which is the more conservative stance.
189+
(14, is_counterparty_balance_on_anchors, (legacy, (), |_| Some(()))),
190190
(15, channel_parameters, (option: ReadableArgs, None)), // Added in 0.2.
191191
});
192192

@@ -750,11 +750,17 @@ impl PackageSolvingData {
750750
PackageSolvingData::CounterpartyOfferedHTLCOutput(ref outp) => outp.htlc.amount_msat / 1000,
751751
PackageSolvingData::CounterpartyReceivedHTLCOutput(ref outp) => outp.htlc.amount_msat / 1000,
752752
PackageSolvingData::HolderHTLCOutput(ref outp) => {
753-
debug_assert!(outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
753+
let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
754+
let free_commitments =
755+
outp.channel_type_features.supports_anchor_zero_fee_commitments();
756+
debug_assert!(free_htlcs || free_commitments);
754757
outp.amount_msat / 1000
755758
},
756759
PackageSolvingData::HolderFundingOutput(ref outp) => {
757-
debug_assert!(outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
760+
let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
761+
let free_commitments =
762+
outp.channel_type_features.supports_anchor_zero_fee_commitments();
763+
debug_assert!(free_htlcs || free_commitments);
758764
outp.funding_amount_sats.unwrap()
759765
}
760766
};
@@ -768,7 +774,10 @@ impl PackageSolvingData {
768774
PackageSolvingData::CounterpartyOfferedHTLCOutput(ref outp) => weight_offered_htlc(&outp.channel_type_features) as usize,
769775
PackageSolvingData::CounterpartyReceivedHTLCOutput(ref outp) => weight_received_htlc(&outp.channel_type_features) as usize,
770776
PackageSolvingData::HolderHTLCOutput(ref outp) => {
771-
debug_assert!(outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
777+
let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
778+
let free_commitments =
779+
outp.channel_type_features.supports_anchor_zero_fee_commitments();
780+
debug_assert!(free_htlcs || free_commitments);
772781
if outp.preimage.is_none() {
773782
weight_offered_htlc(&outp.channel_type_features) as usize
774783
} else {
@@ -988,6 +997,7 @@ impl PackageSolvingData {
988997
match self {
989998
PackageSolvingData::HolderHTLCOutput(ref outp) => {
990999
debug_assert!(!outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
1000+
debug_assert!(!outp.channel_type_features.supports_anchor_zero_fee_commitments());
9911001
outp.get_maybe_signed_htlc_tx(onchain_handler, outpoint)
9921002
}
9931003
PackageSolvingData::HolderFundingOutput(ref outp) => {
@@ -1040,14 +1050,20 @@ impl PackageSolvingData {
10401050
PackageMalleability::Malleable(AggregationCluster::Unpinnable),
10411051
PackageSolvingData::CounterpartyReceivedHTLCOutput(..) =>
10421052
PackageMalleability::Malleable(AggregationCluster::Pinnable),
1043-
PackageSolvingData::HolderHTLCOutput(ref outp) if outp.channel_type_features.supports_anchors_zero_fee_htlc_tx() => {
1044-
if outp.preimage.is_some() {
1045-
PackageMalleability::Malleable(AggregationCluster::Unpinnable)
1053+
PackageSolvingData::HolderHTLCOutput(ref outp) => {
1054+
let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
1055+
let free_commits = outp.channel_type_features.supports_anchor_zero_fee_commitments();
1056+
1057+
if free_htlcs || free_commits {
1058+
if outp.preimage.is_some() {
1059+
PackageMalleability::Malleable(AggregationCluster::Unpinnable)
1060+
} else {
1061+
PackageMalleability::Malleable(AggregationCluster::Pinnable)
1062+
}
10461063
} else {
1047-
PackageMalleability::Malleable(AggregationCluster::Pinnable)
1064+
PackageMalleability::Untractable
10481065
}
10491066
},
1050-
PackageSolvingData::HolderHTLCOutput(..) => PackageMalleability::Untractable,
10511067
PackageSolvingData::HolderFundingOutput(..) => PackageMalleability::Untractable,
10521068
}
10531069
}
@@ -1364,7 +1380,10 @@ impl PackageTemplate {
13641380
for (previous_output, input) in &self.inputs {
13651381
match input {
13661382
PackageSolvingData::HolderHTLCOutput(ref outp) => {
1367-
debug_assert!(outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
1383+
let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
1384+
let free_commitments =
1385+
outp.channel_type_features.supports_anchor_zero_fee_commitments();
1386+
debug_assert!(free_htlcs || free_commitments);
13681387
outp.get_htlc_descriptor(onchain_handler, &previous_output).map(|htlc| {
13691388
htlcs.get_or_insert_with(|| Vec::with_capacity(self.inputs.len())).push(htlc);
13701389
});
@@ -1559,8 +1578,14 @@ impl PackageTemplate {
15591578
#[rustfmt::skip]
15601579
pub(crate) fn requires_external_funding(&self) -> bool {
15611580
self.inputs.iter().find(|input| match input.1 {
1562-
PackageSolvingData::HolderFundingOutput(ref outp) => outp.channel_type_features.supports_anchors_zero_fee_htlc_tx(),
1563-
PackageSolvingData::HolderHTLCOutput(ref outp) => outp.channel_type_features.supports_anchors_zero_fee_htlc_tx(),
1581+
PackageSolvingData::HolderFundingOutput(ref outp) => {
1582+
outp.channel_type_features.supports_anchors_zero_fee_htlc_tx()
1583+
|| outp.channel_type_features.supports_anchor_zero_fee_commitments()
1584+
},
1585+
PackageSolvingData::HolderHTLCOutput(ref outp) => {
1586+
outp.channel_type_features.supports_anchors_zero_fee_htlc_tx()
1587+
|| outp.channel_type_features.supports_anchor_zero_fee_commitments()
1588+
},
15641589
_ => false,
15651590
}).is_some()
15661591
}
@@ -1796,16 +1821,13 @@ mod tests {
17961821

17971822
#[rustfmt::skip]
17981823
macro_rules! dumb_revk_output {
1799-
($is_counterparty_balance_on_anchors: expr) => {
1824+
() => {
18001825
{
18011826
let secp_ctx = Secp256k1::new();
18021827
let dumb_scalar = SecretKey::from_slice(&<Vec<u8>>::from_hex("0101010101010101010101010101010101010101010101010101010101010101").unwrap()[..]).unwrap();
18031828
let dumb_point = PublicKey::from_secret_key(&secp_ctx, &dumb_scalar);
18041829
let channel_parameters = ChannelTransactionParameters::test_dummy(0);
1805-
PackageSolvingData::RevokedOutput(RevokedOutput::build(
1806-
dumb_point, dumb_scalar, Amount::ZERO, $is_counterparty_balance_on_anchors,
1807-
channel_parameters, 0,
1808-
))
1830+
PackageSolvingData::RevokedOutput(RevokedOutput::build(dumb_point, dumb_scalar, Amount::ZERO, channel_parameters, 0))
18091831
}
18101832
}
18111833
}
@@ -2107,9 +2129,9 @@ mod tests {
21072129
#[test]
21082130
#[rustfmt::skip]
21092131
fn test_package_split_malleable() {
2110-
let revk_outp_one = dumb_revk_output!(false);
2111-
let revk_outp_two = dumb_revk_output!(false);
2112-
let revk_outp_three = dumb_revk_output!(false);
2132+
let revk_outp_one = dumb_revk_output!();
2133+
let revk_outp_two = dumb_revk_output!();
2134+
let revk_outp_three = dumb_revk_output!();
21132135

21142136
let mut package_one = PackageTemplate::build_package(fake_txid(1), 0, revk_outp_one, 1100);
21152137
let package_two = PackageTemplate::build_package(fake_txid(1), 1, revk_outp_two, 1100);
@@ -2141,7 +2163,7 @@ mod tests {
21412163

21422164
#[test]
21432165
fn test_package_timer() {
2144-
let revk_outp = dumb_revk_output!(false);
2166+
let revk_outp = dumb_revk_output!();
21452167

21462168
let mut package = PackageTemplate::build_package(fake_txid(1), 0, revk_outp, 1000);
21472169
assert_eq!(package.timer(), 0);
@@ -2165,7 +2187,7 @@ mod tests {
21652187
let weight_sans_output = (4 + 4 + 1 + 36 + 4 + 1 + 1 + 8 + 1) * WITNESS_SCALE_FACTOR as u64 + 2;
21662188

21672189
{
2168-
let revk_outp = dumb_revk_output!(false);
2190+
let revk_outp = dumb_revk_output!();
21692191
let package = PackageTemplate::build_package(fake_txid(1), 0, revk_outp, 0);
21702192
assert_eq!(package.package_weight(&ScriptBuf::new()), weight_sans_output + WEIGHT_REVOKED_OUTPUT);
21712193
}

0 commit comments

Comments
 (0)