Skip to content

tapchannel: fix immediate force-close proof sync and sweep anchoring#2133

Merged
jtobin merged 1 commit into
lightninglabs:mainfrom
GeorgeTsagk:close-immediately-itest
Jun 29, 2026
Merged

tapchannel: fix immediate force-close proof sync and sweep anchoring#2133
jtobin merged 1 commit into
lightninglabs:mainfrom
GeorgeTsagk:close-immediately-itest

Conversation

@GeorgeTsagk

@GeorgeTsagk GeorgeTsagk commented May 13, 2026

Copy link
Copy Markdown
Member

Description

This PR fixes immediate post-funding (N=0) force-close recovery by correctly re-syncing active vPacket outputs with re-anchored proofs when anchor indices are stale, and hardens anchor resolution to return explicit errors instead of panicking on out-of-range indices.

It also adds an integration test that force-closes right after funding confirmation and verifies the swept assets remain spendable via an onward transfer to a third node.

Depends on lightningnetwork/lnd#10804

These last parts of the PR now moved to #2177

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, 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 improves the reliability of asset channel force-closes, particularly in scenarios where a channel is closed immediately after funding. It introduces mechanisms to re-sync proofs and re-anchor outputs correctly, while also hardening the code against potential panics during anchor resolution. These changes ensure that assets remain recoverable and spendable even under edge-case timing conditions.

Highlights

  • Immediate Force-Close Recovery: Fixed post-funding (N=0) force-close recovery by ensuring active vPacket outputs are correctly re-synced with re-anchored proofs.
  • Anchor Resolution Hardening: Replaced potential panics with explicit error handling for out-of-range anchor indices, improving system stability.
  • Proof Metadata Persistence: Added support for storing witness scripts and control blocks in proof metadata, enabling reliable sweeps for commitment outputs.
  • Integration Testing: Added a new integration test to verify that assets remain spendable after an immediate force-close.
New Features

🧠 You can now enable Memory (public preview) 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 the 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 counterproductive. 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.

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.

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

Copy link
Copy Markdown

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 introduces support for immediate force-closing of asset channels by adding a new test case and updating the auxiliary sweeper and commitment logic to handle the necessary proof metadata and script key synchronization. The review comment regarding the robustness of the confirmation fallback logic in waitForTransferTxConf is valid and provides an actionable improvement, so it has been retained.

Comment thread tapfreighter/chain_porter.go Outdated
Comment on lines +459 to +461
if startHeight == 0 || startHeight > currentHeight {
startHeight = currentHeight
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The fallback confirmation logic might not be robust enough if no height hint is provided. If scanHeight is 0, startHeight is set to currentHeight, meaning only the current block is scanned. If the transaction was confirmed in a recent block (e.g., currentHeight - 1) and the primary notification was missed, this fallback will not find it, as scanHeight will be updated to currentHeight + 1 and the missed block will never be scanned.

To make this fallback more reliable, consider scanning a few recent blocks when no height hint is available.

                if startHeight == 0 {
			const rescanDepth = 6
			if currentHeight > rescanDepth {
				startHeight = currentHeight - rescanDepth
			} else {
				startHeight = 1
			}
		} else if startHeight > currentHeight {
			startHeight = currentHeight
		}

@GeorgeTsagk
GeorgeTsagk force-pushed the close-immediately-itest branch 2 times, most recently from 4899471 to 9093b73 Compare May 15, 2026 11:47
@kaldun-tech

kaldun-tech commented May 27, 2026

Copy link
Copy Markdown
Contributor

@GeorgeTsagk I had a failure in the CI run of #2130. Claude & Copilot pointed me towards this PR as the likely cause https://github.com/lightninglabs/taproot-assets/actions/runs/26522178163/job/78117493175?pr=2130

EDIT: Since this is not merged it indicates that the failure is due to flakiness, possibly fixed by sergey's PR #2060

@lightninglabs-deploy

Copy link
Copy Markdown

@jtobin: review reminder
@GeorgeTsagk, remember to re-request review from reviewers when ready

@GeorgeTsagk
GeorgeTsagk force-pushed the close-immediately-itest branch 2 times, most recently from bf189cc to c09446d Compare June 15, 2026 13:54
Comment thread tapchannel/commitment.go Outdated

const (
commitSweepWitnessScriptType tlv.Type = 31337
commitSweepControlBlockType tlv.Type = 31339

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You have some magic numbers here. Want to add comment to explain the reason?

Comment thread tapfreighter/parcel.go Outdated
"funded psbt outputs (len=%d)",
vOut.AnchorOutputIndex, outputCount,
)
}

@kaldun-tech kaldun-tech Jun 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is good to prevent panic on out-of-range indices

