diff --git a/.gitignore b/.gitignore index 87739b9..627445a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/chansummary +/chantools +results \ No newline at end of file diff --git a/Makefile b/Makefile index 8177418..5a7d4b4 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PKG := github.com/guggero/chansummary +PKG := github.com/guggero/chantools GOTEST := GO111MODULE=on go test -v @@ -33,12 +33,12 @@ unit: $(UNIT) build: - @$(call print, "Building chansummary.") - $(GOBUILD) $(PKG)/cmd/chansummary + @$(call print, "Building chantools.") + $(GOBUILD) $(PKG)/cmd/chantools install: - @$(call print, "Installing chansummary.") - $(GOINSTALL) $(PKG)/cmd/chansummary + @$(call print, "Installing chantools.") + $(GOINSTALL) $(PKG)/cmd/chantools fmt: @$(call print, "Formatting source.") diff --git a/README.md b/README.md index 58e92df..a35f3c9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Channel summary +# Channel tools This tool works with the output of lnd's `listchannels` command and creates a summary of the on-chain state of these channels. diff --git a/chainapi.go b/chainapi.go index 159eb98..cbfe8c3 100644 --- a/chainapi.go +++ b/chainapi.go @@ -1,12 +1,17 @@ -package chansummary +package chantools import ( "bytes" "encoding/json" + "errors" "fmt" "net/http" ) +var ( + ErrTxNotFound = errors.New("transaction not found") +) + type chainApi struct { baseUrl string } @@ -17,17 +22,18 @@ type transaction struct { } type vin struct { - Tixid string `json:"txid"` - Vout int `json:"vout"` - Prevout *vout `json:"prevout"` + Tixid string `json:"txid"` + Vout int `json:"vout"` + Prevout *vout `json:"prevout"` + Sequence uint32 `json:"sequence"` } type vout struct { ScriptPubkey string `json:"scriptpubkey"` ScriptPubkeyAsm string `json:"scriptpubkey_asm"` ScriptPubkeyType string `json:"scriptpubkey_type"` - ScriptPubkeyAddr string `json:"scriptpubkey_addr"` - Value uint64 `json:"value"` + ScriptPubkeyAddr string `json:"scriptpubkey_address"` + Value uint64 `json:"value"` outspend *outspend } @@ -76,5 +82,11 @@ func Fetch(url string, target interface{}) error { if err != nil { return err } - return json.Unmarshal(body.Bytes(), target) + err = json.Unmarshal(body.Bytes(), target) + if err != nil { + if string(body.Bytes()) == "Transaction not found" { + return ErrTxNotFound + } + } + return err } diff --git a/chanbruteforce.go b/chanbruteforce.go new file mode 100644 index 0000000..593cc89 --- /dev/null +++ b/chanbruteforce.go @@ -0,0 +1,236 @@ +package chantools + +import ( + "crypto/subtle" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +var ( + hardenedKeyStart = uint32(hdkeychain.HardenedKeyStart) + chainParams = &chaincfg.MainNetParams + cacheSize = 2000 + cache []*cacheEntry + + errAddrNotFound = errors.New("addr not found") +) + +type cacheEntry struct { + privKey *btcec.PrivateKey + pubKey *btcec.PublicKey +} + +func bruteForceChannels(cfg *config, entries []*SummaryEntry, + chanDb *channeldb.DB) error { + + err := fillCache(cfg.RootKey) + 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 *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(&SummaryEntryFile{ + Channels: entries, + }, "", " ") + if err != nil { + return err + } + fileName := fmt.Sprintf("results/bruteforce-%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) { + // First parse address to get targetPubKeyHash from it later. + targetAddr, err := btcutil.DecodeAddress(addr, chainParams) + if err != nil { + return "", err + } + + var targetPubKeyHash []byte + // Make the check on the decoded address according to the active + // network (testnet or mainnet only). + if !targetAddr.IsForNet(chainParams) { + return "", fmt.Errorf( + "address: %v is not valid for this network: %v", + targetAddr.String(), chainParams.Name, + ) + } + + // Must be a bech32 native SegWit address. + switch targetAddr.(type) { + case *btcutil.AddressWitnessPubKeyHash: + targetPubKeyHash = targetAddr.ScriptAddress() + default: + return "", fmt.Errorf("address: must be a bech32 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(rootKey string) error { + extendedKey, err := hdkeychain.NewKeyFromString(rootKey) + if err != nil { + return err + } + + cache = make([]*cacheEntry, cacheSize) + + for i := 0; i < cacheSize; i++ { + key, err := deriveChildren(extendedKey, []uint32{ + hardenedKeyStart + uint32(keychain.BIP0043Purpose), + hardenedKeyStart + chainParams.HDCoinType, + 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 +} + +func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( + *hdkeychain.ExtendedKey, error) { + + var ( + currentKey = key + err error + ) + for _, pathPart := range path { + currentKey, err = currentKey.Child(pathPart) + if err != nil { + return nil, err + } + } + return currentKey, nil +} + +func addrFromDesc(desc *keychain.KeyDescriptor) (string, error) { + hash160 := btcutil.Hash160(desc.PubKey.SerializeCompressed()) + addr, err := btcutil.NewAddressWitnessPubKeyHash( + hash160, chainParams, + ) + if err != nil { + return "", err + } + return addr.String(), nil +} diff --git a/chansummary.go b/chansummary.go index 74c3d28..7642c71 100644 --- a/chansummary.go +++ b/chansummary.go @@ -1,108 +1,117 @@ -package chansummary +package chantools import ( + "encoding/json" "fmt" - "strconv" - "strings" + "io/ioutil" + "time" ) -type channel struct { - RemotePubkey string `json:"remote_pubkey"` - ChannelPoint string `json:"channel_point"` - Capacity string `json:"capacity"` - Initiator bool `json:"initiator"` - LocalBalance string `json:"local_balance"` - RemoteBalance string `json:"remote_balance"` -} - -func (c *channel) FundingTXID() string { - parts := strings.Split(c.ChannelPoint, ":") - if len(parts) != 2 { - panic(fmt.Errorf("channel point not in format :")) +func collectChanSummary(cfg *config, channels []*SummaryEntry) error { + summaryFile := &SummaryEntryFile{ + Channels: channels, } - return parts[0] -} - -func (c *channel) FundingTXIndex() int { - parts := strings.Split(c.ChannelPoint, ":") - if len(parts) != 2 { - panic(fmt.Errorf("channel point not in format :")) - } - return parseInt(parts[1]) -} - -func (c *channel) localBalance() uint64 { - return uint64(parseInt(c.LocalBalance)) -} - -func (c *channel) remoteBalance() uint64 { - return uint64(parseInt(c.RemoteBalance)) -} - -func collectChanSummary(cfg *config, channels []*channel) error { - var ( - chansClosed = 0 - chansOpen = 0 - valueUnspent = uint64(0) - valueSalvage = uint64(0) - valueSafe = uint64(0) - ) chainApi := &chainApi{baseUrl: cfg.ApiUrl} for idx, channel := range channels { - tx, err := chainApi.Transaction(channel.FundingTXID()) + tx, err := chainApi.Transaction(channel.FundingTXID) + if err == ErrTxNotFound { + log.Errorf("Funding TX %s not found. Ignoring.", + channel.FundingTXID) + continue + } if err != nil { + log.Errorf("Problem with channel %d (%s): %v.", + idx, channel.FundingTXID, err) return err } - outspend := tx.Vout[channel.FundingTXIndex()].outspend + outspend := tx.Vout[channel.FundingTXIndex].outspend if outspend.Spent { - chansClosed++ + summaryFile.ClosedChannels++ + channel.ClosingTX = &ClosingTX{ + TXID: outspend.Txid, + } - s, f, err := reportOutspend(chainApi, channel, outspend) + err := reportOutspend( + chainApi, summaryFile, channel, outspend, + ) if err != nil { + log.Errorf("Problem with channel %d (%s): %v.", + idx, channel.FundingTXID, err) return err } - valueSalvage += s - valueSafe += f } else { - chansOpen++ - valueUnspent += channel.localBalance() + summaryFile.OpenChannels++ + summaryFile.FundsOpenChannels += channel.LocalBalance + channel.ClosingTX = nil } if idx%50 == 0 { - fmt.Printf("Queried channel %d of %d.\n", idx, + log.Infof("Queried channel %d of %d.", idx, len(channels)) } } - fmt.Printf("Finished scanning.\nClosed channels: %d\nOpen channels: "+ - "%d\nSats in open channels: %d\nSats that can possibly be "+ - "salvaged: %d\nSats in co-op close channels: %d\n", chansClosed, - chansOpen, valueUnspent, valueSalvage, valueSafe) - - return nil + log.Info("Finished scanning.") + log.Infof("Open channels: %d", summaryFile.OpenChannels) + log.Infof("Sats in open channels: %d", summaryFile.FundsOpenChannels) + log.Infof("Closed channels: %d", summaryFile.ClosedChannels) + log.Infof(" --> force closed channels: %d", + summaryFile.ForceClosedChannels) + log.Infof(" --> coop closed channels: %d", + summaryFile.CoopClosedChannels) + log.Infof(" --> closed channels with all outputs spent: %d", + summaryFile.FullySpentChannels) + log.Infof(" --> closed channels with unspent outputs: %d", + summaryFile.ChannelsWithPotential) + log.Infof("Sats in closed channels: %d", summaryFile.FundsClosedChannels) + log.Infof(" --> closed channel sats that have been swept/spent: %d", + summaryFile.FundsClosedSpent) + log.Infof(" --> closed channel sats that are in force-close outputs: %d", + summaryFile.FundsForceClose) + log.Infof(" --> closed channel sats that are in coop close outputs: %d", + summaryFile.FundsCoopClose) + + summaryBytes, err := json.MarshalIndent(summaryFile, "", " ") + if err != nil { + return err + } + fileName := fmt.Sprintf("results/summary-%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 reportOutspend(api *chainApi, ch *channel, os *outspend) (uint64, uint64, - error) { +func reportOutspend(api *chainApi, summaryFile *SummaryEntryFile, + entry *SummaryEntry, os *outspend) error { spendTx, err := api.Transaction(os.Txid) if err != nil { - return 0, 0, err + return err } + summaryFile.FundsClosedChannels += entry.LocalBalance + + if isCoopClose(spendTx) { + summaryFile.CoopClosedChannels++ + summaryFile.FundsCoopClose += entry.LocalBalance + entry.ClosingTX.ForceClose = false + return nil + } + + summaryFile.ForceClosedChannels++ + entry.ClosingTX.ForceClose = true + numSpent := 0 - salvageBalance := uint64(0) - safeBalance := uint64(0) for _, vout := range spendTx.Vout { if vout.outspend.Spent { numSpent++ } } if numSpent != len(spendTx.Vout) { - fmt.Printf("Channel %s spent by %s:%d which has %d outputs of "+ - "which %d are spent:\n", ch.ChannelPoint, os.Txid, + log.Debugf("Channel %s spent by %s:%d which has %d outputs of "+ + "which %d are spent.", entry.ChannelPoint, os.Txid, os.Vin, len(spendTx.Vout), numSpent) var utxo []*vout for _, vout := range spendTx.Vout { @@ -110,9 +119,11 @@ func reportOutspend(api *chainApi, ch *channel, os *outspend) (uint64, uint64, utxo = append(utxo, vout) } } + entry.ClosingTX.AllOutsSpent = false + summaryFile.ChannelsWithPotential++ - if salvageable(ch, utxo) { - salvageBalance += utxo[0].Value + if couldBeOurs(entry, utxo) { + summaryFile.FundsForceClose += utxo[0].Value outs := spendTx.Vout @@ -121,42 +132,34 @@ func reportOutspend(api *chainApi, ch *channel, os *outspend) (uint64, uint64, outs[0].ScriptPubkeyType == "v0_p2wpkh" && outs[0].outspend.Spent == false: - safeBalance += utxo[0].Value - - case len(outs) == 2 && - outs[0].ScriptPubkeyType == "v0_p2wpkh" && - outs[1].ScriptPubkeyType == "v0_p2wpkh": - - safeBalance += utxo[0].Value + entry.ClosingTX.OurAddr = outs[0].ScriptPubkeyAddr } } else { for idx, vout := range spendTx.Vout { if !vout.outspend.Spent { - fmt.Printf("UTXO %d of type %s with "+ - "value %d\n", idx, + log.Debugf("UTXO %d of type %s with "+ + "value %d", idx, vout.ScriptPubkeyType, vout.Value) } } - fmt.Printf("Local balance: %s\n", ch.LocalBalance) - fmt.Printf("Remote balance: %s\n", ch.RemoteBalance) - fmt.Printf("Initiator: %v\n", ch.Initiator) + log.Debugf("Local balance: %d", entry.LocalBalance) + log.Debugf("Remote balance: %d", entry.RemoteBalance) + log.Debugf("Initiator: %v", entry.Initiator) } - + } else { + entry.ClosingTX.AllOutsSpent = true + summaryFile.FundsClosedSpent += entry.LocalBalance + summaryFile.FullySpentChannels++ } - return salvageBalance, safeBalance, nil + return nil } -func salvageable(ch *channel, utxo []*vout) bool { - return ch.localBalance() == utxo[0].Value || - ch.remoteBalance() == 0 +func couldBeOurs(entry *SummaryEntry, utxo []*vout) bool { + return utxo[0].ScriptPubkeyType == "v0_p2wpkh" && entry.LocalBalance != 0 } -func parseInt(str string) int { - index, err := strconv.Atoi(str) - if err != nil { - panic(fmt.Errorf("error parsing '%s' as int: %v", str, err)) - } - return index +func isCoopClose(tx *transaction) bool { + return tx.Vin[0].Sequence == 0xffffffff } diff --git a/cmd/chansummary/main.go b/cmd/chansummary/main.go deleted file mode 100644 index 1bfc0f7..0000000 --- a/cmd/chansummary/main.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/guggero/chansummary" -) - -func main() { - if err := chansummary.Main(); err != nil { - fmt.Printf("Error running chansummary: %v\n", err) - } - - os.Exit(0) -} diff --git a/cmd/chantools/main.go b/cmd/chantools/main.go new file mode 100644 index 0000000..8aa051c --- /dev/null +++ b/cmd/chantools/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "os" + + "github.com/guggero/chantools" +) + +func main() { + if err := chantools.Main(); err != nil { + fmt.Printf("Error running chantools: %v\n", err) + } + + os.Exit(0) +} diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..a1b8dd9 --- /dev/null +++ b/entry.go @@ -0,0 +1,36 @@ +package chantools + +type ClosingTX struct { + TXID string `json:"txid"` + ForceClose bool `json:"force_close"` + AllOutsSpent bool `json:"all_outputs_spent"` + OurAddr string `json:"our_addr"` + SweepPrivkey string `json:"sweep_privkey"` +} + +type SummaryEntry struct { + RemotePubkey string `json:"remote_pubkey"` + ChannelPoint string `json:"channel_point"` + FundingTXID string `json:"funding_txid"` + FundingTXIndex uint32 `json:"funding_tx_index"` + Capacity uint64 `json:"capacity"` + Initiator bool `json:"initiator"` + LocalBalance uint64 `json:"local_balance"` + RemoteBalance uint64 `json:"remote_balance"` + ClosingTX *ClosingTX `json:"closing_tx,omitempty"` +} + +type SummaryEntryFile struct { + Channels []*SummaryEntry `json:"channels"` + OpenChannels uint32 `json:"open_channels"` + ClosedChannels uint32 `json:"closed_channels"` + ForceClosedChannels uint32 `json:"force_closed_channels"` + CoopClosedChannels uint32 `json:"coop_closed_channels"` + FullySpentChannels uint32 `json:"fully_spent_channels"` + ChannelsWithPotential uint32 `json:"channels_with_potential_funds"` + FundsOpenChannels uint64 `json:"funds_open_channels"` + FundsClosedChannels uint64 `json:"funds_closed_channels"` + FundsClosedSpent uint64 `json:"funds_closed_channels_spent"` + FundsForceClose uint64 `json:"funds_force_closed_maybe_ours"` + FundsCoopClose uint64 `json:"funds_coop_closed_maybe_ours"` +} diff --git a/go.mod b/go.mod index e3535fc..d76dd28 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,15 @@ -module github.com/guggero/chansummary +module github.com/guggero/chantools -require github.com/jessevdk/go-flags v1.4.0 +require ( + github.com/btcsuite/btcd v0.20.1-beta + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f + github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d + github.com/davecgh/go-spew v1.1.1 + github.com/jessevdk/go-flags v1.4.0 + github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191025122959-1a0ab538d53c + github.com/prometheus/common v0.4.0 + github.com/urfave/cli/v2 v2.0.0 // indirect + golang.org/x/crypto v0.0.0-20191111213947-16651526fdb4 // indirect +) go 1.13 diff --git a/go.sum b/go.sum index 1b3c118..248db95 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,213 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= +github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= +github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= +github.com/btcsuite/btcd v0.20.0-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcwallet v0.10.0 h1:fFZncfYJ7VByePTGttzJc3qfCyDzU95ucZYk0M912lU= +github.com/btcsuite/btcwallet v0.10.0/go.mod h1:4TqBEuceheGNdeLNrelliLHJzmXauMM2vtWfuy1pFiM= +github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c= +github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 h1:2VsfS0sBedcM5KmDzRMT3+b6xobqWveZGvjb+jFez5w= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZwEiu3jNAtfXj2n2+c8RWiE/WNA= +github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 h1:6DxkcoMnCPY4E9cUDPB5tbuuf40SmmMkSQkoE8vCT+s= +github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= +github.com/btcsuite/btcwallet/walletdb v1.0.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk= +github.com/btcsuite/btcwallet/walletdb v1.1.0 h1:JHAL7wZ8pX4SULabeAv/wPO9sseRWMGzE80lfVmRw6Y= +github.com/btcsuite/btcwallet/walletdb v1.1.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk= +github.com/btcsuite/btcwallet/wtxmgr v1.0.0 h1:aIHgViEmZmZfe0tQQqF1xyd2qBqFWxX5vZXkkbjtbeA= +github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY= +github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941 h1:kij1x2aL7VE6gtx8KMIt8PGPgI5GV9LgtHFG5KaEMPY= +github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:QcFA8DZHtuIAdYKCq/BzELOaznRsCvwf4zTPmaYwaig= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= +github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= +github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= +github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= +github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lightninglabs/gozmq v0.0.0-20190710231225-cea2a031735d h1:tt8hwvxl6fksSfchjBGaWu+pnWJQfG1OWiCM20qOSAE= +github.com/lightninglabs/gozmq v0.0.0-20190710231225-cea2a031735d/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= +github.com/lightninglabs/neutrino v0.10.0 h1:yWVy2cOCCXbKFdpYCE9vD1fWRJDd9FtGXhUws4l9RkU= +github.com/lightninglabs/neutrino v0.10.0/go.mod h1:C3KhCMk1Mcx3j8v0qRVWM1Ow6rIJSvSPnUAq00ZNAfk= +github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a h1:GoWPN4i4jTKRxhVNh9a2vvBBO1Y2seiJB+SopUYoKyo= +github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4= +github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191025122959-1a0ab538d53c h1:eZcbiUop12hTTVIjicfm85do4kftmJqAwGVWYPh6+Xo= +github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191025122959-1a0ab538d53c/go.mod h1:nq06y2BDv7vwWeMmwgB7P3pT7/Uj7sGf5FzHISVD6t4= +github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0= +github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms= +github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU= +github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= +github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws= +github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= +github.com/urfave/cli v1.18.0 h1:m9MfmZWX7bwr9kUcs/Asr95j0IVXzGNNc+/5ku2m26Q= +github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/urfave/cli/v2 v2.0.0 h1:+HU9SCbu8GnEUFtIBfuUNXN39ofWViIEJIp6SURMpCg= +github.com/urfave/cli/v2 v2.0.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191111213947-16651526fdb4 h1:AGVXd+IAyeAb3FuQvYDYQ9+WR2JHm0+C0oYJaU1C4rs= +golang.org/x/crypto v0.0.0-20191111213947-16651526fdb4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= +gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/input.go b/input.go new file mode 100644 index 0000000..ce9905d --- /dev/null +++ b/input.go @@ -0,0 +1,221 @@ +package chantools + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/lightningnetwork/lnd/channeldb" + "io/ioutil" + "os" + "strconv" + "strings" +) + +type InputFile interface { + AsSummaryEntries() ([]*SummaryEntry, error) +} + +type Input interface { + AsSummaryEntry() *SummaryEntry +} + +func ParseInput(cfg *config) ([]*SummaryEntry, error) { + var ( + content []byte + err error + target InputFile + ) + + switch { + case cfg.ListChannels != "": + content, err = readInput(cfg.ListChannels) + target = &listChannelsFile{} + + case cfg.PendingChannels != "": + content, err = readInput(cfg.PendingChannels) + target = &pendingChannelsFile{} + + case cfg.FromSummary != "": + content, err = readInput(cfg.FromSummary) + target = &SummaryEntryFile{} + + case cfg.FromChannelDB != "": + db, err := channeldb.Open(cfg.FromChannelDB) + if err != nil { + return nil, fmt.Errorf("error opening channel DB: %v", + err) + } + target = &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) +} + +type listChannelsFile struct { + Channels []*listChannelsChannel `json:"channels"` +} + +func (f *listChannelsFile) AsSummaryEntries() ([]*SummaryEntry, error) { + result := make([]*SummaryEntry, len(f.Channels)) + for idx, entry := range f.Channels { + result[idx] = entry.AsSummaryEntry() + } + return result, nil +} + +type listChannelsChannel struct { + RemotePubkey string `json:"remote_pubkey"` + ChannelPoint string `json:"channel_point"` + CapacityStr string `json:"capacity"` + Initiator bool `json:"initiator"` + LocalBalanceStr string `json:"local_balance"` + RemoteBalanceStr string `json:"remote_balance"` +} + +func (c *listChannelsChannel) AsSummaryEntry() *SummaryEntry { + return &SummaryEntry{ + RemotePubkey: c.RemotePubkey, + ChannelPoint: c.ChannelPoint, + FundingTXID: fundingTXID(c.ChannelPoint), + FundingTXIndex: fundingTXIndex(c.ChannelPoint), + Capacity: uint64(parseInt(c.CapacityStr)), + Initiator: c.Initiator, + LocalBalance: uint64(parseInt(c.LocalBalanceStr)), + RemoteBalance: uint64(parseInt(c.RemoteBalanceStr)), + } +} + +type pendingChannelsFile struct { + PendingOpen []*pendingChannelsChannel `json:"pending_open_channels"` + PendingClosing []*pendingChannelsChannel `json:"pending_closing_channels"` + PendingForceClosing []*pendingChannelsChannel `json:"pending_force_closing_channels"` + WaitingClose []*pendingChannelsChannel `json:"waiting_close_channels"` +} + +func (f *pendingChannelsFile) AsSummaryEntries() ([]*SummaryEntry, error) { + numChannels := len(f.PendingOpen) + len(f.PendingClosing) + + len(f.PendingForceClosing) + len(f.WaitingClose) + result := make([]*SummaryEntry, numChannels) + idx := 0 + for _, entry := range f.PendingOpen { + result[idx] = entry.AsSummaryEntry() + idx++ + } + for _, entry := range f.PendingClosing { + result[idx] = entry.AsSummaryEntry() + idx++ + } + for _, entry := range f.PendingForceClosing { + result[idx] = entry.AsSummaryEntry() + idx++ + } + for _, entry := range f.WaitingClose { + result[idx] = entry.AsSummaryEntry() + idx++ + } + return result, nil +} + +type pendingChannelsChannel struct { + Channel struct { + RemotePubkey string `json:"remote_node_pub"` + ChannelPoint string `json:"channel_point"` + CapacityStr string `json:"capacity"` + LocalBalanceStr string `json:"local_balance"` + RemoteBalanceStr string `json:"remote_balance"` + } `json:"channel"` +} + +func (c *pendingChannelsChannel) AsSummaryEntry() *SummaryEntry { + return &SummaryEntry{ + RemotePubkey: c.Channel.RemotePubkey, + ChannelPoint: c.Channel.ChannelPoint, + FundingTXID: fundingTXID(c.Channel.ChannelPoint), + FundingTXIndex: fundingTXIndex(c.Channel.ChannelPoint), + Capacity: uint64(parseInt(c.Channel.CapacityStr)), + Initiator: false, + LocalBalance: uint64(parseInt(c.Channel.LocalBalanceStr)), + RemoteBalance: uint64(parseInt(c.Channel.RemoteBalanceStr)), + } +} + +type channelDBFile struct { + db *channeldb.DB +} + +func (c *channelDBFile) AsSummaryEntries() ([]*SummaryEntry, error) { + channels, err := c.db.FetchAllChannels() + if err != nil { + return nil, fmt.Errorf("error fetching channels: %v", err) + } + result := make([]*SummaryEntry, len(channels)) + for idx, channel := range channels { + result[idx] = &SummaryEntry{ + RemotePubkey: hex.EncodeToString( + channel.IdentityPub.SerializeCompressed(), + ), + ChannelPoint: channel.FundingOutpoint.String(), + FundingTXID: channel.FundingOutpoint.Hash.String(), + FundingTXIndex: channel.FundingOutpoint.Index, + Capacity: uint64(channel.Capacity), + Initiator: channel.IsInitiator, + LocalBalance: uint64( + channel.LocalCommitment.LocalBalance.ToSatoshis(), + ), + RemoteBalance: uint64( + channel.LocalCommitment.RemoteBalance.ToSatoshis(), + ), + } + } + return result, nil +} + +func (f *SummaryEntryFile) AsSummaryEntries() ([]*SummaryEntry, error) { + return f.Channels, nil +} + +func fundingTXID(chanPoint string) string { + parts := strings.Split(chanPoint, ":") + if len(parts) != 2 { + panic(fmt.Errorf("channel point not in format :: %s", + chanPoint)) + } + return parts[0] +} + +func fundingTXIndex(chanPoint string) uint32 { + parts := strings.Split(chanPoint, ":") + if len(parts) != 2 { + panic(fmt.Errorf("channel point not in format :", + chanPoint)) + } + return uint32(parseInt(parts[1])) +} + +func parseInt(str string) int { + index, err := strconv.Atoi(str) + if err != nil { + panic(fmt.Errorf("error parsing '%s' as int: %v", str, err)) + } + return index +} diff --git a/main.go b/main.go index c541c28..3aedad7 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,12 @@ -package chansummary +package chantools import ( - "bytes" - "encoding/json" "fmt" + + "github.com/btcsuite/btcutil/hdkeychain" "github.com/jessevdk/go-flags" - "io/ioutil" + "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/channeldb" ) const ( @@ -13,42 +14,90 @@ const ( ) type config struct { - ApiUrl string `long:"apiurl" description:"API URL to use (must be esplora compatible)"` + ApiUrl string `long:"apiurl" description:"API URL to use (must be esplora compatible)."` + RootKey string `long:"rootkey" description:"BIP32 HD root key to use."` + 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. Specify '-' to read from stdin."` + RescueDB string `long:"rescuedb" description:"The lnd channel.db file to use for rescuing remote force-closed channels."` } -type fileContent struct { - Channels []*channel `json:"channels"` -} +var ( + logWriter = build.NewRotatingLogWriter() + log = build.NewSubLogger("CHAN", logWriter.GenSubLogger) + cfg = &config{ + ApiUrl: defaultApiUrl, + } +) func Main() error { - var ( - err error - args []string - ) - + setupLogging() + // Parse command line. - config := &config{ - ApiUrl: defaultApiUrl, - } - if args, err = flags.Parse(config); err != nil { + 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{}, + ) + + _, err := parser.Parse() + return err +} + +type summaryCommand struct{} + +func (c *summaryCommand) Execute(args []string) error { + // Parse channel entries from any of the possible input files. + entries, err := ParseInput(cfg) + if err != nil { return err } - if len(args) != 1 { - return fmt.Errorf("exactly one file argument needed") + return collectChanSummary(cfg, entries) +} + +type rescueClosedCommand struct{} + +func (c *rescueClosedCommand) Execute(args []string) error { + // Check that root key is valid. + if cfg.RootKey == "" { + return fmt.Errorf("root key is required") } - file := args[0] - - // Read file and parse into channel. - content, err := ioutil.ReadFile(file) + _, err := hdkeychain.NewKeyFromString(cfg.RootKey) if err != nil { - return err + return fmt.Errorf("error parsing root key: %v", err) + } + + // Check that we have a rescue DB. + if cfg.RescueDB == "" { + return fmt.Errorf("rescue DB is required") } - decoder := json.NewDecoder(bytes.NewReader(content)) - channels := fileContent{} - err = decoder.Decode(&channels) + db, err := channeldb.Open(cfg.RescueDB) + 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 bruteForceChannels(cfg, entries, db) +} - return collectChanSummary(config, channels.Channels) +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) + } }