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.
chantools/cmd/chantools/rescueclosed.go

252 lines
6.3 KiB
Go

package main
import (
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"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 = 2000
cache []*cacheEntry
errAddrNotFound = errors.New("addr not found")
)
type cacheEntry struct {
privKey *btcec.PrivateKey
pubKey *btcec.PublicKey
}
type rescueClosedCommand struct {
ChannelDB 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.`,
Example: `chantools rescueclosed --rootkey xprvxxxxxxxxxx \
--fromsummary results/summary-xxxxxx.json \
--channeldb ~/.lnd/data/graph/mainnet/channel.db`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to use "+
"for rescuing force-closed channels",
)
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)
}
// Check that we have a channel DB.
if c.ChannelDB == "" {
return fmt.Errorf("rescue DB is required")
}
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
}
return rescueClosedChannels(extendedKey, entries, db)
}
func rescueClosedChannels(extendedKey *hdkeychain.ExtendedKey,
entries []*dataformat.SummaryEntry, chanDb *channeldb.DB) error {
err := fillCache(extendedKey)
if err != nil {
return err
}
channels, err := chanDb.FetchAllChannels()
if err != nil {
return err
}
// Try naive/lucky guess with information from channel DB.
for _, channel := range channels {
channelPoint := channel.FundingOutpoint.String()
var channelEntry *dataformat.SummaryEntry
for _, entry := range entries {
if entry.ChannelPoint == channelPoint {
channelEntry = entry
}
}
// Don't try anything with open channels, fully closed channels
// or channels where we already have the private key.
if channelEntry == nil || channelEntry.ClosingTX == nil ||
channelEntry.ClosingTX.AllOutsSpent ||
channelEntry.ClosingTX.OurAddr == "" ||
channelEntry.ClosingTX.SweepPrivkey != "" {
continue
}
if channel.RemoteNextRevocation != nil {
wif, err := addrInCache(
channelEntry.ClosingTX.OurAddr,
channel.RemoteNextRevocation,
)
switch {
case err == nil:
channelEntry.ClosingTX.SweepPrivkey = wif
case err == errAddrNotFound:
default:
return err
}
}
if channel.RemoteCurrentRevocation != nil {
wif, err := addrInCache(
channelEntry.ClosingTX.OurAddr,
channel.RemoteCurrentRevocation,
)
switch {
case err == nil:
channelEntry.ClosingTX.SweepPrivkey = wif
case err == errAddrNotFound:
default:
return err
}
}
}
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 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")
}
// 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
}