Skip to content

Commit

Permalink
Expose liquidity flags.
Browse files Browse the repository at this point in the history
  • Loading branch information
nyonson committed Mar 26, 2023
1 parent e2376b4 commit 966e7cc
Show file tree
Hide file tree
Showing 8 changed files with 579 additions and 584 deletions.
24 changes: 10 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,13 @@ By default, only nodes with clearnet addresses are listed. TOR-only nodes tend t

**Passively manage channel liquidity**

Set channel fees based on the channel's current liquidity.
Set channel fees based on the channel's current liquidity. The idea here is to encourage passive channel re-balancing through fees. If a channel has a too much local liquidity (high), fees are lowered in order to encourage relatively more outbound transactions. Visa versa for a channel with too little local liquidity (low).

The strategy for fee amounts is hardcoded (although I might try to add some more in the future) all fees are derived from the `-standard-liquidity-fee-ppm` flag. Channels are bucketed into three coarse grained groups: *high liquidity*, *standard liquidity*, and *low liquidity*. The idea here is to encourage passive channel re-balancing through fees. If a channel has a too much local liquidity (high), fees are lowered in order to encourage relatively more outbound transactions. Visa versa for a channel with too little local liquidity (low). So `fees` applies the `standard-liquidity-fee-ppm` to standard channels, `standard-liquidity-fee-ppm / 10` to high channels, and `standard-liquidity-fee-ppm * 10` to low channels. The following example sets fees based on a `200 ppm` standard fee:
The global `-liquidity-thresholds` flag determines how channels are grouped into liquidity buckets, while the `-liquidity-fees` flag determines the fee settings applied to those groups. For example, if thresholds are set to `80,20` and fees set to `5,50,500`, then channels with over 80% local liquidity will have a 5 PPM fee, channels between 80% and 20% local liquidity will have a 50 PPM fee, and channels with less than 20% liquidity will have a 500 PPM fee.

```
$ raiju fees -standard-liquidity-fee-ppm 200
```
The `-liquidity-thresholds` and `-liquidity-fees` are global (not `fees` specific) because they are also used in the `rebalance` command to help coordinate the right amount of fees to pay in active rebalancing.

