Add test coverage for `TransactionType::Splice`
What changed, and why it matters
This commit only adds and improves test code. It gives the project's test mock broadcaster the ability to remember what 'type' each broadcast transaction was (for example, a splice transaction), and updates tests to check that splice transactions are labeled correctly. There is no change to production code that handles real money or network messages, so it does not introduce or fix a security vulnerability on its own.
No security action required. This is a test-coverage-only change. Reviewers may optionally verify that the new txn_types vector stays synchronized with txn_broadcasted in all future test modifications.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch modifies TestBroadcaster in lightning/src/util/test_utils.rs to store a parallel txn_types vector alongside txn_broadcasted. It adds txn_broadcast_with_types() and clear() helpers, keeps txn_broadcast() for backward compatibility, and updates several test files to clear both vectors via clear(). Splicing tests now assert that broadcast splice transactions carry TransactionType::Splice. No production BroadcasterInterface behavior is changed; the same transactions were already being passed through broadcast_transactions() with their types.
Changed components
lightning/src/util/test_utils.rs (TestBroadcaster test mock)lightning/src/ln/splicing_tests.rslightning/src/ln/functional_test_utils.rslightning/src/ln/channel_open_tests.rslightning/src/ln/monitor_tests.rslightning/src/ln/payment_tests.rslightning/src/ln/shutdown_tests.rslightning/src/ln/chanmon_update_fail_tests.rsInspect captured patch +51 / −22
diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs
index ff499d0..f602bbe 100644
--- a/lightning/src/ln/chanmon_update_fail_tests.rs
+++ b/lightning/src/ln/chanmon_update_fail_tests.rs
@@ -86,6 +86,7 @@ fn test_monitor_and_persister_update_fail() {
let persister = test_utils::TestPersister::new();
let tx_broadcaster = TestBroadcaster {
txn_broadcasted: Mutex::new(Vec::new()),
+ txn_types: Mutex::new(Vec::new()),
// Because we will connect a block at height 200 below, we need the TestBroadcaster to know
// that we are at height 200 so that it doesn't think we're violating the time lock
// requirements of transactions broadcasted at that point.
diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs
index 3a9c266..b7965c4 100644
--- a/lightning/src/ln/channel_open_tests.rs
+++ b/lightning/src/ln/channel_open_tests.rs
@@ -1699,7 +1699,7 @@ pub fn test_invalid_funding_tx() {
assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx);
- nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[0].tx_broadcaster.clear();
let expected_err = "funding tx had wrong script/value or output index";
confirm_transaction_at(&nodes[1], &tx, 1);
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index cea9ea4..4739af9 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -861,6 +861,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> {
txn_broadcasted: Mutex::new(
self.tx_broadcaster.txn_broadcasted.lock().unwrap().clone(),
),
+ txn_types: Mutex::new(self.tx_broadcaster.txn_types.lock().unwrap().clone()),
blocks: Arc::new(Mutex::new(self.tx_broadcaster.blocks.lock().unwrap().clone())),
};
@@ -1538,7 +1539,7 @@ pub fn sign_funding_transaction<'a, 'b, 'c>(
assert_eq!(node_a.tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
assert_eq!(node_a.tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx);
- node_a.tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ node_a.tx_broadcaster.clear();
// Ensure that funding_transaction_generated is idempotent.
assert!(node_a
@@ -1641,10 +1642,8 @@ pub fn open_zero_conf_channel_with_value<'a, 'b, 'c, 'd>(
check_added_monitors(&initiator, 1);
assert_eq!(initiator.tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
- assert_eq!(
- initiator.tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0)[0],
- tx
- );
+ assert_eq!(initiator.tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx);
+ initiator.tx_broadcaster.clear();
as_channel_ready =
get_event_msg!(initiator, MessageSendEvent::SendChannelReady, receiver_node_id);
@@ -2014,7 +2013,7 @@ pub fn create_unannounced_chan_between_nodes_with_value<'a, 'b, 'c, 'd>(
assert_eq!(nodes[a].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
assert_eq!(nodes[a].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx);
- nodes[a].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[a].tx_broadcaster.clear();
let conf_height =
core::cmp::max(nodes[a].best_block_info().1 + 1, nodes[b].best_block_info().1 + 1);
diff --git a/lightning/src/ln/monitor_tests.rs b/lightning/src/ln/monitor_tests.rs
index 04915af..5caff13 100644
--- a/lightning/src/ln/monitor_tests.rs
+++ b/lightning/src/ln/monitor_tests.rs
@@ -1208,7 +1208,7 @@ fn test_no_preimage_inbound_htlc_balances() {
}, a_received_htlc_balance.clone(), a_sent_htlc_balance.clone()]);
mine_transaction(&nodes[0], &as_txn[0]);
- nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[0].tx_broadcaster.clear();
check_closed_broadcast!(nodes[0], true);
check_added_monitors(&nodes[0], 1);
check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[1].node.get_our_node_id()], 1000000);
@@ -1255,7 +1255,7 @@ fn test_no_preimage_inbound_htlc_balances() {
bs_pre_spend_claims.retain(|e| if let Balance::ClaimableAwaitingConfirmations { .. } = e { false } else { true });
// The next few blocks for B look the same as for A, though for the opposite HTLC
- nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[1].tx_broadcaster.clear();
connect_blocks(&nodes[1], TEST_FINAL_CLTV - (ANTI_REORG_DELAY - 1));
expect_htlc_failure_conditions(nodes[1].node.get_and_clear_pending_events(), &[HTLCHandlingFailureType::Receive { payment_hash: to_b_failed_payment_hash }]);
nodes[1].node.process_pending_htlc_forwards();
diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs
index e41e60a..c44f3d1 100644
--- a/lightning/src/ln/payment_tests.rs
+++ b/lightning/src/ln/payment_tests.rs
@@ -838,7 +838,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) {
let as_commitment_tx = get_local_commitment_txn!(nodes[0], chan_id)[0].clone();
if confirm_before_reload {
mine_transaction(&nodes[0], &as_commitment_tx);
- nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[0].tx_broadcaster.clear();
}
// The ChannelMonitor should always be the latest version, as we're required to persist it
@@ -893,7 +893,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) {
&node_b_id)) }, &[node_a_id], 100000);
check_added_monitors(&nodes[1], 1);
assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
- nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[1].tx_broadcaster.clear();
},
_ => panic!("Unexpected event"),
}
@@ -954,7 +954,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) {
} else {
confirm_transaction(&nodes[0], &first_htlc_timeout_tx);
}
- nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[0].tx_broadcaster.clear();
let conditions = PaymentFailedConditions::new().from_mon_update();
expect_payment_failed_conditions(&nodes[0], payment_hash, false, conditions);
diff --git a/lightning/src/ln/shutdown_tests.rs b/lightning/src/ln/shutdown_tests.rs
index 192bc63..66f8df6 100644
--- a/lightning/src/ln/shutdown_tests.rs
+++ b/lightning/src/ln/shutdown_tests.rs
@@ -495,7 +495,7 @@ fn updates_shutdown_wait() {
assert!(nodes[0].node.list_channels().is_empty());
assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
- nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[1].tx_broadcaster.clear();
close_channel(&nodes[1], &nodes[2], &chan_2.2, chan_2.3, true);
assert!(nodes[1].node.list_channels().is_empty());
@@ -625,7 +625,7 @@ fn do_htlc_fail_async_shutdown(blinded_recipient: bool) {
assert!(nodes[0].node.list_channels().is_empty());
assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
- nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[1].tx_broadcaster.clear();
close_channel(&nodes[1], &nodes[2], &chan_2.2, chan_2.3, true);
assert!(nodes[1].node.list_channels().is_empty());
assert!(nodes[2].node.list_channels().is_empty());
@@ -842,7 +842,7 @@ fn do_test_shutdown_rebroadcast(recv_count: u8) {
assert!(nodes[0].node.list_channels().is_empty());
assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
- nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear();
+ nodes[1].tx_broadcaster.clear();
close_channel(&nodes[1], &nodes[2], &chan_2.2, chan_2.3, true);
assert!(nodes[1].node.list_channels().is_empty());
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index ef524db..05303cf 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -9,7 +9,7 @@
#![cfg_attr(not(test), allow(unused_imports))]
-use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW;
+use crate::chain::chaininterface::{TransactionType, FEERATE_FLOOR_SATS_PER_KW};
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::ChannelMonitorUpdateStatus;
@@ -333,11 +333,18 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>(
check_added_monitors(&acceptor, 1);
let tx = {
- let mut initiator_txn = initiator.tx_broadcaster.txn_broadcast();
+ let mut initiator_txn = initiator.tx_broadcaster.txn_broadcast_with_types();
assert_eq!(initiator_txn.len(), 1);
- let acceptor_txn = acceptor.tx_broadcaster.txn_broadcast();
- assert_eq!(initiator_txn, acceptor_txn,);
- initiator_txn.remove(0)
+ let acceptor_txn = acceptor.tx_broadcaster.txn_broadcast_with_types();
+ assert_eq!(initiator_txn, acceptor_txn);
+ let (tx, tx_type) = initiator_txn.remove(0);
+ // Verify transaction type is Splice
+ assert!(
+ matches!(tx_type, TransactionType::Splice { .. }),
+ "Expected TransactionType::Splice, got {:?}",
+ tx_type
+ );
+ tx
};
(tx, splice_locked)
}
diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs
index f5c73ca..18d003c 100644
--- a/lightning/src/util/test_utils.rs
+++ b/lightning/src/util/test_utils.rs
@@ -1126,31 +1126,50 @@ unsafe impl Send for TestStore {}
pub struct TestBroadcaster {
pub txn_broadcasted: Mutex<Vec<Transaction>>,
+ pub txn_types: Mutex<Vec<TransactionType>>,
pub blocks: Arc<Mutex<Vec<(Block, u32)>>>,
}
impl TestBroadcaster {
pub fn new(network: Network) -> Self {
let txn_broadcasted = Mutex::new(Vec::new());
+ let txn_types = Mutex::new(Vec::new());
let blocks = Arc::new(Mutex::new(vec![(genesis_block(network), 0)]));
- Self { txn_broadcasted, blocks }
+ Self { txn_broadcasted, txn_types, blocks }
}
pub fn with_blocks(blocks: Arc<Mutex<Vec<(Block, u32)>>>) -> Self {
let txn_broadcasted = Mutex::new(Vec::new());
- Self { txn_broadcasted, blocks }
+ let txn_types = Mutex::new(Vec::new());
+ Self { txn_broadcasted, txn_types, blocks }
}
pub fn txn_broadcast(&self) -> Vec<Transaction> {
+ self.txn_types.lock().unwrap().clear();
self.txn_broadcasted.lock().unwrap().split_off(0)
}
pub fn unique_txn_broadcast(&self) -> Vec<Transaction> {
let mut txn = self.txn_broadcasted.lock().unwrap().split_off(0);
+ self.txn_types.lock().unwrap().clear();
let mut seen = new_hash_set();
txn.retain(|tx| seen.insert(tx.compute_txid()));
txn
}
+
+ /// Returns all broadcast transactions with their types, clearing both internal lists.
+ pub fn txn_broadcast_with_types(&self) -> Vec<(Transaction, TransactionType)> {
+ let txn = self.txn_broadcasted.lock().unwrap().split_off(0);
+ let types = self.txn_types.lock().unwrap().split_off(0);
+ assert_eq!(txn.len(), types.len(), "Transaction and type vectors out of sync");
+ txn.into_iter().zip(types.into_iter()).collect()
+ }
+
+ /// Clears both the transaction and type vectors.
+ pub fn clear(&self) {
+ self.txn_broadcasted.lock().unwrap().clear();
+ self.txn_types.lock().unwrap().clear();
+ }
}
impl chaininterface::BroadcasterInterface for TestBroadcaster {
@@ -1198,7 +1217,10 @@ impl chaininterface::BroadcasterInterface for TestBroadcaster {
}
}
let owned_txs: Vec<Transaction> = txs.iter().map(|(tx, _)| (*tx).clone()).collect();
+ let owned_types: Vec<TransactionType> =
+ txs.iter().map(|(_, tx_type)| tx_type.clone()).collect();
self.txn_broadcasted.lock().unwrap().extend(owned_txs);
+ self.txn_types.lock().unwrap().extend(owned_types);
}
}
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.