diff --git a/charge_lnd/charge_lnd.py b/charge_lnd/charge_lnd.py index 630edb3..83bc6b9 100755 --- a/charge_lnd/charge_lnd.py +++ b/charge_lnd/charge_lnd.py @@ -9,6 +9,7 @@ from .lnd import Lnd from .policy import Policies +from .strategy import is_defined from .config import Config from .electrum import Electrum import charge_lnd.fmt as fmt @@ -51,7 +52,7 @@ def main(): if not policy: continue - (new_base_fee_msat, new_fee_ppm, new_inbound_base_fee_msat, new_inbound_fee_ppm, new_min_htlc, new_max_htlc, new_time_lock_delta, disable) = policy.strategy.execute(channel) + chp = policy.strategy.execute(channel) if channel.chan_id in lnd.feereport: (current_base_fee_msat, current_fee_ppm) = lnd.feereport[channel.chan_id] @@ -60,26 +61,33 @@ def main(): if not chan_info: print ("could not lookup channel info for " + fmt.print_chanid(channel.chan_id).ljust(14) + ", skipping") continue + my_policy = chan_info.node1_policy if chan_info.node1_pub == my_pubkey else chan_info.node2_policy min_fee_ppm_delta = policy.getint('min_fee_ppm_delta',0) - fee_ppm_changed = new_fee_ppm is not None and current_fee_ppm != new_fee_ppm and abs(current_fee_ppm - new_fee_ppm) >= min_fee_ppm_delta - inbound_fee_ppm_changed = new_inbound_fee_ppm is not None and my_policy.inbound_fee_rate_milli_msat != new_inbound_fee_ppm and \ - abs(my_policy.inbound_fee_rate_milli_msat - new_inbound_fee_ppm) >= min_fee_ppm_delta + fee_ppm_changed = is_defined(chp.fee_ppm) and current_fee_ppm != chp.fee_ppm and abs(current_fee_ppm - chp.fee_ppm) >= min_fee_ppm_delta + base_fee_changed = is_defined(chp.base_fee_msat) and current_base_fee_msat != chp.base_fee_msat + + inbound_fee_ppm_changed = lnd.supports_inbound_fees() \ + and is_defined(chp.inbound_fee_ppm) \ + and my_policy.inbound_fee_rate_milli_msat != chp.inbound_fee_ppm \ + and abs(my_policy.inbound_fee_rate_milli_msat - chp.inbound_fee_ppm) >= min_fee_ppm_delta + + inbound_base_fee_changed = lnd.supports_inbound_fees() \ + and is_defined(chp.inbound_base_fee_msat) \ + and my_policy.inbound_fee_base_msat != chp.inbound_base_fee_msat - base_fee_changed = new_base_fee_msat is not None and current_base_fee_msat != new_base_fee_msat - inbound_base_fee_changed = new_inbound_base_fee_msat is not None and my_policy.inbound_fee_base_msat != new_inbound_base_fee_msat - min_htlc_changed = new_min_htlc is not None and my_policy.min_htlc != new_min_htlc - max_htlc_changed = new_max_htlc is not None and my_policy.max_htlc_msat != new_max_htlc - time_lock_delta_changed = new_time_lock_delta is not None and my_policy.time_lock_delta != new_time_lock_delta + min_htlc_changed = is_defined(chp.min_htlc_msat) and my_policy.min_htlc != chp.min_htlc_msat + max_htlc_changed = is_defined(chp.max_htlc_msat) and my_policy.max_htlc_msat != chp.max_htlc_msat + time_lock_delta_changed = is_defined(chp.time_lock_delta) and my_policy.time_lock_delta != chp.time_lock_delta is_changed = fee_ppm_changed or base_fee_changed or min_htlc_changed or max_htlc_changed or \ - time_lock_delta_changed or inbound_base_fee_changed + inbound_fee_ppm_changed + time_lock_delta_changed or inbound_base_fee_changed or inbound_fee_ppm_changed chan_status_changed = False - if lnd.min_version(0,13) and channel.active and disable != my_policy.disabled and policy.get('strategy') != 'ignore': + if lnd.min_version(0, 13) and channel.active and chp.disabled != my_policy.disabled and policy.get('strategy') != 'ignore': if not arguments.dry_run: - lnd.update_chan_status(channel.chan_id, disable) + lnd.update_chan_status(channel.chan_id, chp.disabled) chan_status_changed = True if is_changed or chan_status_changed or arguments.verbose: @@ -89,8 +97,7 @@ def main(): ) if is_changed and not arguments.dry_run: - lnd.update_chan_policy(channel.chan_id, new_base_fee_msat, new_fee_ppm, new_min_htlc, - new_max_htlc, new_time_lock_delta, new_inbound_base_fee_msat, new_inbound_fee_ppm) + lnd.update_chan_policy(channel.chan_id, chp) if is_changed or chan_status_changed or arguments.verbose: print(" policy: %s" % fmt.col_hi(policy.name) ) @@ -99,46 +106,46 @@ def main(): s = 'disabled' if my_policy.disabled else 'enabled' if chan_status_changed: s = s + ' ➜ ' - s = s + 'disabled' if disable else 'enabled' + s = s + 'disabled' if chp.disabled else 'enabled' print(" channel status: %s" % fmt.col_hi(s)) - if new_base_fee_msat is not None or arguments.verbose: + if is_defined(chp.base_fee_msat) or arguments.verbose: s = '' if base_fee_changed: - s = ' ➜ ' + fmt.col_hi(new_base_fee_msat) + s = ' ➜ ' + fmt.col_hi(chp.base_fee_msat) print(" base_fee_msat: %s%s" % (fmt.col_hi(current_base_fee_msat), s) ) - if new_fee_ppm is not None or arguments.verbose: + if is_defined(chp.fee_ppm) or arguments.verbose: s = '' if fee_ppm_changed: - s = ' ➜ ' + fmt.col_hi(new_fee_ppm) - if min_fee_ppm_delta > abs(new_fee_ppm - current_fee_ppm): + s = ' ➜ ' + fmt.col_hi(chp.fee_ppm) + if min_fee_ppm_delta > abs(chp.fee_ppm - current_fee_ppm): s = s + ' (min_fee_ppm_delta=%d)' % min_fee_ppm_delta print(" fee_ppm: %s%s" % (fmt.col_hi(current_fee_ppm), s) ) - if new_inbound_base_fee_msat is not None or arguments.verbose: + if is_defined(chp.inbound_base_fee_msat) or arguments.verbose: s = '' if inbound_base_fee_changed: - s = ' ➜ ' + fmt.col_hi(new_inbound_base_fee_msat) + s = ' ➜ ' + fmt.col_hi(chp.inbound_base_fee_msat) print(" inbound_base_fee_msat: %s%s" % (fmt.col_hi(my_policy.inbound_fee_base_msat), s) ) - if new_inbound_fee_ppm is not None or arguments.verbose: + if is_defined(chp.inbound_fee_ppm) or arguments.verbose: s = '' if inbound_fee_ppm_changed: - s = ' ➜ ' + fmt.col_hi(new_inbound_fee_ppm) - if min_fee_ppm_delta > abs(new_inbound_fee_ppm - my_policy.inbound_fee_rate_milli_msat): + s = ' ➜ ' + fmt.col_hi(chp.inbound_fee_ppm) + if min_fee_ppm_delta > abs(chp.inbound_fee_ppm - my_policy.inbound_fee_rate_milli_msat): s = s + ' (min_fee_ppm_delta=%d)' % min_fee_ppm_delta print(" inbound_fee_ppm: %s%s" % (fmt.col_hi(my_policy.inbound_fee_rate_milli_msat), s) ) - if new_min_htlc is not None or arguments.verbose: + if is_defined(chp.min_htlc_msat) or arguments.verbose: s = '' if min_htlc_changed: - s = ' ➜ ' + fmt.col_hi(new_min_htlc) + s = ' ➜ ' + fmt.col_hi(chp.min_htlc_msat) print(" min_htlc_msat: %s%s" % (fmt.col_hi(my_policy.min_htlc), s) ) - if new_max_htlc is not None or arguments.verbose: + if is_defined(chp.max_htlc_msat) or arguments.verbose: s = '' if max_htlc_changed: - s = ' ➜ ' + fmt.col_hi(new_max_htlc) + s = ' ➜ ' + fmt.col_hi(chp.max_htlc_msat) print(" max_htlc_msat: %s%s" % (fmt.col_hi(my_policy.max_htlc_msat), s) ) - if new_time_lock_delta is not None or arguments.verbose: + if is_defined(chp.time_lock_delta) or arguments.verbose: s = '' if time_lock_delta_changed: - s = ' ➜ ' + fmt.col_hi(new_time_lock_delta) + s = ' ➜ ' + fmt.col_hi(chp.time_lock_delta) print(" time_lock_delta: %s%s" % (fmt.col_hi(my_policy.time_lock_delta), s) ) return True diff --git a/charge_lnd/lnd.py b/charge_lnd/lnd.py index 2e892af..f662c67 100644 --- a/charge_lnd/lnd.py +++ b/charge_lnd/lnd.py @@ -10,6 +10,8 @@ from .grpc_generated import lightning_pb2_grpc as lnrpc, lightning_pb2 as ln from .grpc_generated import router_pb2_grpc as routerrpc, router_pb2 as router +from .strategy import ChanParams, KEEP, DONTCARE + MESSAGE_SIZE_MB = 50 * 1024 * 1024 @@ -27,10 +29,11 @@ def __init__(self, lnd_dir, server, tls_cert_path=None, macaroon_path=None): ('grpc.max_receive_message_length', MESSAGE_SIZE_MB) ] grpc_channel = grpc.secure_channel(server, combined_credentials, channel_options) - self.stub = lnrpc.LightningStub(grpc_channel) + self.lnstub = lnrpc.LightningStub(grpc_channel) self.routerstub = routerrpc.RouterStub(grpc_channel) self.graph = None self.info = None + self.version = None self.channels = None self.node_info = {} self.chan_info = {} @@ -59,11 +62,14 @@ def get_credentials(lnd_dir, tls_cert_path, macaroon_path): def get_info(self): if self.info is None: - self.info = self.stub.GetInfo(ln.GetInfoRequest()) + self.info = self.lnstub.GetInfo(ln.GetInfoRequest()) return self.info + def supports_inbound_fees(self): + return self.min_version(0, 18) + def get_feereport(self): - feereport = self.stub.FeeReport(ln.FeeReportRequest()) + feereport = self.lnstub.FeeReport(ln.FeeReportRequest()) feedict = {} for channel_fee in feereport.channel_fees: feedict[channel_fee.chan_id] = (channel_fee.base_fee_msat, channel_fee.fee_per_mil) @@ -82,7 +88,7 @@ def get_forward_history(self, chanid, seconds): done = False thishistory = {} while not done: - forwards = self.stub.ForwardingHistory(ln.ForwardingHistoryRequest( + forwards = self.lnstub.ForwardingHistory(ln.ForwardingHistoryRequest( start_time=start_time, end_time=last_time, index_offset=index_offset)) if forwards.forwarding_events: for forward in forwards.forwarding_events: @@ -128,20 +134,19 @@ def get_forward_history(self, chanid, seconds): def get_node_info(self, nodepubkey): if not nodepubkey in self.node_info: - self.node_info[nodepubkey] = self.stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=nodepubkey)) + self.node_info[nodepubkey] = self.lnstub.GetNodeInfo(ln.NodeInfoRequest(pub_key=nodepubkey)) return self.node_info[nodepubkey] def get_chan_info(self, chanid): if not chanid in self.chan_info: try: - self.chan_info[chanid] = self.stub.GetChanInfo(ln.ChanInfoRequest(chan_id=chanid)) + self.chan_info[chanid] = self.lnstub.GetChanInfo(ln.ChanInfoRequest(chan_id=chanid)) except: print("Failed to lookup {}".format(chanid),file=sys.stderr) return None return self.chan_info[chanid] - def update_chan_policy(self, chanid, base_fee_msat, fee_ppm, min_htlc_msat, max_htlc_msat, - time_lock_delta, inbound_base_fee_msat, inbound_fee_ppm): + def update_chan_policy(self, chanid, chp: ChanParams): chan_info = self.get_chan_info(chanid) if not chan_info: return None @@ -150,27 +155,27 @@ def update_chan_policy(self, chanid, base_fee_msat, fee_ppm, min_htlc_msat, max_ output_index=int(chan_info.chan_point.split(':')[1]) ) my_policy = chan_info.node1_policy if chan_info.node1_pub == self.get_own_pubkey() else chan_info.node2_policy - return self.stub.UpdateChannelPolicy(ln.PolicyUpdateRequest( + return self.lnstub.UpdateChannelPolicy(ln.PolicyUpdateRequest( chan_point=channel_point, - base_fee_msat=(base_fee_msat if base_fee_msat is not None else my_policy.fee_base_msat), - fee_rate=fee_ppm/1000000 if fee_ppm is not None else my_policy.fee_rate_milli_msat/1000000, - min_htlc_msat=(min_htlc_msat if min_htlc_msat is not None else my_policy.min_htlc), - min_htlc_msat_specified=min_htlc_msat is not None, - max_htlc_msat=(max_htlc_msat if max_htlc_msat is not None else my_policy.max_htlc_msat), - time_lock_delta=(time_lock_delta if time_lock_delta is not None else my_policy.time_lock_delta), - inbound_base_fee_msat=(inbound_base_fee_msat if inbound_base_fee_msat is not None else my_policy.inbound_fee_base_msat), - inbound_fee_rate_ppm=(inbound_fee_ppm if inbound_fee_ppm is not None else my_policy.inbound_fee_rate_milli_msat) + base_fee_msat=(chp.base_fee_msat if chp.base_fee_msat is not None else my_policy.fee_base_msat), + fee_rate=chp.fee_ppm/1000000 if chp.fee_ppm is not None else my_policy.fee_rate_milli_msat/1000000, + min_htlc_msat=(chp.min_htlc_msat if chp.min_htlc_msat is not None else my_policy.min_htlc), + min_htlc_msat_specified=chp.min_htlc_msat is not None, + max_htlc_msat=(chp.max_htlc_msat if chp.max_htlc_msat is not None else my_policy.max_htlc_msat), + time_lock_delta=(chp.time_lock_delta if chp.time_lock_delta is not None else my_policy.time_lock_delta), + inbound_base_fee_msat=(chp.inbound_base_fee_msat if chp.inbound_base_fee_msat is not None else my_policy.inbound_fee_base_msat), + inbound_fee_rate_ppm=(chp.inbound_fee_ppm if chp.inbound_fee_ppm is not None else my_policy.inbound_fee_rate_milli_msat) )) def get_txns(self, start_height = None, end_height = None): - return self.stub.GetTransactions(ln.GetTransactionsRequest( + return self.lnstub.GetTransactions(ln.GetTransactionsRequest( start_height=start_height, end_height=end_height )) def get_graph(self): if self.graph is None: - self.graph = self.stub.DescribeGraph(ln.ChannelGraphRequest(include_unannounced=True)) + self.graph = self.lnstub.DescribeGraph(ln.ChannelGraphRequest(include_unannounced=True)) return self.graph def get_own_pubkey(self): @@ -182,7 +187,7 @@ def get_edges(self): def get_channels(self): if self.channels is None: request = ln.ListChannelsRequest() - self.channels = self.stub.ListChannels(request).channels + self.channels = self.lnstub.ListChannels(request).channels return self.channels # Get all channels shared with a node @@ -191,7 +196,7 @@ def get_shared_channels(self, peerid): byte_peerid=bytes.fromhex(peerid) if peerid not in self.peer_channels: request = ln.ListChannelsRequest(peer=byte_peerid) - self.peer_channels[peerid] = self.stub.ListChannels(request).channels + self.peer_channels[peerid] = self.lnstub.ListChannels(request).channels return self.peer_channels[peerid] def min_version(self, major, minor, patch=0): diff --git a/charge_lnd/policy.py b/charge_lnd/policy.py index b12d403..4f2d561 100644 --- a/charge_lnd/policy.py +++ b/charge_lnd/policy.py @@ -2,7 +2,7 @@ import sys import re import time -from .strategy import StrategyDelegate +from .strategy import StrategyDelegate, DONTCARE from . import fmt def debug(message): diff --git a/charge_lnd/strategy.py b/charge_lnd/strategy.py index 7f2ecc9..b6f6dec 100644 --- a/charge_lnd/strategy.py +++ b/charge_lnd/strategy.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 import sys import functools +from typing import Optional, Union +from types import SimpleNamespace +from enum import Enum from . import fmt from .config import Config @@ -9,6 +12,21 @@ def debug(message): sys.stderr.write(message + "\n") +KEEP='keep' +DONTCARE='dontcare' + +def is_defined(x): + return x not in [KEEP, DONTCARE] and x is not None + +class ChanParams(SimpleNamespace): + base_fee_msat: Optional[Union[str, int]] = DONTCARE + fee_ppm: Optional[Union[str, int]] = DONTCARE + min_htlc_msat: Optional[Union[str, int]] = DONTCARE + max_htlc_msat: Optional[Union[str, int]] = DONTCARE + time_lock_delta: Optional[Union[str, int]] = DONTCARE + inbound_base_fee_msat: Optional[Union[str, int]] = DONTCARE + inbound_fee_ppm: Optional[Union[str, int]] = DONTCARE + disabled: Optional[Union[str, bool]] = DONTCARE def strategy(_func=None,*,name): def register_strategy(func): @@ -32,19 +50,19 @@ def execute(self, channel): try: result = StrategyDelegate.STRATEGIES[strategy](channel, self.policy, name=self.policy.name, lnd=self.policy.lnd) - # set policy htlc limits if not overruled by the strategy - if len(result) == 4: - result = result + ( self.policy.getint('min_htlc_msat'), - self.effective_max_htlc_msat(channel), - self.policy.getint('time_lock_delta') ) - # disabled = False by default - if len(result) == 7: - result = result + ( False, ) + if result.min_htlc_msat == DONTCARE: + result.min_htlc_msat = self.policy.getint('min_htlc_msat') + if result.max_htlc_msat == DONTCARE: + result.max_htlc_msat = self.effective_max_htlc_msat(channel) + if result.time_lock_delta == DONTCARE: + result.time_lock_delta = self.policy.getint('time_lock_delta') + if result.disabled == DONTCARE: + result.disabled = False return result except Exception as e: debug("Error executing strategy '%s'. (Error=%s)" % (strategy, str(e)) ) - return strategy_ignore(channel, self.policy) + (False,) + return strategy_ignore(channel, self.policy) def effective_max_htlc_msat(self, channel): result = self.policy.getint('max_htlc_msat') @@ -62,16 +80,34 @@ def effective_max_htlc_msat(self, channel): @strategy(name = 'ignore') def strategy_ignore(channel, policy, **kwargs): - return (None, None, None, None, None, None, None) + return ChanParams( + base_fee_msat=KEEP, + fee_ppm=KEEP, + min_htlc_msat=KEEP, + max_htlc_msat=KEEP, + time_lock_delta=KEEP, + inbound_base_fee_msat=KEEP, + inbound_fee_ppm=KEEP, + disabled=KEEP + ) @strategy(name = 'ignore_fees') def strategy_ignore_fees(channel, policy, **kwargs): - return (None, None, None, None) + return ChanParams( + base_fee_msat=KEEP, + fee_ppm=KEEP, + inbound_base_fee_msat=KEEP, + inbound_fee_ppm=KEEP + ) @strategy(name = 'static') def strategy_static(channel, policy, **kwargs): - return (policy.getint('base_fee_msat'), policy.getint('fee_ppm'), - policy.getint('inbound_base_fee_msat'), policy.getint('inbound_fee_ppm')) + return ChanParams( + base_fee_msat=policy.getint('base_fee_msat'), + fee_ppm=policy.getint('fee_ppm'), + inbound_base_fee_msat=policy.getint('inbound_base_fee_msat'), + inbound_fee_ppm=policy.getint('inbound_fee_ppm') + ) @strategy(name = 'proportional') def strategy_proportional(channel, policy, **kwargs): @@ -108,7 +144,13 @@ def strategy_proportional(channel, policy, **kwargs): ppm = int(ppm_min + (1.0 - ratio) * (ppm_max - ppm_min)) # clamp to 0..inf ppm = max(ppm,0) - return (policy.getint('base_fee_msat'), ppm, None, None) + + return ChanParams( + base_fee_msat=policy.getint('base_fee_msat'), + fee_ppm=ppm, + inbound_base_fee_msat=policy.getint('inbound_base_fee_msat'), + inbound_fee_ppm=policy.getint('inbound_fee_ppm') + ) @strategy(name = 'match_peer') def strategy_match_peer(channel, policy, **kwargs): @@ -116,10 +158,13 @@ def strategy_match_peer(channel, policy, **kwargs): chan_info = lnd.get_chan_info(channel.chan_id) my_pubkey = lnd.get_own_pubkey() peernode_policy = chan_info.node1_policy if chan_info.node2_pub == my_pubkey else chan_info.node2_policy - return (policy.getint('base_fee_msat', peernode_policy.fee_base_msat), - policy.getint('fee_ppm', peernode_policy.fee_rate_milli_msat), - policy.getint('inbound_base_fee_msat', peernode_policy.inbound_fee_base_msat), - policy.getint('inbound_fee_ppm', peernode_policy.inbound_fee_rate_milli_msat)) + + return ChanParams( + base_fee_msat=policy.getint('base_fee_msat', peernode_policy.fee_base_msat), + fee_ppm=policy.getint('fee_ppm', peernode_policy.fee_rate_milli_msat), + inbound_base_fee_msat=policy.getint('inbound_base_fee_msat', peernode_policy.inbound_fee_base_msat), + inbound_fee_ppm=policy.getint('inbound_fee_ppm', peernode_policy.inbound_fee_rate_milli_msat) + ) @strategy(name = 'cost') def strategy_cost(channel, policy, **kwargs): @@ -138,7 +183,14 @@ def strategy_cost(channel, policy, **kwargs): ppm = int(policy.getfloat('cost_factor', 1.0) * 1_000_000 * chan_open_tx.total_fees / chan_info.capacity) else: ppm = 1 # tx not found, incoming channel, default to 1 - return (policy.getint('base_fee_msat'), ppm, None, None) + + return ChanParams( + base_fee_msat=policy.getint('base_fee_msat'), + fee_ppm=ppm, + inbound_base_fee_msat=policy.getint('inbound_base_fee_msat'), + inbound_fee_ppm=policy.getint('inbound_fee_ppm') + ) + @strategy(name = 'onchain_fee') def strategy_onchain_fee(channel, policy, **kwargs): @@ -154,7 +206,14 @@ def strategy_onchain_fee(channel, policy, **kwargs): return (None, None, None, None, None) reference_payment = policy.getfloat('onchain_fee_btc', 0.1) fee_ppm = int((0.01 / reference_payment) * (223 * sat_per_byte)) - return (policy.getint('base_fee_msat'), fee_ppm, None, None) + + return ChanParams( + base_fee_msat=policy.getint('base_fee_msat'), + fee_ppm=fee_ppm, + inbound_base_fee_msat=policy.getint('inbound_base_fee_msat'), + inbound_fee_ppm=policy.getint('inbound_fee_ppm') + ) + @strategy(name = 'use_config') def strategy_use_config(channel, policy, **kwargs): @@ -169,7 +228,7 @@ def strategy_use_config(channel, policy, **kwargs): ext_policy = policies.get_policy_for(channel) if not ext_policy: - return (None,None) + return ChanParams() r = ext_policy.strategy.execute(channel) @@ -187,4 +246,6 @@ def strategy_disable(channel, policy, **kwargs): if not lnd.min_version(0,13): raise Exception("Cannot use strategy 'disable', lnd must be at least version 0.13.0") - return strategy_ignore(channel, policy) + ( True, ) + chanparams = strategy_ignore(channel, policy) + chanparams.disabled=True + return chanparams diff --git a/examples/include-config.config b/examples/include-config.config new file mode 100644 index 0000000..aaac62e --- /dev/null +++ b/examples/include-config.config @@ -0,0 +1,3 @@ +[default] +strategy = use_config +config_file = examples/all-channels-static.config