rescuetweakedkey: add new command rescuetweakedkey

pull/51/head
Oliver Gugger 1 year ago
parent 602cda19c3
commit 0e856460a7
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -0,0 +1,228 @@
package main
import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/guggero/chantools/lnd"
"github.com/spf13/cobra"
)
var (
AddrNotFoundErr = fmt.Errorf("address not found")
)
type rescueTweakedKeyCommand struct {
Path string
TargetAddr string
NumTries uint64
rootKey *rootKey
cmd *cobra.Command
}
func newRescueTweakedKeyCommand() *cobra.Command {
cc := &rescueTweakedKeyCommand{}
cc.cmd = &cobra.Command{
Use: "rescuetweakedkey",
Short: "Attempt to rescue funds locked in an address with a " +
"key that was affected by a specific bug in lnd",
Long: `There very likely is no reason to run this command
unless you exactly know why or were told by the author of this tool to use it.
`,
Example: `chantools rescuetweakedkey \
--path "m/1017'/0'/5'/0/0'" \
--targetaddr bc1pxxxxxxx`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.Path, "path", "", "BIP32 derivation path to derive the "+
"starting key from; must start with \"m/\"",
)
cc.cmd.Flags().StringVar(
&cc.TargetAddr, "targetaddr", "", "address the funds are "+
"locked in",
)
cc.cmd.Flags().Uint64Var(
&cc.NumTries, "numtries", 10_000_000, "the number of "+
"mutations to try",
)
cc.rootKey = newRootKey(cc.cmd, "deriving starting key")
return cc.cmd
}
func (c *rescueTweakedKeyCommand) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
if c.Path == "" {
return fmt.Errorf("path is required")
}
childKey, _, _, err := lnd.DeriveKey(extendedKey, c.Path, chainParams)
if err != nil {
return fmt.Errorf("could not derive key: %w", err)
}
startKey, err := childKey.ECPrivKey()
if err != nil {
return fmt.Errorf("error deriving private key: %w", err)
}
targetAddr, err := lnd.ParseAddress(c.TargetAddr, chainParams)
if err != nil {
return fmt.Errorf("error parsing target addr: %w", err)
}
return testPattern(startKey, targetAddr, c.NumTries)
}
func testPattern(startKey *btcec.PrivateKey, targetAddr btcutil.Address,
max uint64) error {
currentKey := copyPrivKey(startKey)
for idx := uint64(0); idx <= max; idx++ {
match, err := pubKeyMatchesAddr(currentKey.PubKey(), targetAddr)
if err != nil {
return fmt.Errorf("error matching key to address: %w",
err)
}
if match {
log.Infof("Success! Found private key %x for "+
"address %v\n", currentKey.Serialize(),
targetAddr)
return nil
}
mutateWithTweak(currentKey)
match, err = pubKeyMatchesAddr(currentKey.PubKey(), targetAddr)
if err != nil {
return fmt.Errorf("error matching key to address: %w",
err)
}
if match {
log.Infof("Success! Found private key %x for "+
"address %v\n", currentKey.Serialize(),
targetAddr)
return nil
}
keyCopy := copyPrivKey(currentKey)
mutateWithSign(keyCopy)
match, err = pubKeyMatchesAddr(keyCopy.PubKey(), targetAddr)
if err != nil {
return fmt.Errorf("error matching key to address: %w",
err)
}
if match {
log.Infof("Success! Found private key %x for "+
"address %v\n", keyCopy.Serialize(),
targetAddr)
return nil
}
if idx != 0 && idx%5000 == 0 {
fmt.Printf("Tested %d of %d mutations\n", idx, max)
}
}
match, err := pubKeyMatchesAddr(currentKey.PubKey(), targetAddr)
if err != nil {
return fmt.Errorf("error matching key to address: %w", err)
}
if match {
log.Infof("Success! Found private key %x for address %v\n",
currentKey.Serialize(), targetAddr)
return nil
}
return fmt.Errorf("%w: key for address %v not found after %d attempts",
AddrNotFoundErr, targetAddr.String(), max)
}
func pubKeyMatchesAddr(pubKey *btcec.PublicKey, addr btcutil.Address) (bool,
error) {
switch typedAddr := addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
return bytes.Equal(hash160, typedAddr.WitnessProgram()), nil
case *btcutil.AddressTaproot:
taprootKey := txscript.ComputeTaprootKeyNoScript(pubKey)
return bytes.Equal(
schnorr.SerializePubKey(taprootKey),
typedAddr.WitnessProgram(),
), nil
default:
return false, fmt.Errorf("unsupported address type <%T>",
typedAddr)
}
}
func copyPrivKey(privKey *btcec.PrivateKey) *btcec.PrivateKey {
privKeyCopy := *privKey
return &btcec.PrivateKey{
Key: privKeyCopy.Key,
}
}
func mutateWithSign(privKey *btcec.PrivateKey) {
privKeyScalar := &privKey.Key
pub := privKey.PubKey()
// Step 5.
//
// Negate d if P.y is odd.
pubKeyBytes := pub.SerializeCompressed()
if pubKeyBytes[0] == secp256k1.PubKeyFormatCompressedOdd {
privKeyScalar.Negate()
}
}
func mutateWithTweak(privKey *btcec.PrivateKey) {
// If the corresponding public key has an odd y coordinate, then we'll
// negate the private key as specified in BIP 341.
privKeyScalar := &privKey.Key
pubKeyBytes := privKey.PubKey().SerializeCompressed()
if pubKeyBytes[0] == secp256k1.PubKeyFormatCompressedOdd {
privKeyScalar.Negate()
}
// Next, we'll compute the tap tweak hash that commits to the internal
// key and the merkle script root. We'll snip off the extra parity byte
// from the compressed serialization and use that directly.
schnorrKeyBytes := pubKeyBytes[1:]
tapTweakHash := chainhash.TaggedHash(
chainhash.TagTapTweak, schnorrKeyBytes, []byte{},
)
// Map the private key to a ModNScalar which is needed to perform
// operation mod the curve order.
var tweakScalar btcec.ModNScalar
tweakScalar.SetBytes((*[32]byte)(tapTweakHash))
// Now that we have the private key in its may negated form, we'll add
// the script root as a tweak. As we're using a ModNScalar all
// operations are already normalized mod the curve order.
_ = privKeyScalar.Add(&tweakScalar)
}

