Begin implementing rescuefunding command

pull/17/head
Oliver Gugger 4 years ago
parent 4c92de59e5
commit 338c22fc2e
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -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'/<coin_type>'/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 (<txid>:<txindex>).
--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

@ -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

@ -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)

@ -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)

@ -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
}

@ -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'/<coin_type>'/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 (<txid>:<txindex>)."`
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")
}

@ -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
}

@ -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"
)

@ -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

Loading…
Cancel
Save