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.
351 lines
9.1 KiB
Go
351 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btclog"
|
|
"github.com/lightninglabs/chantools/btc"
|
|
"github.com/lightninglabs/chantools/dataformat"
|
|
"github.com/lightninglabs/chantools/lnd"
|
|
"github.com/lightningnetwork/lnd/build"
|
|
"github.com/lightningnetwork/lnd/chanbackup"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/peer"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const (
|
|
defaultAPIURL = "https://blockstream.info/api"
|
|
defaultTestnetAPIURL = "https://blockstream.info/testnet/api"
|
|
defaultRegtestAPIURL = "http://localhost:3004"
|
|
|
|
// version is the current version of the tool. It is set during build.
|
|
// NOTE: When changing this, please also update the version in the
|
|
// download link shown in the README.
|
|
version = "0.12.1"
|
|
na = "n/a"
|
|
|
|
// lndVersion is the current version of lnd that we support. This is
|
|
// shown in some commands that affect the database and its migrations.
|
|
lndVersion = "v0.17.0-beta"
|
|
|
|
Commit = ""
|
|
)
|
|
|
|
var (
|
|
Testnet bool
|
|
Regtest bool
|
|
Signet bool
|
|
|
|
logWriter = build.NewRotatingLogWriter()
|
|
log = build.NewSubLogger("CHAN", genSubLogger(logWriter))
|
|
chainParams = &chaincfg.MainNetParams
|
|
)
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "chantools",
|
|
Short: "Chantools helps recover funds from lightning channels",
|
|
Long: `This tool provides helper functions that can be used rescue
|
|
funds locked in lnd channels in case lnd itself cannot run properly anymore.
|
|
Complete documentation is available at
|
|
https://github.com/lightninglabs/chantools/.`,
|
|
Version: fmt.Sprintf("v%s, commit %s", version, Commit),
|
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
switch {
|
|
case Testnet:
|
|
chainParams = &chaincfg.TestNet3Params
|
|
|
|
case Regtest:
|
|
chainParams = &chaincfg.RegressionNetParams
|
|
|
|
case Signet:
|
|
chainParams = &chaincfg.SigNetParams
|
|
|
|
default:
|
|
chainParams = &chaincfg.MainNetParams
|
|
}
|
|
|
|
setupLogging()
|
|
|
|
log.Infof("chantools version v%s commit %s", version,
|
|
Commit)
|
|
},
|
|
DisableAutoGenTag: true,
|
|
}
|
|
|
|
func main() {
|
|
rootCmd.PersistentFlags().BoolVarP(
|
|
&Testnet, "testnet", "t", false, "Indicates if testnet "+
|
|
"parameters should be used",
|
|
)
|
|
rootCmd.PersistentFlags().BoolVarP(
|
|
&Regtest, "regtest", "r", false, "Indicates if regtest "+
|
|
"parameters should be used",
|
|
)
|
|
rootCmd.PersistentFlags().BoolVarP(
|
|
&Signet, "signet", "s", false, "Indicates if the public "+
|
|
"signet parameters should be used",
|
|
)
|
|
|
|
rootCmd.AddCommand(
|
|
newChanBackupCommand(),
|
|
newClosePoolAccountCommand(),
|
|
newCompactDBCommand(),
|
|
newDeletePaymentsCommand(),
|
|
newDeriveKeyCommand(),
|
|
newDoubleSpendInputsCommand(),
|
|
newDropChannelGraphCommand(),
|
|
newDropGraphZombiesCommand(),
|
|
newDumpBackupCommand(),
|
|
newDumpChannelsCommand(),
|
|
newDocCommand(),
|
|
newFakeChanBackupCommand(),
|
|
newFilterBackupCommand(),
|
|
newFixOldBackupCommand(),
|
|
newForceCloseCommand(),
|
|
newGenImportScriptCommand(),
|
|
newMigrateDBCommand(),
|
|
newPullAnchorCommand(),
|
|
newRecoverLoopInCommand(),
|
|
newRemoveChannelCommand(),
|
|
newRescueClosedCommand(),
|
|
newRescueFundingCommand(),
|
|
newRescueTweakedKeyCommand(),
|
|
newShowRootKeyCommand(),
|
|
newSignRescueFundingCommand(),
|
|
newSummaryCommand(),
|
|
newSweepTimeLockCommand(),
|
|
newSweepTimeLockManualCommand(),
|
|
newSweepRemoteClosedCommand(),
|
|
newTriggerForceCloseCommand(),
|
|
newVanityGenCommand(),
|
|
newWalletInfoCommand(),
|
|
newZombieRecoveryCommand(),
|
|
)
|
|
|
|
if err := rootCmd.Execute(); err != nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
type rootKey struct {
|
|
RootKey string
|
|
BIP39 bool
|
|
}
|
|
|
|
func newRootKey(cmd *cobra.Command, desc string) *rootKey {
|
|
r := &rootKey{}
|
|
cmd.Flags().StringVar(
|
|
&r.RootKey, "rootkey", "", "BIP32 HD root key of the wallet "+
|
|
"to use for "+desc+"; leave empty to prompt for "+
|
|
"lnd 24 word aezeed",
|
|
)
|
|
cmd.Flags().BoolVar(
|
|
&r.BIP39, "bip39", false, "read a classic BIP39 seed and "+
|
|
"passphrase from the terminal instead of asking for "+
|
|
"lnd seed format or providing the --rootkey flag",
|
|
)
|
|
|
|
return r
|
|
}
|
|
|
|
func (r *rootKey) read() (*hdkeychain.ExtendedKey, error) {
|
|
extendedKey, _, err := r.readWithBirthday()
|
|
return extendedKey, err
|
|
}
|
|
|
|
func (r *rootKey) readWithBirthday() (*hdkeychain.ExtendedKey, time.Time,
|
|
error) {
|
|
|
|
// Check that root key is valid or fall back to console input.
|
|
switch {
|
|
case r.RootKey != "":
|
|
extendedKey, err := hdkeychain.NewKeyFromString(r.RootKey)
|
|
return extendedKey, time.Unix(0, 0), err
|
|
|
|
case r.BIP39:
|
|
extendedKey, err := btc.ReadMnemonicFromTerminal(chainParams)
|
|
return extendedKey, time.Unix(0, 0), err
|
|
|
|
default:
|
|
return lnd.ReadAezeed(chainParams)
|
|
}
|
|
}
|
|
|
|
type inputFlags struct {
|
|
ListChannels string
|
|
PendingChannels string
|
|
FromSummary string
|
|
FromChannelDB string
|
|
}
|
|
|
|
func newInputFlags(cmd *cobra.Command) *inputFlags {
|
|
f := &inputFlags{}
|
|
cmd.Flags().StringVar(&f.ListChannels, "listchannels", "", "channel "+
|
|
"input is in the format of lncli's listchannels format; "+
|
|
"specify '-' to read from stdin",
|
|
)
|
|
cmd.Flags().StringVar(&f.PendingChannels, "pendingchannels", "", ""+
|
|
"channel input is in the format of lncli's pendingchannels "+
|
|
"format; specify '-' to read from stdin",
|
|
)
|
|
cmd.Flags().StringVar(&f.FromSummary, "fromsummary", "", "channel "+
|
|
"input is in the format of chantool's channel summary; "+
|
|
"specify '-' to read from stdin",
|
|
)
|
|
cmd.Flags().StringVar(&f.FromChannelDB, "fromchanneldb", "", "channel "+
|
|
"input is in the format of an lnd channel.db file",
|
|
)
|
|
|
|
return f
|
|
}
|
|
|
|
func (f *inputFlags) parseInputType() ([]*dataformat.SummaryEntry, error) {
|
|
var (
|
|
content []byte
|
|
err error
|
|
target dataformat.InputFile
|
|
)
|
|
|
|
switch {
|
|
case f.ListChannels != "":
|
|
content, err = readInput(f.ListChannels)
|
|
target = &dataformat.ListChannelsFile{}
|
|
|
|
case f.PendingChannels != "":
|
|
content, err = readInput(f.PendingChannels)
|
|
target = &dataformat.PendingChannelsFile{}
|
|
|
|
case f.FromSummary != "":
|
|
content, err = readInput(f.FromSummary)
|
|
target = &dataformat.SummaryEntryFile{}
|
|
|
|
case f.FromChannelDB != "":
|
|
db, err := lnd.OpenDB(f.FromChannelDB, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error opening channel DB: %w",
|
|
err)
|
|
}
|
|
target = &dataformat.ChannelDBFile{DB: db.ChannelStateDB()}
|
|
return target.AsSummaryEntries()
|
|
|
|
default:
|
|
return nil, fmt.Errorf("an input file must be specified")
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(content))
|
|
err = decoder.Decode(&target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return target.AsSummaryEntries()
|
|
}
|
|
|
|
func readInput(input string) ([]byte, error) {
|
|
if strings.TrimSpace(input) == "-" {
|
|
return ioutil.ReadAll(os.Stdin)
|
|
}
|
|
return ioutil.ReadFile(input)
|
|
}
|
|
|
|
func passwordFromConsole(userQuery string) ([]byte, error) {
|
|
// Read from terminal (if there is one).
|
|
if terminal.IsTerminal(int(syscall.Stdin)) { //nolint
|
|
fmt.Print(userQuery)
|
|
pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fmt.Println()
|
|
return pw, nil
|
|
}
|
|
|
|
// Read from stdin as a fallback.
|
|
reader := bufio.NewReader(os.Stdin)
|
|
pw, err := reader.ReadBytes('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pw, nil
|
|
}
|
|
|
|
func setupLogging() {
|
|
setSubLogger("CHAN", log)
|
|
addSubLogger("CHDB", channeldb.UseLogger)
|
|
addSubLogger("BCKP", chanbackup.UseLogger)
|
|
addSubLogger("PEER", peer.UseLogger)
|
|
err := logWriter.InitLogRotator("./results/chantools.log", 10, 3)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = build.ParseAndSetDebugLevels("debug", logWriter)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// genSubLogger creates a sub logger with an empty shutdown function.
|
|
func genSubLogger(logWriter *build.RotatingLogWriter) func(string) btclog.Logger {
|
|
return func(s string) btclog.Logger {
|
|
return logWriter.GenSubLogger(s, func() {})
|
|
}
|
|
}
|
|
|
|
// addSubLogger is a helper method to conveniently create and register the
|
|
// logger of one or more sub systems.
|
|
func addSubLogger(subsystem string, useLoggers ...func(btclog.Logger)) {
|
|
// Create and register just a single logger to prevent them from
|
|
// overwriting each other internally.
|
|
logger := build.NewSubLogger(subsystem, genSubLogger(logWriter))
|
|
setSubLogger(subsystem, logger, useLoggers...)
|
|
}
|
|
|
|
// setSubLogger is a helper method to conveniently register the logger of a sub
|
|
// system.
|
|
func setSubLogger(subsystem string, logger btclog.Logger,
|
|
useLoggers ...func(btclog.Logger)) {
|
|
|
|
logWriter.RegisterSubLogger(subsystem, logger)
|
|
for _, useLogger := range useLoggers {
|
|
useLogger(logger)
|
|
}
|
|
}
|
|
|
|
func noConsole() ([]byte, error) {
|
|
return nil, fmt.Errorf("wallet db requires console access")
|
|
}
|
|
|
|
func newExplorerAPI(apiURL string) *btc.ExplorerAPI {
|
|
// Override for testnet if default is used.
|
|
if apiURL == defaultAPIURL &&
|
|
chainParams.Name == chaincfg.TestNet3Params.Name {
|
|
|
|
return &btc.ExplorerAPI{BaseURL: defaultTestnetAPIURL}
|
|
}
|
|
|
|
// Also override for regtest if default is used.
|
|
if apiURL == defaultAPIURL &&
|
|
chainParams.Name == chaincfg.RegressionNetParams.Name {
|
|
|
|
return &btc.ExplorerAPI{BaseURL: defaultRegtestAPIURL}
|
|
}
|
|
|
|
// Otherwise use the provided URL.
|
|
return &btc.ExplorerAPI{BaseURL: apiURL}
|
|
}
|