Merge pull request #56 from ziggie1984/rapid-rebalance-strategy

master
rkfg 1 year ago committed by GitHub
commit 3467c7edbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -256,6 +256,7 @@ func preflightChecks(params *configParams) error {
if params.TimeoutRoute == 0 { if params.TimeoutRoute == 0 {
params.TimeoutRoute = 30 params.TimeoutRoute = 30
} }
return nil return nil
} }

@ -19,7 +19,10 @@ func (e ErrRetry) Error() string {
return fmt.Sprintf("retry payment with %d sats", e.amount) return fmt.Sprintf("retry payment with %d sats", e.amount)
} }
var ErrProbeFailed = fmt.Errorf("probe failed") var (
ErrProbeFailed = fmt.Errorf("probe failed")
ErrFeeExceeded = fmt.Errorf("fee-limit exceeded")
)
func (r *regolancer) createInvoice(ctx context.Context, amount int64) (result *lnrpc.AddInvoiceResponse, err error) { func (r *regolancer) createInvoice(ctx context.Context, amount int64) (result *lnrpc.AddInvoiceResponse, err error) {
var ok bool var ok bool
@ -45,7 +48,7 @@ func (r *regolancer) pay(ctx context.Context, amount int64, minAmount int64, max
if route.TotalFeesMsat > maxFeeMsat { if route.TotalFeesMsat > maxFeeMsat {
log.Printf("fee on the route exceeds our limits: %s ppm (max fee %s ppm)", formatFeePPM(amount*1000, route.TotalFeesMsat), formatFeePPM(amount*1000, maxFeeMsat)) log.Printf("fee on the route exceeds our limits: %s ppm (max fee %s ppm)", formatFeePPM(amount*1000, route.TotalFeesMsat), formatFeePPM(amount*1000, maxFeeMsat))
return fmt.Errorf("fee-limit exceeded") return ErrFeeExceeded
} }
invoice, err := r.createInvoice(ctx, amount) invoice, err := r.createInvoice(ctx, amount)

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"encoding/hex" "encoding/hex"
"errors"
"log" "log"
"time" "time"
@ -11,10 +12,16 @@ import (
type rebalanceResult struct { type rebalanceResult struct {
successfulAttempts int successfulAttempts int
failedAttempts int
successfulAmt int64 successfulAmt int64
paidFeeMsat int64 paidFeeMsat int64
} }
const (
increaseAmtRapidRebalance string = "increase"
decreaseAmtRapidRebalance string = "decrease"
)
func (r *regolancer) tryRebalance(ctx context.Context, attempt *int) (err error, func (r *regolancer) tryRebalance(ctx context.Context, attempt *int) (err error,
repeat bool) { repeat bool) {
attemptCtx, attemptCancel := context.WithTimeout(ctx, time.Minute*time.Duration(params.TimeoutAttempt)) attemptCtx, attemptCancel := context.WithTimeout(ctx, time.Minute*time.Duration(params.TimeoutAttempt))
@ -48,10 +55,11 @@ func (r *regolancer) tryRebalance(ctx context.Context, attempt *int) (err error,
if params.AllowRapidRebalance { if params.AllowRapidRebalance {
rebalanceResult, _ := r.tryRapidRebalance(ctx, route) rebalanceResult, _ := r.tryRapidRebalance(ctx, route)
if rebalanceResult.successfulAttempts > 0 { if rebalanceResult.successfulAttempts > 0 || rebalanceResult.failedAttempts > 0 {
log.Printf("%s rapid rebalances were successful, total amount: %s (fee: %s sat | %s ppm)\n", log.Printf("%s rapid rebalances were successful, total amount: %s (fee: %s sat | %s ppm) - Failed Attempts: %s\n",
hiWhiteColor(rebalanceResult.successfulAttempts), hiWhiteColor(rebalanceResult.successfulAmt), hiWhiteColor(rebalanceResult.successfulAttempts), hiWhiteColor(rebalanceResult.successfulAmt),
formatFee(rebalanceResult.paidFeeMsat), formatFeePPM(rebalanceResult.successfulAmt*1000, rebalanceResult.paidFeeMsat)) formatFee(rebalanceResult.paidFeeMsat), formatFeePPM(rebalanceResult.successfulAmt*1000, rebalanceResult.paidFeeMsat),
hiWhiteColor(rebalanceResult.failedAttempts))
} }
log.Printf("Finished rapid rebalancing") log.Printf("Finished rapid rebalancing")
} }
@ -99,11 +107,15 @@ func (r *regolancer) tryRapidRebalance(ctx context.Context, route *lnrpc.Route)
amtLocal int64 = amt amtLocal int64 = amt
accelerator int64 = 1 accelerator int64 = 1
hittingTheWall bool hittingTheWall bool
exitEarly bool
capReached bool capReached bool
maxAmountOnRouteMsat uint64 maxAmountOnRouteMsat uint64
minAmount uint64
rebalanceStrategy string = increaseAmtRapidRebalance
) )
result.successfulAttempts = 0 result.successfulAttempts = 0
result.failedAttempts = 0
// Include Initial Rebalance // Include Initial Rebalance
result.successfulAmt = amt result.successfulAmt = amt
result.paidFeeMsat = route.TotalFeesMsat result.paidFeeMsat = route.TotalFeesMsat
@ -113,32 +125,64 @@ func (r *regolancer) tryRapidRebalance(ctx context.Context, route *lnrpc.Route)
return result, err return result, err
} }
if params.MinAmount > 0 {
minAmount = uint64(params.MinAmount)
} else {
minAmount = 10000
}
Loop:
for { for {
if hittingTheWall { switch rebalanceStrategy {
accelerator >>= 1 case increaseAmtRapidRebalance:
// In case we encounter that we are already constrained if hittingTheWall {
// by the liquidity on the channels we are waiting for accelerator >>= 1
// the accelerator to go below this amount to save // In case we encounter that we are already constrained
// already failed rebalances // by the liquidity on the channels we are waiting for
if amtLocal < accelerator*amt && amtLocal > 0 { // the accelerator to go below this amount to save
continue // already failed rebalances
if amtLocal < accelerator*amt && amtLocal > int64(minAmount) {
continue
}
} else if !capReached {
// we only increase the amount if the max Amount on the
// route is still not reached
accelerator <<= 1
} }
} else if !capReached {
// we only increase the amount if the max Amount on the if uint64(accelerator*amt) < maxAmountOnRouteMsat/1000 {
// route is still not reached amtLocal = accelerator * amt
} else if !capReached {
capReached = true
log.Printf("Max amount on route reached capping amount at %s sats "+
"| max amount on route (max htlc size) %s sats\n", infoColor(amtLocal), infoColor(maxAmountOnRouteMsat/1000))
}
// We reached the initial amount again.
// now we switch to the decreasing strategy.
// We half the amount on every step we go down.
if accelerator < 1 {
accelerator = 2
rebalanceStrategy = decreaseAmtRapidRebalance
amtLocal = amt / accelerator
if amtLocal < int64(minAmount) {
break Loop
}
}
case decreaseAmtRapidRebalance:
accelerator <<= 1 accelerator <<= 1
} if amtLocal < amt/accelerator {
continue
}
if uint64(accelerator*amt) < maxAmountOnRouteMsat/1000 { amtLocal = amt / accelerator
amtLocal = accelerator * amt if amtLocal < int64(minAmount) {
} else if !capReached { break Loop
capReached = true }
log.Printf("Max amount on route reached capping amount at %s sats "+
"| max amount on route (max htlc size) %s sats\n", infoColor(amtLocal), infoColor(maxAmountOnRouteMsat/1000))
} }
if accelerator < 1 { if exitEarly {
break break Loop
} }
log.Printf("Rapid rebalance attempt %s, amount: %s\n", hiWhiteColor(result.successfulAttempts+1), hiWhiteColor(amtLocal)) log.Printf("Rapid rebalance attempt %s, amount: %s\n", hiWhiteColor(result.successfulAttempts+1), hiWhiteColor(amtLocal))
@ -211,14 +255,29 @@ func (r *regolancer) tryRapidRebalance(ctx context.Context, route *lnrpc.Route)
return result, err return result, err
} }
amtLocalTemp := amtLocal
_, _, amtLocal, err = r.pickChannelPair(amtLocal, params.MinAmount, params.RelAmountFrom, params.RelAmountTo) _, _, amtLocal, err = r.pickChannelPair(amtLocal, params.MinAmount, params.RelAmountFrom, params.RelAmountTo)
if err != nil { if err != nil {
log.Printf(errColor("Error during picking channel: %s"), err) log.Printf(errColor("Error during picking channel: %s"), err)
return result, err hittingTheWall = true
// We are not returning an error here because
// in we still could rebalance an amount in the
// decreasing strategy.
// return result, err
continue
} }
log.Printf("Rapid fire starting with actual amount: %s (could be lower than the attempted amount in case there is less liquidity available on the channel)", hiWhiteColor(amtLocal)) if amtLocalTemp > amtLocal {
log.Printf("Rapid fire starting with actual amount: %s (could be lower than the attempted amount in case there is less liquidity available on the channel)", hiWhiteColor(amtLocal))
// We are already using maximum available liquidity so we can begin decreasing amounts again.
hittingTheWall = true
// This is needed so we do not test further amounts
// when in the decreasing strategy.
if rebalanceStrategy == decreaseAmtRapidRebalance {
exitEarly = true
}
}
routeLocal, err = r.rebuildRoute(ctx, route, amtLocal) routeLocal, err = r.rebuildRoute(ctx, route, amtLocal)
@ -241,6 +300,12 @@ func (r *regolancer) tryRapidRebalance(ctx context.Context, route *lnrpc.Route)
err = r.pay(attemptCtx, amtLocal, params.MinAmount, maxFeeMsat, routeLocal, 0) err = r.pay(attemptCtx, amtLocal, params.MinAmount, maxFeeMsat, routeLocal, 0)
// In case we are already decreasing the amount we can exit early because
// for even smaller amounts the fee will be higher (reason is the basefee).
if rebalanceStrategy == decreaseAmtRapidRebalance && errors.Is(err, ErrFeeExceeded) {
exitEarly = true
}
attemptCancel() attemptCancel()
if attemptCtx.Err() == context.DeadlineExceeded { if attemptCtx.Err() == context.DeadlineExceeded {
@ -250,6 +315,8 @@ func (r *regolancer) tryRapidRebalance(ctx context.Context, route *lnrpc.Route)
if err != nil { if err != nil {
log.Printf("Rebalance failed with %s", err) log.Printf("Rebalance failed with %s", err)
log.Println()
result.failedAttempts++
hittingTheWall = true hittingTheWall = true
} else { } else {
result.successfulAttempts++ result.successfulAttempts++

Loading…
Cancel
Save