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