package main import ( "bufio" "bytes" "encoding/json" "fmt" "io/ioutil" "os" "path" "strings" "syscall" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btclog" "github.com/guggero/chantools/dataformat" "github.com/jessevdk/go-flags" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" "golang.org/x/crypto/ssh/terminal" ) const ( defaultAPIURL = "https://blockstream.info/api" version = "0.4.0" ) var ( Commit = "" ) type config struct { Testnet bool `long:"testnet" description:"Set to true if testnet parameters should be used."` Regtest bool `long:"regtest" description:"Set to true if regtest parameters should be used."` APIURL string `long:"apiurl" description:"API URL to use (must be esplora compatible)."` ListChannels string `long:"listchannels" description:"The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin."` PendingChannels string `long:"pendingchannels" description:"The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin."` FromSummary string `long:"fromsummary" description:"The channel input is in the format of this tool's channel summary. Specify '-' to read from stdin."` FromChannelDB string `long:"fromchanneldb" description:"The channel input is in the format of an lnd channel.db file."` } var ( logWriter = build.NewRotatingLogWriter() log = build.NewSubLogger("CHAN", logWriter.GenSubLogger) cfg = &config{ APIURL: defaultAPIURL, } chainParams = &chaincfg.MainNetParams ) func main() { err := runCommandParser() if err == nil { return } _, ok := err.(*flags.Error) if !ok { fmt.Printf("Error running chantools: %v\n", err) } os.Exit(0) } func runCommandParser() error { setupLogging() // Parse command line. parser := flags.NewParser(cfg, flags.Default) log.Infof("chantools version v%s commit %s", version, Commit) _, _ = parser.AddCommand( "summary", "Compile a summary about the current state of "+ "channels.", "", &summaryCommand{}, ) _, _ = parser.AddCommand( "rescueclosed", "Try finding the private keys for funds that "+ "are in outputs of remotely force-closed channels.", "", &rescueClosedCommand{}, ) _, _ = parser.AddCommand( "forceclose", "Force-close the last state that is in the "+ "channel.db provided.", "", &forceCloseCommand{}, ) _, _ = parser.AddCommand( "sweeptimelock", "Sweep the force-closed state after the time "+ "lock has expired.", "", &sweepTimeLockCommand{}, ) _, _ = parser.AddCommand( "dumpchannels", "Dump all channel information from lnd's "+ "channel database.", "", &dumpChannelsCommand{}, ) _, _ = parser.AddCommand( "showrootkey", "Extract and show the BIP32 HD root key from "+ "the 24 word lnd aezeed.", "", &showRootKeyCommand{}, ) _, _ = parser.AddCommand( "dumpbackup", "Dump the content of a channel.backup file.", "", &dumpBackupCommand{}, ) _, _ = parser.AddCommand( "derivekey", "Derive a key with a specific derivation path "+ "from the BIP32 HD root key.", "", &deriveKeyCommand{}, ) _, _ = parser.AddCommand( "filterbackup", "Filter an lnd channel.backup file and "+ "remove certain channels.", "", &filterBackupCommand{}, ) _, _ = parser.AddCommand( "fixoldbackup", "Fixes an old channel.backup file that is "+ "affected by the lnd issue #3881 (unable to derive "+ "shachain root key).", "", &fixOldBackupCommand{}, ) _, _ = parser.AddCommand( "genimportscript", "Generate a script containing the on-chain "+ "keys of an lnd wallet that can be imported into "+ "other software like bitcoind.", "", &genImportScriptCommand{}, ) _, _ = parser.AddCommand( "walletinfo", "Shows relevant information about an lnd "+ "wallet.db file and optionally extracts the BIP32 HD "+ "root key.", "", &walletInfoCommand{}, ) _, _ = parser.AddCommand( "chanbackup", "Create a channel.backup file from a channel "+ "database.", "", &chanBackupCommand{}, ) _, _ = parser.AddCommand( "compactdb", "Open a source channel.db database file in safe/"+ "read-only mode and copy it to a fresh database, "+ "compacting it in the process.", "", &compactDBCommand{}, ) _, _ = parser.AddCommand( "vanitygen", "Generate a seed with a custom lnd node identity "+ "public key that starts with the given prefix.", "", &vanityGenCommand{}, ) _, _ = parser.AddCommand( "rescuefunding", "Rescue funds locked in a funding multisig "+ "output that never resulted in a proper channel. This "+ "is the command the initiator of the channel needs to "+ "run.", "", &rescueFundingCommand{}, ) _, _ = parser.AddCommand( "signrescuefunding", "Rescue funds locked in a funding "+ "multisig output that never resulted in a proper "+ "channel. This is the command the remote node (the non"+ "-initiator) of the channel needs to run.", "", &signRescueFundingCommand{}, ) _, err := parser.Parse() return err } func parseInputType(cfg *config) ([]*dataformat.SummaryEntry, error) { var ( content []byte err error target dataformat.InputFile ) switch { case cfg.ListChannels != "": content, err = readInput(cfg.ListChannels) target = &dataformat.ListChannelsFile{} case cfg.PendingChannels != "": content, err = readInput(cfg.PendingChannels) target = &dataformat.PendingChannelsFile{} case cfg.FromSummary != "": content, err = readInput(cfg.FromSummary) target = &dataformat.SummaryEntryFile{} case cfg.FromChannelDB != "": db, err := channeldb.Open( path.Dir(cfg.FromChannelDB), path.Base(cfg.FromChannelDB), channeldb.OptionSetSyncFreelist(true), channeldb.OptionReadOnly(true), ) if err != nil { return nil, fmt.Errorf("error opening channel DB: %v", err) } target = &dataformat.ChannelDBFile{DB: db} 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 setupChainParams(cfg *config) { switch { case cfg.Testnet: chainParams = &chaincfg.TestNet3Params case cfg.Regtest: chainParams = &chaincfg.RegressionNetParams default: chainParams = &chaincfg.MainNetParams } } func setupLogging() { setSubLogger("CHAN", log) addSubLogger("CHDB", channeldb.UseLogger) addSubLogger("BCKP", chanbackup.UseLogger) err := logWriter.InitLogRotator("./results/chantools.log", 10, 3) if err != nil { panic(err) } err = build.ParseAndSetDebugLevels("trace", logWriter) if err != nil { panic(err) } } // 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, logWriter.GenSubLogger) 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") }