Skip to content

Commit

Permalink
Add stickiness setting
Browse files Browse the repository at this point in the history
  • Loading branch information
nyonson committed Mar 30, 2023
1 parent c54a667 commit 6e39ef9
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 16 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ All of `raiju`'s commands can be listed with the global help flag, `raiju -h`, a

List the best nodes to open a channel to from the current node. `candidates` does not automatically open any channels, that needs to be done out-of-band with a different tool such as `lncli`. `candidates` just lists suggestions and is not intended to be automated (for now...).

The current node has distance `0` to itself and distance `1` to the nodes it has channels with. A node with distance `2` has a channel with a node the current node is connected too, but no channel with the current node, and so on. "Distant Neighbors" are distant (greater than `2`) from the current node, but have a channel with the candidate. Theoretically, these most distant nodes with the most distant neighbor connections are the best to open a channel to for some off the beaten path efficient routing (vs. just connecting to the biggest node in the network).
The current node has distance `0` to itself and distance `1` to the nodes it has channels with. A node with distance `2` has a channel with a node the current node is connected too, but no channel with the current node, and so on. "Distant Neighbors" are distant (greater than `2`) from the current node, but have a channel with the candidate. By default, only nodes with clearnet addresses are listed. TOR-only nodes tend to be unreliable due to the nature of TOR.

