Conversation
In this commit, we carry the tapscript root stored in the optional CloseTxInputs extension into the reconstructed OpenChannel for TapscriptRootVersion backups. Without the root, the chain watcher derives a BIP-86 funding script, which can cause Neutrino to miss the DLP force close. We preserve an empty root when the extension is absent so older backups retain their existing full-node recovery behavior.
🟡 PR Severity: MEDIUM
🟡 Medium (1 file)
🟢 Low (1 file)
AnalysisThe only non-test file touched is The change itself is small and scoped (11 added lines in the non-test file, no deletions) — well under the thresholds for a severity bump (>20 files or >500 lines changed). It fixes a bug in To override, add a |
Lrifton92
left a comment
There was a problem hiding this comment.
Reviewed at 6c283e3.
The fix is correct for the case it targets. Deserialize always populates TapscriptRoot for TapscriptRootVersion when the close-tx bit is set (chanbackup/single.go:731-744), and Serialize refuses to write that version without one (:462-467), so any version 6 backup that carries CloseTxInputs now yields a shell with a root. The root also survives persistence: RestoreChannelShells goes through syncNewChannel -> PutOpenChannel -> PutChanInfo, which encodes it as TLV type 6 (chanstate/kv_open_channel.go:362, :779). From there deriveFundingPkScript (contractcourt/chain_watcher.go:1850) derives the tweaked funding script instead of the BIP-86 one.
I ran go test -run TestOpenChannelShellTapscriptRoot . at this commit (passes), then dropped the TapscriptRoot: assignment from the shell: the top-level assertion fails with Option[chainhash.Hash] was None(), so the test does pin the regression. The subtest passes with or without the fix; it guards the rootless path against a panic or error, which is worth having, but it is not what catches the bug.
Two things.
Lint is red on this change. chanrestore.go:58 is 85 columns and fails the ll check in the "Lint code" job. See inline.
The rootless overlay case is still silent. A TapscriptRootVersion backup can lack CloseTxInputs when the local CommitTx was nil at backup time (chanbackup/backup.go:66-71). The shell still gets TapscriptRootBit (chanrestore.go:177-182) but no root, so the chain watcher registers the BIP-86 script, which is the exact Neutrino miss this PR describes. Recovering it needs the root and is out of scope here, but a warning at restore time would make that failure mode visible to a Neutrino user instead of leaving them waiting for a close that is never detected.
| if backup.Version.HasTapscriptRoot() { | ||
| // TapscriptRootVersion stores the root within the optional | ||
| // CloseTxInputs extension. | ||
| backup.CloseTxInputs.WhenSome(func(inputs chanbackup.CloseTxInputs) { |
There was a problem hiding this comment.
This line is 85 columns and is what fails the ll linter in CI (chanrestore.go:58:1: the line is 85 characters long). Binding the option first keeps it under 80:
closeInputs := backup.CloseTxInputs
closeInputs.WhenSome(func(in chanbackup.CloseTxInputs) {
tapscriptRoot = in.TapscriptRoot
})| *channeldb.ChannelShell, error) { | ||
|
|
||
| tapscriptRoot := fn.None[chainhash.Hash]() | ||
| if backup.Version.HasTapscriptRoot() { |
There was a problem hiding this comment.
When HasTapscriptRoot() is true but CloseTxInputs is None (possible per chanbackup/backup.go:66-71), tapscriptRoot stays None while chanType still gets TapscriptRootBit below. On Neutrino that shell will never see the remote DLP close, since the spend is registered against the untweaked script. Would a ltndLog.Warnf naming the channel point be worth adding for that branch?
In this PR, we retain the tapscript root carried by the optional
CloseTxInputsextension when reconstructing a channel shell for a simple Taproot overlay channel.openChannelShellrebuilt theTapscriptRootBit, but leftOpenChannel.TapscriptRootempty. The chain watcher would then derive a BIP-86 funding script, which can cause Neutrino to miss the peer's DLP force close.Rootless backups continue through the existing recovery path. This preserves outpoint-based recovery with full-node backends while fixing backups that contain the authenticated root.
The regression test asserts that restore retains a nonzero root and that a backup without
CloseTxInputsstill restores with an empty root.Test Plan