Rebalance-lnd

Added files from Otto's --> https://github.com/C-Otto/rebalance-lnd
This commit is contained in:
curly60e 2020-10-12 17:51:39 -03:00 committed by GitHub
parent 0d0c159dbc
commit fdef46dda0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 13506 additions and 0 deletions

1448
grpc_generated/router_pb2.py Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,184 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
import grpc
from grpc_generated import router_pb2 as router__pb2
class RouterStub(object):
# missing associated documentation comment in .proto file
pass
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.SendPayment = channel.unary_stream(
'/routerrpc.Router/SendPayment',
request_serializer=router__pb2.SendPaymentRequest.SerializeToString,
response_deserializer=router__pb2.PaymentStatus.FromString,
)
self.TrackPayment = channel.unary_stream(
'/routerrpc.Router/TrackPayment',
request_serializer=router__pb2.TrackPaymentRequest.SerializeToString,
response_deserializer=router__pb2.PaymentStatus.FromString,
)
self.EstimateRouteFee = channel.unary_unary(
'/routerrpc.Router/EstimateRouteFee',
request_serializer=router__pb2.RouteFeeRequest.SerializeToString,
response_deserializer=router__pb2.RouteFeeResponse.FromString,
)
self.SendToRoute = channel.unary_unary(
'/routerrpc.Router/SendToRoute',
request_serializer=router__pb2.SendToRouteRequest.SerializeToString,
response_deserializer=router__pb2.SendToRouteResponse.FromString,
)
self.ResetMissionControl = channel.unary_unary(
'/routerrpc.Router/ResetMissionControl',
request_serializer=router__pb2.ResetMissionControlRequest.SerializeToString,
response_deserializer=router__pb2.ResetMissionControlResponse.FromString,
)
self.QueryMissionControl = channel.unary_unary(
'/routerrpc.Router/QueryMissionControl',
request_serializer=router__pb2.QueryMissionControlRequest.SerializeToString,
response_deserializer=router__pb2.QueryMissionControlResponse.FromString,
)
self.QueryProbability = channel.unary_unary(
'/routerrpc.Router/QueryProbability',
request_serializer=router__pb2.QueryProbabilityRequest.SerializeToString,
response_deserializer=router__pb2.QueryProbabilityResponse.FromString,
)
self.BuildRoute = channel.unary_unary(
'/routerrpc.Router/BuildRoute',
request_serializer=router__pb2.BuildRouteRequest.SerializeToString,
response_deserializer=router__pb2.BuildRouteResponse.FromString,
)
class RouterServicer(object):
# missing associated documentation comment in .proto file
pass
def SendPayment(self, request, context):
"""*
SendPayment attempts to route a payment described by the passed
PaymentRequest to the final destination. The call returns a stream of
payment status updates.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def TrackPayment(self, request, context):
"""*
TrackPayment returns an update stream for the payment identified by the
payment hash.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def EstimateRouteFee(self, request, context):
"""*
EstimateRouteFee allows callers to obtain a lower bound w.r.t how much it
may cost to send an HTLC to the target end destination.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def SendToRoute(self, request, context):
"""*
SendToRoute attempts to make a payment via the specified route. This method
differs from SendPayment in that it allows users to specify a full route
manually. This can be used for things like rebalancing, and atomic swaps.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def ResetMissionControl(self, request, context):
"""*
ResetMissionControl clears all mission control state and starts with a clean
slate.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def QueryMissionControl(self, request, context):
"""*
QueryMissionControl exposes the internal mission control state to callers.
It is a development feature.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def QueryProbability(self, request, context):
"""*
QueryProbability returns the current success probability estimate for a
given node pair and amount.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def BuildRoute(self, request, context):
"""*
BuildRoute builds a fully specified route based on a list of hop public
keys. It retrieves the relevant channel policies from the graph in order to
calculate the correct fees and time locks.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_RouterServicer_to_server(servicer, server):
rpc_method_handlers = {
'SendPayment': grpc.unary_stream_rpc_method_handler(
servicer.SendPayment,
request_deserializer=router__pb2.SendPaymentRequest.FromString,
response_serializer=router__pb2.PaymentStatus.SerializeToString,
),
'TrackPayment': grpc.unary_stream_rpc_method_handler(
servicer.TrackPayment,
request_deserializer=router__pb2.TrackPaymentRequest.FromString,
response_serializer=router__pb2.PaymentStatus.SerializeToString,
),
'EstimateRouteFee': grpc.unary_unary_rpc_method_handler(
servicer.EstimateRouteFee,
request_deserializer=router__pb2.RouteFeeRequest.FromString,
response_serializer=router__pb2.RouteFeeResponse.SerializeToString,
),
'SendToRoute': grpc.unary_unary_rpc_method_handler(
servicer.SendToRoute,
request_deserializer=router__pb2.SendToRouteRequest.FromString,
response_serializer=router__pb2.SendToRouteResponse.SerializeToString,
),
'ResetMissionControl': grpc.unary_unary_rpc_method_handler(
servicer.ResetMissionControl,
request_deserializer=router__pb2.ResetMissionControlRequest.FromString,
response_serializer=router__pb2.ResetMissionControlResponse.SerializeToString,
),
'QueryMissionControl': grpc.unary_unary_rpc_method_handler(
servicer.QueryMissionControl,
request_deserializer=router__pb2.QueryMissionControlRequest.FromString,
response_serializer=router__pb2.QueryMissionControlResponse.SerializeToString,
),
'QueryProbability': grpc.unary_unary_rpc_method_handler(
servicer.QueryProbability,
request_deserializer=router__pb2.QueryProbabilityRequest.FromString,
response_serializer=router__pb2.QueryProbabilityResponse.SerializeToString,
),
'BuildRoute': grpc.unary_unary_rpc_method_handler(
servicer.BuildRoute,
request_deserializer=router__pb2.BuildRouteRequest.FromString,
response_serializer=router__pb2.BuildRouteResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'routerrpc.Router', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))

