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.
252 lines
6.3 KiB
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
|
|
}
|