Skip to content

Commit 1434e9c

Browse files
authored
Merge pull request #3564 from TheBlueMatt/2025-01-revoked-htlc-not-pinnable
Set correct `counterparty_spendable_height` on c.p. revoked HTLCs
2 parents 31b32c5 + 6c57a1f commit 1434e9c

File tree

4 files changed

+88
-54
lines changed

4 files changed

+88
-54
lines changed

Diff for: lightning/src/chain/channelmonitor.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -3564,11 +3564,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
35643564
return (claimable_outpoints, to_counterparty_output_info);
35653565
}
35663566
let revk_htlc_outp = RevokedHTLCOutput::build(per_commitment_point, self.counterparty_commitment_params.counterparty_delayed_payment_base_key, self.counterparty_commitment_params.counterparty_htlc_base_key, per_commitment_key, htlc.amount_msat / 1000, htlc.clone(), &self.onchain_tx_handler.channel_transaction_parameters.channel_type_features);
3567+
let counterparty_spendable_height = if htlc.offered {
3568+
htlc.cltv_expiry
3569+
} else {
3570+
height
3571+
};
35673572
let justice_package = PackageTemplate::build_package(
35683573
commitment_txid,
35693574
transaction_output_index,
35703575
PackageSolvingData::RevokedHTLCOutput(revk_htlc_outp),
3571-
htlc.cltv_expiry,
3576+
counterparty_spendable_height,
35723577
);
35733578
claimable_outpoints.push(justice_package);
35743579
}

Diff for: lightning/src/chain/package.rs

