regolancer/main.go

261 lines
10 KiB
Go
Raw Normal View History

2022-08-20 14:11:45 +00:00
package main
import (
"context"
2022-08-20 17:43:51 +00:00
"encoding/json"
2022-08-20 14:11:45 +00:00
"log"
"math/rand"
"os"
2022-09-28 15:08:32 +00:00
"strings"
2022-08-20 14:11:45 +00:00
"time"
2022-09-28 15:08:32 +00:00
"github.com/BurntSushi/toml"
2022-08-20 14:11:45 +00:00
"github.com/jessevdk/go-flags"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
)
2022-08-20 17:43:51 +00:00
var mainParams struct {
Config string `short:"f" long:"config" description:"config file path"`
}
2022-08-20 14:11:45 +00:00
var params struct {
2022-08-21 14:16:00 +00:00
Config string `short:"f" long:"config" description:"config file path"`
2022-08-21 08:26:50 +00:00
Connect string `short:"c" long:"connect" description:"connect to lnd using host:port" json:"connect"`
TLSCert string `short:"t" long:"tlscert" description:"path to tls.cert to connect" required:"false" json:"tlscert"`
2022-09-28 15:08:32 +00:00
MacaroonDir string `long:"macaroon-dir" description:"path to the macaroon directory" required:"false" json:"macaroon_dir" toml:"macaroon_dir"`
MacaroonFilename string `long:"macaroon-filename" description:"macaroon filename" json:"macaroon_filename" toml:"macaroon_filename"`
2022-08-21 08:26:50 +00:00
Network string `short:"n" long:"network" description:"bitcoin network to use" json:"network"`
2022-09-28 15:08:32 +00:00
FromPerc int64 `long:"pfrom" description:"channels with less than this inbound liquidity percentage will be considered as source channels" json:"pfrom" toml:"pfrom"`
ToPerc int64 `long:"pto" description:"channels with less than this outbound liquidity percentage will be considered as target channels" json:"pto" toml:"pto"`
2022-08-21 08:26:50 +00:00
Perc int64 `short:"p" long:"perc" description:"use this value as both pfrom and pto from above" json:"perc"`
Amount int64 `short:"a" long:"amount" description:"amount to rebalance" json:"amount"`
2022-09-28 15:08:32 +00:00
EconRatio float64 `short:"r" long:"econ-ratio" description:"economical ratio for fee limit calculation as a multiple of target channel fee (for example, 0.5 means you want to pay at max half the fee you might earn for routing out of the target channel)" json:"econ_ratio" toml:"econ_ratio"`
2022-10-01 15:06:44 +00:00
FeeLimitPPM int64 `short:"F" long:"fee-limit-ppm" description:"don't consider the target channel fee and use this max fee ppm instead (can rebalance at a loss, be careful)" json:"fee_limit_ppm" toml:"fee_limit_ppm"`
2022-10-01 13:02:19 +00:00
LostProfit bool `short:"l" long:"lost-profit" description:"also consider the outbound channel fees when looking for profitable routes so that outbound_fee+inbound_fee < route_fee" json:"lost_profit" toml:"lost_profit"`
2022-09-28 15:08:32 +00:00
ProbeSteps int `short:"b" long:"probe-steps" description:"if the payment fails at the last hop try to probe lower amount using this many steps" json:"probe_steps" toml:"probe_steps"`
MinAmount int64 `long:"min-amount" description:"if probing is enabled this will be the minimum amount to try" json:"min_amount" toml:"min_amount"`
ExcludeChannelsIn []uint64 `short:"i" long:"exclude-channel-in" description:"don't use this channel as incoming (can be specified multiple times)" json:"exclude_channels_in" toml:"exclude_channels_in"`
ExcludeChannelsOut []uint64 `short:"o" long:"exclude-channel-out" description:"don't use this channel as outgoing (can be specified multiple times)" json:"exclude_channels_out" toml:"exclude_channels_out"`
ExcludeChannels []uint64 `short:"e" long:"exclude-channel" description:"don't use this channel at all (can be specified multiple times)" json:"exclude_channels" toml:"exclude_channels"`
ExcludeNodes []string `short:"d" long:"exclude-node" description:"don't use this node for routing (can be specified multiple times)" json:"exclude_nodes" toml:"exclude_nodes"`
ToChannel uint64 `long:"to" description:"try only this channel as target (should satisfy other constraints too)" json:"to" toml:"to"`
FromChannel uint64 `long:"from" description:"try only this channel as source (should satisfy other constraints too)" json:"from" toml:"from"`
2022-09-28 15:08:32 +00:00
StatFilename string `short:"s" long:"stat" description:"save successful rebalance information to the specified CSV file" json:"stat" toml:"stat"`
2022-08-20 14:11:45 +00:00
}
2022-08-27 14:40:27 +00:00
type failedRoute struct {
channelPair [2]*lnrpc.Channel
expiration *time.Time
}
2022-08-20 14:11:45 +00:00
type regolancer struct {
2022-08-21 10:20:52 +00:00
lnClient lnrpc.LightningClient
routerClient routerrpc.RouterClient
myPK string
channels []*lnrpc.Channel
fromChannels []*lnrpc.Channel
fromChannelId uint64
toChannels []*lnrpc.Channel
toChannelId uint64
2022-08-27 14:40:27 +00:00
channelPairs map[string][2]*lnrpc.Channel
2022-08-21 10:20:52 +00:00
nodeCache map[string]*lnrpc.NodeInfo
chanCache map[uint64]*lnrpc.ChannelEdge
2022-08-27 14:40:27 +00:00
failureCache map[string]failedRoute
2022-08-21 10:20:52 +00:00
excludeIn map[uint64]struct{}
excludeOut map[uint64]struct{}
excludeBoth map[uint64]struct{}
excludeNodes [][]byte
statFilename string
2022-09-03 00:50:00 +00:00
routeFound bool
2022-08-20 14:11:45 +00:00
}
2022-08-20 17:43:51 +00:00
func loadConfig() {
2022-08-21 14:16:00 +00:00
flags.NewParser(&mainParams, flags.PrintErrors|flags.IgnoreUnknown).Parse()
2022-09-28 15:08:32 +00:00
2022-08-20 17:43:51 +00:00
if mainParams.Config == "" {
return
}
2022-09-28 15:08:32 +00:00
if strings.Contains(mainParams.Config, ".toml") {
_, err := toml.DecodeFile(mainParams.Config, &params)
if err != nil {
log.Fatalf("Error opening config file %s: %s", mainParams.Config, err)
}
2022-08-20 17:43:51 +00:00
} else {
2022-09-28 15:08:32 +00:00
f, err := os.Open(mainParams.Config)
2022-08-20 17:43:51 +00:00
if err != nil {
2022-09-28 15:08:32 +00:00
log.Fatalf("Error opening config file %s: %s", mainParams.Config, err)
} else {
defer f.Close()
err = json.NewDecoder(f).Decode(&params)
if err != nil {
log.Fatalf("Error reading config file %s: %s", mainParams.Config, err)
}
2022-08-20 17:43:51 +00:00
}
}
}
2022-08-22 08:44:28 +00:00
func tryRebalance(ctx context.Context, r *regolancer, invoice **lnrpc.AddInvoiceResponse,
attempt *int) (err error, repeat bool) {
from, to, amt, err := r.pickChannelPair(params.Amount, params.MinAmount)
2022-08-22 08:44:28 +00:00
if err != nil {
log.Printf(errColor("Error during picking channel: %s"), err)
return err, false
}
2022-08-23 12:15:25 +00:00
routeCtx, routeCtxCancel := context.WithTimeout(ctx, time.Second*30)
defer routeCtxCancel()
2022-10-01 13:02:19 +00:00
routes, fee, err := r.getRoutes(routeCtx, from, to, amt*1000)
if err != nil {
2022-08-22 08:44:28 +00:00
if routeCtx.Err() == context.DeadlineExceeded {
log.Print(errColor("Timed out looking for a route"))
return err, false
}
r.addFailedRoute(from, to)
return err, true
}
2022-08-22 08:44:28 +00:00
routeCtxCancel()
if params.Amount == 0 || *invoice == nil {
2022-08-22 08:44:28 +00:00
*invoice, err = r.createInvoice(ctx, from, to, amt)
if err != nil {
2022-08-21 09:51:05 +00:00
log.Printf("Error creating invoice: %s", err)
2022-08-22 08:44:28 +00:00
return err, true
}
}
for _, route := range routes {
log.Printf("Attempt %s, amount: %s (max fee: %s)", hiWhiteColorF("#%d", *attempt),
hiWhiteColor(amt), hiWhiteColor(fee/1000))
2022-08-22 08:44:28 +00:00
r.printRoute(ctx, route)
2022-08-22 20:01:46 +00:00
err = r.pay(ctx, *invoice, amt, params.MinAmount, route, params.ProbeSteps)
if err == nil {
2022-08-22 08:44:28 +00:00
return nil, false
}
if retryErr, ok := err.(ErrRetry); ok {
amt = retryErr.amount
log.Printf("Trying to rebalance again with %s", hiWhiteColor(amt))
2022-08-22 08:44:28 +00:00
probedInvoice, err := r.createInvoice(ctx, from, to, amt)
if err != nil {
2022-08-21 09:51:05 +00:00
log.Printf("Error creating invoice: %s", err)
2022-08-22 08:44:28 +00:00
return err, true
}
2022-08-22 08:44:28 +00:00
probedRoute, err := r.rebuildRoute(ctx, route, amt)
if err != nil {
log.Printf("Error rebuilding the route for probed payment: %s", errColor(err))
} else {
2022-08-22 20:01:46 +00:00
err = r.pay(ctx, probedInvoice, amt, 0, probedRoute, 0)
if err == nil {
2022-08-22 08:44:28 +00:00
return nil, false
} else {
log.Printf("Probed rebalance failed with error: %s", errColor(err))
}
}
}
*attempt++
}
2022-08-22 08:44:28 +00:00
return nil, true
}
2022-08-20 14:11:45 +00:00
func main() {
2022-08-20 17:43:51 +00:00
rand.Seed(time.Now().UnixNano())
loadConfig()
_, err := flags.NewParser(&params, flags.Default|flags.IgnoreUnknown).Parse()
2022-08-20 14:11:45 +00:00
if err != nil {
os.Exit(1)
}
2022-08-20 17:43:51 +00:00
if params.Connect == "" {
params.Connect = "127.0.0.1:10009"
}
if params.MacaroonFilename == "" {
params.MacaroonFilename = "admin.macaroon"
}
2022-08-20 18:12:19 +00:00
if params.Network == "" {
params.Network = "mainnet"
}
2022-08-20 17:43:51 +00:00
if params.FromPerc == 0 {
params.FromPerc = 50
}
if params.ToPerc == 0 {
params.ToPerc = 50
}
2022-10-01 15:06:44 +00:00
if params.EconRatio == 0 && params.FeeLimitPPM == 0 {
2022-08-20 17:43:51 +00:00
params.EconRatio = 1
}
2022-08-20 14:11:45 +00:00
if params.Perc > 0 {
params.FromPerc = params.Perc
params.ToPerc = params.Perc
}
2022-08-22 20:01:46 +00:00
if params.MinAmount > 0 && params.MinAmount > params.Amount {
log.Fatal("Minimum amount should be more than amount")
}
2022-08-20 14:11:45 +00:00
conn, err := lndclient.NewBasicConn(params.Connect, params.TLSCert, params.MacaroonDir, params.Network,
lndclient.MacFilename(params.MacaroonFilename))
if err != nil {
log.Fatal(err)
}
r := regolancer{
nodeCache: map[string]*lnrpc.NodeInfo{},
chanCache: map[uint64]*lnrpc.ChannelEdge{},
2022-08-27 14:40:27 +00:00
channelPairs: map[string][2]*lnrpc.Channel{},
failureCache: map[string]failedRoute{},
statFilename: params.StatFilename,
}
2022-08-20 14:11:45 +00:00
r.lnClient = lnrpc.NewLightningClient(conn)
r.routerClient = routerrpc.NewRouterClient(conn)
2022-08-22 10:59:38 +00:00
mainCtx, mainCtxCancel := context.WithTimeout(context.Background(), time.Hour*6)
defer mainCtxCancel()
infoCtx, infoCtxCancel := context.WithTimeout(mainCtx, time.Second*30)
defer infoCtxCancel()
info, err := r.lnClient.GetInfo(infoCtx, &lnrpc.GetInfoRequest{})
2022-08-20 14:11:45 +00:00
if err != nil {
log.Fatal(err)
}
r.myPK = info.IdentityPubkey
err = r.getChannels(infoCtx)
2022-08-20 14:11:45 +00:00
if err != nil {
log.Fatal("Error listing own channels: ", err)
}
2022-08-21 10:20:52 +00:00
if params.FromChannel > 0 {
r.fromChannelId = params.FromChannel
}
if params.ToChannel > 0 {
r.toChannelId = params.ToChannel
}
2022-08-21 08:26:50 +00:00
r.excludeIn = makeChanSet(params.ExcludeChannelsIn)
r.excludeOut = makeChanSet(params.ExcludeChannelsOut)
r.excludeBoth = makeChanSet(params.ExcludeChannels)
err = r.makeNodeList(params.ExcludeNodes)
if err != nil {
log.Fatal("Error parsing excluded node list: ", err)
}
err = r.getChannelCandidates(params.FromPerc, params.ToPerc, params.Amount)
2022-08-20 14:11:45 +00:00
if err != nil {
log.Fatal("Error choosing channels: ", err)
}
if len(r.fromChannels) == 0 {
log.Fatal("No source channels selected")
}
if len(r.toChannels) == 0 {
log.Fatal("No target channels selected")
}
2022-08-22 10:59:38 +00:00
infoCtxCancel()
2022-08-20 14:11:45 +00:00
var invoice *lnrpc.AddInvoiceResponse
attempt := 1
for {
2022-08-22 08:44:28 +00:00
attemptCtx, attemptCancel := context.WithTimeout(mainCtx, time.Minute*5)
_, retry := tryRebalance(attemptCtx, &r, &invoice, &attempt)
attemptCancel()
2022-08-22 10:59:38 +00:00
if attemptCtx.Err() == context.DeadlineExceeded {
log.Print(errColor("Attempt timed out"))
invoice = nil // create a new invoice next time
2022-08-22 10:59:38 +00:00
}
if mainCtx.Err() == context.DeadlineExceeded {
log.Println(errColor("Rebalancing timed out"))
return
}
2022-08-22 08:44:28 +00:00
if !retry {
return
2022-08-20 14:11:45 +00:00
}
}
}