Skip to content

Commit

Permalink
Support for setting inbound fees
Browse files Browse the repository at this point in the history
  • Loading branch information
feelancer21 committed May 12, 2024
1 parent 5b4cff4 commit 6372dc2
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 26 deletions.
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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**<br>**base_fee_msat**<br>**inbound_fee_ppm**<br>**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<br>within **onchain_fee_numblocks** blocks.|
|**proportional** | sets fee ppm according to balancedness.|**min_fee_ppm**<br>**max_fee_ppm**<br>**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**<br>**max_fee_ppm**<br>**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**|

Expand Down
39 changes: 28 additions & 11 deletions charge_lnd/charge_lnd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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':
Expand All @@ -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

Expand Down
7 changes: 4 additions & 3 deletions charge_lnd/lnd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
21 changes: 12 additions & 9 deletions charge_lnd/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions examples/all-channels-static.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
strategy = static
base_fee_msat = 1000
fee_ppm = 200
inbound_base_fee_msat = -500
inbound_fee_ppm = -100
2 changes: 2 additions & 0 deletions examples/complex-ruleset.config
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6372dc2

Please sign in to comment.