What changed, and why it matters
This commit only adds new integration tests for an existing feature. It does not change production code, fix a bug, or alter behavior. The tests verify that a counter showing how many blocks remain until a cooperative channel close is final behaves correctly, including when the blockchain reorganizes. There is no security issue in this change itself.
No security action needed. Review as normal test-quality change if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit extends an existing LND integration test (itest/lnd_coop_close_rbf_test.go) to assert that the WaitingCloseChannel RPC fields BlocksTilCloseConfirmed and CloseHeight update correctly through unconfirmed, confirming, reorged, and re-mined states. The diff is purely additive test code: assertions, mining helpers, and waits. No production logic is modified.
Changed components
itest/lnd_coop_close_rbf_test.goInspect captured patch +128 / −2
diff --git a/itest/lnd_coop_close_rbf_test.go b/itest/lnd_coop_close_rbf_test.go
index 13e10c9..c7c6b09 100644
--- a/itest/lnd_coop_close_rbf_test.go
+++ b/itest/lnd_coop_close_rbf_test.go
@@ -162,6 +162,8 @@ func testRBFCoopCloseDisconnect(ht *lntest.HarnessTest) {
// testCoopCloseRBFWithReorg tests that when a cooperative close transaction
// is reorganized out during confirmation waiting, the system properly handles
// RBF replacements and re-registration for any spend of the funding output.
+// It also verifies the blocks_til_close_confirmed field correctly tracks
+// remaining confirmations and resets appropriately after a reorg.
func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
// Skip this test for neutrino backend as we can't trigger reorgs.
if ht.IsNeutrinoBackend() {
@@ -224,6 +226,22 @@ func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
require.NoError(ht, err)
firstRbfTx := ht.AssertTxInMempool(*firstRbfTxid)
+ // Verify blocks_til_close_confirmed equals requiredConfs and
+ // close_height is zero when the tx is unconfirmed.
+ waitingClose := ht.AssertNumWaitingClose(alice, 1)
+ blocksTilCloseConfirmed := waitingClose[0].BlocksTilCloseConfirmed
+ require.Equal(
+ ht, uint32(requiredConfs), blocksTilCloseConfirmed,
+ "expected blocks_til_close_confirmed to equal %d when "+
+ "unconfirmed, got %d", requiredConfs,
+ blocksTilCloseConfirmed,
+ )
+ require.Equal(
+ ht, uint32(0), waitingClose[0].CloseHeight,
+ "expected close_height=0 when unconfirmed, got %d",
+ waitingClose[0].CloseHeight,
+ )
+
_, bestHeight := ht.GetBestBlock()
ht.Logf("Current block height: %d", bestHeight)
@@ -235,10 +253,55 @@ func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
ht.Logf("Mined block %d with first RBF tx", bestHeight+1)
+ // Verify blocks_til_close_confirmed decremented to
+ // requiredConfs - 1 = 2, and close_height is set to the mined height.
+ _, closeHeight := ht.GetBestBlock()
+ err = wait.NoError(func() error {
+ resp := alice.RPC.PendingChannels()
+ if len(resp.WaitingCloseChannels) != 1 {
+ return fmt.Errorf("expected 1 waiting close channel, "+
+ "got %d", len(resp.WaitingCloseChannels))
+ }
+ wc := resp.WaitingCloseChannels[0]
+ expected := uint32(requiredConfs - 1)
+ if wc.BlocksTilCloseConfirmed != expected {
+ return fmt.Errorf("expected "+
+ "blocks_til_close_confirmed=%d, got %d",
+ expected, wc.BlocksTilCloseConfirmed)
+ }
+
+ return nil
+ }, defaultTimeout)
+ require.NoError(ht, err)
+
+ waitingClose = ht.AssertNumWaitingClose(alice, 1)
+ require.Equal(
+ ht, uint32(closeHeight), waitingClose[0].CloseHeight,
+ "expected close_height=%d, got %d",
+ closeHeight, waitingClose[0].CloseHeight,
+ )
+
block2 := ht.MineEmptyBlocks(1)[0]
ht.Logf("Mined block %d", bestHeight+2)
+ // Verify blocks_til_close_confirmed decremented to 1.
+ err = wait.NoError(func() error {
+ resp := alice.RPC.PendingChannels()
+ if len(resp.WaitingCloseChannels) != 1 {
+ return fmt.Errorf("expected 1 waiting close channel, "+
+ "got %d", len(resp.WaitingCloseChannels))
+ }
+ blocks := resp.WaitingCloseChannels[0].BlocksTilCloseConfirmed
+ if blocks != 1 {
+ return fmt.Errorf("expected "+
+ "blocks_til_close_confirmed=1, got %d", blocks)
+ }
+
+ return nil
+ }, defaultTimeout)
+ require.NoError(ht, err)
+
ht.Logf("Re-orging two blocks to remove first RBF tx")
// Trigger a reorganization that removes the last 2 blocks. This is safe
@@ -257,12 +320,47 @@ func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
ht.Log("Mining blocks to surpass previous chain")
- // Mine 2 empty blocks to trigger the reorg on the nodes.
- ht.MineEmptyBlocks(2)
+ // Mine 3 empty blocks to create a longer chain without the closing tx.
+ // This ensures the reorg is fully processed by the nodes.
+ ht.MineEmptyBlocks(3)
_, bestHeight = ht.GetBestBlock()
ht.Logf("Mined blocks to reach height: %d", bestHeight)
+ // Wait for Alice to sync to the new chain.
+ ht.WaitForNodeBlockHeight(alice, bestHeight)
+
+ // After the reorg, the closing tx is no longer confirmed.
+ // blocks_til_close_confirmed should reset to requiredConfs.
+ err = wait.NoError(func() error {
+ resp := alice.RPC.PendingChannels()
+ if len(resp.WaitingCloseChannels) != 1 {
+ return fmt.Errorf("expected 1 waiting close channel, "+
+ "got %d", len(resp.WaitingCloseChannels))
+ }
+ wc := resp.WaitingCloseChannels[0]
+ if wc.BlocksTilCloseConfirmed != uint32(requiredConfs) {
+ return fmt.Errorf("expected "+
+ "blocks_til_close_confirmed=%d after reorg, "+
+ "got %d", requiredConfs,
+ wc.BlocksTilCloseConfirmed)
+ }
+
+ return nil
+ }, defaultTimeout)
+ require.NoError(ht, err)
+
+ // close_height should also be zero after the reorg.
+ waitingClose = ht.AssertNumWaitingClose(alice, 1)
+ require.Equal(
+ ht, uint32(0), waitingClose[0].CloseHeight,
+ "expected close_height=0 after reorg, got %d",
+ waitingClose[0].CloseHeight,
+ )
+
+ ht.Logf("blocks_til_close_confirmed correctly reset to %d after reorg",
+ requiredConfs)
+
// Now, instead of mining the second RBF, mine the INITIAL transaction
// to test that the system can handle any valid spend of the funding
// output.
@@ -271,6 +369,34 @@ func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
)
ht.AssertTxInBlock(block, *initialCloseTxid)
+ // Verify blocks_til_close_confirmed resumes countdown after re-mining
+ // and close_height is set to the new confirmation height.
+ _, reCloseHeight := ht.GetBestBlock()
+ err = wait.NoError(func() error {
+ resp := alice.RPC.PendingChannels()
+ if len(resp.WaitingCloseChannels) != 1 {
+ return fmt.Errorf("expected 1 waiting close channel, "+
+ "got %d", len(resp.WaitingCloseChannels))
+ }
+ blocks := resp.WaitingCloseChannels[0].BlocksTilCloseConfirmed
+ expected := uint32(requiredConfs - 1)
+ if blocks != expected {
+ return fmt.Errorf("expected "+
+ "blocks_til_close_confirmed=%d, got %d",
+ expected, blocks)
+ }
+
+ return nil
+ }, defaultTimeout)
+ require.NoError(ht, err)
+
+ waitingClose = ht.AssertNumWaitingClose(alice, 1)
+ require.Equal(
+ ht, uint32(reCloseHeight), waitingClose[0].CloseHeight,
+ "expected close_height=%d after re-mine, got %d",
+ reCloseHeight, waitingClose[0].CloseHeight,
+ )
+
// Mine additional blocks to reach the required confirmations (3 total).
ht.MineEmptyBlocks(requiredConfs - 1)
Why this scored 14/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.