10187
grpc_generated/rpc_pb2.py Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

256
rebalance.py Normal file
View file

@ -0,0 +1,256 @@
#!/usr/bin/env python
import argparse
import math
import os
import platform
import sys
from lnd import Lnd
from logic import Logic
MAX_CHANNEL_CAPACITY = 16777215
MAX_SATOSHIS_PER_TRANSACTION = 4294967
def main():
argument_parser = get_argument_parser()
arguments = argument_parser.parse_args()
lnd = Lnd(arguments.lnddir, arguments.grpc)
first_hop_channel_id = vars(arguments)['from']
to_channel = arguments.to
if arguments.ratio < 1 or arguments.ratio > 50:
print("--ratio must be between 1 and 50")
sys.exit(1)
channel_ratio = float(arguments.ratio) / 100
if arguments.incoming is not None and not arguments.list_candidates:
print("--outgoing and --incoming only work in conjunction with --list-candidates")
sys.exit(1)
if arguments.list_candidates:
incoming = arguments.incoming is None or arguments.incoming
if incoming:
list_incoming_candidates(lnd, channel_ratio)
else:
list_outgoing_candidates(lnd, channel_ratio)
sys.exit(0)
if to_channel is None and first_hop_channel_id is None:
argument_parser.print_help()
sys.exit(1)
percentage = arguments.percentage
if percentage:
if percentage < 1 or percentage > 100:
print("--percentage must be between 1 and 100")
argument_parser.print_help()
sys.exit(1)
# the 'to' argument might be an index, or a channel ID
if to_channel and to_channel < 10000:
# here we are in the "channel index" case
index = int(to_channel) - 1
candidates = get_incoming_rebalance_candidates(lnd, channel_ratio)
candidate = candidates[index]
last_hop_channel = candidate
else:
# else the channel argument should be the channel ID
last_hop_channel = get_channel_for_channel_id(lnd, to_channel)
first_hop_channel = get_channel_for_channel_id(lnd, first_hop_channel_id)
amount = get_amount(arguments, first_hop_channel, last_hop_channel)
if amount == 0:
print("Amount is 0, nothing to do")
sys.exit(0)
max_fee_factor = arguments.max_fee_factor
excluded = arguments.exclude
return Logic(lnd, first_hop_channel, last_hop_channel, amount, channel_ratio, excluded,
max_fee_factor).rebalance()
def get_amount(arguments, first_hop_channel, last_hop_channel):
if last_hop_channel:
amount = get_rebalance_amount(last_hop_channel)
else:
amount = get_rebalance_amount(first_hop_channel)
if arguments.percentage:
amount = int(round(amount * arguments.percentage / 100))
if last_hop_channel and first_hop_channel:
rebalance_amount_from_channel = get_rebalance_amount(first_hop_channel)
amount = min(amount, rebalance_amount_from_channel)
if arguments.amount:
amount = min(amount, int(arguments.amount))
amount = min(amount, MAX_SATOSHIS_PER_TRANSACTION)
return amount
def get_channel_for_channel_id(lnd, channel_id):
for channel in lnd.get_channels():
if channel.chan_id == channel_id:
return channel
return None
def get_argument_parser():
parser = argparse.ArgumentParser()
parser.add_argument("--lnddir",
default="~/.lnd",
dest="lnddir",
help="(default ~/.lnd) lnd directory")
parser.add_argument("--grpc",
default="localhost:10009",
dest="grpc",
help="(default localhost:10009) lnd gRPC endpoint")
parser.add_argument("-r", "--ratio",
type=int,
default=50,
help="(default: 50) ratio for channel imbalance between 1 and 50, "
"eg. 45 to only show channels (-l) with less than 45%% of the "
"funds on the local (-i) or remote (-o) side")
list_group = parser.add_argument_group("list candidates", "Show the unbalanced channels.")
list_group.add_argument("-l", "--list-candidates", action="store_true",
help="list candidate channels for rebalance")
direction_group = list_group.add_mutually_exclusive_group()
direction_group.add_argument("-o", "--outgoing",
action="store_const",
const=False,
dest="incoming",
help="lists channels with less than x%% of the funds on the remote side (see --ratio)")
direction_group.add_argument("-i", "--incoming",
action="store_const",
const=True,
dest="incoming",
help="(default) lists channels with less than x%% of the funds on the local side "
"(see --ratio)")
rebalance_group = parser.add_argument_group("rebalance",
"Rebalance a channel. You need to specify at least"
" the 'from' channel (-f) or the 'to' channel (-t).")
rebalance_group.add_argument("-f", "--from",
metavar="CHANNEL",
type=int,
help="channel ID of the outgoing channel "
"(funds will be taken from this channel)")
rebalance_group.add_argument("-t", "--to",
metavar="CHANNEL",
type=int,
help="channel ID of the incoming channel "
"(funds will be sent to this channel). "
"You may also use the index as shown in the incoming candidate list (-l -i).")
amount_group = rebalance_group.add_mutually_exclusive_group()
amount_group.add_argument("-a", "--amount",
type=int,
help="Amount of the rebalance, in satoshis. If not specified, "
"the amount computed for a perfect rebalance will be used"
" (up to the maximum of 4,294,967 satoshis)")
amount_group.add_argument("-p", "--percentage",
type=int,
help="Set the amount to send to a percentage of the amount required to rebalance. "
"As an example, if this is set to 50, the amount will half of the default. "
"See --amount.")
rebalance_group.add_argument("-e", "--exclude",
type=int,
action="append",
help="Exclude the given channel ID as the outgoing channel (no funds will be taken "
"out of excluded channels)")
rebalance_group.add_argument("--max-fee-factor",
type=float,
default=10,
help="(default: 10) Reject routes that cost more than x times the lnd default "
"(base: 1 sat, rate: 1 millionth sat) per hop on average")
return parser
def list_incoming_candidates(lnd, channel_ratio):
candidates = get_incoming_rebalance_candidates(lnd, channel_ratio)
list_candidates(candidates)
def list_outgoing_candidates(lnd, channel_ratio):
candidates = get_outgoing_rebalance_candidates(lnd, channel_ratio)
list_candidates(candidates)
def list_candidates(candidates):
index = 0
for candidate in candidates:
index += 1
rebalance_amount_int = get_rebalance_amount(candidate)
rebalance_amount = "{:,}".format(rebalance_amount_int)
if rebalance_amount_int > MAX_SATOSHIS_PER_TRANSACTION:
rebalance_amount += " (max per transaction: {:,})".format(MAX_SATOSHIS_PER_TRANSACTION)
print("(%2d) Channel ID: " % index + str(candidate.chan_id))
print("Pubkey: " + candidate.remote_pubkey)
print("Local ratio: {:.3f}".format(get_local_ratio(candidate)))
print("Capacity: {:,}".format(candidate.capacity))
print("Remote balance: {:,}".format(candidate.remote_balance))
print("Local balance: {:,}".format(candidate.local_balance))
print("Amount for 50-50: " + rebalance_amount)
print(get_capacity_and_ratio_bar(candidate))
print("")
def get_rebalance_amount(channel):
return abs(int(math.ceil(float(get_remote_surplus(channel)) / 2)))
def get_incoming_rebalance_candidates(lnd, channel_ratio):
low_local = list(filter(lambda c: get_local_ratio(c) < channel_ratio, lnd.get_channels()))
low_local = list(filter(lambda c: get_rebalance_amount(c) > 0, low_local))
return sorted(low_local, key=get_remote_surplus, reverse=False)
def get_outgoing_rebalance_candidates(lnd, channel_ratio):
high_local = list(filter(lambda c: get_local_ratio(c) > 1 - channel_ratio, lnd.get_channels()))
high_local = list(filter(lambda c: get_rebalance_amount(c) > 0, high_local))
return sorted(high_local, key=get_remote_surplus, reverse=True)
def get_local_ratio(channel):
remote = channel.remote_balance
local = channel.local_balance
return float(local) / (remote + local)
def get_remote_surplus(channel):
return channel.remote_balance - channel.local_balance
def get_capacity_and_ratio_bar(candidate):
columns = get_columns()
columns_scaled_to_capacity = int(round(columns * float(candidate.capacity) / MAX_CHANNEL_CAPACITY))
bar_width = columns_scaled_to_capacity - 2
result = "|"
ratio = get_local_ratio(candidate)
length = int(round(ratio * bar_width))
for x in range(0, length):
result += "="
for x in range(length, bar_width):
result += " "
return result + "|"
def get_columns():
if platform.system() == 'Linux' and sys.__stdin__.isatty():
return int(os.popen('stty size', 'r').read().split()[1])
else:
return 80
success = main()
if success:
sys.exit(0)
sys.exit(1)

