Skip to content

Commit eba82b8

Browse files
jkczyzclaude
andcommitted
Reject counterparty RBF with non-confirming feerate after several attempts
After a few RBF attempts, the counterparty should be RBFing to a feerate that will actually confirm. Reject attempts with feerates below the fee estimator's NonAnchorChannelFee target to prevent the counterparty from exhausting the RBF budget at low feerates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 187037c commit eba82b8

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12682,6 +12682,18 @@ where
1268212682
return Err(ChannelError::Abort(AbortReason::InsufficientRbfFeerate));
1268312683
}
1268412684

12685+
// After several RBF attempts, require the feerate to be high enough to confirm.
12686+
// This prevents the counterparty from exhausting the RBF budget at low feerates
12687+
// that won't lead to timely confirmation.
12688+
const MAX_LOW_FEERATE_RBF_ATTEMPTS: usize = 3;
12689+
if pending_splice.negotiated_candidates.len() > MAX_LOW_FEERATE_RBF_ATTEMPTS {
12690+
let min_feerate =
12691+
fee_estimator.bounded_sat_per_1000_weight(ConfirmationTarget::NonAnchorChannelFee);
12692+
if new_feerate < min_feerate {
12693+
return Err(ChannelError::Abort(AbortReason::InsufficientRbfFeerate));
12694+
}
12695+
}
12696+
1268512697
let their_funding_contribution = match msg.funding_output_contribution {
1268612698
Some(value) => SignedAmount::from_sat(value),
1268712699
None => SignedAmount::ZERO,

lightning/src/ln/splicing_tests.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6305,3 +6305,76 @@ fn test_splice_revalidation_at_quiescence() {
63056305

63066306
expect_splice_failed_events(&nodes[0], &channel_id, contribution);
63076307
}
6308+
6309+
#[test]
6310+
fn test_splice_rbf_rejects_low_feerate_after_several_attempts() {
6311+
// After several RBF attempts, the counterparty's RBF feerate must be high enough to
6312+
// confirm (per the fee estimator). Early attempts at low feerates are accepted, but
6313+
// once the threshold is crossed and the fee estimator expects a higher feerate, the
6314+
// attempt is rejected.
6315+
let chanmon_cfgs = create_chanmon_cfgs(2);
6316+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
6317+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
6318+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
6319+
6320+
let node_id_0 = nodes[0].node.get_our_node_id();
6321+
let node_id_1 = nodes[1].node.get_our_node_id();
6322+
6323+
let initial_channel_value_sat = 100_000;
6324+
let (_, _, channel_id, _) =
6325+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
6326+
6327+
let added_value = Amount::from_sat(50_000);
6328+
provide_utxo_reserves(&nodes, 2, added_value * 2);
6329+
6330+
// Round 0: Initial splice-in at floor feerate (253).
6331+
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
6332+
let (_, new_funding_script) =
6333+
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
6334+
6335+
// Feerate progression: 253 → 264 → 275 → 287 → 300
6336+
let feerate_1 = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24);
6337+
let feerate_2 = (feerate_1 * 25).div_ceil(24);
6338+
let feerate_3 = (feerate_2 * 25).div_ceil(24);
6339+
let feerate_4 = (feerate_3 * 25).div_ceil(24);
6340+
6341+
// Rounds 1-3: RBF at minimum bump. Accepted (at or below threshold).
6342+
for feerate in [feerate_1, feerate_2, feerate_3] {
6343+
provide_utxo_reserves(&nodes, 2, added_value * 2);
6344+
let rbf_feerate = FeeRate::from_sat_per_kwu(feerate);
6345+
let contribution =
6346+
do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
6347+
complete_rbf_handshake(&nodes[0], &nodes[1]);
6348+
complete_interactive_funding_negotiation(
6349+
&nodes[0],
6350+
&nodes[1],
6351+
channel_id,
6352+
contribution,
6353+
new_funding_script.clone(),
6354+
);
6355+
let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
6356+
assert!(splice_locked.is_none());
6357+
expect_splice_pending_event(&nodes[0], &node_id_1);
6358+
expect_splice_pending_event(&nodes[1], &node_id_0);
6359+
}
6360+
6361+
// Now 4 negotiated candidates (round 0 + rounds 1-3). Bump the fee estimator on node 1
6362+
// (the RBF receiver) so the next minimum RBF feerate (300) is below it.
6363+
let high_feerate = 1000;
6364+
*chanmon_cfgs[1].fee_estimator.sat_per_kw.lock().unwrap() = high_feerate;
6365+
6366+
// Round 4: RBF at minimum bump (300). Should be rejected because 300 < 1000.
6367+
provide_utxo_reserves(&nodes, 2, added_value * 2);
6368+
let rbf_feerate_3 = FeeRate::from_sat_per_kwu(feerate_4);
6369+
let _contribution =
6370+
do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate_3);
6371+
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
6372+
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
6373+
let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
6374+
nodes[0].node.handle_stfu(node_id_1, &stfu_1);
6375+
6376+
// Node 0 sends tx_init_rbf. Node 1 rejects the low feerate after the threshold.
6377+
let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1);
6378+
nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf);
6379+
get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0);
6380+
}

0 commit comments

Comments
 (0)