multi: add SCB restore support for production taproot channels
What changed, and why it matters
This commit fixes a bug in LND where backups of a new type of Bitcoin Lightning channel (called a 'production taproot channel') could not be properly restored. Without the fix, the channel type information was lost during restore, which could prevent users from recovering their funds through the Data Loss Protection (DLP) protocol. The commit also adds integration tests for both confirmed and unconfirmed (zero-conf) versions of these channels, and updates a warning comment about a test-only randomness option to make clear it must never be used in production because it could leak private keys.
Treat this as a functional/security fix and include it in release notes. Users relying on SCB backups for production taproot channels should upgrade before they need to restore. Review whether any production deployments are using WithCustomSigningRand, as the updated warning indicates it is unsafe outside test vector generation.
Security signals we found
Missing case in backup restoration logic for a new channel type, causing DLP failure and potential fund-recovery issues
Channel type bit reconstruction bug in SCB restore path
Strengthened warning around deterministic MuSig2 signing nonce source that could lead to private key extraction if used in production
Evidence from the diff
The patch adds the missing SimpleTaprootFinalVersion case to chanrestore.openChannelShell() so that SCB (Static Channel Backup) restoration reconstructs the correct channeldb type bits for production taproot channels. Previously, this case fell through to the default error path, so the channel type bits were not reconstructed and DLP would fail. The change sets ZeroHtlcTxFeeBit, AnchorOutputsBit, SingleFunderTweaklessBit, SimpleTaprootFeatureBit, and TaprootFinalBit. It also adds integration tests for confirmed and zero-conf SimpleTaprootFinal channel backup restoration. A separate comment-only change in lnwallet/channel.go strengthens the warning that WithCustomSigningRand is for test vector generation only because deterministic nonces enable private key extraction via nonce reuse in production.
Changed components
chanrestore.go: openChannelShell()itest/lnd_channel_backup_test.golnwallet/channel.go: WithCustomSigningRand commentInspect captured patch +36 / −2
diff --git a/chanrestore.go b/chanrestore.go
index a041f57..1f506cc 100644
--- a/chanrestore.go
+++ b/chanrestore.go
@@ -170,6 +170,13 @@ func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) (
chanType |= channeldb.SimpleTaprootFeatureBit
chanType |= channeldb.TapscriptRootBit
+ case chanbackup.SimpleTaprootFinalVersion:
+ chanType = channeldb.ZeroHtlcTxFeeBit
+ chanType |= channeldb.AnchorOutputsBit
+ chanType |= channeldb.SingleFunderTweaklessBit
+ chanType |= channeldb.SimpleTaprootFeatureBit
+ chanType |= channeldb.TaprootFinalBit
+
default:
return nil, fmt.Errorf("unknown Single version: %w", err)
}
diff --git a/itest/lnd_channel_backup_test.go b/itest/lnd_channel_backup_test.go
index b100fe5..bdfb21a 100644
--- a/itest/lnd_channel_backup_test.go
+++ b/itest/lnd_channel_backup_test.go
@@ -85,6 +85,30 @@ var channelRestoreTestCases = []*lntest.TestCase{
)
},
},
+ {
+ // Restore a channel back up of a confirmed production
+ // taproot channel.
+ Name: "restore simple taproot final",
+ TestFunc: func(ht *lntest.HarnessTest) {
+ runChanRestoreScenarioCommitTypes(
+ ht,
+ lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL,
+ false,
+ )
+ },
+ },
+ {
+ // Restore a channel back up of an unconfirmed production
+ // taproot channel.
+ Name: "restore simple taproot final zero conf",
+ TestFunc: func(ht *lntest.HarnessTest) {
+ runChanRestoreScenarioCommitTypes(
+ ht,
+ lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL,
+ true,
+ )
+ },
+ },
{
Name: "restore from rpc",
TestFunc: testChannelBackupRestoreFromRPC,
diff --git a/lnwallet/channel.go b/lnwallet/channel.go
index 694aaed..78ca895 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -922,8 +922,11 @@ func WithAuxResolver(resolver AuxContractResolver) ChannelOpt {
}
// WithCustomSigningRand is used to provide a custom random source for
-// generating deterministic JIT signing nonces in MuSig2 sessions. This should
-// only be used in tests that need reproducible MuSig2 signatures.
+// generating deterministic JIT signing nonces in MuSig2 sessions.
+//
+// WARNING: This MUST only be used for test vector generation. Setting this in
+// production will produce deterministic nonces, enabling private key extraction
+// via nonce reuse.
func WithCustomSigningRand(rand io.Reader) ChannelOpt {
return func(o *channelOpts) {
o.customSigningRand = fn.Some[io.Reader](rand)
Why this scored 63/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.