mirror of
https://github.com/aljazceru/plugins.git
synced 2025-12-23 08:04:20 +01:00
rebalance plugin in python
- this plugin helps to move some liquidity between your channels using circular payments - previous conversations here: https://github.com/ElementsProject/lightning/pull/2567
This commit is contained in:
32
rebalance/README.md
Normal file
32
rebalance/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Rebalance plugin
|
||||
|
||||
This plugin helps to move some msatoshis between your channels using circular payments.
|
||||
|
||||
The plugin can be started with `lightningd` by adding the following `--plugin` option
|
||||
(adjusting the path to wherever the plugins are actually stored):
|
||||
|
||||
```
|
||||
lightningd --plugin=/path/to/plugins/rebalance.py
|
||||
```
|
||||
|
||||
Once the plugin is active you can rebalance your channels liquidity by running:
|
||||
`lightning-cli rebalance outgoing_channel_id incoming_channel_id msatoshi [maxfeepercent] [retry_for] [exemptfee]`
|
||||
|
||||
The `outgoing_channel_id` is the short_channel_id of the sending channel, `incoming_channel_id` is the id of the
|
||||
receiving channel. The `maxfeepercent` limits the money paid in fees and defaults to 0.5. The maxfeepercent' is a
|
||||
percentage of the amount that is to be paid. 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).
|
||||
|
||||
The command will keep finding routes and retrying the payment until it succeeds, or the given `retry_for` seconds
|
||||
pass. retry_for defaults to 60 seconds and can only be an integer.
|
||||
|
||||
### Tips and Tricks ###
|
||||
- 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.
|
||||
135
rebalance/rebalance.py
Executable file
135
rebalance/rebalance.py
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
from lightning import Plugin, RpcError
|
||||
import time
|
||||
import uuid
|
||||
|
||||
plugin = Plugin()
|
||||
|
||||
|
||||
def setup_routing_fees(plugin, route, msatoshi):
|
||||
delay = int(plugin.get_option('cltv-final'))
|
||||
for r in reversed(route):
|
||||
r['msatoshi'] = msatoshi
|
||||
r['amount_msat'] = str(msatoshi) + "msat"
|
||||
r['delay'] = delay
|
||||
channels = plugin.rpc.listchannels(r['channel'])
|
||||
for ch in channels.get('channels'):
|
||||
if ch['destination'] == r['id']:
|
||||
fee = ch['base_fee_millisatoshi']
|
||||
fee += msatoshi * ch['fee_per_millionth'] // 1000000
|
||||
msatoshi += fee
|
||||
delay += ch['delay']
|
||||
|
||||
|
||||
def peer2channel(plugin, channel_id, my_node_id, payload):
|
||||
channels = plugin.rpc.listchannels(channel_id).get('channels')
|
||||
for ch in channels:
|
||||
if ch['source'] == my_node_id:
|
||||
return ch['destination']
|
||||
raise RpcError("rebalance", payload, {'message': 'Cannot find peer for channel: ' + channel_id})
|
||||
|
||||
|
||||
def find_worst_channel(route):
|
||||
if len(route) < 4:
|
||||
return None
|
||||
start_id = 2
|
||||
worst = route[start_id]['channel']
|
||||
worst_val = route[start_id - 1]['msatoshi'] - route[start_id]['msatoshi']
|
||||
for i in range(start_id + 1, len(route) - 1):
|
||||
val = route[i - 1]['msatoshi'] - route[i]['msatoshi']
|
||||
if val > worst_val:
|
||||
worst = route[i]['channel']
|
||||
worst_val = val
|
||||
return worst
|
||||
|
||||
|
||||
def rebalance_fail(plugin, label, payload, success_msg, error=None):
|
||||
try:
|
||||
plugin.rpc.delinvoice(label, 'unpaid')
|
||||
except RpcError as e:
|
||||
# race condition: waitsendpay timed out, but invoice get paid
|
||||
if 'status is paid' in e.error.get('message', ""):
|
||||
return success_msg
|
||||
if error is None:
|
||||
error = RpcError("rebalance", payload, {'message': 'Rebalance failed'})
|
||||
raise error
|
||||
|
||||
|
||||
@plugin.method("rebalance")
|
||||
def rebalance(plugin, outgoing_channel_id, incoming_channel_id, msatoshi, maxfeepercent="0.5",
|
||||
retry_for="60", exemptfee="5000"):
|
||||
"""Rebalancing channel liquidity with circular payments.
|
||||
|
||||
This tool helps to move some msatoshis between your channels.
|
||||
|
||||
"""
|
||||
payload = {
|
||||
"outgoing_channel_id": outgoing_channel_id,
|
||||
"incoming_channel_id": incoming_channel_id,
|
||||
"msatoshi": msatoshi,
|
||||
"maxfeepercent": maxfeepercent,
|
||||
"retry_for": retry_for,
|
||||
"exemptfee": exemptfee
|
||||
}
|
||||
my_node_id = plugin.rpc.getinfo().get('id')
|
||||
outgoing_node_id = peer2channel(plugin, outgoing_channel_id, my_node_id, payload)
|
||||
incoming_node_id = peer2channel(plugin, incoming_channel_id, my_node_id, payload)
|
||||
plugin.log("Outgoing node: %s, channel: %s" % (outgoing_node_id, outgoing_channel_id))
|
||||
plugin.log("Incoming node: %s, channel: %s" % (incoming_node_id, incoming_channel_id))
|
||||
|
||||
route_out = {'id': outgoing_node_id, 'channel': outgoing_channel_id}
|
||||
route_in = {'id': my_node_id, 'channel': incoming_channel_id}
|
||||
start_ts = int(time.time())
|
||||
label = "Rebalance-" + str(uuid.uuid4())
|
||||
description = "%s to %s" % (outgoing_channel_id, incoming_channel_id)
|
||||
invoice = plugin.rpc.invoice(msatoshi, label, description, int(retry_for) + 60)
|
||||
payment_hash = invoice['payment_hash']
|
||||
plugin.log("Invoice payment_hash: %s" % payment_hash)
|
||||
success_msg = ""
|
||||
try:
|
||||
excludes = [outgoing_channel_id + "/0", incoming_channel_id + "/0"]
|
||||
while int(time.time()) - start_ts < int(retry_for):
|
||||
r = plugin.rpc.getroute(incoming_node_id, msatoshi, riskfactor=1, cltv=9, fromid=outgoing_node_id,
|
||||
exclude=excludes)
|
||||
route_mid = r['route']
|
||||
route = [route_out] + route_mid + [route_in]
|
||||
setup_routing_fees(plugin, route, msatoshi)
|
||||
fees = route[0]['msatoshi'] - route[-1]['msatoshi']
|
||||
if fees > int(exemptfee) and fees > msatoshi * float(maxfeepercent) / 100:
|
||||
worst_channel_id = find_worst_channel(route)
|
||||
if worst_channel_id is None:
|
||||
raise RpcError("rebalance", payload, {'message': 'Insufficient fee'})
|
||||
excludes += [worst_channel_id + '/0', worst_channel_id + '/1']
|
||||
continue
|
||||
try:
|
||||
plugin.log("Sending %dmsat over %d hops to rebalance %dmsat" % (msatoshi + fees, len(route), msatoshi))
|
||||
for r in route:
|
||||
plugin.log("Node: %s, channel: %13s, %d msat" % (r['id'], r['channel'], r['msatoshi']))
|
||||
success_msg = "%d msat sent over %d hops to rebalance %d msat" % (msatoshi + fees, len(route), msatoshi)
|
||||
plugin.rpc.sendpay(route, payment_hash)
|
||||
plugin.rpc.waitsendpay(payment_hash, int(retry_for) + start_ts - int(time.time()))
|
||||
return success_msg
|
||||
except RpcError as e:
|
||||
plugin.log("RpcError: " + str(e))
|
||||
erring_channel = e.error.get('data', {}).get('erring_channel')
|
||||
if erring_channel == incoming_channel_id:
|
||||
raise RpcError("rebalance", payload, {'message': 'Error with incoming channel'})
|
||||
if erring_channel == outgoing_channel_id:
|
||||
raise RpcError("rebalance", payload, {'message': 'Error with outgoing channel'})
|
||||
erring_direction = e.error.get('data', {}).get('erring_direction')
|
||||
if erring_channel is not None and erring_direction is not None:
|
||||
excludes.append(erring_channel + '/' + str(erring_direction))
|
||||
except Exception as e:
|
||||
plugin.log("Exception: " + str(e))
|
||||
return rebalance_fail(plugin, label, payload, success_msg, e)
|
||||
return rebalance_fail(plugin, label, payload, success_msg)
|
||||
|
||||
|
||||
@plugin.init()
|
||||
def init(options, configuration, plugin):
|
||||
plugin.options['cltv-final']['value'] = plugin.rpc.listconfigs().get('cltv-final')
|
||||
plugin.log("Plugin rebalance.py initialized")
|
||||
|
||||
|
||||
plugin.add_option('cltv-final', 10, 'Number of blocks for final CheckLockTimeVerify expiry')
|
||||
plugin.run()
|
||||
Reference in New Issue
Block a user