Skip to content

Commit eaaf326

Browse files
jkczyzclaude
andcommitted
Fix output filtering in into_unique_contributions
Filter outputs by script_pubkey rather than full TxOut equality. Outputs reusing the same address as a prior round are still considered committed even if the value differs (e.g., different change amounts across RBF rounds with different feerates). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8ba5e9b commit eaaf326

2 files changed

Lines changed: 33 additions & 21 deletions

File tree

lightning/src/ln/funding.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,7 @@ impl FundingContribution {
775775
inputs.retain(|input| *input != existing);
776776
}
777777
for existing in existing_outputs {
778-
outputs.retain(|output| *output != *existing);
778+
outputs.retain(|output| output.script_pubkey != existing.script_pubkey);
779779
}
780780
if inputs.is_empty() && outputs.is_empty() {
781781
None

lightning/src/ln/splicing_tests.rs

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3641,11 +3641,11 @@ fn test_funding_contributed_splice_already_pending() {
36413641
)
36423642
.unwrap();
36433643

3644-
// Initiate a second splice with a DIFFERENT output to test that different outputs
3645-
// are included in DiscardFunding (not filtered out)
3644+
// Initiate a second splice with a DIFFERENT output (different script_pubkey) to test that
3645+
// non-overlapping outputs are included in DiscardFunding (not filtered out).
36463646
let second_splice_out = TxOut {
3647-
value: Amount::from_sat(6_000), // Different amount
3648-
script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_raw_hash(Hash::all_zeros())),
3647+
value: Amount::from_sat(6_000),
3648+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
36493649
};
36503650

36513651
// Clear UTXOs and add a LARGER one for the second contribution to ensure
@@ -3678,8 +3678,7 @@ fn test_funding_contributed_splice_already_pending() {
36783678
// Second funding_contributed with a different contribution - this should trigger
36793679
// DiscardFunding because there's already a pending quiescent action (splice contribution).
36803680
// Only inputs/outputs NOT in the existing contribution should be discarded.
3681-
let (expected_inputs, expected_outputs) =
3682-
second_contribution.clone().into_contributed_inputs_and_outputs();
3681+
let expected_inputs: Vec<_> = second_contribution.contributed_inputs().collect();
36833682

36843683
// Returns Err(APIMisuseError) and emits DiscardFunding for the non-duplicate parts of the second contribution
36853684
assert_eq!(
@@ -3699,11 +3698,10 @@ fn test_funding_contributed_splice_already_pending() {
36993698
if let FundingInfo::Contribution { inputs, outputs } = funding_info {
37003699
// The input is different, so it should be in the discard event
37013700
assert_eq!(*inputs, expected_inputs);
3702-
// The splice-out output is different (6000 vs 5000), so it should be in discard event
3703-
assert!(expected_outputs.contains(&second_splice_out));
3704-
assert!(!expected_outputs.contains(&first_splice_out));
3705-
// The different outputs should NOT be filtered out
3706-
assert_eq!(*outputs, expected_outputs);
3701+
// The splice-out output (different script_pubkey) survives filtering;
3702+
// the change output (same script_pubkey as first contribution) is filtered.
3703+
assert_eq!(outputs.len(), 1);
3704+
assert!(outputs.contains(&second_splice_out));
37073705
} else {
37083706
panic!("Expected FundingInfo::Contribution");
37093707
}
@@ -3796,14 +3794,26 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) {
37963794
let first_contribution =
37973795
funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
37983796

3799-
// Build second contribution with different UTXOs so inputs/outputs don't overlap
3797+
// Build second contribution with different UTXOs and a splice-out output using a different
3798+
// script_pubkey (node 1's address) so it survives script_pubkey-based filtering.
38003799
nodes[0].wallet_source.clear_utxos();
38013800
provide_utxo_reserves(&nodes, 1, splice_in_amount * 3);
3801+
let splice_out_output = TxOut {
3802+
value: Amount::from_sat(1_000),
3803+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
3804+
};
38023805

38033806
let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
38043807
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
3805-
let second_contribution =
3806-
funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
3808+
let second_contribution = funding_template
3809+
.splice_in_and_out_sync(
3810+
splice_in_amount,
3811+
vec![splice_out_output.clone()],
3812+
feerate,
3813+
FeeRate::MAX,
3814+
&wallet,
3815+
)
3816+
.unwrap();
38073817

38083818
// First funding_contributed - sets up the quiescent action and queues STFU
38093819
nodes[0]
@@ -3848,26 +3858,28 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) {
38483858
}
38493859
}
38503860

3851-
// Call funding_contributed with a different contribution (non-overlapping inputs/outputs).
3852-
// This hits the funding_negotiation path and returns DiscardFunding.
3853-
let (expected_inputs, expected_outputs) =
3854-
second_contribution.clone().into_contributed_inputs_and_outputs();
3861+
// Call funding_contributed with the second contribution. Inputs don't overlap (different
3862+
// UTXOs) so they all survive. The splice-out output (different script_pubkey) survives
3863+
// while the change output (same script_pubkey as first contribution) is filtered.
3864+
let expected_inputs: Vec<_> = second_contribution.contributed_inputs().collect();
38553865
assert_eq!(
38563866
nodes[0].node.funding_contributed(&channel_id, &node_id_1, second_contribution, None),
38573867
Err(APIError::APIMisuseError {
38583868
err: format!("Channel {} already has a pending funding contribution", channel_id),
38593869
})
38603870
);
38613871

3862-
// Assert DiscardFunding event with the non-duplicate inputs/outputs
38633872
let events = nodes[0].node.get_and_clear_pending_events();
38643873
assert_eq!(events.len(), 1, "{events:?}");
38653874
match &events[0] {
38663875
Event::DiscardFunding { channel_id: event_channel_id, funding_info } => {
38673876
assert_eq!(*event_channel_id, channel_id);
38683877
if let FundingInfo::Contribution { inputs, outputs } = funding_info {
3878+
// Inputs are unique (different UTXOs) so none are filtered.
38693879
assert_eq!(*inputs, expected_inputs);
3870-
assert_eq!(*outputs, expected_outputs);
3880+
// Only the splice-out output survives; the change output is filtered
3881+
// (same script_pubkey as first contribution's change).
3882+
assert_eq!(*outputs, vec![splice_out_output]);
38713883
} else {
38723884
panic!("Expected FundingInfo::Contribution");
38733885
}

0 commit comments

Comments
 (0)