mirror of https://github.com/guggero/chantools
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
482 lines
13 KiB
Go
482 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/btcsuite/btcutil/hdkeychain"
|
|
"github.com/guggero/chantools/dataformat"
|
|
"github.com/guggero/chantools/lnd"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
cacheSize = 5000
|
|
cache []*cacheEntry
|
|
|
|
errAddrNotFound = errors.New("addr not found")
|
|
|
|
patternCommitPoint = regexp.MustCompile(`commit_point=([0-9a-f]{66})`)
|
|
)
|
|
|
|
type cacheEntry struct {
|
|
privKey *btcec.PrivateKey
|
|
pubKey *btcec.PublicKey
|
|
}
|
|
|
|
type rescueClosedCommand struct {
|
|
ChannelDB string
|
|
Addr string
|
|
CommitPoint string
|
|
LndLog string
|
|
|
|
rootKey *rootKey
|
|
inputs *inputFlags
|
|
cmd *cobra.Command
|
|
}
|
|
|
|
func newRescueClosedCommand() *cobra.Command {
|
|
cc := &rescueClosedCommand{}
|
|
cc.cmd = &cobra.Command{
|
|
Use: "rescueclosed",
|
|
Short: "Try finding the private keys for funds that " +
|
|
"are in outputs of remotely force-closed channels",
|
|
Long: `If channels have already been force-closed by the remote
|
|
peer, this command tries to find the private keys to sweep the funds from the
|
|
output that belongs to our side. This can only be used if we have a channel DB
|
|
that contains the latest commit point. Normally you would use SCB to get the
|
|
funds from those channels. But this method can help if the other node doesn't
|
|
know about the channels any more but we still have the channel.db from the
|
|
moment they force-closed.
|
|
|
|
The alternative use case for this command is if you got the commit point by
|
|
running the fund-recovery branch of my guggero/lnd fork (see
|
|
https://github.com/guggero/lnd/releases for a binary release) in combination
|
|
with the fakechanbackup command. Then you need to specify the --commit_point and
|
|
--force_close_addr flags instead of the --channeldb and --fromsummary flags.
|
|
|
|
If you need to rescue a whole bunch of channels all at once, you can also
|
|
specify the --fromsummary and --lnd_log flags to automatically look for force
|
|
close addresses in the summary and the corresponding commit points in the
|
|
lnd log file. This only works if lnd is running the fund-recovery branch of my
|
|
guggero/lnd (https://github.com/guggero/lnd/releases) fork and only if the
|
|
debuglevel is set to debug (lnd.conf, set 'debuglevel=debug').`,
|
|
Example: `chantools rescueclosed \
|
|
--fromsummary results/summary-xxxxxx.json \
|
|
--channeldb ~/.lnd/data/graph/mainnet/channel.db
|
|
|
|
chantools rescueclosed --force_close_addr bc1q... --commit_point 03xxxx
|
|
|
|
chantools rescueclosed --fromsummary results/summary-xxxxxx.json \
|
|
--lnd_log ~/.lnd/logs/bitcoin/mainnet/lnd.log`,
|
|
RunE: cc.Execute,
|
|
}
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to use "+
|
|
"for rescuing force-closed channels",
|
|
)
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.Addr, "force_close_addr", "", "the address the channel "+
|
|
"was force closed to",
|
|
)
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.CommitPoint, "commit_point", "", "the commit point that "+
|
|
"was obtained from the logs after running the "+
|
|
"fund-recovery branch of guggero/lnd",
|
|
)
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.LndLog, "lnd_log", "", "the lnd log file to read to get "+
|
|
"the commit_point values when rescuing multiple "+
|
|
"channels at the same time")
|
|
cc.rootKey = newRootKey(cc.cmd, "decrypting the backup")
|
|
cc.inputs = newInputFlags(cc.cmd)
|
|
|
|
return cc.cmd
|
|
}
|
|
|
|
func (c *rescueClosedCommand) Execute(_ *cobra.Command, _ []string) error {
|
|
extendedKey, err := c.rootKey.read()
|
|
if err != nil {
|
|
return fmt.Errorf("error reading root key: %v", err)
|
|
}
|
|
|
|
// What way of recovery has the user chosen? From summary and DB or from
|
|
// address and commit point?
|
|
switch {
|
|
case c.ChannelDB != "":
|
|
db, err := lnd.OpenDB(c.ChannelDB, true)
|
|
if err != nil {
|
|
return fmt.Errorf("error opening rescue DB: %v", err)
|
|
}
|
|
|
|
// Parse channel entries from any of the possible input files.
|
|
entries, err := c.inputs.parseInputType()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
commitPoints, err := commitPointsFromDB(db)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading commit points from "+
|
|
"db: %v", err)
|
|
}
|
|
return rescueClosedChannels(extendedKey, entries, commitPoints)
|
|
|
|
case c.Addr != "":
|
|
// First parse address to get targetPubKeyHash from it later.
|
|
targetAddr, err := btcutil.DecodeAddress(c.Addr, chainParams)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing addr: %v", err)
|
|
}
|
|
|
|
// Now parse the commit point.
|
|
commitPointRaw, err := hex.DecodeString(c.CommitPoint)
|
|
if err != nil {
|
|
return fmt.Errorf("error decoding commit point: %v",
|
|
err)
|
|
}
|
|
commitPoint, err := btcec.ParsePubKey(
|
|
commitPointRaw, btcec.S256(),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing commit point: %v", err)
|
|
}
|
|
|
|
return rescueClosedChannel(extendedKey, targetAddr, commitPoint)
|
|
|
|
case c.LndLog != "":
|
|
// Parse channel entries from any of the possible input files.
|
|
entries, err := c.inputs.parseInputType()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
commitPoints, err := commitPointsFromLogFile(c.LndLog)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing commit points from "+
|
|
"log file: %v", err)
|
|
}
|
|
return rescueClosedChannels(extendedKey, entries, commitPoints)
|
|
|
|
default:
|
|
return fmt.Errorf("you either need to specify --channeldb and " +
|
|
"--fromsummary or --force_close_addr and " +
|
|
"--commit_point but not a mixture of them")
|
|
}
|
|
}
|
|
|
|
func commitPointsFromDB(chanDb *channeldb.DB) ([]*btcec.PublicKey, error) {
|
|
var result []*btcec.PublicKey
|
|
|
|
channels, err := chanDb.FetchAllChannels()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Try naive/lucky guess with information from channel DB.
|
|
for _, channel := range channels {
|
|
if channel.RemoteNextRevocation != nil {
|
|
result = append(result, channel.RemoteNextRevocation)
|
|
}
|
|
|
|
if channel.RemoteCurrentRevocation != nil {
|
|
result = append(result, channel.RemoteCurrentRevocation)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func commitPointsFromLogFile(lndLog string) ([]*btcec.PublicKey, error) {
|
|
logFileBytes, err := ioutil.ReadFile(lndLog)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading log file %s: %v", lndLog,
|
|
err)
|
|
}
|
|
|
|
allMatches := patternCommitPoint.FindAllStringSubmatch(
|
|
string(logFileBytes), -1,
|
|
)
|
|
dedupMap := make(map[string]*btcec.PublicKey, len(allMatches))
|
|
for _, groups := range allMatches {
|
|
commitPointBytes, err := hex.DecodeString(groups[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing commit point "+
|
|
"hex: %v", err)
|
|
}
|
|
|
|
commitPoint, err := btcec.ParsePubKey(
|
|
commitPointBytes, btcec.S256(),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing commit point: %v",
|
|
err)
|
|
}
|
|
|
|
dedupMap[groups[1]] = commitPoint
|
|
}
|
|
|
|
result := make([]*btcec.PublicKey, 0, len(dedupMap))
|
|
for _, commitPoint := range dedupMap {
|
|
result = append(result, commitPoint)
|
|
}
|
|
|
|
log.Infof("Extracted %d commit points from log file %s", len(result),
|
|
lndLog)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func rescueClosedChannels(extendedKey *hdkeychain.ExtendedKey,
|
|
entries []*dataformat.SummaryEntry,
|
|
possibleCommitPoints []*btcec.PublicKey) error {
|
|
|
|
err := fillCache(extendedKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add a nil commit point to the list of possible commit points to also
|
|
// try brute forcing a static_remote_key address.
|
|
possibleCommitPoints = append(possibleCommitPoints, nil)
|
|
|
|
// We'll also keep track of all rescued keys for an additional log
|
|
// output.
|
|
resultMap := make(map[string]string)
|
|
|
|
// Try naive/lucky guess by trying out all combinations.
|
|
outer:
|
|
for _, entry := range entries {
|
|
// Don't try anything with open channels, fully closed channels
|
|
// or channels where we already have the private key.
|
|
if entry.ClosingTX == nil ||
|
|
entry.ClosingTX.AllOutsSpent ||
|
|
(entry.ClosingTX.OurAddr == "" &&
|
|
entry.ClosingTX.ToRemoteAddr == "") ||
|
|
entry.ClosingTX.SweepPrivkey != "" {
|
|
|
|
continue
|
|
}
|
|
|
|
// Try with every possible commit point now.
|
|
for _, commitPoint := range possibleCommitPoints {
|
|
addr := entry.ClosingTX.OurAddr
|
|
if addr == "" {
|
|
addr = entry.ClosingTX.ToRemoteAddr
|
|
}
|
|
|
|
wif, err := addrInCache(addr, commitPoint)
|
|
switch {
|
|
case err == nil:
|
|
entry.ClosingTX.SweepPrivkey = wif
|
|
resultMap[addr] = wif
|
|
|
|
continue outer
|
|
|
|
case err == errAddrNotFound:
|
|
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
importStr := ""
|
|
for addr, wif := range resultMap {
|
|
importStr += fmt.Sprintf(`importprivkey "%s" "%s" false%s`, wif,
|
|
addr, "\n")
|
|
}
|
|
log.Infof("Found %d private keys! Import them into bitcoind through "+
|
|
"the console by pasting: \n%srescanblockchain 481824\n",
|
|
len(resultMap), importStr)
|
|
|
|
summaryBytes, err := json.MarshalIndent(&dataformat.SummaryEntryFile{
|
|
Channels: entries,
|
|
}, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileName := fmt.Sprintf("results/rescueclosed-%s.json",
|
|
time.Now().Format("2006-01-02-15-04-05"))
|
|
log.Infof("Writing result to %s", fileName)
|
|
return ioutil.WriteFile(fileName, summaryBytes, 0644)
|
|
}
|
|
|
|
func rescueClosedChannel(extendedKey *hdkeychain.ExtendedKey,
|
|
addr btcutil.Address, commitPoint *btcec.PublicKey) error {
|
|
|
|
// Make the check on the decoded address according to the active
|
|
// network (testnet or mainnet only).
|
|
if !addr.IsForNet(chainParams) {
|
|
return fmt.Errorf("address: %v is not valid for this network: "+
|
|
"%v", addr, chainParams.Name)
|
|
}
|
|
|
|
// Must be a bech32 native SegWit address.
|
|
switch addr.(type) {
|
|
case *btcutil.AddressWitnessPubKeyHash:
|
|
log.Infof("Brute forcing private key for tweaked public key "+
|
|
"hash %x\n", addr.ScriptAddress())
|
|
|
|
default:
|
|
return fmt.Errorf("address: must be a bech32 P2WPKH address")
|
|
}
|
|
|
|
err := fillCache(extendedKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wif, err := addrInCache(addr.String(), commitPoint)
|
|
switch {
|
|
case err == nil:
|
|
log.Infof("Found private key %s for address %v!", wif, addr)
|
|
|
|
return nil
|
|
|
|
case err == errAddrNotFound:
|
|
// Try again as a static_remote_key.
|
|
|
|
default:
|
|
return err
|
|
}
|
|
|
|
// Try again as a static_remote_key address.
|
|
wif, err = addrInCache(addr.String(), nil)
|
|
switch {
|
|
case err == nil:
|
|
log.Infof("Found private key %s for address %v!", wif, addr)
|
|
|
|
return nil
|
|
|
|
case err == errAddrNotFound:
|
|
return fmt.Errorf("did not find private key for address %v",
|
|
addr)
|
|
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) {
|
|
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")
|
|
}
|
|
|
|
// If the commit point is nil, we try with plain private keys to match
|
|
// static_remote_key outputs.
|
|
if perCommitPoint == nil {
|
|
for i := 0; i < cacheSize; i++ {
|
|
cacheEntry := cache[i]
|
|
hashedPubKey := btcutil.Hash160(
|
|
cacheEntry.pubKey.SerializeCompressed(),
|
|
)
|
|
equal := subtle.ConstantTimeCompare(
|
|
targetPubKeyHash, hashedPubKey,
|
|
)
|
|
if equal == 1 {
|
|
wif, err := btcutil.NewWIF(
|
|
cacheEntry.privKey, chainParams, true,
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
log.Infof("The private key for addr %s "+
|
|
"(static_remote_key) found after "+
|
|
"%d tries: %s", addr, i, wif.String(),
|
|
)
|
|
return wif.String(), nil
|
|
}
|
|
}
|
|
|
|
return "", errAddrNotFound
|
|
}
|
|
|
|
// Loop through all cached payment base point keys, tweak each of it
|
|
// with the per_commit_point and see if the hashed public key
|
|
// corresponds to the target pubKeyHash of the given address.
|
|
for i := 0; i < cacheSize; i++ {
|
|
cacheEntry := cache[i]
|
|
basePoint := cacheEntry.pubKey
|
|
tweakedPubKey := input.TweakPubKey(basePoint, perCommitPoint)
|
|
tweakBytes := input.SingleTweakBytes(perCommitPoint, basePoint)
|
|
tweakedPrivKey := input.TweakPrivKey(
|
|
cacheEntry.privKey, tweakBytes,
|
|
)
|
|
hashedPubKey := btcutil.Hash160(
|
|
tweakedPubKey.SerializeCompressed(),
|
|
)
|
|
equal := subtle.ConstantTimeCompare(
|
|
targetPubKeyHash, hashedPubKey,
|
|
)
|
|
if equal == 1 {
|
|
wif, err := btcutil.NewWIF(
|
|
tweakedPrivKey, chainParams, true,
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
log.Infof("The private key for addr %s found after "+
|
|
"%d tries: %s", addr, i, wif.String(),
|
|
)
|
|
return wif.String(), nil
|
|
}
|
|
}
|
|
|
|
return "", errAddrNotFound
|
|
}
|
|
|
|
func fillCache(extendedKey *hdkeychain.ExtendedKey) error {
|
|
cache = make([]*cacheEntry, cacheSize)
|
|
|
|
for i := 0; i < cacheSize; i++ {
|
|
key, err := lnd.DeriveChildren(extendedKey, []uint32{
|
|
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
|
|
lnd.HardenedKeyStart + chainParams.HDCoinType,
|
|
lnd.HardenedKeyStart +
|
|
uint32(keychain.KeyFamilyPaymentBase),
|
|
0,
|
|
uint32(i),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
privKey, err := key.ECPrivKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pubKey, err := key.ECPubKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cache[i] = &cacheEntry{
|
|
privKey: privKey,
|
|
pubKey: pubKey,
|
|
}
|
|
|
|
if i > 0 && i%10000 == 0 {
|
|
fmt.Printf("Filled cache with %d of %d keys.\n",
|
|
i, cacheSize)
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|