Skip to content

Commit 08c05f6

Browse files
jkczyzclaude
andcommitted
Add test for sequential RBF splice attempts
Add test_splice_rbf_sequential that exercises three consecutive RBF rounds on the same splice (initial → RBF #1 → RBF #2) to verify: - Each round requires the 25/24 feerate increase (253 → 264 → 275) - DiscardFunding events reference the correct funding txid from each replaced candidate - The final RBF splice can be mined and splice_locked successfully Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d1c3e1 commit 08c05f6

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

lightning/src/ln/splicing_tests.rs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5030,3 +5030,216 @@ fn test_splice_rbf_recontributes_feerate_too_high() {
50305030
other => panic!("Expected HandleError/DisconnectPeerWithWarning, got {:?}", other),
50315031
}
50325032
}
5033+
5034+
#[test]
5035+
fn test_splice_rbf_sequential() {
5036+
// Three consecutive RBF rounds on the same splice (initial → RBF #1 → RBF #2).
5037+
// Node 0 is the quiescence initiator; node 1 is the acceptor with no contribution.
5038+
// Verifies:
5039+
// - Each round satisfies the 25/24 feerate rule
5040+
// - DiscardFunding events reference the correct txids from previous rounds
5041+
// - The final RBF can be mined and splice_locked successfully
5042+
let chanmon_cfgs = create_chanmon_cfgs(2);
5043+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
5044+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
5045+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
5046+
5047+
let node_id_0 = nodes[0].node.get_our_node_id();
5048+
let node_id_1 = nodes[1].node.get_our_node_id();
5049+
5050+
let initial_channel_value_sat = 100_000;
5051+
let (_, _, channel_id, _) =
5052+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
5053+
5054+
let added_value = Amount::from_sat(50_000);
5055+
provide_utxo_reserves(&nodes, 2, added_value * 2);
5056+
5057+
// Save the pre-splice funding outpoint.
5058+
let original_funding_outpoint = nodes[0]
5059+
.chain_monitor
5060+
.chain_monitor
5061+
.get_monitor(channel_id)
5062+
.map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script()))
5063+
.unwrap();
5064+
5065+
// --- Round 0: Initial splice-in from node 0 at floor feerate (253). ---
5066+
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
5067+
let (splice_tx_0, new_funding_script) =
5068+
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
5069+
5070+
// Feerate progression: 253 → ceil(253*25/24) = 264 → ceil(264*25/24) = 275
5071+
let feerate_1_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25 + 23) / 24; // 264
5072+
let feerate_2_sat_per_kwu = (feerate_1_sat_per_kwu * 25 + 23) / 24; // 275
5073+
5074+
// --- Round 1: RBF #1 at feerate 264. ---
5075+
provide_utxo_reserves(&nodes, 2, added_value * 2);
5076+
5077+
let rbf_feerate_1 = FeeRate::from_sat_per_kwu(feerate_1_sat_per_kwu);
5078+
let funding_template =
5079+
nodes[0].node.rbf_channel(&channel_id, &node_id_1, rbf_feerate_1).unwrap();
5080+
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
5081+
let funding_contribution_1 = funding_template.splice_in_sync(added_value, &wallet).unwrap();
5082+
nodes[0]
5083+
.node
5084+
.funding_contributed(&channel_id, &node_id_1, funding_contribution_1.clone(), None)
5085+
.unwrap();
5086+
5087+
let stfu_a = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
5088+
nodes[1].node.handle_stfu(node_id_0, &stfu_a);
5089+
let stfu_b = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
5090+
nodes[0].node.handle_stfu(node_id_1, &stfu_b);
5091+
5092+
let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1);
5093+
assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, feerate_1_sat_per_kwu as u32);
5094+
nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf);
5095+
let tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0);
5096+
nodes[0].node.handle_tx_ack_rbf(node_id_1, &tx_ack_rbf);
5097+
5098+
complete_interactive_funding_negotiation(
5099+
&nodes[0],
5100+
&nodes[1],
5101+
channel_id,
5102+
funding_contribution_1,
5103+
new_funding_script.clone(),
5104+
);
5105+
let (splice_tx_1, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
5106+
assert!(splice_locked.is_none());
5107+
expect_splice_pending_event(&nodes[0], &node_id_1);
5108+
expect_splice_pending_event(&nodes[1], &node_id_0);
5109+
5110+
// --- Round 2: RBF #2 at feerate 275. ---
5111+
provide_utxo_reserves(&nodes, 2, added_value * 2);
5112+
5113+
let rbf_feerate_2 = FeeRate::from_sat_per_kwu(feerate_2_sat_per_kwu);
5114+
let funding_template =
5115+
nodes[0].node.rbf_channel(&channel_id, &node_id_1, rbf_feerate_2).unwrap();
5116+
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
5117+
let funding_contribution_2 = funding_template.splice_in_sync(added_value, &wallet).unwrap();
5118+
nodes[0]
5119+
.node
5120+
.funding_contributed(&channel_id, &node_id_1, funding_contribution_2.clone(), None)
5121+
.unwrap();
5122+
5123+
let stfu_a = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
5124+
nodes[1].node.handle_stfu(node_id_0, &stfu_a);
5125+
let stfu_b = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
5126+
nodes[0].node.handle_stfu(node_id_1, &stfu_b);
5127+
5128+
let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1);
5129+
assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, feerate_2_sat_per_kwu as u32);
5130+
nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf);
5131+
let tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0);
5132+
nodes[0].node.handle_tx_ack_rbf(node_id_1, &tx_ack_rbf);
5133+
5134+
complete_interactive_funding_negotiation(
5135+
&nodes[0],
5136+
&nodes[1],
5137+
channel_id,
5138+
funding_contribution_2,
5139+
new_funding_script.clone(),
5140+
);
5141+
let (rbf_tx_final, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
5142+
assert!(splice_locked.is_none());
5143+
expect_splice_pending_event(&nodes[0], &node_id_1);
5144+
expect_splice_pending_event(&nodes[1], &node_id_0);
5145+
5146+
// --- Mine and lock the final RBF. ---
5147+
mine_transaction(&nodes[0], &rbf_tx_final);
5148+
mine_transaction(&nodes[1], &rbf_tx_final);
5149+
5150+
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
5151+
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1);
5152+
5153+
let splice_locked_b = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1);
5154+
nodes[1].node.handle_splice_locked(node_id_0, &splice_locked_b);
5155+
5156+
let mut msg_events = nodes[1].node.get_and_clear_pending_msg_events();
5157+
assert_eq!(msg_events.len(), 2, "{msg_events:?}");
5158+
let splice_locked_a =
5159+
if let MessageSendEvent::SendSpliceLocked { msg, .. } = msg_events.remove(0) {
5160+
msg
5161+
} else {
5162+
panic!("Expected SendSpliceLocked, got {:?}", msg_events[0]);
5163+
};
5164+
let announcement_sigs_b =
5165+
if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) {
5166+
msg
5167+
} else {
5168+
panic!("Expected SendAnnouncementSignatures");
5169+
};
5170+
nodes[0].node.handle_splice_locked(node_id_1, &splice_locked_a);
5171+
nodes[0].node.handle_announcement_signatures(node_id_1, &announcement_sigs_b);
5172+
5173+
// --- Verify DiscardFunding events for both replaced candidates. ---
5174+
let splice_tx_0_txid = splice_tx_0.compute_txid();
5175+
let splice_tx_1_txid = splice_tx_1.compute_txid();
5176+
5177+
// Node 0 (initiator): ChannelReady + 2 DiscardFunding.
5178+
let events_a = nodes[0].node.get_and_clear_pending_events();
5179+
assert_eq!(events_a.len(), 3, "{events_a:?}");
5180+
assert!(matches!(events_a[0], Event::ChannelReady { .. }));
5181+
let discard_txids_a: Vec<_> = events_a[1..]
5182+
.iter()
5183+
.map(|e| match e {
5184+
Event::DiscardFunding { funding_info: FundingInfo::Tx { transaction }, .. } => {
5185+
transaction.compute_txid()
5186+
},
5187+
Event::DiscardFunding { funding_info: FundingInfo::OutPoint { outpoint }, .. } => {
5188+
outpoint.txid
5189+
},
5190+
other => panic!("Expected DiscardFunding, got {:?}", other),
5191+
})
5192+
.collect();
5193+
assert!(discard_txids_a.contains(&splice_tx_0_txid), "Missing discard for initial splice");
5194+
assert!(discard_txids_a.contains(&splice_tx_1_txid), "Missing discard for RBF #1");
5195+
check_added_monitors(&nodes[0], 1);
5196+
5197+
// Node 1 (acceptor): ChannelReady + 2 DiscardFunding.
5198+
let events_b = nodes[1].node.get_and_clear_pending_events();
5199+
assert_eq!(events_b.len(), 3, "{events_b:?}");
5200+
assert!(matches!(events_b[0], Event::ChannelReady { .. }));
5201+
let discard_txids_b: Vec<_> = events_b[1..]
5202+
.iter()
5203+
.map(|e| match e {
5204+
Event::DiscardFunding { funding_info: FundingInfo::Tx { transaction }, .. } => {
5205+
transaction.compute_txid()
5206+
},
5207+
Event::DiscardFunding { funding_info: FundingInfo::OutPoint { outpoint }, .. } => {
5208+
outpoint.txid
5209+
},
5210+
other => panic!("Expected DiscardFunding, got {:?}", other),
5211+
})
5212+
.collect();
5213+
assert!(discard_txids_b.contains(&splice_tx_0_txid), "Missing discard for initial splice");
5214+
assert!(discard_txids_b.contains(&splice_tx_1_txid), "Missing discard for RBF #1");
5215+
check_added_monitors(&nodes[1], 1);
5216+
5217+
// Complete the announcement exchange.
5218+
let mut msg_events = nodes[0].node.get_and_clear_pending_msg_events();
5219+
assert_eq!(msg_events.len(), 2, "{msg_events:?}");
5220+
if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) {
5221+
nodes[1].node.handle_announcement_signatures(node_id_0, &msg);
5222+
} else {
5223+
panic!("Expected SendAnnouncementSignatures");
5224+
}
5225+
assert!(matches!(msg_events.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. }));
5226+
5227+
let mut msg_events = nodes[1].node.get_and_clear_pending_msg_events();
5228+
assert_eq!(msg_events.len(), 1, "{msg_events:?}");
5229+
assert!(matches!(msg_events.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. }));
5230+
5231+
// Clean up old watched outpoints.
5232+
let (orig_outpoint, orig_script) = original_funding_outpoint;
5233+
let splice_funding_idx = |tx: &Transaction| {
5234+
tx.output.iter().position(|o| o.script_pubkey == new_funding_script).unwrap()
5235+
};
5236+
let outpoint_0 =
5237+
OutPoint { txid: splice_tx_0_txid, index: splice_funding_idx(&splice_tx_0) as u16 };
5238+
let outpoint_1 =
5239+
OutPoint { txid: splice_tx_1_txid, index: splice_funding_idx(&splice_tx_1) as u16 };
5240+
for node in &nodes {
5241+
node.chain_source.remove_watched_txn_and_outputs(orig_outpoint, orig_script.clone());
5242+
node.chain_source.remove_watched_txn_and_outputs(outpoint_0, new_funding_script.clone());
5243+
node.chain_source.remove_watched_txn_and_outputs(outpoint_1, new_funding_script.clone());
5244+
}
5245+
}

0 commit comments

Comments
 (0)