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) + [genimportscript](#genimportscript)
+ [forceclose](#forceclose) + [forceclose](#forceclose)
+ [rescueclosed](#rescueclosed) + [rescueclosed](#rescueclosed)
+ [rescuefunding](#rescuefunding)
+ [showrootkey](#showrootkey) + [showrootkey](#showrootkey)
+ [summary](#summary) + [summary](#summary)
+ [sweeptimelock](#sweeptimelock) + [sweeptimelock](#sweeptimelock)
@ -69,6 +70,7 @@ Available commands:
forceclose Force-close the last state that is in the channel.db provided. 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. 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. 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. 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. summary Compile a summary about the current state of channels.
sweeptimelock Sweep the force-closed state after the time lock has expired. 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 --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 ### showrootkey
This command converts the 24 word `lnd` aezeed phrase and password to the BIP32 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 { neuter bool) error {
fmt.Printf("Deriving path %s for network %s.\n", path, chainParams.Name) 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 { if err != nil {
return fmt.Errorf("could not derive keys: %v", err) 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("Public key: %x\n", pubKey.SerializeCompressed())
fmt.Printf("Extended public key (xpub): %s\n", neutered.String())
if !neuter { if !neuter {
fmt.Printf("Private key (WIF): %s\n", wif.String()) fmt.Printf("Private key (WIF): %s\n", wif.String())
fmt.Printf("Extended private key (xprv): %s\n", child.String())
} }
return nil return nil

@ -65,16 +65,6 @@ func (c *genImportScriptCommand) Execute(_ []string) error {
// Decide what derivation path(s) to use. // Decide what derivation path(s) to use.
switch { 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: default:
c.DerivationPath = lnd.WalletDefaultDerivationPath c.DerivationPath = lnd.WalletDefaultDerivationPath
fallthrough fallthrough
@ -86,6 +76,16 @@ func (c *genImportScriptCommand) Execute(_ []string) error {
} }
strPaths = []string{c.DerivationPath} strPaths = []string{c.DerivationPath}
paths = [][]uint32{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) exporter := btc.ParseFormat(c.Format)

@ -128,6 +128,11 @@ func runCommandParser() error {
"compacting it in the process.", "", "compacting it in the process.", "",
&compactDBCommand{}, &compactDBCommand{},
) )
_, _ = parser.AddCommand(
"rescuefunding", "Rescue funds locked in a funding multisig "+
"output that never resulted in a proper channel.", "",
&rescueFundingCommand{},
)
_, err := parser.Parse() _, err := parser.Parse()
return err return err
@ -221,6 +226,7 @@ func rootKeyFromConsole() (*hdkeychain.ExtendedKey, time.Time, error) {
if err != nil { if err != nil {
return nil, time.Unix(0, 0), err return nil, time.Unix(0, 0), err
} }
fmt.Println()
var mnemonic aezeed.Mnemonic var mnemonic aezeed.Mnemonic
copy(mnemonic[:], cipherSeedMnemonic) copy(mnemonic[:], cipherSeedMnemonic)

@ -154,10 +154,15 @@ func rescueClosedChannels(extendedKey *hdkeychain.ExtendedKey,
} }
func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) { func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) {
targetPubKeyHash, err := parseAddr(addr) targetPubKeyHash, scriptHash, err := lnd.DecodeAddressHash(
addr, chainParams,
)
if err != nil { if err != nil {
return "", fmt.Errorf("error parsing addr: %v", err) 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 // Loop through all cached payment base point keys, tweak each of it
// with the per_commit_point and see if the hashed public key // with the per_commit_point and see if the hashed public key
@ -229,30 +234,3 @@ func fillCache(extendedKey *hdkeychain.ExtendedKey) error {
} }
return nil 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. // Add our sweep destination output.
sweepScript, err := getWP2PKHScript(sweepAddr) sweepScript, err := getP2WPKHScript(sweepAddr)
if err != nil { if err != nil {
return err return err
} }
@ -256,8 +256,10 @@ func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
) )
} }
func getWP2PKHScript(addr string) ([]byte, error) { func getP2WPKHScript(addr string) ([]byte, error) {
targetPubKeyHash, err := parseAddr(addr) targetPubKeyHash, _, err := lnd.DecodeAddressHash(
addr, chainParams,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/lightningnetwork/lnd/input"
"net" "net"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
@ -13,6 +12,7 @@ import (
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
) )

@ -2,12 +2,12 @@ package lnd
import ( import (
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil"
"strconv" "strconv"
"strings" "strings"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcutil/hdkeychain"
"github.com/lightningnetwork/lnd/keychain" "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 // DeriveKey derives the public key and private key in the WIF format for a
// given key path of the extended key. // given key path of the extended key.
func DeriveKey(extendedKey *hdkeychain.ExtendedKey, path string, 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) parsedPath, err := ParsePath(path)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("could not parse derivation path: "+ return nil, nil, nil, fmt.Errorf("could not parse derivation "+
"%v", err) "path: %v", err)
} }
derivedKey, err := DeriveChildren(extendedKey, parsedPath) derivedKey, err := DeriveChildren(extendedKey, parsedPath)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("could not derive children: %v", return nil, nil, nil, fmt.Errorf("could not derive children: "+
err) "%v", err)
} }
pubKey, err := derivedKey.ECPubKey() pubKey, err := derivedKey.ECPubKey()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("could not derive public key: %v", return nil, nil, nil, fmt.Errorf("could not derive public "+
err) "key: %v", err)
} }
privKey, err := derivedKey.ECPrivKey() privKey, err := derivedKey.ECPrivKey()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("could not derive private key: %v", return nil, nil, nil, fmt.Errorf("could not derive private "+
err) "key: %v", err)
} }
wif, err := btcutil.NewWIF(privKey, params, true) wif, err := btcutil.NewWIF(privKey, params, true)
if err != nil { 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) { func AllDerivationPaths(params *chaincfg.Params) ([]string, [][]uint32, error) {
@ -127,6 +129,46 @@ func AllDerivationPaths(params *chaincfg.Params) ([]string, [][]uint32, error) {
return pathStrings, paths, nil 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 { type HDKeyRing struct {
ExtendedKey *hdkeychain.ExtendedKey ExtendedKey *hdkeychain.ExtendedKey
ChainParams *chaincfg.Params ChainParams *chaincfg.Params

Loading…
Cancel
Save