diff --git a/Makefile b/Makefile index 5a7d4b4..3a69738 100644 --- a/Makefile +++ b/Makefile @@ -34,11 +34,11 @@ unit: build: @$(call print, "Building chantools.") - $(GOBUILD) $(PKG)/cmd/chantools + $(GOBUILD) ./... install: @$(call print, "Installing chantools.") - $(GOINSTALL) $(PKG)/cmd/chantools + $(GOINSTALL) ./... fmt: @$(call print, "Formatting source.") diff --git a/cmd_derivekey.go b/cmd/chantools/derivekey.go similarity index 60% rename from cmd_derivekey.go rename to cmd/chantools/derivekey.go index 0aa5014..1fa809b 100644 --- a/cmd_derivekey.go +++ b/cmd/chantools/derivekey.go @@ -1,4 +1,4 @@ -package chantools +package main import ( "fmt" @@ -8,6 +8,27 @@ import ( "github.com/guggero/chantools/btc" ) +type deriveKeyCommand struct { + RootKey string `long:"rootkey" description:"BIP32 HD root key to derive the key from."` + Path string `long:"path" description:"The BIP32 derivation path to derive. Must start with \"m/\"."` + Neuter bool `long:"neuter" description:"Do not output the private key, just the public key."` +} + +func (c *deriveKeyCommand) Execute(_ []string) error { + setupChainParams(cfg) + + // Check that root key is valid. + if c.RootKey == "" { + return fmt.Errorf("root key is required") + } + extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) + if err != nil { + return fmt.Errorf("error parsing root key: %v", err) + } + + return deriveKey(extendedKey, c.Path, c.Neuter) +} + func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string, neuter bool) error { diff --git a/cmd/chantools/dumpbackup.go b/cmd/chantools/dumpbackup.go new file mode 100644 index 0000000..695da1e --- /dev/null +++ b/cmd/chantools/dumpbackup.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/davecgh/go-spew/spew" + "github.com/guggero/chantools/btc" + "github.com/guggero/chantools/dump" + "github.com/lightningnetwork/lnd/chanbackup" +) + +type dumpBackupCommand struct { + RootKey string `long:"rootkey" description:"BIP32 HD root key of the wallet that was used to create the backup."` + MultiFile string `long:"multi_file" description:"The lnd channel.backup file to dump."` +} + +func (c *dumpBackupCommand) Execute(_ []string) error { + setupChainParams(cfg) + + // Check that root key is valid. + if c.RootKey == "" { + return fmt.Errorf("root key is required") + } + extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) + if err != nil { + return fmt.Errorf("error parsing root key: %v", err) + } + + // Check that we have a backup file. + if c.MultiFile == "" { + return fmt.Errorf("backup file is required") + } + multiFile := chanbackup.NewMultiFile(c.MultiFile) + multi, err := multiFile.ExtractMulti(&btc.ChannelBackupEncryptionRing{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + }) + if err != nil { + return fmt.Errorf("could not extract multi file: %v", err) + } + return dumpChannelBackup(multi) +} + +func dumpChannelBackup(multi *chanbackup.Multi) error { + dumpSingles := make([]dump.BackupSingle, len(multi.StaticBackups)) + for idx, single := range multi.StaticBackups { + dumpSingles[idx] = dump.BackupSingle{ + Version: single.Version, + IsInitiator: single.IsInitiator, + ChainHash: single.ChainHash.String(), + FundingOutpoint: single.FundingOutpoint.String(), + ShortChannelID: single.ShortChannelID, + RemoteNodePub: dump.PubKeyToString( + single.RemoteNodePub, + ), + Addresses: single.Addresses, + Capacity: single.Capacity, + LocalChanCfg: dump.ToChannelConfig( + single.LocalChanCfg, + ), + RemoteChanCfg: dump.ToChannelConfig( + single.RemoteChanCfg, + ), + ShaChainRootDesc: dump.ToKeyDescriptor( + single.ShaChainRootDesc, + ), + } + } + + spew.Dump(dump.BackupMulti{ + Version: multi.Version, + StaticBackups: dumpSingles, + }) + return nil +} diff --git a/cmd_dumpchannels.go b/cmd/chantools/dumpchannels.go similarity index 81% rename from cmd_dumpchannels.go rename to cmd/chantools/dumpchannels.go index 74808f2..589d80b 100644 --- a/cmd_dumpchannels.go +++ b/cmd/chantools/dumpchannels.go @@ -1,8 +1,10 @@ -package chantools +package main import ( "bytes" "encoding/hex" + "fmt" + "path" "github.com/davecgh/go-spew/spew" "github.com/guggero/chantools/dump" @@ -10,6 +12,22 @@ import ( "github.com/lightningnetwork/lnd/input" ) +type dumpChannelsCommand struct { + ChannelDB string `long:"channeldb" description:"The lnd channel.db file to dump the channels from."` +} + +func (c *dumpChannelsCommand) Execute(_ []string) error { + // Check that we have a channel DB. + if c.ChannelDB == "" { + return fmt.Errorf("channel DB is required") + } + db, err := channeldb.Open(path.Dir(c.ChannelDB)) + if err != nil { + return fmt.Errorf("error opening rescue DB: %v", err) + } + return dumpChannelInfo(db) +} + func dumpChannelInfo(chanDb *channeldb.DB) error { channels, err := chanDb.FetchAllChannels() if err != nil { diff --git a/cmd_forceclose.go b/cmd/chantools/forceclose.go similarity index 78% rename from cmd_forceclose.go rename to cmd/chantools/forceclose.go index 05df781..b7dedb7 100644 --- a/cmd_forceclose.go +++ b/cmd/chantools/forceclose.go @@ -1,4 +1,4 @@ -package chantools +package main import ( "bytes" @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/ioutil" + "path" "time" "github.com/btcsuite/btcd/txscript" @@ -17,6 +18,38 @@ import ( "github.com/lightningnetwork/lnd/input" ) +type forceCloseCommand struct { + RootKey string `long:"rootkey" description:"BIP32 HD root key to use."` + ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for force-closing channels."` + Publish bool `long:"publish" description:"Should the force-closing TX be published to the chain API?"` +} + +func (c *forceCloseCommand) Execute(_ []string) error { + // Check that root key is valid. + if c.RootKey == "" { + return fmt.Errorf("root key is required") + } + extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) + if err != nil { + return fmt.Errorf("error parsing root key: %v", err) + } + // Check that we have a channel DB. + if c.ChannelDB == "" { + return fmt.Errorf("rescue DB is required") + } + db, err := channeldb.Open(path.Dir(c.ChannelDB)) + if err != nil { + return fmt.Errorf("error opening rescue DB: %v", err) + } + + // Parse channel entries from any of the possible input files. + entries, err := parseInput(cfg) + if err != nil { + return err + } + return forceCloseChannels(extendedKey, entries, db, c.Publish) +} + func forceCloseChannels(extendedKey *hdkeychain.ExtendedKey, entries []*dataformat.SummaryEntry, chanDb *channeldb.DB, publish bool) error { diff --git a/cmd/chantools/main.go b/cmd/chantools/main.go index 258c48b..5518581 100644 --- a/cmd/chantools/main.go +++ b/cmd/chantools/main.go @@ -1,16 +1,163 @@ package main import ( + "bytes" + "encoding/json" "fmt" + "io/ioutil" "os" + "strings" - "github.com/guggero/chantools" + "github.com/btcsuite/btcd/chaincfg" + "github.com/guggero/chantools/dataformat" + "github.com/jessevdk/go-flags" + "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/channeldb" +) + +const ( + defaultApiUrl = "https://blockstream.info/api" +) + +type config struct { + Testnet bool `long:"testnet" description:"Set to true if testnet 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() { - if err := chantools.Main(); err != nil { - fmt.Printf("Error running chantools: %v\n", err) + err := Main() + if err == nil { + return } + _, ok := err.(*flags.Error) + if !ok { + fmt.Printf("Error running chantools: %v\n", err) + } os.Exit(0) } + +func Main() error { + setupLogging() + + // Parse command line. + parser := flags.NewParser(cfg, flags.Default) + _, _ = 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{}, + ) + + _, err := parser.Parse() + return err +} + +func parseInput(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(cfg.FromChannelDB) + 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 setupChainParams(cfg *config) { + if cfg.Testnet { + chainParams = &chaincfg.TestNet3Params + } +} + +func setupLogging() { + logWriter.RegisterSubLogger("CHAN", log) + err := logWriter.InitLogRotator("./results/chantools.log", 10, 3) + if err != nil { + panic(err) + } + err = build.ParseAndSetDebugLevels("trace", logWriter) + if err != nil { + panic(err) + } +} diff --git a/cmd_rescueclosed.go b/cmd/chantools/rescueclosed.go similarity index 84% rename from cmd_rescueclosed.go rename to cmd/chantools/rescueclosed.go index 24eb982..530fdd4 100644 --- a/cmd_rescueclosed.go +++ b/cmd/chantools/rescueclosed.go @@ -1,17 +1,18 @@ -package chantools +package main import ( "crypto/subtle" "encoding/json" "errors" "fmt" - "github.com/guggero/chantools/btc" "io/ioutil" + "path" "time" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" + "github.com/guggero/chantools/btc" "github.com/guggero/chantools/dataformat" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" @@ -30,6 +31,38 @@ type cacheEntry struct { pubKey *btcec.PublicKey } +type rescueClosedCommand struct { + RootKey string `long:"rootkey" description:"BIP32 HD root key to use."` + ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for rescuing force-closed channels."` +} + +func (c *rescueClosedCommand) Execute(_ []string) error { + // Check that root key is valid. + if c.RootKey == "" { + return fmt.Errorf("root key is required") + } + extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) + if err != nil { + return fmt.Errorf("error parsing root key: %v", err) + } + + // Check that we have a channel DB. + if c.ChannelDB == "" { + return fmt.Errorf("rescue DB is required") + } + db, err := channeldb.Open(path.Dir(c.ChannelDB)) + if err != nil { + return fmt.Errorf("error opening rescue DB: %v", err) + } + + // Parse channel entries from any of the possible input files. + entries, err := parseInput(cfg) + if err != nil { + return err + } + return rescueClosedChannels(extendedKey, entries, db) +} + func rescueClosedChannels(extendedKey *hdkeychain.ExtendedKey, entries []*dataformat.SummaryEntry, chanDb *channeldb.DB) error { diff --git a/cmd/chantools/showrootkey.go b/cmd/chantools/showrootkey.go new file mode 100644 index 0000000..6bcd62d --- /dev/null +++ b/cmd/chantools/showrootkey.go @@ -0,0 +1,66 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + "syscall" + + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/lightningnetwork/lnd/aezeed" + "golang.org/x/crypto/ssh/terminal" +) + +type showRootKeyCommand struct{} + +func (c *showRootKeyCommand) Execute(_ []string) error { + // We'll now prompt the user to enter in their 24-word mnemonic. + fmt.Printf("Input your 24-word mnemonic separated by spaces: ") + reader := bufio.NewReader(os.Stdin) + mnemonicStr, err := reader.ReadString('\n') + if err != nil { + return err + } + + // We'll trim off extra spaces, and ensure the mnemonic is all + // lower case, then populate our request. + mnemonicStr = strings.TrimSpace(mnemonicStr) + mnemonicStr = strings.ToLower(mnemonicStr) + + cipherSeedMnemonic := strings.Split(mnemonicStr, " ") + + fmt.Println() + + if len(cipherSeedMnemonic) != 24 { + return fmt.Errorf("wrong cipher seed mnemonic "+ + "length: got %v words, expecting %v words", + len(cipherSeedMnemonic), 24) + } + + // Additionally, the user may have a passphrase, that will also + // need to be provided so the daemon can properly decipher the + // cipher seed. + fmt.Printf("Input your cipher seed passphrase (press enter if " + + "your seed doesn't have a passphrase): ") + passphrase, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + return err + } + + var mnemonic aezeed.Mnemonic + copy(mnemonic[:], cipherSeedMnemonic[:]) + + // If we're unable to map it back into the ciphertext, then either the + // mnemonic is wrong, or the passphrase is wrong. + cipherSeed, err := mnemonic.ToCipherSeed(passphrase) + if err != nil { + return err + } + rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], chainParams) + if err != nil { + return fmt.Errorf("failed to derive master extended key") + } + fmt.Printf("\nYour BIP32 HD root key is: %s\n", rootKey.String()) + return nil +} diff --git a/cmd_summary.go b/cmd/chantools/summary.go similarity index 94% rename from cmd_summary.go rename to cmd/chantools/summary.go index 287346a..0358a0b 100644 --- a/cmd_summary.go +++ b/cmd/chantools/summary.go @@ -1,4 +1,4 @@ -package chantools +package main import ( "encoding/json" @@ -10,6 +10,17 @@ import ( "github.com/guggero/chantools/dataformat" ) +type summaryCommand struct{} + +func (c *summaryCommand) Execute(_ []string) error { + // Parse channel entries from any of the possible input files. + entries, err := parseInput(cfg) + if err != nil { + return err + } + return summarizeChannels(cfg.ApiUrl, entries) +} + func summarizeChannels(apiUrl string, channels []*dataformat.SummaryEntry) error { diff --git a/cmd_sweeptimelock.go b/cmd/chantools/sweeptimelock.go similarity index 85% rename from cmd_sweeptimelock.go rename to cmd/chantools/sweeptimelock.go index 5b623f9..6d95696 100644 --- a/cmd_sweeptimelock.go +++ b/cmd/chantools/sweeptimelock.go @@ -1,4 +1,4 @@ -package chantools +package main import ( "bytes" @@ -19,6 +19,44 @@ const ( feeSatPerByte = 2 ) +type sweepTimeLockCommand struct { + RootKey string `long:"rootkey" description:"BIP32 HD root key to use."` + Publish bool `long:"publish" description:"Should the sweep TX be published to the chain API?"` + SweepAddr string `long:"sweepaddr" description:"The address the funds should be sweeped to"` + MaxCsvLimit int `long:"maxcsvlimit" description:"Maximum CSV limit to use. (default 2000)"` +} + +func (c *sweepTimeLockCommand) Execute(_ []string) error { + // Check that root key is valid. + if c.RootKey == "" { + return fmt.Errorf("root key is required") + } + extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) + if err != nil { + return fmt.Errorf("error parsing root key: %v", err) + } + + // Make sure sweep addr is set. + if c.SweepAddr == "" { + return fmt.Errorf("sweep addr is required") + } + + // Parse channel entries from any of the possible input files. + entries, err := parseInput(cfg) + if err != nil { + return err + } + + // Set default value + if c.MaxCsvLimit == 0 { + c.MaxCsvLimit = 2000 + } + return sweepTimeLock( + extendedKey, cfg.ApiUrl, entries, c.SweepAddr, c.MaxCsvLimit, + c.Publish, + ) +} + func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiUrl string, entries []*dataformat.SummaryEntry, sweepAddr string, maxCsvTimeout int, publish bool) error { diff --git a/cmd_dumpbackup.go b/cmd_dumpbackup.go deleted file mode 100644 index 27ef3d2..0000000 --- a/cmd_dumpbackup.go +++ /dev/null @@ -1,40 +0,0 @@ -package chantools - -import ( - "github.com/davecgh/go-spew/spew" - "github.com/guggero/chantools/dump" - "github.com/lightningnetwork/lnd/chanbackup" -) - -func dumpChannelBackup(multi *chanbackup.Multi) error { - dumpSingles := make([]dump.BackupSingle, len(multi.StaticBackups)) - for idx, single := range multi.StaticBackups { - dumpSingles[idx] = dump.BackupSingle{ - Version: single.Version, - IsInitiator: single.IsInitiator, - ChainHash: single.ChainHash.String(), - FundingOutpoint: single.FundingOutpoint.String(), - ShortChannelID: single.ShortChannelID, - RemoteNodePub: dump.PubKeyToString( - single.RemoteNodePub, - ), - Addresses: single.Addresses, - Capacity: single.Capacity, - LocalChanCfg: dump.ToChannelConfig( - single.LocalChanCfg, - ), - RemoteChanCfg: dump.ToChannelConfig( - single.RemoteChanCfg, - ), - ShaChainRootDesc: dump.ToKeyDescriptor( - single.ShaChainRootDesc, - ), - } - } - - spew.Dump(dump.BackupMulti{ - Version: multi.Version, - StaticBackups: dumpSingles, - }) - return nil -} diff --git a/main.go b/main.go deleted file mode 100644 index 6414241..0000000 --- a/main.go +++ /dev/null @@ -1,393 +0,0 @@ -package chantools - -import ( - "bufio" - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path" - "strings" - "syscall" - - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcutil/hdkeychain" - "github.com/guggero/chantools/btc" - "github.com/guggero/chantools/dataformat" - "github.com/jessevdk/go-flags" - "github.com/lightningnetwork/lnd/aezeed" - "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" -) - -type config struct { - Testnet bool `long:"testnet" description:"Set to true if testnet 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() error { - setupLogging() - - // Parse command line. - parser := flags.NewParser(cfg, flags.Default) - _, _ = 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{}, - ) - - _, err := parser.Parse() - return err -} - -type summaryCommand struct{} - -func (c *summaryCommand) Execute(_ []string) error { - // Parse channel entries from any of the possible input files. - entries, err := ParseInput(cfg) - if err != nil { - return err - } - return summarizeChannels(cfg.ApiUrl, entries) -} - -type rescueClosedCommand struct { - RootKey string `long:"rootkey" description:"BIP32 HD root key to use."` - ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for rescuing force-closed channels."` -} - -func (c *rescueClosedCommand) Execute(_ []string) error { - // Check that root key is valid. - if c.RootKey == "" { - return fmt.Errorf("root key is required") - } - extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) - if err != nil { - return fmt.Errorf("error parsing root key: %v", err) - } - - // Check that we have a channel DB. - if c.ChannelDB == "" { - return fmt.Errorf("rescue DB is required") - } - db, err := channeldb.Open(path.Dir(c.ChannelDB)) - if err != nil { - return fmt.Errorf("error opening rescue DB: %v", err) - } - - // Parse channel entries from any of the possible input files. - entries, err := ParseInput(cfg) - if err != nil { - return err - } - return rescueClosedChannels(extendedKey, entries, db) -} - -type forceCloseCommand struct { - RootKey string `long:"rootkey" description:"BIP32 HD root key to use."` - ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for force-closing channels."` - Publish bool `long:"publish" description:"Should the force-closing TX be published to the chain API?"` -} - -func (c *forceCloseCommand) Execute(_ []string) error { - // Check that root key is valid. - if c.RootKey == "" { - return fmt.Errorf("root key is required") - } - extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) - if err != nil { - return fmt.Errorf("error parsing root key: %v", err) - } - // Check that we have a channel DB. - if c.ChannelDB == "" { - return fmt.Errorf("rescue DB is required") - } - db, err := channeldb.Open(path.Dir(c.ChannelDB)) - if err != nil { - return fmt.Errorf("error opening rescue DB: %v", err) - } - - // Parse channel entries from any of the possible input files. - entries, err := ParseInput(cfg) - if err != nil { - return err - } - return forceCloseChannels(extendedKey, entries, db, c.Publish) -} - -type sweepTimeLockCommand struct { - RootKey string `long:"rootkey" description:"BIP32 HD root key to use."` - Publish bool `long:"publish" description:"Should the sweep TX be published to the chain API?"` - SweepAddr string `long:"sweepaddr" description:"The address the funds should be sweeped to"` - MaxCsvLimit int `long:"maxcsvlimit" description:"Maximum CSV limit to use. (default 2000)"` -} - -func (c *sweepTimeLockCommand) Execute(_ []string) error { - // Check that root key is valid. - if c.RootKey == "" { - return fmt.Errorf("root key is required") - } - extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) - if err != nil { - return fmt.Errorf("error parsing root key: %v", err) - } - - // Make sure sweep addr is set. - if c.SweepAddr == "" { - return fmt.Errorf("sweep addr is required") - } - - // Parse channel entries from any of the possible input files. - entries, err := ParseInput(cfg) - if err != nil { - return err - } - - // Set default value - if c.MaxCsvLimit == 0 { - c.MaxCsvLimit = 2000 - } - return sweepTimeLock( - extendedKey, cfg.ApiUrl, entries, c.SweepAddr, c.MaxCsvLimit, - c.Publish, - ) -} - -type dumpChannelsCommand struct { - ChannelDB string `long:"channeldb" description:"The lnd channel.db file to dump the channels from."` -} - -func (c *dumpChannelsCommand) Execute(_ []string) error { - // Check that we have a channel DB. - if c.ChannelDB == "" { - return fmt.Errorf("channel DB is required") - } - db, err := channeldb.Open(path.Dir(c.ChannelDB)) - if err != nil { - return fmt.Errorf("error opening rescue DB: %v", err) - } - return dumpChannelInfo(db) -} - -type showRootKeyCommand struct{} - -func (c *showRootKeyCommand) Execute(_ []string) error { - // We'll now prompt the user to enter in their 24-word mnemonic. - fmt.Printf("Input your 24-word mnemonic separated by spaces: ") - reader := bufio.NewReader(os.Stdin) - mnemonicStr, err := reader.ReadString('\n') - if err != nil { - return err - } - - // We'll trim off extra spaces, and ensure the mnemonic is all - // lower case, then populate our request. - mnemonicStr = strings.TrimSpace(mnemonicStr) - mnemonicStr = strings.ToLower(mnemonicStr) - - cipherSeedMnemonic := strings.Split(mnemonicStr, " ") - - fmt.Println() - - if len(cipherSeedMnemonic) != 24 { - return fmt.Errorf("wrong cipher seed mnemonic "+ - "length: got %v words, expecting %v words", - len(cipherSeedMnemonic), 24) - } - - // Additionally, the user may have a passphrase, that will also - // need to be provided so the daemon can properly decipher the - // cipher seed. - fmt.Printf("Input your cipher seed passphrase (press enter if " + - "your seed doesn't have a passphrase): ") - passphrase, err := terminal.ReadPassword(syscall.Stdin) - if err != nil { - return err - } - - var mnemonic aezeed.Mnemonic - copy(mnemonic[:], cipherSeedMnemonic[:]) - - // If we're unable to map it back into the ciphertext, then either the - // mnemonic is wrong, or the passphrase is wrong. - cipherSeed, err := mnemonic.ToCipherSeed(passphrase) - if err != nil { - return err - } - rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], chainParams) - if err != nil { - return fmt.Errorf("failed to derive master extended key") - } - fmt.Printf("\nYour BIP32 HD root key is: %s\n", rootKey.String()) - return nil -} - -type dumpBackupCommand struct { - RootKey string `long:"rootkey" description:"BIP32 HD root key of the wallet that was used to create the backup."` - MultiFile string `long:"multi_file" description:"The lnd channel.backup file to dump."` -} - -func (c *dumpBackupCommand) Execute(_ []string) error { - setupChainParams(cfg) - - // Check that root key is valid. - if c.RootKey == "" { - return fmt.Errorf("root key is required") - } - extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) - if err != nil { - return fmt.Errorf("error parsing root key: %v", err) - } - - // Check that we have a backup file. - if c.MultiFile == "" { - return fmt.Errorf("backup file is required") - } - multiFile := chanbackup.NewMultiFile(c.MultiFile) - multi, err := multiFile.ExtractMulti(&btc.ChannelBackupEncryptionRing{ - ExtendedKey: extendedKey, - ChainParams: chainParams, - }) - if err != nil { - return fmt.Errorf("could not extract multi file: %v", err) - } - return dumpChannelBackup(multi) -} - -type deriveKeyCommand struct { - RootKey string `long:"rootkey" description:"BIP32 HD root key to derive the key from."` - Path string `long:"path" description:"The BIP32 derivation path to derive. Must start with \"m/\"."` - Neuter bool `long:"neuter" description:"Do not output the private key, just the public key."` -} - -func (c *deriveKeyCommand) Execute(_ []string) error { - setupChainParams(cfg) - - // Check that root key is valid. - if c.RootKey == "" { - return fmt.Errorf("root key is required") - } - extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) - if err != nil { - return fmt.Errorf("error parsing root key: %v", err) - } - - return deriveKey(extendedKey, c.Path, c.Neuter) -} - -func ParseInput(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(cfg.FromChannelDB) - 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 setupChainParams(cfg *config) { - if cfg.Testnet { - chainParams = &chaincfg.TestNet3Params - } -} - -func setupLogging() { - logWriter.RegisterSubLogger("CHAN", log) - err := logWriter.InitLogRotator("./results/chantools.log", 10, 3) - if err != nil { - panic(err) - } - err = build.ParseAndSetDebugLevels("trace", logWriter) - if err != nil { - panic(err) - } -}