115
routes.py Normal file
View file

@ -0,0 +1,115 @@
import base64
import sys
MAX_ROUTES_TO_REQUEST = 100
def debug(message):
sys.stderr.write(message + "\n")
def debugnobreak(message):
sys.stderr.write(message)
class Routes:
def __init__(self, lnd, payment_request, first_hop_channel, last_hop_channel):
self.lnd = lnd
self.payment_request = payment_request
self.first_hop_channel = first_hop_channel
self.last_hop_channel = last_hop_channel
self.all_routes = []
self.returned_routes = []
self.ignored_edges = []
self.ignored_nodes = []
self.num_requested_routes = 0
def has_next(self):
self.update_routes()
return self.returned_routes < self.all_routes
def get_next(self):
self.update_routes()
for route in self.all_routes:
if route not in self.returned_routes:
self.returned_routes.append(route)
return route
return None
def update_routes(self):
while True:
if self.returned_routes < self.all_routes:
return
if self.num_requested_routes >= MAX_ROUTES_TO_REQUEST:
return
self.request_route()
def request_route(self):
amount = self.get_amount()
if self.last_hop_channel:
last_hop_pubkey = self.last_hop_channel.remote_pubkey
else:
last_hop_pubkey = None
if self.first_hop_channel:
first_hop_channel_id = self.first_hop_channel.chan_id
else:
first_hop_channel_id = None
routes = self.lnd.get_route(last_hop_pubkey, amount, self.ignored_edges,
self.ignored_nodes, first_hop_channel_id)
if routes is None:
self.num_requested_routes = MAX_ROUTES_TO_REQUEST
else:
self.num_requested_routes += 1
for route in routes:
self.add_route(route)
def add_route(self, route):
if route is None:
return
if route not in self.all_routes:
self.all_routes.append(route)
@staticmethod
def print_route(route):
route_str = " -> ".join(str(h.chan_id) for h in route.hops)
return route_str
def get_amount(self):
return self.payment_request.num_satoshis
def ignore_first_hop(self, channel, show_message=True):
own_key = self.lnd.get_own_pubkey()
other_key = channel.remote_pubkey
self.ignore_edge_from_to(channel.chan_id, own_key, other_key, show_message)
def ignore_edge_on_route(self, failure_source_pubkey, route):
ignore_next = False
for hop in route.hops:
if ignore_next:
self.ignore_edge_from_to(hop.chan_id, failure_source_pubkey, hop.pub_key)
return
if hop.pub_key == failure_source_pubkey:
ignore_next = True
def ignore_node_with_highest_fee(self, route):
max_fee_msat = 0
max_fee_hop = None
for hop in route.hops:
if hop.fee_msat > max_fee_msat:
max_fee_msat = hop.fee_msat
max_fee_hop = hop
pub_key = max_fee_hop.pub_key
debugnobreak("High fees (%s msat), " % max_fee_msat)
self.ignore_node(pub_key)
def ignore_edge_from_to(self, chan_id, from_pubkey, to_pubkey, show_message=True):
if show_message:
debug("ignoring channel %s (from %s to %s)" % (chan_id, from_pubkey, to_pubkey))
direction_reverse = from_pubkey > to_pubkey
edge = {"channel_id": chan_id, "direction_reverse": direction_reverse}
self.ignored_edges.append(edge)
def ignore_node(self, pub_key):
debug("ignoring node %s" % pub_key)
self.ignored_nodes.append(base64.b16decode(pub_key, True))