diff --git a/btc/summary.go b/btc/summary.go new file mode 100644 index 0000000..9a79bc4 --- /dev/null +++ b/btc/summary.go @@ -0,0 +1,154 @@ +package btc + +import ( + "github.com/btcsuite/btclog" + "github.com/guggero/chantools/dataformat" +) + +func SummarizeChannels(apiURL string, channels []*dataformat.SummaryEntry, + log btclog.Logger) (*dataformat.SummaryEntryFile, error) { + + summaryFile := &dataformat.SummaryEntryFile{ + Channels: channels, + } + api := &ExplorerAPI{BaseURL: apiURL} + + for idx, channel := range channels { + tx, err := api.Transaction(channel.FundingTXID) + if err == ErrTxNotFound { + log.Errorf("Funding TX %s not found. Ignoring.", + channel.FundingTXID) + channel.ChanExists = false + continue + } + if err != nil { + log.Errorf("Problem with channel %d (%s): %v.", + idx, channel.FundingTXID, err) + return nil, err + } + channel.ChanExists = true + outspend := tx.Vout[channel.FundingTXIndex].Outspend + if outspend.Spent { + summaryFile.ClosedChannels++ + channel.ClosingTX = &dataformat.ClosingTX{ + TXID: outspend.Txid, + ConfHeight: uint32(outspend.Status.BlockHeight), + } + + err := reportOutspend( + api, summaryFile, channel, outspend, log, + ) + if err != nil { + log.Errorf("Problem with channel %d (%s): %v.", + idx, channel.FundingTXID, err) + return nil, err + } + } else { + summaryFile.OpenChannels++ + summaryFile.FundsOpenChannels += channel.LocalBalance + channel.ClosingTX = nil + channel.HasPotential = true + } + + if idx%50 == 0 { + log.Infof("Queried channel %d of %d.", idx, + len(channels)) + } + } + + return summaryFile, nil +} + +func reportOutspend(api *ExplorerAPI, + summaryFile *dataformat.SummaryEntryFile, + entry *dataformat.SummaryEntry, os *Outspend, log btclog.Logger) error { + + spendTx, err := api.Transaction(os.Txid) + if err != nil { + return err + } + + summaryFile.FundsClosedChannels += entry.LocalBalance + var utxo []*Vout + for _, vout := range spendTx.Vout { + if !vout.Outspend.Spent { + utxo = append(utxo, vout) + } + } + + if isCoopClose(spendTx) { + summaryFile.CoopClosedChannels++ + summaryFile.FundsCoopClose += entry.LocalBalance + entry.ClosingTX.ForceClose = false + entry.ClosingTX.AllOutsSpent = len(utxo) == 0 + entry.HasPotential = entry.LocalBalance > 0 && len(utxo) != 0 + return nil + } + + summaryFile.ForceClosedChannels++ + entry.ClosingTX.ForceClose = true + entry.HasPotential = false + + if len(utxo) > 0 { + log.Debugf("Channel %s spent by %s:%d which has %d outputs of "+ + "which %d are unspent.", entry.ChannelPoint, os.Txid, + os.Vin, len(spendTx.Vout), len(utxo)) + + entry.ClosingTX.AllOutsSpent = false + summaryFile.ChannelsWithUnspent++ + + if couldBeOurs(entry, utxo) { + summaryFile.ChannelsWithPotential++ + summaryFile.FundsForceClose += utxo[0].Value + entry.HasPotential = true + + // Could maybe be brute forced. + if len(utxo) == 1 && + utxo[0].ScriptPubkeyType == "v0_p2wpkh" && + !utxo[0].Outspend.Spent { + + entry.ClosingTX.OurAddr = utxo[0].ScriptPubkeyAddr + } + } else { + // It's theirs, ignore. + if entry.LocalBalance == 0 || + (len(utxo) == 1 && + utxo[0].Value == entry.RemoteBalance) { + + return nil + } + + // We don't know what this output is, logging for debug. + for idx, vout := range spendTx.Vout { + if !vout.Outspend.Spent { + log.Debugf("UTXO %d of type %s with "+ + "value %d", idx, + vout.ScriptPubkeyType, + vout.Value) + } + } + 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 + entry.HasPotential = false + summaryFile.FundsClosedSpent += entry.LocalBalance + summaryFile.FullySpentChannels++ + } + + return nil +} + +func couldBeOurs(entry *dataformat.SummaryEntry, utxo []*Vout) bool { + if len(utxo) == 1 && utxo[0].Value == entry.RemoteBalance { + return false + } + + return entry.LocalBalance != 0 +} + +func isCoopClose(tx *TX) bool { + return tx.Vin[0].Sequence == 0xffffffff +} diff --git a/cmd/chantools/summary.go b/cmd/chantools/summary.go index 2adf0ee..e9da76e 100644 --- a/cmd/chantools/summary.go +++ b/cmd/chantools/summary.go @@ -26,52 +26,9 @@ func (c *summaryCommand) Execute(_ []string) error { func summarizeChannels(apiURL string, channels []*dataformat.SummaryEntry) error { - summaryFile := &dataformat.SummaryEntryFile{ - Channels: channels, - } - api := &btc.ExplorerAPI{BaseURL: apiURL} - - for idx, channel := range channels { - tx, err := api.Transaction(channel.FundingTXID) - if err == btc.ErrTxNotFound { - log.Errorf("Funding TX %s not found. Ignoring.", - channel.FundingTXID) - channel.ChanExists = false - continue - } - if err != nil { - log.Errorf("Problem with channel %d (%s): %v.", - idx, channel.FundingTXID, err) - return err - } - channel.ChanExists = true - outspend := tx.Vout[channel.FundingTXIndex].Outspend - if outspend.Spent { - summaryFile.ClosedChannels++ - channel.ClosingTX = &dataformat.ClosingTX{ - TXID: outspend.Txid, - ConfHeight: uint32(outspend.Status.BlockHeight), - } - - err := reportOutspend( - api, summaryFile, channel, outspend, - ) - if err != nil { - log.Errorf("Problem with channel %d (%s): %v.", - idx, channel.FundingTXID, err) - return err - } - } else { - summaryFile.OpenChannels++ - summaryFile.FundsOpenChannels += channel.LocalBalance - channel.ClosingTX = nil - channel.HasPotential = true - } - - if idx%50 == 0 { - log.Infof("Queried channel %d of %d.", idx, - len(channels)) - } + summaryFile, err := btc.SummarizeChannels(apiURL, channels, log) + if err != nil { + return fmt.Errorf("error running summary: %v", err) } log.Info("Finished scanning.") @@ -105,97 +62,3 @@ func summarizeChannels(apiURL string, log.Infof("Writing result to %s", fileName) return ioutil.WriteFile(fileName, summaryBytes, 0644) } - -func reportOutspend(api *btc.ExplorerAPI, - summaryFile *dataformat.SummaryEntryFile, - entry *dataformat.SummaryEntry, os *btc.Outspend) error { - - spendTx, err := api.Transaction(os.Txid) - if err != nil { - return err - } - - summaryFile.FundsClosedChannels += entry.LocalBalance - var utxo []*btc.Vout - for _, vout := range spendTx.Vout { - if !vout.Outspend.Spent { - utxo = append(utxo, vout) - } - } - - if isCoopClose(spendTx) { - summaryFile.CoopClosedChannels++ - summaryFile.FundsCoopClose += entry.LocalBalance - entry.ClosingTX.ForceClose = false - entry.ClosingTX.AllOutsSpent = len(utxo) == 0 - entry.HasPotential = entry.LocalBalance > 0 && len(utxo) != 0 - return nil - } - - summaryFile.ForceClosedChannels++ - entry.ClosingTX.ForceClose = true - entry.HasPotential = false - - if len(utxo) > 0 { - log.Debugf("Channel %s spent by %s:%d which has %d outputs of "+ - "which %d are unspent.", entry.ChannelPoint, os.Txid, - os.Vin, len(spendTx.Vout), len(utxo)) - - entry.ClosingTX.AllOutsSpent = false - summaryFile.ChannelsWithUnspent++ - - if couldBeOurs(entry, utxo) { - summaryFile.ChannelsWithPotential++ - summaryFile.FundsForceClose += utxo[0].Value - entry.HasPotential = true - - // Could maybe be brute forced. - if len(utxo) == 1 && - utxo[0].ScriptPubkeyType == "v0_p2wpkh" && - !utxo[0].Outspend.Spent { - - entry.ClosingTX.OurAddr = utxo[0].ScriptPubkeyAddr - } - } else { - // It's theirs, ignore. - if entry.LocalBalance == 0 || - (len(utxo) == 1 && - utxo[0].Value == entry.RemoteBalance) { - - return nil - } - - // We don't know what this output is, logging for debug. - for idx, vout := range spendTx.Vout { - if !vout.Outspend.Spent { - log.Debugf("UTXO %d of type %s with "+ - "value %d", idx, - vout.ScriptPubkeyType, - vout.Value) - } - } - 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 - entry.HasPotential = false - summaryFile.FundsClosedSpent += entry.LocalBalance - summaryFile.FullySpentChannels++ - } - - return nil -} - -func couldBeOurs(entry *dataformat.SummaryEntry, utxo []*btc.Vout) bool { - if len(utxo) == 1 && utxo[0].Value == entry.RemoteBalance { - return false - } - - return entry.LocalBalance != 0 -} - -func isCoopClose(tx *btc.TX) bool { - return tx.Vin[0].Sequence == 0xffffffff -}