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.
313 lines
9.3 KiB
Go
313 lines
9.3 KiB
Go
package chantools
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcutil/hdkeychain"
|
|
"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{},
|
|
)
|
|
|
|
_, 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(&channelBackupEncryptionRing{
|
|
extendedKey: extendedKey,
|
|
chainParams: chainParams,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("could not extract multi file: %v", err)
|
|
}
|
|
return dumpChannelBackup(multi)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|