chancloser: add nil-tx guard to legacy close test
What changed, and why it matters
This commit only changes a test file. It adds a safety check to make sure a function called during cooperative channel closes is always given a real transaction, not a blank one. The test now records every call and fails if any call has a nil transaction. It does not change the actual production code that users run, so it cannot by itself fix or introduce a live bug.
Treat this as a test-hardening commit. If the referenced limbo state is a real production concern, review the chancloser production code that calls MarkCoopBroadcasted to ensure it never passes a nil transaction, and consider adding a runtime guard or explicit error return there. No urgent deployment action is needed for this test-only change.
Security signals we found
Test-only regression guard for nil transaction in cooperative close path
References external issue about a 'limbo state' when ChanStatusCoopBroadcasted is set without a stored transaction
No production code change; no patch to the actual MarkCoopBroadcasted caller or channel state machine
Evidence from the diff
The change is in lnwallet/chancloser/chancloser_test.go. The legacy mockChannel now tracks each MarkCoopBroadcasted invocation in coopBroadcastTxns. TestTaprootFastClose asserts that both alice and bob made at least one MarkCoopBroadcasted call and that none of those calls passed a nil *wire.MsgTx. The commit message links this to a ‘limbo state’ in a taproot-assets issue, but the patch is purely a regression test guard; no implementation logic is modified.
Changed components
lnwallet/chancloser/chancloser_test.golegacy mockChannel test helperTestTaprootFastCloseInspect captured patch +25 / −2
diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go
index 5d96b96..16afb65 100644
--- a/lnwallet/chancloser/chancloser_test.go
+++ b/lnwallet/chancloser/chancloser_test.go
@@ -147,6 +147,8 @@ type mockChannel struct {
chanType channeldb.ChannelType
localKey keychain.KeyDescriptor
remoteKey keychain.KeyDescriptor
+
+ coopBroadcastTxns []*wire.MsgTx
}
func (m *mockChannel) ChannelPoint() wire.OutPoint {
@@ -161,8 +163,10 @@ func (m *mockChannel) FundingBlob() fn.Option[tlv.Blob] {
return fn.None[tlv.Blob]()
}
-func (m *mockChannel) MarkCoopBroadcasted(*wire.MsgTx,
- lntypes.ChannelParty) error {
+func (m *mockChannel) MarkCoopBroadcasted(tx *wire.MsgTx,
+ _ lntypes.ChannelParty) error {
+
+ m.coopBroadcastTxns = append(m.coopBroadcastTxns, tx)
return nil
}
@@ -643,4 +647,23 @@ func TestTaprootFastClose(t *testing.T) {
tx, _ = bobCloser.ClosingTx()
require.NotNil(t, tx)
require.True(t, oClosingSigned.IsNone())
+
+ // Every MarkCoopBroadcasted call must have a real close tx.
+ // A nil tx would set ChanStatusCoopBroadcasted without a
+ // stored transaction, creating the limbo state described in
+ // https://github.com/lightninglabs/taproot-assets/issues/2108.
+ require.NotEmpty(t, aliceChan.coopBroadcastTxns,
+ "expected at least one MarkCoopBroadcasted call "+
+ "from alice")
+ require.NotEmpty(t, bobChan.coopBroadcastTxns,
+ "expected at least one MarkCoopBroadcasted call "+
+ "from bob")
+ for i, broadcastTx := range aliceChan.coopBroadcastTxns {
+ require.NotNilf(t, broadcastTx,
+ "alice MarkCoopBroadcasted call %d had nil tx", i)
+ }
+ for i, broadcastTx := range bobChan.coopBroadcastTxns {
+ require.NotNilf(t, broadcastTx,
+ "bob MarkCoopBroadcasted call %d had nil tx", i)
+ }
}
Why this scored 22/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.