Comment thread tapchannel/aux_sweeper.go Outdated
}
if matchIdx == -1 {
continue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The matching logic here is subtle and challenging to understand. Think it could use some comments explaining why, for example we skip when fallbackCount > 1 rather than erroring.

Should this log a warning when it skips? Silent skips can be hard to debug if a sweep fails

@GeorgeTsagk
GeorgeTsagk force-pushed the close-immediately-itest branch from c09446d to 6e66ff2 Compare June 18, 2026 11:40
// and then force closed immediately after the funding transaction confirms,
// without any in-channel asset movement.
func testCustomChannelsImmediateClose(ctx context.Context,
net *itest.IntegratedNetworkHarness, t *ccHarnessTest) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to validate the N=0 force-close path end to end

totalFee := commitFee
if lndOpenChan.ChanType.HasAnchors() {
totalFee += 2 * lnwallet.AnchorSize
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the totalFee here represents total reserved sats not just miner fees. the calculation is good, is there a more descriptive name for it?

Comment thread itest/custom_channels/breach_test.go Outdated
// transaction in the mempool sweeping commitment outputs.
// We wait for at least one tx as other background txns can race in.
charlieJusticeTxid, err := waitForAtLeastNTxsInMempool(
net.Miner, 1, time.Second*60,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any particular reason to increase to 60s timeout here?

Comment thread tapchannel/aux_sweeper.go Outdated
}
}

func syncActiveOutputProofsFromVPackets(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the new functions here could use header comments

Comment thread tapchannel/aux_sweeper.go Outdated
// commitment at height 0 — i.e. an immediate post-funding force close,
// before the channel_ready commit-point rotation.
func isInitialCommitState(req lnwallet.ResolutionReq) bool {
return req.CommitHeight.UnwrapOr(1) == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The UnwrapOr(1) default means if CommitHeight is not set, we assume it's NOT the initial state with a default value of 1. This is a safe default that should be documented.

@jtobin jtobin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think you need the special-case reconstruction stuff for commit height 0 anymore.

I think that idea can be summarized as: "the data at commit height 0 is possibly bad, so before doing anything, reconstruct it to ensure it's good." But IIUC the other fixes ensure that the initial commit data is good, so it should be safe to trust.

N.b., the other fixes being:

  • lnd should populate negotiated CsvDelay and DustLimit before tapd constructs the initial commitment blob (lightningnetwork/lnd#10804),
  • tapd should build initial commitment blobs with actual/nonzero BTC balances (in 5275b38), and
  • tapd should walk the commitment tx to figure out where assets actually are/aren't during force close (also in 5275b38).

Is there still anything that the initial commit height special-case machinery accomplishes that those fixes don't?

If not, I vote to cut this one down: include just those latter two tapd fixes, and then add the itest in a separate PR. The tapd-side fixes don't depend on the lnd change, so we can merge/release them without having to wait for an lnd release. Eh, eh?

@github-project-automation github-project-automation Bot moved this from 🆕 New to 👀 In review in Taproot-Assets Project Board Jun 24, 2026
@GeorgeTsagk
GeorgeTsagk force-pushed the close-immediately-itest branch from 6e66ff2 to 45c60da Compare June 25, 2026 13:17
@GeorgeTsagk

GeorgeTsagk commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review @kaldun-tech & @jtobin . Applied your recommendations and also split the itest/lnd bump part to a separate PR #2177

Comment thread tapchannel/aux_sweeper.go Outdated

@kaldun-tech kaldun-tech left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The only blocker here I see is the fee calculation underflow. Otherwise looks great with solid fixes.

Comment thread tapchannel/aux_funding_controller.go

@jtobin jtobin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I still think this should be stripped down further. 😅

There's still machinery in here to work around the issue that lightningnetwork/lnd#10804 should ultimately fix by populating the initial aux channel state appropriately, but the cure is worse than the disease, IMO.

The additional TLV records, the skipOutputProofVerify bool threaded through everything, the added CommitWitness{Script, ControlBlock} fields -- I don't think any of that will be necessary when the upstream PR lands. IIUC, the initial aux channel state will be populated properly, and then there'll be nothing new on tapd's end to do with it. The porter's normal verification procedure should just work on its own.

If we can safely assume that lnd gives us correct initial negotiated values, then the diff of the purely tapd-side fixes (for init. balances and commit tx asset locations/nonlocations) shrinks to something like (closer to like +120/-50 ish):

https://gist.github.com/jtobin/50132672406173b31ee328205175f673

Essentially I argue that "lnd gives us incorrect negotiated initial values" is an lnd problem, so the fix for it should be in lnd. Fixing it in tapd feels too gnarly.

jtobin ducks

@GeorgeTsagk
GeorgeTsagk force-pushed the close-immediately-itest branch from 45c60da to c55c913 Compare June 29, 2026 12:46
@GeorgeTsagk
GeorgeTsagk force-pushed the close-immediately-itest branch from c55c913 to 79cac9f Compare June 29, 2026 13:33

@jtobin jtobin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice, LGTM. 👍 👍

@jtobin
jtobin enabled auto-merge June 29, 2026 14:31
@jtobin
jtobin added this pull request to the merge queue Jun 29, 2026
Merged via the queue into lightninglabs:main with commit fa7f4d8 Jun 29, 2026
120 of 122 checks passed
@github-project-automation github-project-automation Bot moved this from 👀 In review to ✅ Done in Taproot-Assets Project Board Jun 29, 2026
jtobin added a commit that referenced this pull request Jul 13, 2026
tapchannel: fix immediate force-close proof sync and sweep anchoring
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

4 participants