@ -0,0 +1,63 @@
package main
import (
"encoding/hex"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/stretchr/testify/require"
)
var (
privKeyBytes, _ = hex.DecodeString(
"571e2fc5e99f91596f7561da9f605cbf2e2342a166593eef041862b6a8b7" +
"4f35",
)
pubKeyOrigBytes, _ = hex.DecodeString(
"032ec305fb12642fd3b1091d1cba88ebb7b1a8dbc256b35789b7e223a1b3" +
"75f0b7",
)
pubKeyNegBytes, _ = hex.DecodeString(
"022ec305fb12642fd3b1091d1cba88ebb7b1a8dbc256b35789b7e223a1b3" +
"75f0b7",
)
pubKeyNegTweakBytes, _ = hex.DecodeString(
"0322b5c94ec4dc3a8843edc7448a0aad389d43e0f8d1b35b546dd1aad70f" +
"b2c45b",
)
pubKeyNegTweakTweakBytes, _ = hex.DecodeString(
"03f4cd1ff9efa8198e33e5a110dc690c1472d56c01287893c2f8ed55f61e" +
"a767d1",
)
)
func TestTweak(t *testing.T) {
privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes)
require.Equal(t, pubKeyOrigBytes, pubKey.SerializeCompressed())
privKeyCopy := copyPrivKey(privKey)
require.Equal(t, privKey, privKeyCopy)
mutateWithSign(privKeyCopy)
require.NotEqual(t, privKey, privKeyCopy)
require.Equalf(
t, pubKeyNegBytes, privKeyCopy.PubKey().SerializeCompressed(),
"%x", privKeyCopy.PubKey().SerializeCompressed(),
)
mutateWithTweak(privKeyCopy)
require.NotEqual(t, privKey, privKeyCopy)
require.Equalf(
t, pubKeyNegTweakBytes,
privKeyCopy.PubKey().SerializeCompressed(),
"%x", privKeyCopy.PubKey().SerializeCompressed(),
)
mutateWithTweak(privKeyCopy)
require.NotEqual(t, privKey, privKeyCopy)
require.Equalf(
t, pubKeyNegTweakTweakBytes,
privKeyCopy.PubKey().SerializeCompressed(),
"%x", privKeyCopy.PubKey().SerializeCompressed(),
)
}

@ -97,6 +97,7 @@ func main() {
newRemoveChannelCommand(),
newRescueClosedCommand(),
newRescueFundingCommand(),
newRescueTweakedKeyCommand(),
newShowRootKeyCommand(),
newSignRescueFundingCommand(),
newSummaryCommand(),

@ -35,6 +35,7 @@ Complete documentation is available at https://github.com/guggero/chantools/.
* [chantools removechannel](chantools_removechannel.md) - Remove a single channel from the given channel DB
* [chantools rescueclosed](chantools_rescueclosed.md) - Try finding the private keys for funds that are in outputs of remotely force-closed channels
* [chantools rescuefunding](chantools_rescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run
* [chantools rescuetweakedkey](chantools_rescuetweakedkey.md) - Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
* [chantools showrootkey](chantools_showrootkey.md) - Extract and show the BIP32 HD root key from the 24 word lnd aezeed
* [chantools signrescuefunding](chantools_signrescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run
* [chantools summary](chantools_summary.md) - Compile a summary about the current state of channels

@ -0,0 +1,44 @@
## chantools rescuetweakedkey
Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
### Synopsis
There very likely is no reason to run this command
unless you exactly know why or were told by the author of this tool to use it.
```
chantools rescuetweakedkey [flags]
```
### Examples
```
chantools rescuetweakedkey \
--path "m/1017'/0'/5'/0/0'" \
--targetaddr bc1pxxxxxxx
```
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for rescuetweakedkey
--numtries uint the number of mutations to try (default 10000000)
--path string BIP32 derivation path to derive the starting key from; must start with "m/"
--rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed
--targetaddr string address the funds are locked in
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -26,6 +26,7 @@ chantools walletinfo --withrootkey \
### Options
```
--dumpaddrs print all addresses, including private keys
-h, --help help for walletinfo
--walletdb string lnd wallet.db file to dump the contents from
--withrootkey print BIP32 HD root key of wallet to standard out

Loading…
Cancel
Save