+29-17
Original file line numberDiff line numberDiff line change
@@ -699,8 +699,13 @@ impl PackageSolvingData {
699699
match self {
700700
PackageSolvingData::RevokedOutput(RevokedOutput { .. }) =>
701701
PackageMalleability::Malleable(AggregationCluster::Unpinnable),
702-
PackageSolvingData::RevokedHTLCOutput(..) =>
703-
PackageMalleability::Malleable(AggregationCluster::Pinnable),
702+
PackageSolvingData::RevokedHTLCOutput(RevokedHTLCOutput { htlc, .. }) => {
703+
if htlc.offered {
704+
PackageMalleability::Malleable(AggregationCluster::Unpinnable)
705+
} else {
706+
PackageMalleability::Malleable(AggregationCluster::Pinnable)
707+
}
708+
},
704709
PackageSolvingData::CounterpartyOfferedHTLCOutput(..) =>
705710
PackageMalleability::Malleable(AggregationCluster::Unpinnable),
706711
PackageSolvingData::CounterpartyReceivedHTLCOutput(..) =>
@@ -771,10 +776,12 @@ pub struct PackageTemplate {
771776
/// Block height at which our counterparty can potentially claim this output as well (assuming
772777
/// they have the keys or information required to do so).
773778
///
774-
/// This is used primarily by external consumers to decide when an output becomes "pinnable"
775-
/// because the counterparty can potentially spend it. It is also used internally by
776-
/// [`Self::get_height_timer`] to identify when an output must be claimed by, depending on the
777-
/// type of output.
779+
/// This is used primarily to decide when an output becomes "pinnable" because the counterparty
780+
/// can potentially spend it. It is also used internally by [`Self::get_height_timer`] to
781+
/// identify when an output must be claimed by, depending on the type of output.
782+
///
783+
/// Note that for revoked counterparty HTLC outputs the value may be zero in some cases where
784+
/// we upgraded from LDK 0.1 or prior.
778785
counterparty_spendable_height: u32,
779786
// Cache of package feerate committed at previous (re)broadcast. If bumping resources
780787
// (either claimed output value or external utxo), it will keep increasing until holder
@@ -834,17 +841,17 @@ impl PackageTemplate {
834841
// Now check that we only merge packages if they are both unpinnable or both
835842
// pinnable.
836843
let self_pinnable = self_cluster == AggregationCluster::Pinnable ||
837-
self.counterparty_spendable_height() <= cur_height + COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE;
844+
self.counterparty_spendable_height <= cur_height + COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE;
838845
let other_pinnable = other_cluster == AggregationCluster::Pinnable ||
839-
other.counterparty_spendable_height() <= cur_height + COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE;
846+
other.counterparty_spendable_height <= cur_height + COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE;
840847
if self_pinnable && other_pinnable {
841848
return true;
842849
}
843850

844851
let self_unpinnable = self_cluster == AggregationCluster::Unpinnable &&
845-
self.counterparty_spendable_height() > cur_height + COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE;
852+
self.counterparty_spendable_height > cur_height + COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE;
846853
let other_unpinnable = other_cluster == AggregationCluster::Unpinnable &&
847-
other.counterparty_spendable_height() > cur_height + COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE;
854+
other.counterparty_spendable_height > cur_height + COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE;
848855
if self_unpinnable && other_unpinnable {
849856
return true;
850857
}
@@ -855,13 +862,6 @@ impl PackageTemplate {
855862
pub(crate) fn is_malleable(&self) -> bool {
856863
matches!(self.malleability, PackageMalleability::Malleable(..))
857864
}
858-
/// The height at which our counterparty may be able to spend this output.
859-
///
860-
/// This is an important limit for aggregation as after this height our counterparty may be
861-
/// able to pin transactions spending this output in the mempool.
862-
pub(crate) fn counterparty_spendable_height(&self) -> u32 {
863-
self.counterparty_spendable_height
864-
}
865865
pub(crate) fn previous_feerate(&self) -> u64 {
866866
self.feerate_previous
867867
}
@@ -1225,6 +1225,18 @@ impl Readable for PackageTemplate {
12251225
(4, _height_original, option), // Written with a dummy value since 0.1
12261226
(6, height_timer, option),
12271227
});
1228+
for (_, input) in &inputs {
1229+
if let PackageSolvingData::RevokedHTLCOutput(RevokedHTLCOutput { htlc, .. }) = input {
1230+
// LDK versions through 0.1 set the wrong counterparty_spendable_height for
1231+
// non-offered revoked HTLCs (ie HTLCs we sent to our counterparty which they can
1232+
// claim with a preimage immediately). Here we detect this and reset the value to
1233+
// zero, as the value is unused except for merging decisions which doesn't care
1234+
// about any values below the current height.
1235+
if !htlc.offered && htlc.cltv_expiry == counterparty_spendable_height {
1236+
counterparty_spendable_height = 0;
1237+
}
1238+
}
1239+
}
12281240
Ok(PackageTemplate {
12291241
inputs,
12301242
malleability,

Diff for: lightning/src/ln/functional_tests.rs

+46-33
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::chain;
1515
use crate::chain::{ChannelMonitorUpdateStatus, Confirm, Listen, Watch};
1616
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
1717
use crate::chain::channelmonitor;
18-
use crate::chain::channelmonitor::{Balance, ChannelMonitorUpdateStep, CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY};
18+
use crate::chain::channelmonitor::{Balance, ChannelMonitorUpdateStep, CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY, COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE};
1919
use crate::chain::transaction::OutPoint;
2020
use crate::sign::{ecdsa::EcdsaChannelSigner, EntropySource, OutputSpender, SignerProvider};
2121
use crate::events::bump_transaction::WalletSource;
@@ -2631,14 +2631,12 @@ fn test_justice_tx_htlc_timeout() {
26312631
mine_transaction(&nodes[1], &revoked_local_txn[0]);
26322632
{
26332633
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
2634-
// The unpinnable, revoked to_self output, and the pinnable, revoked htlc output will
2635-
// be claimed in separate transactions.
2636-
assert_eq!(node_txn.len(), 2);
2637-
for tx in node_txn.iter() {
2638-
assert_eq!(tx.input.len(), 1);
2639-
check_spends!(tx, revoked_local_txn[0]);
2640-
}
2641-
assert_ne!(node_txn[0].input[0].previous_output, node_txn[1].input[0].previous_output);
2634+
// The revoked HTLC output is not pinnable for another `TEST_FINAL_CLTV` blocks, and is
2635+
// thus claimed in the same transaction with the revoked to_self output.
2636+
assert_eq!(node_txn.len(), 1);
2637+
assert_eq!(node_txn[0].input.len(), 2);
2638+
check_spends!(node_txn[0], revoked_local_txn[0]);
2639+
assert_ne!(node_txn[0].input[0].previous_output, node_txn[0].input[1].previous_output);
26422640
node_txn.clear();
26432641
}
26442642
check_added_monitors!(nodes[1], 1);
@@ -2858,28 +2856,26 @@ fn claim_htlc_outputs() {
28582856
assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
28592857

28602858
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
2861-
assert_eq!(node_txn.len(), 2); // Two penalty transactions:
2862-
assert_eq!(node_txn[0].input.len(), 1); // Claims the unpinnable, revoked output.
2863-
assert_eq!(node_txn[1].input.len(), 2); // Claims both pinnable, revoked HTLC outputs separately.
2864-
check_spends!(node_txn[0], revoked_local_txn[0]);
2865-
check_spends!(node_txn[1], revoked_local_txn[0]);
2866-
assert_ne!(node_txn[0].input[0].previous_output, node_txn[1].input[0].previous_output);
2867-
assert_ne!(node_txn[0].input[0].previous_output, node_txn[1].input[1].previous_output);
2868-
assert_ne!(node_txn[1].input[0].previous_output, node_txn[1].input[1].previous_output);
2859+
assert_eq!(node_txn.len(), 2); // ChannelMonitor: penalty txn
2860+
2861+
// The ChannelMonitor should claim the accepted HTLC output separately from the offered
2862+
// HTLC and to_self outputs.
2863+
let accepted_claim = node_txn.iter().filter(|tx| tx.input.len() == 1).next().unwrap();
2864+
let offered_to_self_claim = node_txn.iter().filter(|tx| tx.input.len() == 2).next().unwrap();
2865+
check_spends!(accepted_claim, revoked_local_txn[0]);
2866+
check_spends!(offered_to_self_claim, revoked_local_txn[0]);
2867+
assert_eq!(accepted_claim.input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
28692868

28702869
let mut witness_lens = BTreeSet::new();
2871-
witness_lens.insert(node_txn[0].input[0].witness.last().unwrap().len());
2872-
witness_lens.insert(node_txn[1].input[0].witness.last().unwrap().len());
2873-
witness_lens.insert(node_txn[1].input[1].witness.last().unwrap().len());
2874-
assert_eq!(witness_lens.len(), 3);
2870+
witness_lens.insert(offered_to_self_claim.input[0].witness.last().unwrap().len());
2871+
witness_lens.insert(offered_to_self_claim.input[1].witness.last().unwrap().len());
2872+
assert_eq!(witness_lens.len(), 2);
28752873
assert_eq!(*witness_lens.iter().skip(0).next().unwrap(), 77); // revoked to_local
2876-
assert_eq!(*witness_lens.iter().skip(1).next().unwrap(), OFFERED_HTLC_SCRIPT_WEIGHT); // revoked offered HTLC
2877-
assert_eq!(*witness_lens.iter().skip(2).next().unwrap(), ACCEPTED_HTLC_SCRIPT_WEIGHT); // revoked received HTLC
2874+
assert_eq!(*witness_lens.iter().skip(1).next().unwrap(), OFFERED_HTLC_SCRIPT_WEIGHT);
28782875

2879-
// Finally, mine the penalty transactions and check that we get an HTLC failure after
2876+
// Finally, mine the penalty transaction and check that we get an HTLC failure after
28802877
// ANTI_REORG_DELAY confirmations.
2881-
mine_transaction(&nodes[1], &node_txn[0]);
2882-
mine_transaction(&nodes[1], &node_txn[1]);
2878+
mine_transaction(&nodes[1], accepted_claim);
28832879
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1);
28842880
expect_payment_failed!(nodes[1], payment_hash_2, false);
28852881
}
@@ -5042,8 +5038,7 @@ fn test_static_spendable_outputs_timeout_tx() {
50425038
check_spends!(spend_txn[2], node_txn[0], commitment_tx[0]); // All outputs
50435039
}
50445040

5045-
#[test]
5046-
fn test_static_spendable_outputs_justice_tx_revoked_commitment_tx() {
5041+
fn do_test_static_spendable_outputs_justice_tx_revoked_commitment_tx(split_tx: bool) {
50475042
let chanmon_cfgs = create_chanmon_cfgs(2);
50485043
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
50495044
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
@@ -5059,20 +5054,28 @@ fn test_static_spendable_outputs_justice_tx_revoked_commitment_tx() {
50595054

50605055
claim_payment(&nodes[0], &vec!(&nodes[1])[..], payment_preimage);
50615056

5057+
if split_tx {
5058+
connect_blocks(&nodes[1], TEST_FINAL_CLTV - COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE + 1);
5059+
}
5060+
50625061
mine_transaction(&nodes[1], &revoked_local_txn[0]);
50635062
check_closed_broadcast!(nodes[1], true);
50645063
check_added_monitors!(nodes[1], 1);
50655064
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed, [nodes[0].node.get_our_node_id()], 100000);
50665065

5067-
// The unpinnable, revoked to_self output and the pinnable, revoked HTLC output will be claimed
5068-
// in separate transactions.
5066+
// If the HTLC expires in more than COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE blocks, we'll
5067+
// claim both the revoked and HTLC outputs in one transaction, otherwise we'll split them as we
5068+
// consider the HTLC output as pinnable and want to claim pinnable and unpinnable outputs
5069+
// separately.
50695070
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
5070-
assert_eq!(node_txn.len(), 2);
5071+
assert_eq!(node_txn.len(), if split_tx { 2 } else { 1 });
50715072
for tx in node_txn.iter() {
5072-
assert_eq!(tx.input.len(), 1);
5073+
assert_eq!(tx.input.len(), if split_tx { 1 } else { 2 });
50735074
check_spends!(tx, revoked_local_txn[0]);
50745075
}
5075-
assert_ne!(node_txn[0].input[0].previous_output, node_txn[1].input[0].previous_output);
5076+
if split_tx {
5077+
assert_ne!(node_txn[0].input[0].previous_output, node_txn[1].input[0].previous_output);
5078+
}
50765079

50775080
mine_transaction(&nodes[1], &node_txn[0]);
50785081
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1);
@@ -5082,6 +5085,12 @@ fn test_static_spendable_outputs_justice_tx_revoked_commitment_tx() {
50825085
check_spends!(spend_txn[0], node_txn[0]);
50835086
}
50845087

5088+
#[test]
5089+
fn test_static_spendable_outputs_justice_tx_revoked_commitment_tx() {
5090+
do_test_static_spendable_outputs_justice_tx_revoked_commitment_tx(true);
5091+
do_test_static_spendable_outputs_justice_tx_revoked_commitment_tx(false);
5092+
}
5093+
50855094
#[test]
50865095
fn test_static_spendable_outputs_justice_tx_revoked_htlc_timeout_tx() {
50875096
let mut chanmon_cfgs = create_chanmon_cfgs(2);
@@ -5114,6 +5123,10 @@ fn test_static_spendable_outputs_justice_tx_revoked_htlc_timeout_tx() {
51145123
check_spends!(revoked_htlc_txn[0], revoked_local_txn[0]);
51155124
assert_ne!(revoked_htlc_txn[0].lock_time, LockTime::ZERO); // HTLC-Timeout
51165125

5126+
// In order to connect `revoked_htlc_txn[0]` we must first advance the chain by
5127+
// `TEST_FINAL_CLTV` blocks as otherwise the transaction is consensus-invalid due to its
5128+
// locktime.
5129+
connect_blocks(&nodes[1], TEST_FINAL_CLTV);
51175130
// B will generate justice tx from A's revoked commitment/HTLC tx
51185131
connect_block(&nodes[1], &create_dummy_block(nodes[1].best_block_hash(), 42, vec![revoked_local_txn[0].clone(), revoked_htlc_txn[0].clone()]));
51195132
check_closed_broadcast!(nodes[1], true);

Diff for: lightning/src/ln/monitor_tests.rs

+7-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//! Further functional tests which test blockchain reorganizations.
1111
1212
use crate::sign::{ecdsa::EcdsaChannelSigner, OutputSpender, SpendableOutputDescriptor};
13-
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, ARCHIVAL_DELAY_BLOCKS,LATENCY_GRACE_PERIOD_BLOCKS, Balance, BalanceSource, ChannelMonitorUpdateStep};
13+
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, ARCHIVAL_DELAY_BLOCKS,LATENCY_GRACE_PERIOD_BLOCKS, COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE, Balance, BalanceSource, ChannelMonitorUpdateStep};
1414
use crate::chain::transaction::OutPoint;
1515
use crate::chain::chaininterface::{ConfirmationTarget, LowerBoundedFeeEstimator, compute_feerate_sat_per_1000_weight};
1616
use crate::events::bump_transaction::{BumpTransactionEvent, WalletSource};
@@ -1734,6 +1734,12 @@ fn do_test_revoked_counterparty_htlc_tx_balances(anchors: bool) {
17341734
assert_eq!(revoked_htlc_success.lock_time, LockTime::ZERO);
17351735
assert_ne!(revoked_htlc_timeout.lock_time, LockTime::ZERO);
17361736

1737+
// First connect blocks until the HTLC expires with
1738+
// `COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE` blocks, making us consider all the HTLCs
1739+
// pinnable claims, which the remainder of the test assumes.
1740+
connect_blocks(&nodes[0], TEST_FINAL_CLTV - COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE);
1741+
expect_pending_htlcs_forwardable_and_htlc_handling_failed_ignore!(&nodes[0],
1742+
[HTLCDestination::FailedPayment { payment_hash: failed_payment_hash }]);
17371743
// A will generate justice tx from B's revoked commitment/HTLC tx
17381744
mine_transaction(&nodes[0], &revoked_local_txn[0]);
17391745
check_closed_broadcast!(nodes[0], true);
@@ -1846,8 +1852,6 @@ fn do_test_revoked_counterparty_htlc_tx_balances(anchors: bool) {
18461852
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
18471853

18481854
connect_blocks(&nodes[0], revoked_htlc_timeout.lock_time.to_consensus_u32() - nodes[0].best_block_info().1);
1849-
expect_pending_htlcs_forwardable_and_htlc_handling_failed_ignore!(&nodes[0],
1850-
[HTLCDestination::FailedPayment { payment_hash: failed_payment_hash }]);
18511855
// As time goes on A may split its revocation claim transaction into multiple.
18521856
let as_fewer_input_rbf = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
18531857
for tx in as_fewer_input_rbf.iter() {

0 commit comments

Comments
 (0)