mirror of https://github.com/guggero/chantools
rescuetweakedkey: add new command rescuetweakedkey
parent
602cda19c3
commit
0e856460a7
@ -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(),
|
||||
)
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue