Test manual broadcast tracking and holder commit flow
What changed, and why it matters
This commit only adds new tests and a test helper for the 'manual broadcast' funding flow in a Lightning network library. It does not change production behavior, fix a bug, or alter security-critical code. The tiny code change in channelmonitor.rs is just adding a trailing comma in a test module. There is no security issue here.
No action required. This is a test-only commit with no production code changes.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces test coverage for channels funded via manual transaction broadcast. It adds a helper create_channel_manual_funding in functional_test_utils.rs and three new tests in functional_tests.rs verifying that: (1) holder commitment transactions are not broadcast until the funding transaction is confirmed, (2) automatic broadcast happens after funding confirmation when HTLCs time out, and (3) no bump-transaction events are emitted before funding is seen. The only non-test code change is a cosmetic trailing comma in a ChannelMonitor::new call inside channelmonitor.rs’s test module.
Changed components
lightning/src/ln/functional_test_utils.rslightning/src/ln/functional_tests.rslightning/src/chain/channelmonitor.rs (test module only)Inspect captured patch +258 / −1
diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 6fcc125..f49764b 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -7373,7 +7373,7 @@ mod tests {
let monitor = ChannelMonitor::new(
Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(),
&channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()),
- best_block, dummy_key, channel_id, false
+ best_block, dummy_key, channel_id, false,
);
let chan_id = monitor.inner.lock().unwrap().channel_id();
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index e58a758..17ac959 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -1804,6 +1804,66 @@ pub fn create_chan_between_nodes_with_value_a<'a, 'b, 'c: 'd, 'd>(
(msgs, chan_id, tx)
}
+pub fn create_channel_manual_funding<'a, 'b, 'c: 'd, 'd>(
+ nodes: &'a Vec<Node<'b, 'c, 'd>>, initiator: usize, counterparty: usize, channel_value: u64,
+ push_msat: u64,
+) -> (ChannelId, Transaction, OutPoint) {
+ let node_a = &nodes[initiator];
+ let node_b = &nodes[counterparty];
+ let node_a_id = node_a.node.get_our_node_id();
+ let node_b_id = node_b.node.get_our_node_id();
+
+ let temp_channel_id = exchange_open_accept_chan(node_a, node_b, channel_value, push_msat);
+
+ let (funding_temp_id, funding_tx, funding_outpoint) =
+ create_funding_transaction(node_a, &node_b_id, channel_value, 42);
+ assert_eq!(temp_channel_id, funding_temp_id);
+
+ node_a
+ .node
+ .funding_transaction_generated_manual_broadcast(
+ funding_temp_id,
+ node_b_id,
+ funding_tx.clone(),
+ )
+ .unwrap();
+ check_added_monitors!(node_a, 0);
+
+ let funding_created = get_event_msg!(node_a, MessageSendEvent::SendFundingCreated, node_b_id);
+ node_b.node.handle_funding_created(node_a_id, &funding_created);
+ check_added_monitors!(node_b, 1);
+ let channel_id_b = expect_channel_pending_event(node_b, &node_a_id);
+
+ let funding_signed = get_event_msg!(node_b, MessageSendEvent::SendFundingSigned, node_a_id);
+ node_a.node.handle_funding_signed(node_b_id, &funding_signed);
+ check_added_monitors!(node_a, 1);
+
+ let events = node_a.node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 2);
+ let funding_txid = funding_tx.compute_txid();
+ let mut channel_id = None;
+ for event in events {
+ match event {
+ Event::FundingTxBroadcastSafe { funding_txo, counterparty_node_id, .. } => {
+ assert_eq!(counterparty_node_id, node_b_id);
+ assert_eq!(funding_txo.txid, funding_txid);
+ assert_eq!(funding_txo.vout, u32::from(funding_outpoint.index));
+ },
+ Event::ChannelPending { channel_id: pending_id, counterparty_node_id, .. } => {
+ assert_eq!(counterparty_node_id, node_b_id);
+ channel_id = Some(pending_id);
+ },
+ _ => panic!("Unexpected event"),
+ }
+ }
+ let channel_id = channel_id.expect("channel pending event missing");
+ assert_eq!(channel_id, channel_id_b);
+
+ assert!(node_a.tx_broadcaster.txn_broadcasted.lock().unwrap().is_empty());
+
+ (channel_id, funding_tx, funding_outpoint)
+}
+
pub fn create_chan_between_nodes_with_value_b<'a, 'b, 'c>(
node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>,
as_funding_msgs: &(msgs::ChannelReady, msgs::AnnouncementSignatures),
diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs
index d79b307..a1863cf 100644
--- a/lightning/src/ln/functional_tests.rs
+++ b/lightning/src/ln/functional_tests.rs
@@ -20,6 +20,7 @@ use crate::chain::channelmonitor::{
};
use crate::chain::transaction::OutPoint;
use crate::chain::{ChannelMonitorUpdateStatus, Confirm, Listen, Watch};
+use crate::events::bump_transaction::BumpTransactionEvent;
use crate::events::{
ClosureReason, Event, HTLCHandlingFailureType, PathFailure, PaymentFailureReason,
PaymentPurpose,
@@ -9628,6 +9629,202 @@ pub fn test_remove_expired_inbound_unfunded_channels() {
check_closed_event(&nodes[1], 1, reason, false, &[node_a_id], 100000);
}
+#[test]
+fn test_manual_broadcast_skips_commitment_until_funding_seen() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_b_id = nodes[1].node.get_our_node_id();
+
+ let (channel_id, funding_tx, funding_outpoint) =
+ create_channel_manual_funding(&nodes, 0, 1, 100_000, 10_000);
+
+ nodes[0]
+ .node
+ .force_close_broadcasting_latest_txn(&channel_id, &node_b_id, "manual close".to_owned())
+ .unwrap();
+ check_added_monitors!(&nodes[0], 1);
+ check_added_monitors!(&nodes[1], 0);
+
+ assert!(nodes[0].tx_broadcaster.txn_broadcast().is_empty());
+ nodes[0].node.get_and_clear_pending_msg_events();
+ nodes[1].node.get_and_clear_pending_msg_events();
+ let events = nodes[0].node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match &events[0] {
+ Event::ChannelClosed {
+ reason: ClosureReason::HolderForceClosed { broadcasted_latest_txn, message },
+ counterparty_node_id: Some(id),
+ ..
+ } => {
+ assert_eq!(*broadcasted_latest_txn, Some(true));
+ assert_eq!(message, "manual close");
+ assert_eq!(id, &node_b_id);
+ },
+ _ => panic!("Unexpected event"),
+ }
+ nodes[1].node.get_and_clear_pending_events();
+
+ let monitor_events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert!(monitor_events.is_empty());
+
+ confirm_transaction(&nodes[0], &funding_tx);
+ confirm_transaction(&nodes[1], &funding_tx);
+ nodes[0].node.get_and_clear_pending_msg_events();
+ nodes[1].node.get_and_clear_pending_msg_events();
+
+ {
+ let monitor = get_monitor!(&nodes[0], channel_id);
+ // manual override
+ monitor.broadcast_latest_holder_commitment_txn(
+ &nodes[0].tx_broadcaster,
+ &nodes[0].fee_estimator,
+ &nodes[0].logger,
+ );
+ }
+ let funding_txid = funding_tx.compute_txid();
+ let broadcasts = nodes[0].tx_broadcaster.txn_broadcast();
+ assert!(!broadcasts.is_empty());
+ let commitment_tx = broadcasts
+ .iter()
+ .find(|tx| {
+ tx.input.iter().any(|input| {
+ input.previous_output.txid == funding_txid
+ && input.previous_output.vout == u32::from(funding_outpoint.index)
+ })
+ })
+ .expect("commitment transaction not broadcast");
+ check_spends!(commitment_tx, funding_tx);
+ assert_eq!(commitment_tx.input.len(), 1);
+ let commitment_input = &commitment_tx.input[0];
+ assert_eq!(commitment_input.previous_output.txid, funding_txid);
+ assert_eq!(commitment_input.previous_output.vout, u32::from(funding_outpoint.index));
+
+ let monitor_events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert!(monitor_events.iter().all(|event| !matches!(event, Event::BumpTransaction(_))));
+ assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
+ assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
+}
+
+#[test]
+fn test_manual_broadcast_detects_funding_and_broadcasts_on_timeout() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_b_id = nodes[1].node.get_our_node_id();
+
+ let (channel_id, funding_tx, funding_outpoint) =
+ create_channel_manual_funding(&nodes, 0, 1, 100_000, 10_000);
+
+ let funding_msgs =
+ create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &funding_tx);
+ let confirmed_channel_id = funding_msgs.1;
+ assert_eq!(confirmed_channel_id, channel_id);
+ let _announcements =
+ create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &funding_msgs.0);
+
+ let usable_channels = nodes[0].node.list_usable_channels();
+ assert_eq!(usable_channels.len(), 1);
+ assert_eq!(usable_channels[0].channel_id, channel_id);
+
+ let (_payment_preimage, _payment_hash, _payment_secret, _payment_id) =
+ route_payment(&nodes[0], &[&nodes[1]], 10_000_000);
+ nodes[1].node.get_and_clear_pending_events();
+
+ connect_blocks(&nodes[0], TEST_FINAL_CLTV + LATENCY_GRACE_PERIOD_BLOCKS + 1);
+ connect_blocks(&nodes[1], TEST_FINAL_CLTV + LATENCY_GRACE_PERIOD_BLOCKS + 1);
+
+ let events = nodes[0].node.get_and_clear_pending_events();
+ assert!(events.iter().any(|event| matches!(
+ event,
+ Event::ChannelClosed {
+ reason: ClosureReason::HTLCsTimedOut { .. },
+ counterparty_node_id: Some(id),
+ ..
+ } if id == &node_b_id
+ )));
+ nodes[1].node.get_and_clear_pending_events();
+ nodes[0].node.get_and_clear_pending_msg_events();
+ nodes[1].node.get_and_clear_pending_msg_events();
+ check_added_monitors!(&nodes[0], 1);
+ check_added_monitors!(&nodes[1], 0);
+
+ let funding_txid = funding_tx.compute_txid();
+ let broadcasts = nodes[0].tx_broadcaster.txn_broadcast();
+ assert!(!broadcasts.is_empty());
+ let commitment_tx = broadcasts
+ .iter()
+ .find(|tx| {
+ tx.input.iter().any(|input| {
+ input.previous_output.txid == funding_txid
+ && input.previous_output.vout == u32::from(funding_outpoint.index)
+ })
+ })
+ .expect("commitment transaction not broadcast");
+ check_spends!(commitment_tx, funding_tx);
+ assert_eq!(commitment_tx.input.len(), 1);
+
+ for event in nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events() {
+ if let Event::BumpTransaction(bump_event) = event {
+ if let BumpTransactionEvent::ChannelClose {
+ channel_id: event_channel_id,
+ counterparty_node_id,
+ ..
+ } = &bump_event
+ {
+ assert_eq!(*event_channel_id, channel_id);
+ assert_eq!(*counterparty_node_id, node_b_id);
+ }
+ nodes[0].bump_tx_handler.handle_event(&bump_event);
+ }
+ }
+}
+
+#[test]
+fn test_manual_broadcast_no_bump_events_before_funding_seen() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_b_id = nodes[1].node.get_our_node_id();
+
+ let (channel_id, _, _) = create_channel_manual_funding(&nodes, 0, 1, 100_000, 10_000);
+
+ nodes[0]
+ .node
+ .force_close_broadcasting_latest_txn(&channel_id, &node_b_id, "manual close".to_owned())
+ .unwrap();
+ check_added_monitors!(&nodes[0], 1);
+ check_added_monitors!(&nodes[1], 0);
+
+ assert!(nodes[0].tx_broadcaster.txn_broadcast().is_empty());
+ nodes[0].node.get_and_clear_pending_msg_events();
+ nodes[1].node.get_and_clear_pending_msg_events();
+ let events = nodes[0].node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match &events[0] {
+ Event::ChannelClosed {
+ reason: ClosureReason::HolderForceClosed { broadcasted_latest_txn, message },
+ counterparty_node_id: Some(id),
+ ..
+ } => {
+ assert_eq!(*broadcasted_latest_txn, Some(true));
+ assert_eq!(message, "manual close");
+ assert_eq!(id, &node_b_id);
+ },
+ _ => panic!("Unexpected event"),
+ }
+ nodes[1].node.get_and_clear_pending_events();
+
+ let monitor_events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert!(monitor_events.iter().all(|event| !matches!(event, Event::BumpTransaction(_))));
+}
+
fn do_test_multi_post_event_actions(do_reload: bool) {
// Tests handling multiple post-Event actions at once.
// There is specific code in ChannelManager to handle channels where multiple post-Event
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.