From 6372dc2dab22e5c97791c8b83eadf4e7d4b7a56f Mon Sep 17 00:00:00 2001 From: feelancer21 <2828397+feelancer21@users.noreply.github.com> Date: Sun, 12 May 2024 00:48:49 +0200 Subject: [PATCH] Support for setting inbound fees --- README.md | 22 +++++++++++++--- charge_lnd/charge_lnd.py | 39 +++++++++++++++++++++-------- charge_lnd/lnd.py | 7 +++--- charge_lnd/strategy.py | 21 +++++++++------- examples/all-channels-static.config | 2 ++ examples/complex-ruleset.config | 2 ++ 6 files changed, 67 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 5685ef3..bb7c112 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,20 @@ This policy matches the channels against the `chan.min_capacity` criterium. Only If a channel matches this policy, the `static` strategy is then used, which takes the `base_fee_msat` and `fee_ppm` properties defined in the policy and applies them to the channel. +If at least lnd 0.18 is used, charge-lnd also supports the experimental support of inbound fees. By default, lnd only supports negative inbound fees on the inbound channel, which then act as a “discount” on the outbound fees of the outgoing channel. However, the entire forward fee cannot become negative. + +Example with inbound fees: +``` +[example-policy] +chan.min_capacity = 500000 + +strategy = static +base_fee_msat = 1000 +fee_ppm = 2000 +inbound_base_fee_msat = -500 +inbound_fee_ppm = -1000 +``` + ### Non-final policies You can also define a 'non-final' policy. This is simply a policy without a strategy. @@ -102,6 +116,8 @@ chan.min_capacity = 250000 strategy = static base_fee_msat = 10000 fee_ppm = 500 +inbound_base_fee_msat = -8000 +inbound_fee_ppm = -400 [encourage-routing-to-balance] chan.min_ratio = 0.9 @@ -187,11 +203,11 @@ Available strategies: |:--|:--|:--| |**ignore** | ignores the channel completely|| |**ignore_fees** | don't make any fee changes, only update htlc size limits and time_lock_delta|| -|**static** | sets fixed base fee and fee rate values.| **fee_ppm**| -|**match_peer** | sets the same base fee and fee rate values as the peer|if **base_fee_msat** or **fee_ppm** are set the override the peer values| +|**static** | sets fixed base fee and fee rate values for the outbound and inbound side.| **fee_ppm**
**base_fee_msat**
**inbound_fee_ppm**
**inbound_base_fee_msat**| +|**match_peer** | sets the same base fee and fee rate values as the peer for the outbound and inbound side.|if **base_fee_msat**, **fee_ppm**, **inbound_base_fee_msat** or **inbound_fee_ppm** are set the override the peer values| |**cost** | calculate cost for opening channel, and set ppm to cover cost when channel depletes.|**cost_factor**| |**onchain_fee** | sets the fees to a % equivalent of a standard onchain payment (Requires --electrum-server to be specified.)| **onchain_fee_btc** BTC
within **onchain_fee_numblocks** blocks.| -|**proportional** | sets fee ppm according to balancedness.|**min_fee_ppm**
**max_fee_ppm**
**sum_peer_chans** consider all channels with peer for balance calculations| +|**proportional** | sets outbound fee ppm according to balancedness. Inbound fee ppm keeps unchanged.|**min_fee_ppm**
**max_fee_ppm**
**sum_peer_chans** consider all channels with peer for balance calculations| |**disable** | disables the channel in the outgoing direction. Channel will be re-enabled again if it matches another policy (except when that policy uses an 'ignore' strategy).|| |**use_config** | process channel according to rules defined in another config file.|**config_file**| diff --git a/charge_lnd/charge_lnd.py b/charge_lnd/charge_lnd.py index acf64b2..630edb3 100755 --- a/charge_lnd/charge_lnd.py +++ b/charge_lnd/charge_lnd.py @@ -51,7 +51,7 @@ def main(): if not policy: continue - (new_base_fee_msat, new_fee_ppm, new_min_htlc, new_max_htlc, new_time_lock_delta, disable) = policy.strategy.execute(channel) + (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) if channel.chan_id in lnd.feereport: (current_base_fee_msat, current_fee_ppm) = lnd.feereport[channel.chan_id] @@ -65,12 +65,16 @@ def main(): 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 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 - is_changed = fee_ppm_changed or base_fee_changed or min_htlc_changed or max_htlc_changed or time_lock_delta_changed + 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 chan_status_changed = False if lnd.min_version(0,13) and channel.active and disable != my_policy.disabled and policy.get('strategy') != 'ignore': @@ -85,44 +89,57 @@ 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) + 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) if is_changed or chan_status_changed or arguments.verbose: - print(" policy: %s" % fmt.col_hi(policy.name) ) - print(" strategy: %s" % fmt.col_hi(policy.get('strategy')) ) + print(" policy: %s" % fmt.col_hi(policy.name) ) + print(" strategy: %s" % fmt.col_hi(policy.get('strategy')) ) if chan_status_changed or arguments.verbose: s = 'disabled' if my_policy.disabled else 'enabled' if chan_status_changed: s = s + ' ➜ ' s = s + 'disabled' if disable else 'enabled' - print(" channel status: %s" % fmt.col_hi(s)) + print(" channel status: %s" % fmt.col_hi(s)) if new_base_fee_msat is not None or arguments.verbose: s = '' if base_fee_changed: s = ' ➜ ' + fmt.col_hi(new_base_fee_msat) - print(" base_fee_msat: %s%s" % (fmt.col_hi(current_base_fee_msat), s) ) + print(" base_fee_msat: %s%s" % (fmt.col_hi(current_base_fee_msat), s) ) if new_fee_ppm is not None 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 = s + ' (min_fee_ppm_delta=%d)' % min_fee_ppm_delta - print(" fee_ppm: %s%s" % (fmt.col_hi(current_fee_ppm), s) ) + print(" fee_ppm: %s%s" % (fmt.col_hi(current_fee_ppm), s) ) + if new_inbound_base_fee_msat is not None or arguments.verbose: + s = '' + if inbound_base_fee_changed: + s = ' ➜ ' + fmt.col_hi(new_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: + 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 = 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: s = '' if min_htlc_changed: s = ' ➜ ' + fmt.col_hi(new_min_htlc) - print(" min_htlc_msat: %s%s" % (fmt.col_hi(my_policy.min_htlc), s) ) + print(" min_htlc_msat: %s%s" % (fmt.col_hi(my_policy.min_htlc), s) ) if new_max_htlc is not None or arguments.verbose: s = '' if max_htlc_changed: s = ' ➜ ' + fmt.col_hi(new_max_htlc) - print(" max_htlc_msat: %s%s" % (fmt.col_hi(my_policy.max_htlc_msat), s) ) + 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: s = '' if time_lock_delta_changed: s = ' ➜ ' + fmt.col_hi(new_time_lock_delta) - print(" time_lock_delta: %s%s" % (fmt.col_hi(my_policy.time_lock_delta), s) ) + 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 dc17eb1..2e892af 100644 --- a/charge_lnd/lnd.py +++ b/charge_lnd/lnd.py @@ -140,7 +140,8 @@ def get_chan_info(self, chanid): 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): + 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): chan_info = self.get_chan_info(chanid) if not chan_info: return None @@ -157,8 +158,8 @@ def update_chan_policy(self, chanid, base_fee_msat, fee_ppm, min_htlc_msat, max_ 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=my_policy.inbound_fee_base_msat, - inbound_fee_rate_ppm=my_policy.inbound_fee_rate_milli_msat + 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) )) def get_txns(self, start_height = None, end_height = None): diff --git a/charge_lnd/strategy.py b/charge_lnd/strategy.py index 2b2e1fc..7f2ecc9 100644 --- a/charge_lnd/strategy.py +++ b/charge_lnd/strategy.py @@ -33,12 +33,12 @@ 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) == 2: + 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) == 5: + if len(result) == 7: result = result + ( False, ) return result @@ -62,15 +62,16 @@ def effective_max_htlc_msat(self, channel): @strategy(name = 'ignore') def strategy_ignore(channel, policy, **kwargs): - return (None, None, None, None, None) + return (None, None, None, None, None, None, None) @strategy(name = 'ignore_fees') def strategy_ignore_fees(channel, policy, **kwargs): - return (None, None) + return (None, None, None, None) @strategy(name = 'static') def strategy_static(channel, policy, **kwargs): - return (policy.getint('base_fee_msat'), policy.getint('fee_ppm')) + return (policy.getint('base_fee_msat'), policy.getint('fee_ppm'), + policy.getint('inbound_base_fee_msat'), policy.getint('inbound_fee_ppm')) @strategy(name = 'proportional') def strategy_proportional(channel, policy, **kwargs): @@ -107,7 +108,7 @@ 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) + return (policy.getint('base_fee_msat'), ppm, None, None) @strategy(name = 'match_peer') def strategy_match_peer(channel, policy, **kwargs): @@ -116,7 +117,9 @@ def strategy_match_peer(channel, policy, **kwargs): 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('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)) @strategy(name = 'cost') def strategy_cost(channel, policy, **kwargs): @@ -135,7 +138,7 @@ 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) + return (policy.getint('base_fee_msat'), ppm, None, None) @strategy(name = 'onchain_fee') def strategy_onchain_fee(channel, policy, **kwargs): @@ -151,7 +154,7 @@ 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) + return (policy.getint('base_fee_msat'), fee_ppm, None, None) @strategy(name = 'use_config') def strategy_use_config(channel, policy, **kwargs): diff --git a/examples/all-channels-static.config b/examples/all-channels-static.config index 909cc5d..5c77a8d 100644 --- a/examples/all-channels-static.config +++ b/examples/all-channels-static.config @@ -4,3 +4,5 @@ strategy = static base_fee_msat = 1000 fee_ppm = 200 +inbound_base_fee_msat = -500 +inbound_fee_ppm = -100 diff --git a/examples/complex-ruleset.config b/examples/complex-ruleset.config index 8d562ed..34d8d9e 100644 --- a/examples/complex-ruleset.config +++ b/examples/complex-ruleset.config @@ -3,6 +3,8 @@ strategy = static base_fee_msat = 1_000 fee_ppm = 10 +inbound_base_fee_msat = -500 +inbound_fee_ppm = -5 [mydefaults] # no strategy, so this only sets some defaults