From 338c22fc2ee2a729e6ef819ba0ce212e05dd83d2 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 26 Apr 2020 22:36:44 +0200 Subject: [PATCH] Begin implementing rescuefunding command --- README.md | 20 +++++ cmd/chantools/derivekey.go | 8 +- cmd/chantools/genimportscript.go | 20 ++--- cmd/chantools/main.go | 6 ++ cmd/chantools/rescueclosed.go | 34 ++------ cmd/chantools/rescuefunding.go | 145 +++++++++++++++++++++++++++++++ cmd/chantools/sweeptimelock.go | 8 +- dump/dump.go | 2 +- lnd/hdkeychain.go | 68 ++++++++++++--- 9 files changed, 255 insertions(+), 56 deletions(-) create mode 100644 cmd/chantools/rescuefunding.go diff --git a/README.md b/README.md index e36a898..8b8f08d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ + [genimportscript](#genimportscript) + [forceclose](#forceclose) + [rescueclosed](#rescueclosed) + + [rescuefunding](#rescuefunding) + [showrootkey](#showrootkey) + [summary](#summary) + [sweeptimelock](#sweeptimelock) @@ -69,6 +70,7 @@ Available commands: forceclose Force-close the last state that is in the channel.db provided. genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind. rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels. + rescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel. showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed. summary Compile a summary about the current state of channels. sweeptimelock Sweep the force-closed state after the time lock has expired. @@ -330,6 +332,24 @@ chantools --fromsummary results/summary-xxxx-yyyy.json \ --rootkey xprvxxxxxxxxxx ``` +### rescuefunding + +```text +Usage: + chantools [OPTIONS] rescuefunding [rescuefunding-OPTIONS] + +[rescuefunding command options] + --rootkey= BIP32 HD root (m/) key to derive the key for our node from. + --othernodepub= The extended public key (xpub) of the other node's multisig branch (m/1017'/'/0'/0). + --fundingaddr= The bech32 script address of the funding output where the coins to be spent are locked in. + --fundingoutpoint= The funding transaction outpoint (:). + --fundingamount= The exact amount in satoshis that is locked in the funding output. + --sweepaddr= The address to sweep the rescued funds to. + --satperbyte= The fee rate to use in satoshis/vByte. +``` + +**This command is not fully implemented yet and only listed here as a placeholder.** + ### showrootkey This command converts the 24 word `lnd` aezeed phrase and password to the BIP32 diff --git a/cmd/chantools/derivekey.go b/cmd/chantools/derivekey.go index 93f6e77..ea35fa3 100644 --- a/cmd/chantools/derivekey.go +++ b/cmd/chantools/derivekey.go @@ -40,14 +40,20 @@ func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string, neuter bool) error { fmt.Printf("Deriving path %s for network %s.\n", path, chainParams.Name) - pubKey, wif, err := lnd.DeriveKey(extendedKey, path, chainParams) + child, pubKey, wif, err := lnd.DeriveKey(extendedKey, path, chainParams) if err != nil { return fmt.Errorf("could not derive keys: %v", err) } + neutered, err := child.Neuter() + if err != nil { + return fmt.Errorf("could not neuter child key: %v", err) + } fmt.Printf("Public key: %x\n", pubKey.SerializeCompressed()) + fmt.Printf("Extended public key (xpub): %s\n", neutered.String()) if !neuter { fmt.Printf("Private key (WIF): %s\n", wif.String()) + fmt.Printf("Extended private key (xprv): %s\n", child.String()) } return nil diff --git a/cmd/chantools/genimportscript.go b/cmd/chantools/genimportscript.go index 613c6f0..60c36ea 100644 --- a/cmd/chantools/genimportscript.go +++ b/cmd/chantools/genimportscript.go @@ -65,16 +65,6 @@ func (c *genImportScriptCommand) Execute(_ []string) error { // Decide what derivation path(s) to use. switch { - case c.LndPaths && c.DerivationPath != "": - return fmt.Errorf("cannot use --lndpaths and --derivationpath " + - "at the same time") - - case c.LndPaths: - strPaths, paths, err = lnd.AllDerivationPaths(chainParams) - if err != nil { - return fmt.Errorf("error getting lnd paths: %v", err) - } - default: c.DerivationPath = lnd.WalletDefaultDerivationPath fallthrough @@ -86,6 +76,16 @@ func (c *genImportScriptCommand) Execute(_ []string) error { } strPaths = []string{c.DerivationPath} paths = [][]uint32{derivationPath} + + case c.LndPaths && c.DerivationPath != "": + return fmt.Errorf("cannot use --lndpaths and --derivationpath " + + "at the same time") + + case c.LndPaths: + strPaths, paths, err = lnd.AllDerivationPaths(chainParams) + if err != nil { + return fmt.Errorf("error getting lnd paths: %v", err) + } } exporter := btc.ParseFormat(c.Format) diff --git a/cmd/chantools/main.go b/cmd/chantools/main.go index 04f19d2..ca0bc30 100644 --- a/cmd/chantools/main.go +++ b/cmd/chantools/main.go @@ -128,6 +128,11 @@ func runCommandParser() error { "compacting it in the process.", "", &compactDBCommand{}, ) + _, _ = parser.AddCommand( + "rescuefunding", "Rescue funds locked in a funding multisig "+ + "output that never resulted in a proper channel.", "", + &rescueFundingCommand{}, + ) _, err := parser.Parse() return err @@ -221,6 +226,7 @@ func rootKeyFromConsole() (*hdkeychain.ExtendedKey, time.Time, error) { if err != nil { return nil, time.Unix(0, 0), err } + fmt.Println() var mnemonic aezeed.Mnemonic copy(mnemonic[:], cipherSeedMnemonic) diff --git a/cmd/chantools/rescueclosed.go b/cmd/chantools/rescueclosed.go index 02ae5e3..7edb697 100644 --- a/cmd/chantools/rescueclosed.go +++ b/cmd/chantools/rescueclosed.go @@ -154,10 +154,15 @@ func rescueClosedChannels(extendedKey *hdkeychain.ExtendedKey, } func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) { - targetPubKeyHash, err := parseAddr(addr) + targetPubKeyHash, scriptHash, err := lnd.DecodeAddressHash( + addr, chainParams, + ) if err != nil { return "", fmt.Errorf("error parsing addr: %v", err) } + if scriptHash { + return "", fmt.Errorf("address must be a P2WPKH address") + } // Loop through all cached payment base point keys, tweak each of it // with the per_commit_point and see if the hashed public key @@ -229,30 +234,3 @@ func fillCache(extendedKey *hdkeychain.ExtendedKey) error { } return nil } - -func parseAddr(addr string) ([]byte, error) { - // First parse address to get targetPubKeyHash from it later. - targetAddr, err := btcutil.DecodeAddress(addr, chainParams) - if err != nil { - return nil, err - } - - var targetPubKeyHash []byte - // Make the check on the decoded address according to the active - // network (testnet or mainnet only). - if !targetAddr.IsForNet(chainParams) { - return nil, fmt.Errorf( - "address: %v is not valid for this network: %v", - targetAddr.String(), chainParams.Name, - ) - } - - // Must be a bech32 native SegWit address. - switch targetAddr.(type) { - case *btcutil.AddressWitnessPubKeyHash: - targetPubKeyHash = targetAddr.ScriptAddress() - default: - return nil, fmt.Errorf("address: must be a bech32 P2WPKH address") - } - return targetPubKeyHash, nil -} diff --git a/cmd/chantools/rescuefunding.go b/cmd/chantools/rescuefunding.go new file mode 100644 index 0000000..33bd201 --- /dev/null +++ b/cmd/chantools/rescuefunding.go @@ -0,0 +1,145 @@ +package main + +import ( + "bytes" + "fmt" + + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/guggero/chantools/lnd" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +const ( + MaxChannelLookup = 5000 +) + +type rescueFundingCommand struct { + RootKey string `long:"rootkey" description:"BIP32 HD root (m/) key to derive the key for our node from."` + OtherNodePub string `long:"othernodepub" description:"The extended public key (xpub) of the other node's multisig branch (m/1017'/'/0'/0)."` + FundingAddr string `long:"fundingaddr" description:"The bech32 script address of the funding output where the coins to be spent are locked in."` + FundingOutpoint string `long:"fundingoutpoint" description:"The funding transaction outpoint (:)."` + FundingAmount int64 `long:"fundingamount" description:"The exact amount in satoshis that is locked in the funding output."` + SweepAddr string `long:"sweepaddr" description:"The address to sweep the rescued funds to."` + SatPerByte int64 `long:"satperbyte" description:"The fee rate to use in satoshis/vByte."` +} + +func (c *rescueFundingCommand) Execute(_ []string) error { + setupChainParams(cfg) + + var ( + extendedKey *hdkeychain.ExtendedKey + otherPub *hdkeychain.ExtendedKey + err error + ) + + // Check that root key is valid or fall back to console input. + switch { + case c.RootKey != "": + extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) + + default: + extendedKey, _, err = rootKeyFromConsole() + } + if err != nil { + return fmt.Errorf("error reading root key: %v", err) + } + + // Read other node's xpub. + otherPub, err = hdkeychain.NewKeyFromString(c.OtherNodePub) + if err != nil { + return fmt.Errorf("error parsing other node's xpub: %v", err) + } + + // Decode target funding address. + hash, isScript, err := lnd.DecodeAddressHash(c.FundingAddr, chainParams) + if err != nil { + return fmt.Errorf("error decoding funding address: %v", err) + } + if !isScript { + return fmt.Errorf("funding address must be a P2WSH address") + } + + return rescueFunding(extendedKey, otherPub, hash) +} + +func rescueFunding(localNodeKey *hdkeychain.ExtendedKey, + otherNodekey *hdkeychain.ExtendedKey, scriptHash []byte) error { + + // First, we need to derive the correct branch from the local root key. + localMultisig, err := lnd.DeriveChildren(localNodeKey, []uint32{ + lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose), + lnd.HardenedKeyStart + chainParams.HDCoinType, + lnd.HardenedKeyStart + uint32(keychain.KeyFamilyMultiSig), + 0, + }) + if err != nil { + return fmt.Errorf("could not derive local multisig key: %v", + err) + } + + log.Infof("Looking for matching multisig keys, this will take a while") + localIndex, otherIndex, script, err := findMatchingIndices( + localMultisig, otherNodekey, scriptHash, + ) + if err != nil { + return fmt.Errorf("could not derive keys: %v", err) + } + + log.Infof("Found local key with index %d and other key with index %d "+ + "for witness script %x", localIndex, otherIndex, script) + + // TODO(guggero): + // * craft PSBT with input, sweep output and partial signature + // * do fee estimation based on full amount + // * create `signpsbt` command for the other node operator + return nil +} + +func findMatchingIndices(localNodeKey *hdkeychain.ExtendedKey, + otherNodekey *hdkeychain.ExtendedKey, scriptHash []byte) (uint32, + uint32, []byte, error) { + + // Loop through both the local and the remote indices of the branches up + // to MaxChannelLookup. + for local := uint32(0); local < MaxChannelLookup; local++ { + for other := uint32(0); other < MaxChannelLookup; other++ { + localKey, err := localNodeKey.Child(local) + if err != nil { + return 0, 0, nil, fmt.Errorf("error "+ + "deriving local key: %v", err) + } + localPub, err := localKey.ECPubKey() + if err != nil { + return 0, 0, nil, fmt.Errorf("error "+ + "deriving local pubkey: %v", err) + } + otherKey, err := otherNodekey.Child(other) + if err != nil { + return 0, 0, nil, fmt.Errorf("error "+ + "deriving other key: %v", err) + } + otherPub, err := otherKey.ECPubKey() + if err != nil { + return 0, 0, nil, fmt.Errorf("error "+ + "deriving other pubkey: %v", err) + } + script, out, err := input.GenFundingPkScript( + localPub.SerializeCompressed(), + otherPub.SerializeCompressed(), 123, + ) + if err != nil { + return 0, 0, nil, fmt.Errorf("error "+ + "generating funding script: %v", err) + } + if bytes.Contains(out.PkScript, scriptHash) { + return local, other, script, nil + } + } + if local > 0 && local%100 == 0 { + log.Infof("Checked %d of %d local keys", local, + MaxChannelLookup) + } + } + return 0, 0, nil, fmt.Errorf("no matching pubkeys found") +} diff --git a/cmd/chantools/sweeptimelock.go b/cmd/chantools/sweeptimelock.go index f5f6496..84dbb3e 100644 --- a/cmd/chantools/sweeptimelock.go +++ b/cmd/chantools/sweeptimelock.go @@ -182,7 +182,7 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string, } // Add our sweep destination output. - sweepScript, err := getWP2PKHScript(sweepAddr) + sweepScript, err := getP2WPKHScript(sweepAddr) if err != nil { return err } @@ -256,8 +256,10 @@ func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) { ) } -func getWP2PKHScript(addr string) ([]byte, error) { - targetPubKeyHash, err := parseAddr(addr) +func getP2WPKHScript(addr string) ([]byte, error) { + targetPubKeyHash, _, err := lnd.DecodeAddressHash( + addr, chainParams, + ) if err != nil { return nil, err } diff --git a/dump/dump.go b/dump/dump.go index 784e90d..7b75052 100644 --- a/dump/dump.go +++ b/dump/dump.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/hex" "fmt" - "github.com/lightningnetwork/lnd/input" "net" "github.com/btcsuite/btcd/btcec" @@ -13,6 +12,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwire" ) diff --git a/lnd/hdkeychain.go b/lnd/hdkeychain.go index 7e1cb39..bf823c8 100644 --- a/lnd/hdkeychain.go +++ b/lnd/hdkeychain.go @@ -2,12 +2,12 @@ package lnd import ( "fmt" - "github.com/btcsuite/btcd/btcec" - "github.com/btcsuite/btcutil" "strconv" "strings" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" "github.com/lightningnetwork/lnd/keychain" ) @@ -64,35 +64,37 @@ func ParsePath(path string) ([]uint32, error) { // DeriveKey derives the public key and private key in the WIF format for a // given key path of the extended key. func DeriveKey(extendedKey *hdkeychain.ExtendedKey, path string, - params *chaincfg.Params) (*btcec.PublicKey, *btcutil.WIF, error) { + params *chaincfg.Params) (*hdkeychain.ExtendedKey, *btcec.PublicKey, + *btcutil.WIF, error) { parsedPath, err := ParsePath(path) if err != nil { - return nil, nil, fmt.Errorf("could not parse derivation path: "+ - "%v", err) + return nil, nil, nil, fmt.Errorf("could not parse derivation "+ + "path: %v", err) } derivedKey, err := DeriveChildren(extendedKey, parsedPath) if err != nil { - return nil, nil, fmt.Errorf("could not derive children: %v", - err) + return nil, nil, nil, fmt.Errorf("could not derive children: "+ + "%v", err) } pubKey, err := derivedKey.ECPubKey() if err != nil { - return nil, nil, fmt.Errorf("could not derive public key: %v", - err) + return nil, nil, nil, fmt.Errorf("could not derive public "+ + "key: %v", err) } privKey, err := derivedKey.ECPrivKey() if err != nil { - return nil, nil, fmt.Errorf("could not derive private key: %v", - err) + return nil, nil, nil, fmt.Errorf("could not derive private "+ + "key: %v", err) } wif, err := btcutil.NewWIF(privKey, params, true) if err != nil { - return nil, nil, fmt.Errorf("could not encode WIF: %v", err) + return nil, nil, nil, fmt.Errorf("could not encode WIF: %v", + err) } - return pubKey, wif, nil + return derivedKey, pubKey, wif, nil } func AllDerivationPaths(params *chaincfg.Params) ([]string, [][]uint32, error) { @@ -127,6 +129,46 @@ func AllDerivationPaths(params *chaincfg.Params) ([]string, [][]uint32, error) { return pathStrings, paths, nil } +// DecodeAddressHash returns the public key or script hash encoded in a native +// bech32 encoded SegWit address and whether it's a script hash or not. +func DecodeAddressHash(addr string, chainParams *chaincfg.Params) ([]byte, bool, + error) { + + // First parse address to get targetHash from it later. + targetAddr, err := btcutil.DecodeAddress(addr, chainParams) + if err != nil { + return nil, false, err + } + + // Make the check on the decoded address according to the active + // network (testnet or mainnet only). + if !targetAddr.IsForNet(chainParams) { + return nil, false, fmt.Errorf( + "address: %v is not valid for this network: %v", + targetAddr.String(), chainParams.Name, + ) + } + + // Must be a bech32 native SegWit address. + var ( + isScriptHash = false + targetHash []byte + ) + switch targetAddr.(type) { + case *btcutil.AddressWitnessPubKeyHash: + targetHash = targetAddr.ScriptAddress() + + case *btcutil.AddressWitnessScriptHash: + isScriptHash = true + targetHash = targetAddr.ScriptAddress() + + default: + return nil, false, fmt.Errorf("address: must be a bech32 " + + "P2WPKH or P2WSH address") + } + return targetHash, isScriptHash, nil +} + type HDKeyRing struct { ExtendedKey *hdkeychain.ExtendedKey ChainParams *chaincfg.Params