Browse Source

Initial commit

pull/3/head
Oliver Gugger 2 years ago
commit
2774bcee41
No known key found for this signature in database GPG Key ID: 8E4256593F177720
11 changed files with 421 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +24
    -0
      .golangci.yml
  3. +21
    -0
      LICENSE
  4. +49
    -0
      Makefile
  5. +7
    -0
      README.md
  6. +80
    -0
      chainapi.go
  7. +162
    -0
      chansummary.go
  8. +16
    -0
      cmd/chansummary/main.go
  9. +5
    -0
      go.mod
  10. +2
    -0
      go.sum
  11. +54
    -0
      main.go

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
/chansummary

+ 24
- 0
.golangci.yml View File

@ -0,0 +1,24 @@
run:
# timeout for analysis
deadline: 4m
# Linting uses a lot of memory. Keep it under control by only running a single
# worker.
concurrency: 1
linters-settings:
govet:
# Don't report about shadowed variables
check-shadowing: false
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
linters:
enable-all: true
disable:
# Init functions are used by loggers throughout the codebase.
- gochecknoinits
# Global variables are used by loggers.
- gochecknoglobals

+ 21
- 0
LICENSE View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Oliver Gugger
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 49
- 0
Makefile View File

@ -0,0 +1,49 @@
PKG := github.com/guggero/chansummary
GOTEST := GO111MODULE=on go test -v
GO_BIN := ${GOPATH}/bin
GOFILES_NOVENDOR = $(shell find . -type f -name '*.go' -not -path "./vendor/*")
GOLIST := go list $(PKG)/... | grep -v '/vendor/'
LINT_BIN := $(GO_BIN)/golangci-lint
LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint
LINT_COMMIT := v1.18.0
LINT = $(LINT_BIN) run -v
DEPGET := cd /tmp && GO111MODULE=on go get -v
GOBUILD := GO111MODULE=on go build -v
GOINSTALL := GO111MODULE=on go install -v
GOTEST := GO111MODULE=on go test -v
XARGS := xargs -L 1
TEST_FLAGS = -test.timeout=20m
UNIT := $(GOLIST) | $(XARGS) env $(GOTEST) $(TEST_FLAGS)
default: build
$(LINT_BIN):
@$(call print, "Fetching linter")
$(DEPGET) $(LINT_PKG)@$(LINT_COMMIT)
unit:
@$(call print, "Running unit tests.")
$(UNIT)
build:
@$(call print, "Building chansummary.")
$(GOBUILD) $(PKG)/cmd/chansummary
install:
@$(call print, "Installing chansummary.")
$(GOINSTALL) $(PKG)/cmd/chansummary
fmt:
@$(call print, "Formatting source.")
gofmt -l -w -s $(GOFILES_NOVENDOR)
lint: $(LINT_BIN)
@$(call print, "Linting source.")
$(LINT)

+ 7
- 0
README.md View File

@ -0,0 +1,7 @@
# Channel summary
This tool works with the output of lnd's `listchannels` command and creates
a summary of the on-chain state of these channels.
**WARNING**: This tool will query public block explorer APIs, your privacy
might not be preserved. Use at your own risk.

+ 80
- 0
chainapi.go View File

@ -0,0 +1,80 @@
package chansummary
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type chainApi struct {
baseUrl string
}
type transaction struct {
Vin []*vin `json:"vin"`
Vout []*vout `json:"vout"`
}
type vin struct {
Tixid string `json:"txid"`
Vout int `json:"vout"`
Prevout *vout `json:"prevout"`
}
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"`
outspend *outspend
}
type outspend struct {
Spent bool `json:"spent"`
Txid string `json:"txid"`
Vin int `json:"vin"`
Status *status `json:"status"`
}
type status struct {
Confirmed bool `json:"confirmed"`
BlockHeight int `json:"block_height"`
BlockHash string `json:"block_hash"`
}
func (a *chainApi) Transaction(txid string) (*transaction, error) {
tx := &transaction{}
err := Fetch(fmt.Sprintf("%s/tx/%s", a.baseUrl, txid), tx)
if err != nil {
return nil, err
}
for idx, vout := range tx.Vout {
url := fmt.Sprintf(
"%s/tx/%s/outspend/%d", a.baseUrl, txid, idx,
)
outspend := outspend{}
err := Fetch(url, &outspend)
if err != nil {
return nil, err
}
vout.outspend = &outspend
}
return tx, nil
}
func Fetch(url string, target interface{}) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
body := new(bytes.Buffer)
_, err = body.ReadFrom(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(body.Bytes(), target)
}

+ 162
- 0
chansummary.go View File

@ -0,0 +1,162 @@
package chansummary
import (
"fmt"
"strconv"
"strings"
)
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 <txid>:<idx>"))
}
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 <txid>:<idx>"))
}
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())
if err != nil {
return err
}
outspend := tx.Vout[channel.FundingTXIndex()].outspend
if outspend.Spent {
chansClosed++
s, f, err := reportOutspend(chainApi, channel, outspend)
if err != nil {
return err
}
valueSalvage += s
valueSafe += f
} else {
chansOpen++
valueUnspent += channel.localBalance()
}
if idx%50 == 0 {
fmt.Printf("Queried channel %d of %d.\n", 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
}
func reportOutspend(api *chainApi, ch *channel, os *outspend) (uint64, uint64,
error) {
spendTx, err := api.Transaction(os.Txid)
if err != nil {
return 0, 0, err
}
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,
os.Vin, len(spendTx.Vout), numSpent)
var utxo []*vout
for _, vout := range spendTx.Vout {
if !vout.outspend.Spent {
utxo = append(utxo, vout)
}
}
if salvageable(ch, utxo) {
salvageBalance += utxo[0].Value
outs := spendTx.Vout
switch {
case len(outs) == 1 &&
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
}
} else {
for idx, vout := range spendTx.Vout {
if !vout.outspend.Spent {
fmt.Printf("UTXO %d of type %s with "+
"value %d\n", 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)
}
}
return salvageBalance, safeBalance, nil
}
func salvageable(ch *channel, utxo []*vout) bool {
return ch.localBalance() == utxo[0].Value ||
ch.remoteBalance() == 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
}

+ 16
- 0
cmd/chansummary/main.go View File

@ -0,0 +1,16 @@
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)
}

+ 5
- 0
go.mod View File

@ -0,0 +1,5 @@
module github.com/guggero/chansummary
require github.com/jessevdk/go-flags v1.4.0
go 1.13

+ 2
- 0
go.sum View File

@ -0,0 +1,2 @@
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=

+ 54
- 0
main.go View File

@ -0,0 +1,54 @@
package chansummary
import (
"bytes"
"encoding/json"
"fmt"
"github.com/jessevdk/go-flags"
"io/ioutil"
)
const (
defaultApiUrl = "https://blockstream.info/api"
)
type config struct {
ApiUrl string `long:"apiurl" description:"API URL to use (must be esplora compatible)"`
}
type fileContent struct {
Channels []*channel `json:"channels"`
}
func Main() error {
var (
err error
args []string
)
// Parse command line.
config := &config{
ApiUrl: defaultApiUrl,
}
if args, err = flags.Parse(config); err != nil {
return err
}
if len(args) != 1 {
return fmt.Errorf("exactly one file argument needed")
}
file := args[0]
// Read file and parse into channel.
content, err := ioutil.ReadFile(file)
if err != nil {
return err
}
decoder := json.NewDecoder(bytes.NewReader(content))
channels := fileContent{}
err = decoder.Decode(&channels)
if err != nil {
return err
}
return collectChanSummary(config, channels.Channels)
}

Loading…
Cancel
Save