pyblock/pybitblock/logic.py
2021-12-03 15:59:28 -06:00

153 lines
5.7 KiB
Python

import sys
from routes import Routes
DEFAULT_BASE_FEE_SAT_MSAT = 1000
DEFAULT_FEE_RATE_MSAT = 0.001
def debug(message):
sys.stderr.write(message + "\n")
def debugnobreak(message):
sys.stderr.write(message)
class Logic:
def __init__(self, lnd, first_hop_channel, last_hop_channel, amount, channel_ratio, excluded, max_fee_factor):
self.lnd = lnd
self.first_hop_channel = first_hop_channel
self.last_hop_channel = last_hop_channel
self.amount = amount
self.channel_ratio = channel_ratio
self.excluded = []
if excluded:
self.excluded = excluded
self.max_fee_factor = max_fee_factor
def rebalance(self):
if self.last_hop_channel:
debug(("Sending {:,} satoshis to rebalance to channel with ID %d"
% self.last_hop_channel.chan_id).format(self.amount))
else:
debug("Sending {:,} satoshis.".format(self.amount))
if self.channel_ratio != 0.5:
debug("Channel ratio used is %d%%" % int(self.channel_ratio * 100))
if self.first_hop_channel:
debug("Forced first channel has ID %d" % self.first_hop_channel.chan_id)
payment_request = self.generate_invoice()
routes = Routes(self.lnd, payment_request, self.first_hop_channel, self.last_hop_channel)
self.initialize_ignored_channels(routes)
tried_routes = []
while routes.has_next():
route = routes.get_next()
success = self.try_route(payment_request, route, routes, tried_routes)
if success:
return True
debug("Could not find any suitable route")
return False
def try_route(self, payment_request, route, routes, tried_routes):
if self.route_is_invalid(route, routes):
return False
tried_routes.append(route)
debug("")
debug("Trying route #%d" % len(tried_routes))
debug(Routes.print_route(route))
response = self.lnd.send_payment(payment_request, route)
is_successful = response.failure.code == 0
if is_successful:
debug("")
debug("")
debug("")
debug("Success! Paid fees: %s sat (%s msat)" % (route.total_fees, route.total_fees_msat))
debug("Successful route:")
debug(Routes.print_route(route))
debug("")
debug("")
debug("")
return True
else:
self.handle_error(response, route, routes)
return False
@staticmethod
def handle_error(response, route, routes):
code = response.failure.code
failure_source_pubkey = Logic.get_failure_source_pubkey(response, route)
if code == 15:
debugnobreak("Temporary channel failure, ")
routes.ignore_edge_on_route(failure_source_pubkey, route)
elif code == 18:
debugnobreak("Unknown next peer, ")
routes.ignore_edge_on_route(failure_source_pubkey, route)
elif code == 12:
debugnobreak("Fee insufficient, ")
routes.ignore_edge_on_route(failure_source_pubkey, route)
else:
debug(repr(response))
debug("Unknown error code %s" % repr(code))
@staticmethod
def get_failure_source_pubkey(response, route):
if response.failure.failure_source_index == 0:
failure_source_pubkey = route.hops[-1].pub_key
else:
failure_source_pubkey = route.hops[response.failure.failure_source_index - 1].pub_key
return failure_source_pubkey
def route_is_invalid(self, route, routes):
first_hop = route.hops[0]
if self.low_local_ratio_after_sending(first_hop, route.total_amt):
debugnobreak("Low local ratio after sending, ")
routes.ignore_first_hop(self.get_channel_for_channel_id(first_hop.chan_id))
return True
if self.fees_too_high(route):
routes.ignore_node_with_highest_fee(route)
return True
return False
def low_local_ratio_after_sending(self, first_hop, total_amount):
if self.first_hop_channel:
# Just use the computed/specified amount to drain the first hop, ignoring fees
return False
channel_id = first_hop.chan_id
channel = self.get_channel_for_channel_id(channel_id)
remote = channel.remote_balance + total_amount
local = channel.local_balance - total_amount
ratio = float(local) / (remote + local)
return ratio < self.channel_ratio
def fees_too_high(self, route):
hops_with_fees = len(route.hops) - 1
lnd_fees = hops_with_fees * (DEFAULT_BASE_FEE_SAT_MSAT + (self.amount * DEFAULT_FEE_RATE_MSAT))
limit = self.max_fee_factor * lnd_fees
return route.total_fees_msat > limit
def generate_invoice(self):
if self.last_hop_channel:
memo = "Rebalance of channel with ID %d" % self.last_hop_channel.chan_id
else:
memo = "Rebalance of channel with ID %d" % self.first_hop_channel.chan_id
return self.lnd.generate_invoice(memo, self.amount)
def get_channel_for_channel_id(self, channel_id):
for channel in self.lnd.get_channels():
if channel.chan_id == channel_id:
return channel
def initialize_ignored_channels(self, routes):
for channel in self.lnd.get_channels():
if self.low_local_ratio_after_sending(channel, self.amount):
routes.ignore_first_hop(channel, show_message=False)
if channel.chan_id in self.excluded:
debugnobreak("Channel is excluded, ")
routes.ignore_first_hop(channel)