Update functional test block connection to detect block replays
What changed, and why it matters
This commit changes only internal test helper code in the Lightning Dev Kit repository. It makes the functional test framework smarter about 'block replays'—situations where the same block is fed to a test node more than once—so the fake blockchain ledger inside tests does not incorrectly treat the replay as a new block. There is no change to production code, network behavior, or real user funds. The commit message and diff do not describe any security vulnerability.
No security action required. Treat as a normal test-framework improvement. Reviewers may verify that the replay detection correctly preserves test assertions across all ConnectStyle variants.
Security signals we found
No production code modified
No cryptographic, consensus, or networking changes
Commit message frames change as test-framework correctness, not security
No advisory, CVE, or researcher attribution present
Evidence from the diff
The patch modifies do_connect_block_without_consistency_checks in lightning/src/ln/functional_test_utils.rs to detect when a block has already been connected in the test Node::blocks vector. If a matching block is found, it reuses the original height and skips pushing a duplicate and skip updating the test wallet UTXO set. Two test files are adjusted: functional_tests.rs removes a connect_style early-return workaround that is no longer needed, and monitor_tests.rs replaces an explicit match on ConnectStyle variants with a helper method updates_best_block_first(). All changes are confined to the test harness and test assertions.
Changed components
lightning/src/ln/functional_test_utils.rs (test helper)lightning/src/ln/functional_tests.rs (test case)lightning/src/ln/monitor_tests.rs (test case)Inspect captured patch +41 / −33
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 26fed9e..c586101 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -316,11 +316,25 @@ fn do_connect_block_with_consistency_checks<'a, 'b, 'c, 'd>(
fn do_connect_block_without_consistency_checks<'a, 'b, 'c, 'd>(
node: &'a Node<'b, 'c, 'd>, block: Block, skip_intermediaries: bool,
) {
- let height = node.best_block_info().1 + 1;
eprintln!("Connecting block using Block Connection Style: {:?}", *node.connect_style.borrow());
- // Update the block internally before handing it over to LDK, to ensure our assertions regarding
- // transaction broadcast are correct.
- node.blocks.lock().unwrap().push((block.clone(), height));
+ let (new_block, height) = {
+ let mut blocks = node.blocks.lock().unwrap();
+ let existing =
+ blocks.iter().rev().find(|(candidate, _)| candidate == &block).map(|(_, h)| *h);
+ if let Some(height) = existing {
+ // We're being handed a block we've already connected, i.e. this is a redundant rescan
+ // rather than a new block. Reuse the height it was originally connected at rather than
+ // extending the chain.
+ (false, height)
+ } else {
+ let height = blocks.last().unwrap().1 + 1;
+ // Update the block internally before handing it over to LDK, to ensure our assertions
+ // regarding transaction broadcast are correct.
+ blocks.push((block.clone(), height));
+ (true, height)
+ }
+ };
+
if !skip_intermediaries {
let txdata: Vec<_> = block.txdata.iter().enumerate().collect();
match *node.connect_style.borrow() {
@@ -393,14 +407,16 @@ fn do_connect_block_without_consistency_checks<'a, 'b, 'c, 'd>(
}
}
- for tx in &block.txdata {
- for input in &tx.input {
- node.wallet_source.remove_utxo(input.previous_output);
- }
- let wallet_script = node.wallet_source.get_change_script().unwrap();
- for (idx, output) in tx.output.iter().enumerate() {
- if output.script_pubkey == wallet_script {
- node.wallet_source.add_utxo(tx.clone(), idx as u32);
+ if new_block {
+ for tx in &block.txdata {
+ for input in &tx.input {
+ node.wallet_source.remove_utxo(input.previous_output);
+ }
+ let wallet_script = node.wallet_source.get_change_script().unwrap();
+ for (idx, output) in tx.output.iter().enumerate() {
+ if output.script_pubkey == wallet_script {
+ node.wallet_source.add_utxo(tx.clone(), idx as u32);
+ }
}
}
}
diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs
index 1f5bd39..c3a4ce3 100644
--- a/lightning/src/ln/functional_tests.rs
+++ b/lightning/src/ln/functional_tests.rs
@@ -2362,16 +2362,6 @@ pub fn test_htlc_ignore_latest_remote_commitment() {
let node_a_id = nodes[0].node.get_our_node_id();
let node_b_id = nodes[1].node.get_our_node_id();
- match *nodes[1].connect_style.borrow() {
- ConnectStyle::FullBlockViaListen
- | ConnectStyle::FullBlockDisconnectionsSkippingViaListen => {
- // We rely on the ability to connect a block redundantly, which isn't allowed via
- // `chain::Listen`, so we never run the test if we randomly get assigned that
- // connect_style.
- return;
- },
- _ => {},
- }
let funding_tx = create_announced_chan_between_nodes(&nodes, 0, 1).3;
let message = "Channel force-closed".to_owned();
route_payment(&nodes[0], &[&nodes[1]], 10000000);
diff --git a/lightning/src/ln/monitor_tests.rs b/lightning/src/ln/monitor_tests.rs
index f52f093..ff50724 100644
--- a/lightning/src/ln/monitor_tests.rs
+++ b/lightning/src/ln/monitor_tests.rs
@@ -2859,17 +2859,19 @@ fn do_test_anchors_aggregated_revoked_htlc_tx(p2a_anchor: bool) {
let mut events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
// Certain block `ConnectStyle`s cause an extra `ChannelClose` event to be emitted since the
// best block is updated before the confirmed transactions are notified.
- match *nodes[1].connect_style.borrow() {
- ConnectStyle::BestBlockFirst|ConnectStyle::BestBlockFirstReorgsOnlyTip|ConnectStyle::BestBlockFirstSkippingBlocks => {
- assert_eq!(events.len(), 4);
- if let Event::BumpTransaction(BumpTransactionEvent::ChannelClose { .. }) = events.remove(0) {}
- else { panic!("unexpected event"); }
- if let Event::BumpTransaction(BumpTransactionEvent::ChannelClose { .. }) = events.remove(1) {}
- else { panic!("unexpected event"); }
-
- },
- _ => assert_eq!(events.len(), 2),
- };
+ if nodes[1].connect_style.borrow().updates_best_block_first() {
+ assert_eq!(events.len(), 4);
+ if let Event::BumpTransaction(BumpTransactionEvent::ChannelClose { .. }) = events.remove(0) {
+ } else {
+ panic!("unexpected event");
+ }
+ if let Event::BumpTransaction(BumpTransactionEvent::ChannelClose { .. }) = events.remove(1) {
+ } else {
+ panic!("unexpected event");
+ }
+ } else {
+ assert_eq!(events.len(), 2);
+ }
let htlc_tx = {
let secret_key = SecretKey::from_slice(&[1; 32]).unwrap();
let public_key = PublicKey::new(secret_key.public_key(&secp));
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.