Skip to content

Conversation

@jtobin
Copy link
Contributor

@jtobin jtobin commented Jan 9, 2026

Should resolve future cases of lightninglabs/taproot-assets#1890.

Consider a cooperative close of an assets channel. Upon receiving a ClosingSigned message, we call the aux closer's FinalizeClose() method, in practice dispatching to tapd's chain porter to broadcast the closing transaction, before marking the transaction as "broadcasted" and persisting it to the database. If the porter fails to broadcast (e.g., "min relay fee not met"), then we error out, leaving the channel marked as ChanStatusCoopBroadcasted, but having failed to persist the transaction. The result is that the channel gets stuck in the waiting_close_channels state indefinitely, with no persisted transaction to rebroadcast upon restart ("no coop closing tx to re-publish").

This fix moves MarkCoopBroadcasted(closeTx) before FinalizeClose() so that the closing transaction is persisted whether or not the aux closer succeeds. Regular channels were unaffected because they have no aux closer, and are persisted before being broadcast.

@gemini-code-assist
Copy link

Summary of Changes

Hello @jtobin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a potential channel-stalling bug during cooperative closes of asset channels. It reorders the transaction persistence logic to ensure that the closing transaction is always written to the database before any external broadcast attempts by auxiliary closers. This guarantees that even if an auxiliary closer fails to broadcast, the transaction is saved and can be re-broadcasted later, significantly improving the robustness and recoverability of asset channel closures.

Highlights

  • Bug Fix: Cooperative Close Transaction Persistence: Addresses a critical issue where cooperative close transactions for asset channels were not being persisted to the database if the auxiliary closer (e.g., tapd's chain porter) failed to broadcast the transaction. This could leave channels stuck in a waiting_close_channels state.
  • Reordered Transaction Persistence: The MarkCoopBroadcasted call, which persists the closing transaction, has been moved to execute before the FinalizeClose method of the auxiliary channel closer. This ensures the transaction is always saved, even if the subsequent broadcast attempt by the auxiliary closer fails.
  • Enhanced Restart Recovery: By persisting the transaction earlier, the system can now reliably re-broadcast the closing transaction upon restart, preventing channels from getting permanently stuck due to transient broadcast failures.
  • New Test Case: A new test, TestAuxFinalizeCloseFailurePreservesTx, has been added to explicitly verify that MarkCoopBroadcasted is called and the transaction is persisted even when the aux.FinalizeClose method encounters an error.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly addresses a potential race condition during the cooperative close of an assets channel, where a broadcast failure could leave the channel in a stuck state. By persisting the closing transaction before attempting to finalize the close with the auxiliary closer, the change ensures that the transaction can be rebroadcast on restart, significantly improving the robustness of the process. The inclusion of a specific test case that simulates this failure scenario is excellent and provides strong confidence in the fix. The changes are logical, well-contained, and effectively resolve the described issue.

jtobin added 3 commits January 9, 2026 16:41
Moves MarkCoopBroadcasted(closeTx) before aux.FinalizeClose() in the
co-op close flow. This ensures the closing transaction is persisted to
the database before the aux closer attempts to broadcast it.

Previously, if aux.FinalizeClose failed (e.g., due to broadcast error
like "min relay fee not met"), the close tx was never stored. The
channel would remain marked ChanStatusCoopBroadcasted (set earlier with
a nil tx) but have no tx available for re-broadcast on restart, leaving
the channel stuck in the waiting_close_channels state.
Verify that MarkCoopBroadcasted is called with the close tx before
aux.FinalizeClose, ensuring the tx is persisted even if the aux closer
fails during broadcast.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug]: How to Recover Channels Stuck in waiting_close_channels After Low-Fee Closure Attempts?

1 participant