```
$ raiju candidates
Expand All @@ -45,7 +45,7 @@ Pubkey Alias

The `assume` flag allows you to see the remaining candidates and updated stats assuming channels were opened to the given nodes. This can be used to find a set of nodes to open channels too in single batch transaction in order to minimize on onchain fees.

By default, only nodes with clearnet addresses are listed. TOR-only nodes tend to be unreliable due to the nature of TOR.
From a "make money routing" perspective, theoretically, these most distant nodes with the most distant neighbor connections are good to open a channel to for some off the beaten path efficient routing vs. just connecting to the biggest node in the network. Your node could offer cheaper, better routing between two "clusters" of nodes than the biggest nodes. From a "make the network stronger in general" perspective, the hope is that this strategy creates a more decentralized network vs. everything being dependent on a handful of large hub nodes.

## fees

Expand All @@ -55,7 +55,9 @@ Set channel fees based on the channel's current liquidity. The idea here is to e

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.

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.
The `-liquidity-stickiness` attempts to avoid extra gossip by waiting for channels to return to a healthier liquidity state before changing fees. If using the same settings as before, plus a stickiness setting of 5%, if a channel moves from 19% liquidity to 23% liquidity it will still have a 500 PPM fee. It needs to move to something better than 25% (20% + 5%) before the fee will change. The stickiness setting only applies to liquidity moving in a healthy (towards center) direction. If you are drastically changing your fee settings, you probably want to set stickiness to 0 temporarily to ensure fees are updated.

The `-liquidity-thresholds`, `-liquidity-fees`, and `-liquidity-stickiness` 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.

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).

Expand Down
15 changes: 8 additions & 7 deletions cmd/raiju/raiju.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const (
rpcTimeout = time.Minute * 5
)

func parseFees(thresholds string, fees string) (raiju.LiquidityFees, error) {
func parseFees(thresholds string, fees string, stickiness float64) (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))
Expand All @@ -48,7 +48,7 @@ func parseFees(thresholds string, fees string) (raiju.LiquidityFees, error) {
ffs[i] = lightning.FeePPM(ff)
}

lf, err := raiju.NewLiquidityFees(tfs, ffs)
lf, err := raiju.NewLiquidityFees(tfs, ffs, stickiness)
if err != nil {
return raiju.LiquidityFees{}, err
}
Expand Down Expand Up @@ -76,8 +76,9 @@ func main() {
macPath := rootFlagSet.String("mac-path", "", "Macaroon with necessary permissions for lnd node")
network := rootFlagSet.String("network", "mainnet", "The bitcoin network")
// fees flags
liquidityThresholds := rootFlagSet.String("liquidity-thresholds", "80,20", "Comma separated local liquidity percent thresholds")
liquidityThresholds := rootFlagSet.String("liquidity-thresholds", "85,15", "Comma separated local liquidity percent thresholds")
liquidityFees := rootFlagSet.String("liquidity-fees", "5,50,500", "Comma separated local liquidity-based fees PPM")
liquidityStickiness := rootFlagSet.Float64("liquidity-stickiness", 0, "Percent of a channel capacity beyond threshold to wait before changing fees from settings attempting to improve liquidity")

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

c := lnd.New(services.Client, services.Client, services.Router, *network)
f, err := parseFees(*liquidityThresholds, *liquidityFees)
f, err := parseFees(*liquidityThresholds, *liquidityFees, *liquidityStickiness)
if err != nil {
return err
}
Expand Down Expand Up @@ -178,7 +179,7 @@ func main() {
}

c := lnd.New(services.Client, services.Client, services.Router, *network)
f, err := parseFees(*liquidityThresholds, *liquidityFees)
f, err := parseFees(*liquidityThresholds, *liquidityFees, *liquidityStickiness)
if err != nil {
return err
}
Expand Down Expand Up @@ -235,7 +236,7 @@ func main() {
}

c := lnd.New(services.Client, services.Client, services.Router, *network)
f, err := parseFees(*liquidityThresholds, *liquidityFees)
f, err := parseFees(*liquidityThresholds, *liquidityFees, *liquidityStickiness)
if err != nil {
return err
}
Expand Down Expand Up @@ -285,7 +286,7 @@ func main() {
}

c := lnd.New(services.Client, services.Client, services.Router, *network)
f, err := parseFees(*liquidityThresholds, *liquidityFees)
f, err := parseFees(*liquidityThresholds, *liquidityFees, *liquidityStickiness)
if err != nil {
return err
}
Expand Down
52 changes: 47 additions & 5 deletions fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,24 @@ import (
type LiquidityFees struct {
thresholds []float64
fees []lightning.FeePPM
stickiness float64
}

// 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)
return lf.findFee(liquidity, channel.LocalFee)
}

// 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)
return lf.findFee(liquidity, channel.LocalFee)
}

func (lf LiquidityFees) findFee(liquidity float64) lightning.FeePPM {
func (lf LiquidityFees) findFee(liquidity float64, currentFee lightning.FeePPM) lightning.FeePPM {
bucket := 0
for bucket < len(lf.thresholds) {
if liquidity > lf.thresholds[bucket] {
Expand All @@ -43,7 +44,42 @@ func (lf LiquidityFees) findFee(liquidity float64) lightning.FeePPM {

}

return lf.fees[bucket]
newFee := lf.fees[bucket]

// apply stickiness if fee is heading in the right direction, but wanna hold on for a bit to limit gossip
if liquidity < 50 && newFee < currentFee {
lowBucket := 0
for lowBucket < len(lf.thresholds) {
if liquidity > lf.thresholds[lowBucket]+lf.stickiness {
break
} else {
lowBucket += 1
}

}

if lowBucket != bucket {
fmt.Fprintf(os.Stderr, "keeping fee due to stickiness")
}
newFee = lf.fees[lowBucket]
} else if liquidity >= 50 && newFee > currentFee {
highBucket := 0
for highBucket < len(lf.thresholds) {
if liquidity > lf.thresholds[highBucket]-lf.stickiness {
break
} else {
highBucket += 1
}

}

if highBucket != bucket {
fmt.Fprintf(os.Stderr, "keeping fee due to stickiness")
}
newFee = lf.fees[highBucket]
}

return newFee
}

// RebalanceChannels at the far ends of the spectrum.
Expand Down Expand Up @@ -81,7 +117,7 @@ func (lf LiquidityFees) PrintSettings() {
}

// NewLiquidityFees with threshold and fee validation.
func NewLiquidityFees(thresholds []float64, fees []lightning.FeePPM) (LiquidityFees, error) {
func NewLiquidityFees(thresholds []float64, fees []lightning.FeePPM, stickiness float64) (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")
Expand All @@ -102,8 +138,14 @@ func NewLiquidityFees(thresholds []float64, fees []lightning.FeePPM) (LiquidityF
}
}

// ensure stickiness percent makes sense
if stickiness > 100 {
return LiquidityFees{}, errors.New("stickiness must be a percent")
}

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

0 comments on commit 6e39ef9

Please sign in to comment.