From 2040013708c226b63573ae1ec7eb80510ab1d238 Mon Sep 17 00:00:00 2001 From: rkfg Date: Sat, 20 Aug 2022 20:02:25 +0300 Subject: [PATCH] Add route probing --- main.go | 21 ++++++++++++++- payment.go | 25 +++++++++++++++-- routes.go | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 117 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 0a1dc1f..23a47e9 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ var params struct { Perc int64 `short:"p" long:"perc" description:"use this value as both pfrom and pto from above" default:"0"` Amount int64 `short:"a" long:"amount" description:"amount to rebalance" default:"0"` EconRatio float64 `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)" default:"1"` + ProbeSteps int `short:"b" long:"probe" description:"if the payment fails at the last hop try to probe lower amount using binary search" default:"0"` } type regolancer struct { @@ -92,10 +93,28 @@ func main() { log.Printf("Attempt %s, amount: %s (max fee: %s)", hiWhiteColorF("#%d", attempt), hiWhiteColor(amt), hiWhiteColor(fee/1000)) r.printRoute(route) - err = r.pay(invoice, amt, route) + err = r.pay(invoice, amt, route, params.ProbeSteps) if err == nil { return } + if retryErr, ok := err.(ErrRetry); ok { + amt = retryErr.amount + log.Printf("Trying to rebalance again with %s", hiWhiteColor(amt)) + probedInvoice, err := r.createInvoice(from, to, amt) + if err != nil { + log.Fatal("Error creating invoice: ", err) + } + if err != nil { + log.Printf("Error rebuilding the route for probed payment: %s", errColor(err)) + } else { + err = r.pay(probedInvoice, amt, retryErr.route, 0) + if err == nil { + return + } else { + log.Printf("Probed rebalance failed with error: %s", errColor(err)) + } + } + } attempt++ } } diff --git a/payment.go b/payment.go index f5b85f8..2761e46 100644 --- a/payment.go +++ b/payment.go @@ -10,13 +10,24 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/routerrpc" ) +type ErrRetry struct { + amount int64 + route *lnrpc.Route +} + +func (e ErrRetry) Error() string { + return fmt.Sprintf("retry payment with %d sats", e.amount) +} + +var ErrProbeFailed = fmt.Errorf("probe failed") + func (r *regolancer) createInvoice(from, to uint64, amount int64) (*lnrpc.AddInvoiceResponse, error) { return r.lnClient.AddInvoice(context.Background(), &lnrpc.Invoice{Value: amount, Memo: fmt.Sprintf("Rebalance %d ⇒ %d", from, to), Expiry: int64(time.Hour.Seconds() * 24)}) } -func (r *regolancer) pay(invoice *lnrpc.AddInvoiceResponse, amount int64, route *lnrpc.Route) error { +func (r *regolancer) pay(invoice *lnrpc.AddInvoiceResponse, amount int64, route *lnrpc.Route, probeSteps int) error { lastHop := route.Hops[len(route.Hops)-1] lastHop.MppRecord = &lnrpc.MPPRecord{ PaymentAddr: invoice.PaymentAddr, @@ -26,7 +37,6 @@ func (r *regolancer) pay(invoice *lnrpc.AddInvoiceResponse, amount int64, route &routerrpc.SendToRouteRequest{ PaymentHash: invoice.RHash, Route: route, - SkipTempErr: true, }) if err != nil { return err @@ -48,6 +58,17 @@ func (r *regolancer) pay(invoice *lnrpc.AddInvoiceResponse, amount int64, route } fmt.Printf("\n%s %s ⇒ %s\n\n", faintWhiteColor(result.Failure.Code.String()), cyanColor(node1name), cyanColor(node2name)) + if int(result.Failure.FailureSourceIndex) == len(route.Hops)-2 && probeSteps > 0 { + fmt.Println("Probing route...") + maxAmount, goodRoute, err := r.probeRoute(route, 0, amount, amount/2, probeSteps) + if err != nil { + return err + } + if maxAmount == 0 { + return ErrProbeFailed + } + return ErrRetry{amount: maxAmount, route: goodRoute} + } return fmt.Errorf("error: %s @ %d", result.Failure.Code.String(), result.Failure.FailureSourceIndex) } else { log.Printf("Success! Paid %s in fees", hiWhiteColor(result.Route.TotalFeesMsat/1000)) diff --git a/routes.go b/routes.go index 3111806..87dd601 100644 --- a/routes.go +++ b/routes.go @@ -4,9 +4,12 @@ import ( "context" "encoding/hex" "fmt" + "log" + "math/rand" "time" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" ) func calcFeeMsat(amtMsat int64, policy *lnrpc.RoutingPolicy) float64 { @@ -73,16 +76,16 @@ func (r *regolancer) printRoute(route *lnrpc.Route) { return } errs := "" - fmt.Printf("%s %s\n", faintWhiteColor("Total fee:"), hiWhiteColor((route.TotalFeesMsat-route.Hops[0].FeeMsat)/1000)) + fmt.Printf("%s %s\n", faintWhiteColor("Total fee:"), hiWhiteColor(route.TotalFeesMsat/1000)) for i, hop := range route.Hops { nodeInfo, err := r.getNodeInfo(hop.PubKey) if err != nil { errs = errs + err.Error() + "\n" continue } - fee := hiWhiteColorF("%-6d", hop.FeeMsat) - if i == 0 { - fee = hiWhiteColorF("%-6s", "") + fee := hiWhiteColorF("%-6s", "") + if i > 0 { + hiWhiteColorF("%-6d", route.Hops[i-1].FeeMsat) } fmt.Printf("%s %s %s\n", faintWhiteColor(hop.ChanId), fee, cyanColor(nodeInfo.Node.Alias)) } @@ -90,3 +93,70 @@ func (r *regolancer) printRoute(route *lnrpc.Route) { fmt.Println(errColor(errs)) } } + +func (r *regolancer) rebuildRoute(route *lnrpc.Route, amount int64) (*lnrpc.Route, error) { + pks := [][]byte{} + for _, h := range route.Hops { + pk, _ := hex.DecodeString(h.PubKey) + pks = append(pks, pk) + } + resultRoute, err := r.routerClient.BuildRoute(context.Background(), &routerrpc.BuildRouteRequest{ + AmtMsat: amount * 1000, + OutgoingChanId: route.Hops[0].ChanId, + HopPubkeys: pks, + FinalCltvDelta: 144, + }) + return resultRoute.Route, err +} + +func (r *regolancer) probeRoute(route *lnrpc.Route, goodAmount, badAmount, amount int64, steps int) (maxAmount int64, + goodRoute *lnrpc.Route, err error) { + goodRoute, err = r.rebuildRoute(route, amount) + if err != nil { + return 0, nil, err + } + fakeHash := make([]byte, 32) + rand.Read(fakeHash) + result, err := r.routerClient.SendToRouteV2(context.Background(), + &routerrpc.SendToRouteRequest{ + PaymentHash: fakeHash, + Route: goodRoute, + }) + if err != nil { + return + } + if result.Status == lnrpc.HTLCAttempt_SUCCEEDED { + return 0, nil, fmt.Errorf("this should never happen") + } + if result.Status == lnrpc.HTLCAttempt_FAILED { + if result.Failure.Code == lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS { // payment can succeed + if steps == 1 || amount == badAmount { + log.Printf("%s is the best amount", hiWhiteColor(amount)) + return amount, goodRoute, nil + } + nextAmount := amount + (badAmount-amount)/2 + log.Printf("%s is good enough, trying amount %s, %s steps left", + hiWhiteColor(amount), hiWhiteColor(nextAmount), hiWhiteColor(steps-1)) + return r.probeRoute(route, amount, badAmount, nextAmount, steps-1) + } + if result.Failure.Code == lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE { + if steps == 1 { + bestAmount := hiWhiteColor(goodAmount) + if goodAmount == 0 { + bestAmount = hiWhiteColor("unknown") + } + log.Printf("%s is too much, best amount is %s", hiWhiteColor(amount), bestAmount) + return goodAmount, goodRoute, nil + } + nextAmount := amount + (goodAmount-amount)/2 + log.Printf("%s is too much, lowering amount to %s, %s steps left", + hiWhiteColor(amount), hiWhiteColor(nextAmount), hiWhiteColor(steps-1)) + return r.probeRoute(route, goodAmount, amount, nextAmount, steps-1) + } + if result.Failure.Code == lnrpc.Failure_FEE_INSUFFICIENT { + log.Printf("Fee insufficient, retrying...") + return r.probeRoute(route, goodAmount, badAmount, amount, steps) + } + } + return 0, nil, fmt.Errorf("unknown error: %+v", result) +}