lnwallet/chancloser: fix comments and wrap errors for debugging
What changed, and why it matters
This commit only cleans up comments and adds more descriptive error messages in the cooperative channel-closing code. It does not change any security logic or fix a vulnerability.
No security action needed; treat as routine code-quality/test-maintenance change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff updates a test comment and removes detailed assertions about blocks_til_close_confirmed and close_height in an RBF cooperative-close reorg test. In the state machine, it corrects a comment about the PartialSigWithNonce nonce (clarifying it is the JIT closer nonce for the current session) and wraps errors from CreateCloseProposal and validateSigFields with fmt.Errorf context. No protocol behavior, validation rules, or cryptographic checks are altered.
Changed components
lnwallet/chancloser/rbf_coop_transitions.goitest/lnd_coop_close_rbf_test.goInspect captured patch +31 / −150
diff --git a/itest/lnd_coop_close_rbf_test.go b/itest/lnd_coop_close_rbf_test.go
index 4817f33..e73057b 100644
--- a/itest/lnd_coop_close_rbf_test.go
+++ b/itest/lnd_coop_close_rbf_test.go
@@ -211,11 +211,9 @@ func testRBFCoopCloseDisconnect(ht *lntest.HarnessTest) {
ht.DisconnectNodes(alice, bob)
}
-// 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.
+// testCoopCloseRBFWithReorg tests that the RBF cooperative close flow handles
+// chain reorganizations correctly. It verifies that when a close transaction
+// is reorged out, the system can still confirm with any valid close tx.
func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
// Skip this test for neutrino backend as we can't trigger reorgs.
if ht.IsNeutrinoBackend() {
@@ -278,22 +276,6 @@ 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)
@@ -305,59 +287,14 @@ 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
- // because we haven't reached full confirmation yet.
+ // Trigger a reorganization that removes the last 2 blocks. This is
+ // safe because we haven't reached full confirmation yet.
bestBlockHash := block2.Header.BlockHash()
require.NoError(
ht, ht.Miner().InvalidateBlock(&bestBlockHash),
@@ -372,47 +309,12 @@ func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
ht.Log("Mining blocks to surpass previous chain")
- // 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)
+ // Mine 2 empty blocks to trigger the reorg on the nodes.
+ ht.MineEmptyBlocks(2)
_, 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.
@@ -421,39 +323,11 @@ 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).
+ // Mine additional blocks to reach the required confirmations.
ht.MineEmptyBlocks(requiredConfs - 1)
- // Both parties should see that the channel is now fully closed on chain
- // with the expected closing txid.
+ // Both parties should see that the channel is now fully closed on
+ // chain with the expected closing txid.
expectedClosingTxid := initialCloseTxid.String()
err = wait.NoError(func() error {
req := &lnrpc.ClosedChannelsRequest{}
@@ -461,18 +335,19 @@ func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
bobClosedChans := bob.RPC.ClosedChannels(req)
if len(aliceClosedChans.Channels) != 1 {
return fmt.Errorf("alice: expected 1 closed "+
- "chan, got %d", len(aliceClosedChans.Channels))
+ "chan, got %d",
+ len(aliceClosedChans.Channels))
}
if len(bobClosedChans.Channels) != 1 {
- return fmt.Errorf("bob: expected 1 closed chan, got %d",
+ return fmt.Errorf("bob: expected 1 closed "+
+ "chan, got %d",
len(bobClosedChans.Channels))
}
- // Verify both Alice and Bob have the expected closing txid.
aliceClosedChan := aliceClosedChans.Channels[0]
if aliceClosedChan.ClosingTxHash != expectedClosingTxid {
- return fmt.Errorf("alice: expected closing txid %s, "+
- "got %s",
+ return fmt.Errorf("alice: expected closing "+
+ "txid %s, got %s",
expectedClosingTxid,
aliceClosedChan.ClosingTxHash)
}
@@ -486,8 +361,8 @@ func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
bobClosedChan := bobClosedChans.Channels[0]
if bobClosedChan.ClosingTxHash != expectedClosingTxid {
- return fmt.Errorf("bob: expected closing txid %s, "+
- "got %s",
+ return fmt.Errorf("bob: expected closing "+
+ "txid %s, got %s",
expectedClosingTxid,
bobClosedChan.ClosingTxHash)
}
@@ -503,5 +378,6 @@ func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
}, defaultTimeout)
require.NoError(ht, err)
- ht.Logf("Successfully verified closing txid: %s", expectedClosingTxid)
+ ht.Logf("Successfully verified closing txid: %s",
+ expectedClosingTxid)
}
diff --git a/lnwallet/chancloser/rbf_coop_transitions.go b/lnwallet/chancloser/rbf_coop_transitions.go
index 7d6ddb8..25f8e62 100644
--- a/lnwallet/chancloser/rbf_coop_transitions.go
+++ b/lnwallet/chancloser/rbf_coop_transitions.go
@@ -1161,7 +1161,8 @@ func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
closeOpts...,
)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("unable to create close "+
+ "proposal: %w", err)
}
// Depending on the channel type, we'll be encoding a normal
@@ -1605,9 +1606,9 @@ func processRemoteTaprootSig(env *Environment, msg lnwire.ClosingComplete,
return nil, err
}
- // Create a MusigPartialSig from the wire format The Nonce in
- // PartialSigWithNonce is their next closee nonce for future RBF. We
- // store it but don't use it for verification of this signature.
+ // Create a MusigPartialSig from the wire format. The nonce in
+ // PartialSigWithNonce is their JIT closer nonce used for the current
+ // session verification.
remoteSig := lnwallet.NewMusigPartialSig(
&musig2.PartialSignature{
S: &remotePartialSig.PartialSig.Sig,
@@ -1795,7 +1796,9 @@ func validateSigFields(sigFields SigFieldSet, localIsDust bool) error {
if localIsDust {
// Local output is dust, we need CloserNoClosee.
if sigFields.CloserNoClosee.IsNone() {
- return ErrCloserNoClosee
+ return fmt.Errorf("local output is dust but "+
+ "CloserNoClosee sig missing: %w",
+ ErrCloserNoClosee)
}
} else {
// Local output is not dust, we prefer CloserAndClosee, but can
@@ -1803,7 +1806,9 @@ func validateSigFields(sigFields SigFieldSet, localIsDust bool) error {
if sigFields.CloserAndClosee.IsNone() &&
sigFields.NoCloserClosee.IsNone() {
- return ErrCloserAndClosee
+ return fmt.Errorf("local output is not dust "+
+ "but no valid sig field present: %w",
+ ErrCloserAndClosee)
}
}
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.