Skip to content

Commit bc7490d

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 1549b74 commit bc7490d

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
@@ -777,7 +777,7 @@ impl FundingContribution {
777777
inputs.retain(|input| *input != existing);
778778
}
779779
for existing in existing_outputs {
780-
outputs.retain(|output| *output != *existing);
780+
outputs.retain(|output| output.script_pubkey != existing.script_pubkey);
781781
}
782782
if inputs.is_empty() && outputs.is_empty() {
783783
None

lightning/src/ln/splicing_tests.rs

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3766,11 +3766,11 @@ fn test_funding_contributed_splice_already_pending() {
37663766
)
37673767
.unwrap();
37683768

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

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

38093808
// Returns Err(APIMisuseError) and emits DiscardFunding for the non-duplicate parts of the second contribution
38103809
assert_eq!(
@@ -3824,11 +3823,10 @@ fn test_funding_contributed_splice_already_pending() {
38243823
if let FundingInfo::Contribution { inputs, outputs } = funding_info {
38253824
// The input is different, so it should be in the discard event
38263825
assert_eq!(*inputs, expected_inputs);
3827-
// The splice-out output is different (6000 vs 5000), so it should be in discard event
3828-
assert!(expected_outputs.contains(&second_splice_out));
3829-
assert!(!expected_outputs.contains(&first_splice_out));
3830-
// The different outputs should NOT be filtered out
3831-
assert_eq!(*outputs, expected_outputs);
3826+
// The splice-out output (different script_pubkey) survives filtering;
3827+
// the change output (same script_pubkey as first contribution) is filtered.
3828+
assert_eq!(outputs.len(), 1);
3829+
assert!(outputs.contains(&second_splice_out));
38323830
} else {
38333831
panic!("Expected FundingInfo::Contribution");
38343832
}
@@ -3921,14 +3919,26 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) {
39213919
let first_contribution =
39223920
funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
39233921

3924-
// Build second contribution with different UTXOs so inputs/outputs don't overlap
3922+
// Build second contribution with different UTXOs and a splice-out output using a different
3923+
// script_pubkey (node 1's address) so it survives script_pubkey-based filtering.
39253924
nodes[0].wallet_source.clear_utxos();
39263925
provide_utxo_reserves(&nodes, 1, splice_in_amount * 3);
3926+
let splice_out_output = TxOut {
3927+
value: Amount::from_sat(1_000),
3928+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
3929+
};
39273930

39283931
let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
39293932
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
3930-
let second_contribution =
3931-
funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
3933+
let second_contribution = funding_template
3934+
.splice_in_and_out_sync(
3935+
splice_in_amount,
3936+
vec![splice_out_output.clone()],
3937+
feerate,
3938+
FeeRate::MAX,
3939+
&wallet,
3940+
)
3941+
.unwrap();
39323942

39333943
// First funding_contributed - sets up the quiescent action and queues STFU
39343944
nodes[0]
@@ -3973,26 +3983,28 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) {
39733983
}
39743984
}
39753985

3976-
// Call funding_contributed with a different contribution (non-overlapping inputs/outputs).
3977-
// This hits the funding_negotiation path and returns DiscardFunding.
3978-
let (expected_inputs, expected_outputs) =
3979-
second_contribution.clone().into_contributed_inputs_and_outputs();
3986+
// Call funding_contributed with the second contribution. Inputs don't overlap (different
3987+
// UTXOs) so they all survive. The splice-out output (different script_pubkey) survives
3988+
// while the change output (same script_pubkey as first contribution) is filtered.
3989+
let expected_inputs: Vec<_> = second_contribution.contributed_inputs().collect();
39803990
assert_eq!(
39813991
nodes[0].node.funding_contributed(&channel_id, &node_id_1, second_contribution, None),
39823992
Err(APIError::APIMisuseError {
39833993
err: format!("Channel {} already has a pending funding contribution", channel_id),
39843994
})
39853995
);
39863996

3987-
// Assert DiscardFunding event with the non-duplicate inputs/outputs
39883997
let events = nodes[0].node.get_and_clear_pending_events();
39893998
assert_eq!(events.len(), 1, "{events:?}");
39903999
match &events[0] {
39914000
Event::DiscardFunding { channel_id: event_channel_id, funding_info } => {
39924001
assert_eq!(*event_channel_id, channel_id);
39934002
if let FundingInfo::Contribution { inputs, outputs } = funding_info {
4003+
// Inputs are unique (different UTXOs) so none are filtered.
39944004
assert_eq!(*inputs, expected_inputs);
3995-
assert_eq!(*outputs, expected_outputs);
4005+
// Only the splice-out output survives; the change output is filtered
4006+
// (same script_pubkey as first contribution's change).
4007+
assert_eq!(*outputs, vec![splice_out_output]);
39964008
} else {
39974009
panic!("Expected FundingInfo::Contribution");
39984010
}

0 commit comments

Comments
 (0)