Skip to content

Commit fa558da

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 7cbcf4f commit fa558da

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
@@ -3757,11 +3757,11 @@ fn test_funding_contributed_splice_already_pending() {
37573757
)
37583758
.unwrap();
37593759

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

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

38003799
// Returns Err(APIMisuseError) and emits DiscardFunding for the non-duplicate parts of the second contribution
38013800
assert_eq!(
@@ -3815,11 +3814,10 @@ fn test_funding_contributed_splice_already_pending() {
38153814
if let FundingInfo::Contribution { inputs, outputs } = funding_info {
38163815
// The input is different, so it should be in the discard event
38173816
assert_eq!(*inputs, expected_inputs);
3818-
// The splice-out output is different (6000 vs 5000), so it should be in discard event
3819-
assert!(expected_outputs.contains(&second_splice_out));
3820-
assert!(!expected_outputs.contains(&first_splice_out));
3821-
// The different outputs should NOT be filtered out
3822-
assert_eq!(*outputs, expected_outputs);
3817+
// The splice-out output (different script_pubkey) survives filtering;
3818+
// the change output (same script_pubkey as first contribution) is filtered.
3819+
assert_eq!(outputs.len(), 1);
3820+
assert!(outputs.contains(&second_splice_out));
38233821
} else {
38243822
panic!("Expected FundingInfo::Contribution");
38253823
}
@@ -3912,14 +3910,26 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) {
39123910
let first_contribution =
39133911
funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
39143912

3915-
// Build second contribution with different UTXOs so inputs/outputs don't overlap
3913+
// Build second contribution with different UTXOs and a splice-out output using a different
3914+
// script_pubkey (node 1's address) so it survives script_pubkey-based filtering.
39163915
nodes[0].wallet_source.clear_utxos();
39173916
provide_utxo_reserves(&nodes, 1, splice_in_amount * 3);
3917+
let splice_out_output = TxOut {
3918+
value: Amount::from_sat(1_000),
3919+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
3920+
};
39183921

39193922
let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
39203923
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
3921-
let second_contribution =
3922-
funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
3924+
let second_contribution = funding_template
3925+
.splice_in_and_out_sync(
3926+
splice_in_amount,
3927+
vec![splice_out_output.clone()],
3928+
feerate,
3929+
FeeRate::MAX,
3930+
&wallet,
3931+
)
3932+
.unwrap();
39233933

39243934
// First funding_contributed - sets up the quiescent action and queues STFU
39253935
nodes[0]
@@ -3964,26 +3974,28 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) {
39643974
}
39653975
}
39663976

3967-
// Call funding_contributed with a different contribution (non-overlapping inputs/outputs).
3968-
// This hits the funding_negotiation path and returns DiscardFunding.
3969-
let (expected_inputs, expected_outputs) =
3970-
second_contribution.clone().into_contributed_inputs_and_outputs();
3977+
// Call funding_contributed with the second contribution. Inputs don't overlap (different
3978+
// UTXOs) so they all survive. The splice-out output (different script_pubkey) survives
3979+
// while the change output (same script_pubkey as first contribution) is filtered.
3980+
let expected_inputs: Vec<_> = second_contribution.contributed_inputs().collect();
39713981
assert_eq!(
39723982
nodes[0].node.funding_contributed(&channel_id, &node_id_1, second_contribution, None),
39733983
Err(APIError::APIMisuseError {
39743984
err: format!("Channel {} already has a pending funding contribution", channel_id),
39753985
})
39763986
);
39773987

3978-
// Assert DiscardFunding event with the non-duplicate inputs/outputs
39793988
let events = nodes[0].node.get_and_clear_pending_events();
39803989
assert_eq!(events.len(), 1, "{events:?}");
39813990
match &events[0] {
39823991
Event::DiscardFunding { channel_id: event_channel_id, funding_info } => {
39833992
assert_eq!(*event_channel_id, channel_id);
39843993
if let FundingInfo::Contribution { inputs, outputs } = funding_info {
3994+
// Inputs are unique (different UTXOs) so none are filtered.
39853995
assert_eq!(*inputs, expected_inputs);
3986-
assert_eq!(*outputs, expected_outputs);
3996+
// Only the splice-out output survives; the change output is filtered
3997+
// (same script_pubkey as first contribution's change).
3998+
assert_eq!(*outputs, vec![splice_out_output]);
39873999
} else {
39884000
panic!("Expected FundingInfo::Contribution");
39894001
}

0 commit comments

Comments
 (0)