2020-06-14 09:04:59 +00:00
|
|
|
package btc
|
|
|
|
|
|
|
|
import (
|
2022-07-18 07:50:27 +00:00
|
|
|
"errors"
|
|
|
|
|
2020-06-14 09:04:59 +00:00
|
|
|
"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)
|
2022-07-18 07:50:27 +00:00
|
|
|
if errors.Is(err, ErrTxNotFound) {
|
2020-06-14 09:04:59 +00:00
|
|
|
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++
|
|
|
|
|
2021-05-01 10:54:48 +00:00
|
|
|
for _, o := range utxo {
|
|
|
|
if o.ScriptPubkeyType == "v0_p2wpkh" {
|
|
|
|
entry.ClosingTX.ToRemoteAddr = o.ScriptPubkeyAddr
|
|
|
|
}
|
|
|
|
}
|
2021-05-01 20:10:09 +00:00
|
|
|
|
2020-06-14 09:04:59 +00:00
|
|
|
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
|
|
|
|
}
|