From f9335e86017e731a0f681d088a2d3256fc5fa6c1 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Tue, 18 Nov 2025 16:49:56 +0100 Subject: [PATCH 1/2] btcwallet: support combined tweak to private key Previously we'd define either a single or a double tweak for the sign descriptor. We introduce the option to apply both consecutively (double tweak first, single tweak second) if both tweak parameters are set. For callers who define only one of the two parameters we maintain the old behavior. --- lnwallet/btcwallet/signer.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go index 69f7ab60912..e2c0cd29651 100644 --- a/lnwallet/btcwallet/signer.go +++ b/lnwallet/btcwallet/signer.go @@ -238,13 +238,34 @@ func (b *BtcWallet) fetchPrivKey( // maybeTweakPrivKey examines the single and double tweak parameters on the // passed sign descriptor and may perform a mapping on the passed private key -// in order to utilize the tweaks, if populated. +// in order to utilize the tweaks, if populated. If both tweak parameters are +// set, then both are applied in the following order: +// +// a) double tweak +// b) single tweak func maybeTweakPrivKey(signDesc *input.SignDescriptor, privKey *btcec.PrivateKey) (*btcec.PrivateKey, error) { var retPriv *btcec.PrivateKey - switch { + // If both tweak parameters are set, then apply both. + if signDesc.DoubleTweak != nil && signDesc.SingleTweak != nil { + // First apply the double tweak. + retPriv = input.DeriveRevocationPrivKey( + privKey, signDesc.DoubleTweak, + ) + + // Then apply the single tweak. + retPriv = input.TweakPrivKey( + retPriv, signDesc.SingleTweak, + ) + + return retPriv, nil + } + + // If only one tweak parameter is set, apply it respectively over the + // provided private key. + switch { case signDesc.SingleTweak != nil: retPriv = input.TweakPrivKey(privKey, signDesc.SingleTweak) From 643fb39b86b608cdaaa10bd7bc50ac04ccf2a4ec Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Thu, 11 Dec 2025 12:05:57 +0100 Subject: [PATCH 2/2] btcwallet: add test coverage for maybeTweakPrivKey --- lnwallet/btcwallet/signer_test.go | 154 ++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/lnwallet/btcwallet/signer_test.go b/lnwallet/btcwallet/signer_test.go index e45211d82fe..2c511c4a418 100644 --- a/lnwallet/btcwallet/signer_test.go +++ b/lnwallet/btcwallet/signer_test.go @@ -350,3 +350,157 @@ func getChainBackend(t *testing.T, netParams *chaincfg.Params) (chain.Interface, func hardenedKey(part uint32) uint32 { return part + hdkeychain.HardenedKeyStart } + +// TestMaybeTweakPrivKey tests the maybeTweakPrivKey function to ensure it +// correctly applies single tweaks, double tweaks, both tweaks combined, and +// handles the case where no tweaks are applied. +func TestMaybeTweakPrivKey(t *testing.T) { + // Create a test private key. + privKeyBytes, err := hex.DecodeString( + "e68abc8e2a7a5b9f0e4a3c7d8b9e6f5" + + "a4b3c2d1e0f9e8d7c6b5a4938271605", + ) + require.NoError(t, err) + privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) + + // Create test tweak values. + singleTweakBytes, err := hex.DecodeString( + "1234567890abcdef1234567890abcdef" + + "1234567890abcdef1234567890abcdef", + ) + require.NoError(t, err) + + doubleTweakBytes, err := hex.DecodeString( + "fedcba0987654321fedcba0987654321" + + "fedcba0987654321fedcba0987654321", + ) + require.NoError(t, err) + doubleTweak, _ := btcec.PrivKeyFromBytes(doubleTweakBytes) + + testCases := []struct { + name string + singleTweak []byte + doubleTweak *btcec.PrivateKey + validate func(*testing.T, *btcec.PrivateKey) + }{ + { + name: "no tweaks applied", + singleTweak: nil, + doubleTweak: nil, + validate: func(t *testing.T, result *btcec.PrivateKey) { + // Should return the original private key + // unchanged. + require.Equal( + t, privKey.Serialize(), + result.Serialize(), + "expected private key to be unchanged", + ) + }, + }, + { + name: "single tweak only", + singleTweak: singleTweakBytes, + doubleTweak: nil, + validate: func(t *testing.T, result *btcec.PrivateKey) { + // Manually apply single tweak to verify. + expected := input.TweakPrivKey( + privKey, singleTweakBytes, + ) + require.Equal( + t, expected.Serialize(), + result.Serialize(), + "single tweak not applied correctly", + ) + // Ensure it's different from the original. + require.NotEqual( + t, privKey.Serialize(), + result.Serialize(), + "tweaked key should differ from "+ + "original", + ) + }, + }, + { + name: "double tweak only", + singleTweak: nil, + doubleTweak: doubleTweak, + validate: func(t *testing.T, result *btcec.PrivateKey) { + // Manually apply double tweak to verify. + expected := input.DeriveRevocationPrivKey( + privKey, doubleTweak, + ) + require.Equal( + t, expected.Serialize(), + result.Serialize(), + "double tweak not applied correctly", + ) + // Ensure it's different from the original. + require.NotEqual( + t, privKey.Serialize(), + result.Serialize(), + "tweaked key should differ from "+ + "original", + ) + }, + }, + { + name: "both tweaks combined", + singleTweak: singleTweakBytes, + doubleTweak: doubleTweak, + validate: func(t *testing.T, result *btcec.PrivateKey) { + // Manually apply both tweaks in order: double + // first, then single. + afterDouble := input.DeriveRevocationPrivKey( + privKey, doubleTweak, + ) + expected := input.TweakPrivKey( + afterDouble, singleTweakBytes, + ) + require.Equal( + t, expected.Serialize(), + result.Serialize(), + "combined tweaks not applied correctly", + ) + // Ensure it's different from single tweak only. + singleOnly := input.TweakPrivKey( + privKey, singleTweakBytes, + ) + require.NotEqual( + t, singleOnly.Serialize(), + result.Serialize(), + "combined tweak should differ from "+ + "single tweak only", + ) + // Ensure it's different from double tweak only. + doubleOnly := input.DeriveRevocationPrivKey( + privKey, doubleTweak, + ) + require.NotEqual( + t, doubleOnly.Serialize(), + result.Serialize(), + "combined tweak should differ from "+ + "double tweak only", + ) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + // Create a sign descriptor with the test tweaks. + signDesc := &input.SignDescriptor{ + SingleTweak: tc.singleTweak, + DoubleTweak: tc.doubleTweak, + } + + // Call the function under test. + result, err := maybeTweakPrivKey(signDesc, privKey) + require.NoError(t, err) + require.NotNil(t, result) + + // Validate the result. + tc.validate(t, result) + }) + } +}