Fix rebalance plugin

This commit is contained in:
Gálli Zoltán
2024-03-11 13:12:54 +01:00
committed by Chris Guida
parent 4a04caff7e
commit 83a80d134e
6 changed files with 38 additions and 27 deletions

View File

@@ -1,101 +0,0 @@
# Rebalance plugin
This plugin moves liquidity between your channels using circular payments
## Installation
For general plugin installation instructions see the repos main
[README.md](https://github.com/lightningd/plugins/blob/master/README.md#Installation)
## Usage
Once the plugin is installed and active, there are four additional methods for helping to rebalance channels:
1) Either you can call `lightning-cli rebalanceall` to automatically fix all of your channels' liquidity.
2) `lightning-cli rebalancestop` stops the ongoing `rebalanceall`.
3) Or you can call `lightning-cli rebalance outgoing_scid incoming_scid` to rebalance individual channels.
4) `lightning-cli rebalancereport` shows information: plugin settings, past rebalance stats, etc.
## Automatic rebalance
A lightning node usually has multiple channels of different sizes. The node can perform best if all channels have `{enough_liquidity}` for both directions. So the rebalance has multiple purposes with different priority:
1) **The primary goal** is to ensure all channels have `{enough_liquidity}` for both direction, or if a given channel is too small for that, then it has a 50/50 liquidity ratio.
2) **The secondary goal** is to distribute the remaining liquidity evenly between the big channels.
3) For the long run, it is very important **to do this economically**. So the fees of fixing liquidity have to be cheaper than the fees of transaction forwards, which can ruin the liquidity again. (This assumes your node has some rational fee setting.) This way the automatic rebalance can run regularly, and your node can earn more on transaction forwarding than spend for rebalancing.
If the plugin cannot find a cheap enough circular route to rebalancing economically, then it does nothing by default. To not to cause a loss for users.
#### Rebalancing strategy
As a first step, depending on the actual situation, there is a need to get a value of `{enough_liquidity}`. The plugin searches for a maximum possible threshold. For which all channels theoretically can be balanced beyond this threshold. Or smaller than `threshold * 2` channels can be balanced to a 50/50 ratio. `{enough_liquidity}` will be half of this maximum threshold.
The next step is to calculate `{ideal_ratio}` for big channels. Beyond the `{enough_liquidity}` threshold, big channels should share the remaining liquidity evenly, so every big channels' liquidity ratio should be close to the `{ideal_ratio}`.
After we know the current `{enough_liquidity}` threshold and `{ideal_ratio}`, the plugin checks every possible channel pairs to seek a proper rebalance opportunity. If it finds a matching pair, it calls the individual rebalance method for them. If the rebalance fails, the plugin tries again with a lesser amount, until it reaches the minimum rebalancable amount, or the rebalance succeeds.
This process may take a while. Automatic rebalance can run for hours in the background, but you can stop it anytime with `lightning-cli rebalancestop`.
#### Parameters for rebalanceall
- OPTIONAL: The `min_amount` parameter sets the minimum rebalancable amount in millisatoshis. The parameter also can be specified in other denominations by appending a valid suffix, i. e. '1000000sat', '0.01btc' or '10mbtc'. The default value is '50000sat'.
- OPTIONAL: The `feeratio` sets how much the rebalance may cost as a ratio of your default fee. Its default value is `0.5`, which means it can use a maximum of half of your node's default fee.
#### Tips and Tricks for automatic rebalance
- It may work only with well-connected nodes. You should have several different channels to use it with a good chance for success.
- Your node should have some rational default fee setting. If you use cheaper fees than your neighbors, it probably cannot find a cheap enough circular route to rebalance.
## Individual channel rebalance
You can use the `lightning-cli` to rebalance channels like this:
```
lightning-cli rebalance outgoing_scid incoming_scid [msatoshi] [retry_for] [maxfeepercent] [exemptfee] [getroute_method]
```
def rebalance(plugin, outgoing_scid, incoming_scid, msatoshi: Millisatoshi = None,
retry_for: int = 60, maxfeepercent: float = 0.5,
exemptfee: Millisatoshi = Millisatoshi(5000),
getroute_method=None):
If you want to skip/default certain optional parameters but use others, you can
use always the `lightning-cli -k` (key=value) syntax like this:
```bash
lightning-cli rebalance -k outgoing_scid=1514942x51x0 incoming_scid=1515133x10x0 maxfeepercent=1
```
#### Parameters for rebalance
- The `outgoing_scid` is the short_channel_id of the sending channel,
- The `incoming_scid` is the short_channel_id of the receiving channel.
- OPTIONAL: The `msatoshi` parameter sets the amount in milli-satoshis to be
transferred. If the parameter is left out, the plugin will calucate an amount
that will balance the channels 50%/50%. The parameter can also be given in
other denominations by appending i.e. '1000000sat', '0.01btc' or '10mbtc'.
- OPTIONAL: `retry_for` defines the number of seconds the plugin will retry to
find a suitable route. Default: 60 seconds.
- OPTIONAL: `maxfeepercent` is a perecentage limit of the money to be paid in
fees and defaults to 0.5.
- OPTIONAL: The `exemptfee` option can be used for tiny payments which would be
dominated by the fee leveraged by forwarding nodes. Setting `exemptfee`
allows the `maxfeepercent` check to be skipped on fees that are smaller than
exemptfee (default: 5000 millisatoshi).
- OPTIONAL: The `getroute_method` option can be for route search can be 'basic'
or 'iterative'.
'basic': Tries all routes sequentially.
'iterative': Tries shorter and bigger routes first.
#### Tips and Tricks for individual rebalance
- To find the correct channel IDs, you can use the `summary` plugin which can
be found [here](https://github.com/lightningd/plugins/tree/master/summary).
- The ideal amount is not too big, but not too small: it is difficult to find a
route for a big payment, however some node refuses to forward too small
amounts (i.e. less than a thousand msatoshi).
- After some failed attempts, may worth checking the `lightningd` logs for
further information.
- Channels have a `channel_reserve_satoshis` value, which is usually 1% of the
channel's total balance. Initially, this reserve may not be met, as only one
side has funds; but the protocol ensures that there is always progress toward
meeting this reserve, and once met, [it is maintained.](https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#rationale)
Therefore you cannot rebalance a channel to be completely empty or full.

View File

@@ -1,23 +0,0 @@
import re
def cln_parse_rpcversion(string):
"""
Parse cln version string to determine RPC version.
cln switched from 'semver' alike `major.minor.sub[rcX][-mod]`
to ubuntu style with version 22.11 `yy.mm[.patch][-mod]`
make sure we can read all of them for (the next 80 years).
"""
rpcversion = string
if rpcversion.startswith('v'): # strip leading 'v'
rpcversion = rpcversion[1:]
if rpcversion.find('-') != -1: # strip mods
rpcversion = rpcversion[:rpcversion.find('-')]
if re.search('.*(rc[\\d]*)$', rpcversion): # strip release candidates
rpcversion = rpcversion[:rpcversion.find('rc')]
if rpcversion.count('.') == 1: # imply patch version 0 if not given
rpcversion = rpcversion + '.0'
# split and convert numeric string parts to actual integers
return list(map(int, rpcversion.split('.')))

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
pyln-client>=0.12

View File

@@ -1,147 +0,0 @@
import os
from pyln.testing.fixtures import * # noqa: F401,F403
from pyln.client import Millisatoshi
plugin_path = os.path.join(os.path.dirname(__file__), "rebalance.py")
plugin_opt = {'plugin': plugin_path}
# waits for a bunch of nodes HTLCs to settle
def wait_for_all_htlcs(nodes):
for n in nodes:
n.wait_for_htlcs()
# waits for all nodes to have all scids gossip active
def wait_for_all_active(nodes, scids):
for n in nodes:
for scid in scids:
n.wait_channel_active(scid)
def test_rebalance_starts(node_factory):
l1 = node_factory.get_node()
# Test dynamically
l1.rpc.plugin_start(plugin_path)
l1.rpc.plugin_stop(plugin_path)
l1.rpc.plugin_start(plugin_path)
l1.stop()
# Then statically
l1.daemon.opts["plugin"] = plugin_path
l1.start()
def test_rebalance_manual(node_factory, bitcoind):
l1, l2, l3 = node_factory.line_graph(3, opts=plugin_opt)
nodes = [l1, l2, l3]
# form a circle so we can do rebalancing
l3.connect(l1)
l3.fundchannel(l1)
# get scids
scid12 = l1.get_channel_scid(l2)
scid23 = l2.get_channel_scid(l3)
scid31 = l3.get_channel_scid(l1)
scids = [scid12, scid23, scid31]
# wait for each others gossip
bitcoind.generate_block(6)
for n in nodes:
for scid in scids:
n.wait_channel_active(scid)
# check we can do an auto amount rebalance
result = l1.rpc.rebalance(scid12, scid31)
print(result)
assert result['status'] == 'complete'
assert result['outgoing_scid'] == scid12
assert result['incoming_scid'] == scid31
assert result['hops'] == 3
assert result['received'] == '500000000msat'
# wait until listpeers is up2date
wait_for_all_htlcs(nodes)
# check that channels are now balanced
c12 = l1.rpc.listpeerchannels(l2.info['id'])['channels'][0]
c13 = l1.rpc.listpeerchannels(l3.info['id'])['channels'][0]
assert abs(0.5 - (Millisatoshi(c12['to_us_msat']) / Millisatoshi(c12['total_msat']))) < 0.01
assert abs(0.5 - (Millisatoshi(c13['to_us_msat']) / Millisatoshi(c13['total_msat']))) < 0.01
# check we can do a manual amount rebalance in the other direction
result = l1.rpc.rebalance(scid31, scid12, '250000000msat')
assert result['status'] == 'complete'
assert result['outgoing_scid'] == scid31
assert result['incoming_scid'] == scid12
assert result['hops'] == 3
assert result['received'] == '250000000msat'
# briefly check rebalancereport works
report = l1.rpc.rebalancereport()
assert report.get('rebalanceall_is_running') is False
assert report.get('total_successful_rebalances') == 2
def test_rebalance_all(node_factory, bitcoind):
l1, l2, l3 = node_factory.line_graph(3, opts=plugin_opt)
nodes = [l1, l2, l3]
# check we get an error if theres just one channel
result = l1.rpc.rebalanceall()
assert result['message'] == 'Error: Not enough open channels to rebalance anything'
# now we add another 100% outgoing liquidity to l1 which does not help
l4 = node_factory.get_node()
l1.connect(l4)
l1.fundchannel(l4)
# test this is still not possible
result = l1.rpc.rebalanceall()
assert result['message'] == 'Error: Not enough liquidity to rebalance anything'
# remove l4 it does not distort further testing
l1.rpc.close(l1.get_channel_scid(l4))
# now we form a circle so we can do actually rebalanceall
l3.connect(l1)
l3.fundchannel(l1)
# get scids
scid12 = l1.get_channel_scid(l2)
scid23 = l2.get_channel_scid(l3)
scid31 = l3.get_channel_scid(l1)
scids = [scid12, scid23, scid31]
# wait for each others gossip
bitcoind.generate_block(6)
wait_for_all_active(nodes, scids)
# check that theres nothing to stop when theres nothing to stop
result = l1.rpc.rebalancestop()
assert result['message'] == "No rebalance is running, nothing to stop."
# check the rebalanceall starts
result = l1.rpc.rebalanceall(feeratio=5.0) # we need high fees to work
assert result['message'].startswith('Rebalance started')
l1.daemon.wait_for_logs([f"tries to rebalance: {scid12} -> {scid31}",
f"Automatic rebalance finished"])
# check additional calls to stop return 'nothing to stop' + last message
result = l1.rpc.rebalancestop()['message']
assert result.startswith("No rebalance is running, nothing to stop. "
"Last 'rebalanceall' gave: Automatic rebalance finished")
# wait until listpeers is up2date
wait_for_all_htlcs(nodes)
# check that channels are now balanced
c12 = l1.rpc.listpeerchannels(l2.info['id'])['channels'][0]
c13 = l1.rpc.listpeerchannels(l3.info['id'])['channels'][0]
assert abs(0.5 - (Millisatoshi(c12['to_us_msat']) / Millisatoshi(c12['total_msat']))) < 0.01
assert abs(0.5 - (Millisatoshi(c13['to_us_msat']) / Millisatoshi(c13['total_msat']))) < 0.01
# briefly check rebalancereport works
report = l1.rpc.rebalancereport()
assert report.get('rebalanceall_is_running') is False
assert report.get('total_successful_rebalances') == 2