`fees` supports a `-daemon` flag which keeps keeps the process alive listening for channel updates that trigger fee updates (e.g. a channel's liquidity sinks below the low level and needs its fees updated). This is helpful when used with the `rebalance` command which *actively* balances channel liquidity. Without the daemon, there is a worst case scenario of: 1. pay a lot of fees to actively `rebalance` channel's liquidity from low to standard 2. update the channel's fees to standard 3. have a large payment immediately cancel out the rebalance and it only pays standard fees (instead of higher ones which would have canceled out the cost of the rebalance).
The `-daemon` flag keeps keeps the process alive listening for channel updates that trigger fee updates (e.g. a channel's liquidity sinks below the low level and needs its fees updated). This is helpful when used with the `rebalance` command which *actively* balances channel liquidity. Without the daemon, there is a worst case scenario of: 1. pay a lot of fees to actively `rebalance` channel's liquidity from low to standard 2. update the channel's fees to standard 3. have a large payment immediately cancel out the rebalance and it only pays standard fees (instead of higher ones which would have canceled out the cost of the rebalance).

`fees` follows the [zero-base-fee movement](http://www.rene-pickhardt.de/). I am honestly not sure if this is financially sound, but I appreciate the simpler mental model of only thinking in ppm.

Expand All @@ -80,7 +78,7 @@ Restart=always
Environment=RAIJU_HOST=localhost:10009
Environment=RAIJU_MAC_PATH=/home/lightning/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
Environment=RAIJU_TLS_PATH=/home/lightning/.lnd/tls.cert
Environment=RAIJU_STANDARD_LIQUIDITY_FEE_PPM=200
Environment=RAIJU_LIQUIDITY_FEES=5,50,500
ExecStart=/usr/local/bin/raiju fees -daemon
[Install]
Expand All @@ -91,7 +89,7 @@ WantedBy=multi-user.target

**Actively manage channel liquidity**

Circular rebalance a channel or all channels that aren't doing so hot liquidity-wise.
Circular rebalance channels that aren't doing so hot liquidity-wise or force rebalance a single channel.

Where the `fees` command attempts to balance channels passively, this is an *active* approach where liquidity is manually pushed. The cost of active rebalancing are the lightning payment fees. While this command could be used to push large amounts of liquidity, the default settings are intended to just prod things in the right direction.

Expand All @@ -103,7 +101,7 @@ The command takes two arguments:

A smaller step percentage will increase the likely hood of a successful payment, but might also increase fees a bit if the payment collects a lot of `base_fees` on its route.

If no out channel and last hop pubkey are given, the command will roll through all channels with high liquidity (as defined by `raiju`) and attempt to push it through channels of low liquidity (as defined by `raiju`).
If no out channel and last hop pubkey are given, the command will roll through channels with high liquidity and attempt to push it through channels of low liquidity. High and low are defined by the defined by the global `-liquidity-thresholds` flag. For example, if liquidity thresholds is set to `80,20`, channels with local liquidity over 80% are considered "high" and channels with local liquidity under 20% are considered "low".

```
$ raiju rebalance 1 1
Expand Down Expand Up @@ -131,10 +129,8 @@ Group=lightning
Environment=RAIJU_HOST=localhost:10009
Environment=RAIJU_MAC_PATH=/home/lightning/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
Environment=RAIJU_TLS_PATH=/home/lightning/.lnd/tls.cert
Environment=RAIJU_STANDARD_LIQUIDITY_FEE_PPM=200
Environment=RAIJU_LIQUIDITY_FEES=5,50,500
ExecStart=/usr/local/bin/raiju rebalance 1 5
# Optionally run fees afterward to "lock in" new liquidities
ExecStartPost=/usr/local/bin/raiju fees
```

Example `rebalance.timer`:
Expand Down Expand Up @@ -175,15 +171,15 @@ $ go install github.com/nyonson/raiju/cmd/raiju@latest
If a container is preferred, `raiju` images are published at `ghcr.io/nyonson/raiju`.

```
docker pull ghcr.io/nyonson/raiju:v0.3.2
docker pull ghcr.io/nyonson/raiju:v0.6.0
```

A little more configuration is required to pass along settings to the container.

```
docker run -it \
-v /admin.macaroon:/admin.macaroon:ro -v /tls.cert:/tls.cert:ro \
ghcr.io/nyonson/raiju:v0.3.2 \
ghcr.io/nyonson/raiju:v0.6.0 \
-host 192.168.1.187:10009 -mac-path admin.macaroon -tls-path tls.cert
candidates
```
Expand Down
74 changes: 59 additions & 15 deletions cmd/raiju/raiju.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ const (
rpcTimeout = time.Minute * 5
)

func parseFees(thresholds string, fees string) (raiju.LiquidityFees, error) {
// using FieldsFunc to handle empty string case correctly
rawThresholds := strings.FieldsFunc(thresholds, func(c rune) bool { return c == ',' })
tfs := make([]float64, len(rawThresholds))
for i, t := range rawThresholds {
tf, err := strconv.ParseFloat(t, 64)
if err != nil {
return raiju.LiquidityFees{}, err
}
tfs[i] = tf
}

rawFees := strings.FieldsFunc(fees, func(c rune) bool { return c == ',' })
ffs := make([]lightning.FeePPM, len(rawFees))
for i, f := range rawFees {
ff, err := strconv.ParseFloat(f, 64)
if err != nil {
return raiju.LiquidityFees{}, err
}
ffs[i] = lightning.FeePPM(ff)
}

return raiju.NewLiquidityFees(tfs, ffs)
}

func main() {
cmdLog := log.New(os.Stderr, "raiju: ", 0)

Expand All @@ -43,8 +68,9 @@ func main() {
tlsPath := rootFlagSet.String("tls-path", "", "LND node tls certificate")
macPath := rootFlagSet.String("mac-path", "", "Macaroon with necessary permissions for lnd node")
network := rootFlagSet.String("network", "mainnet", "The bitcoin network")
// liquidity flags
standardLiquidityFeePPM := rootFlagSet.Float64("standard-liquidity-fee-ppm", 200, "Default fee in PPM for standard liquidity channels which is shared by subcommands")
// fees flags
liquidityThresholds := rootFlagSet.String("liquidity-thresholds", "80,20", "Comma separated local liquidity percent thresholds")
liquidityFees := rootFlagSet.String("liquidity-fees", "5,50,500", "Comma separated local liquidity-based fees PPM")

candidatesFlagSet := flag.NewFlagSet("candidates", flag.ExitOnError)
minCapacity := candidatesFlagSet.Int64("min-capacity", 1000000, "Minimum capacity of a node in satoshis")
Expand Down Expand Up @@ -85,13 +111,18 @@ func main() {
}

c := lnd.New(services.Client, services.Client, services.Router, *network)
r := raiju.New(c)
f, err := parseFees(*liquidityThresholds, *liquidityFees)
if err != nil {
return err
}

r := raiju.New(c, f)

// using FieldsFunc to handle empty string case correctly
raw := strings.FieldsFunc(*assume, func(c rune) bool { return c == ',' })
assume := make([]lightning.PubKey, len(raw))
for i, a := range raw {
assume[i] = lightning.PubKey(a)
a := strings.FieldsFunc(*assume, func(c rune) bool { return c == ',' })
assume := make([]lightning.PubKey, len(a))
for i, p := range a {
assume[i] = lightning.PubKey(p)
}

request := raiju.CandidatesRequest{
Expand Down Expand Up @@ -119,7 +150,7 @@ func main() {
Name: "fees",
ShortUsage: "raiju fees",
ShortHelp: "Set channel fees based on liquidity to passively rebalance channels",
LongHelp: "Channels are grouped into three coarse grained buckets: standard, high, and low. Channels with standard liquidity will have the standard fee applied. Channels with high liquidity will have a 10x the standard fee applied to discourage routing. And channels with low liquidity will have 1/10 the standard fee applied to encourage routing.",
LongHelp: "Channels are grouped depending on the local liquidity thresholds setting and have fees applied based on the local liquidity fees setting.",
FlagSet: feesFlagSet,
Exec: func(ctx context.Context, args []string) error {
if len(args) != 0 {
Expand All @@ -140,9 +171,14 @@ func main() {
}

c := lnd.New(services.Client, services.Client, services.Router, *network)
r := raiju.New(c)
f, err := parseFees(*liquidityThresholds, *liquidityFees)
if err != nil {
return err
}

_, err = r.Fees(ctx, raiju.NewLiquidityFees(*standardLiquidityFeePPM), *daemon)
r := raiju.New(c, f)

_, err = r.Fees(ctx, *daemon)

return err
},
Expand All @@ -157,7 +193,7 @@ func main() {
Name: "rebalance",
ShortUsage: "raiju rebalance <step-percent> <max-percent>",
ShortHelp: "Send circular payment(s) to actively rebalance channels",
LongHelp: "If the output and input flags are set, a rebalance is attempted (both must be set together). If not, channels are grouped into three coarse grained buckets: standard, high, and low. Standard channels will be ignored since their liquidity is good. High channels will attempt to push the percent of their capacity at a time to the low channels, stopping if their liquidity improves enough or if all channels have been tried.",
LongHelp: "By default, attempts to move liquidity from the channels with the highest local liquidity to the lowest. If an out channel and last hop node are specified however, this is and implicit force command and attempts to move the liquidity damn whatever the current local amounts.",
FlagSet: rebalanceFlagSet,
Exec: func(ctx context.Context, args []string) error {
if len(args) != 2 {
Expand Down Expand Up @@ -192,12 +228,15 @@ func main() {
}

c := lnd.New(services.Client, services.Client, services.Router, *network)
r := raiju.New(c)
f, err := parseFees(*liquidityThresholds, *liquidityFees)
if err != nil {
return err
}

fees := raiju.NewLiquidityFees(*standardLiquidityFeePPM)
r := raiju.New(c, f)

// default to low liquidity fee, override with flag
maxFee := fees.Low()
maxFee := f.RebalanceFee()
if *maxFeePPM != 0 {
maxFee = lightning.FeePPM(*maxFeePPM)
}
Expand Down Expand Up @@ -239,7 +278,12 @@ func main() {
}

c := lnd.New(services.Client, services.Client, services.Router, *network)
r := raiju.New(c)
f, err := parseFees(*liquidityThresholds, *liquidityFees)
if err != nil {
return err
}

r := raiju.New(c, f)

_, err = r.Reaper(ctx)

Expand Down
93 changes: 93 additions & 0 deletions fees.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package raiju

import (
"errors"

"github.com/nyonson/raiju/lightning"
)

// LiquidityFees for channels.
//
// Defining channel liquidity percentage based on (local capacity / total capacity).
// When liquidity is low, there is too much inbound.
// When liquidity is high, there is too much outbound.
type LiquidityFees struct {
thresholds []float64
fees []lightning.FeePPM
}

// Fee for channel based on its current liquidity.
func (lf LiquidityFees) Fee(channel lightning.Channel) lightning.FeePPM {
liquidity := float64(channel.LocalBalance) / float64(channel.Capacity) * 100

return lf.findFee(liquidity)
}

// PotentialFee for channel based on its current liquidity.
func (lf LiquidityFees) PotentialFee(channel lightning.Channel, additionalLocal lightning.Satoshi) lightning.FeePPM {
liquidity := float64(channel.LocalBalance+additionalLocal) / float64(channel.Capacity) * 100

return lf.findFee(liquidity)
}

func (lf LiquidityFees) findFee(liquidity float64) lightning.FeePPM {
bucket := 0
for bucket < len(lf.thresholds) {
if liquidity > lf.thresholds[bucket] {
break
} else {
bucket += 1
}

}

return lf.fees[bucket]
}

// RebalanceChannels at the far ends of the spectrum.
func (lf LiquidityFees) RebalanceChannels(channels lightning.Channels) (high lightning.Channels, low lightning.Channels) {
for _, c := range channels {
l := c.Liquidity()
if l > lf.thresholds[0] {
high = append(high, c)
}

if l <= lf.thresholds[len(lf.thresholds)-1] {
low = append(low, c)
}
}

return high, low
}

func (lf LiquidityFees) RebalanceFee() lightning.FeePPM {
return lf.fees[len(lf.fees)-1]
}

// NewLiquidityFees with threshold and fee validation.
func NewLiquidityFees(thresholds []float64, fees []lightning.FeePPM) (LiquidityFees, error) {
// ensure every bucket has a fee
if len(thresholds)+1 != len(fees) {
return LiquidityFees{}, errors.New("fees must have one more value than thresholds to ensure each bucket has a defined fee")

}

// ensure thresholds are descending
for i := 0; i < len(thresholds)-1; i++ {
if thresholds[i] <= thresholds[i+1] {
return LiquidityFees{}, errors.New("thresholds must be descending")
}
}

// ensure fees are ascending
for i := 0; i < len(fees)-1; i++ {
if fees[i] > fees[i+1] {
return LiquidityFees{}, errors.New("fees must be ascending")
}
}

return LiquidityFees{
thresholds: thresholds,
fees: fees,
}, nil
}
Loading

0 comments on commit 966e7cc

Please sign in to comment.