Skip to content
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ Available strategies:
|**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_peer_inbound** | sets the fee rate according to balancedness around a range based on the peer's inbound weighted average fee rate|**min_fee_ppm**<br>**max_fee_ppm**<br>**sum_peer_chans** consider all channels with peer for balance calculations<br>**fee_avg_calc_cutoff_ppm** Ignore peer inbound ppm above this value<br>**avg_fee_ppm_multiplier** Tweak avg ppm by this multiplier (0..1)<br>**upper_fee_ppm_multiplier** Tweak upper ppm by this multiplier; max_fee_ppm will still be honored (0..1)|
|**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 All @@ -203,6 +204,7 @@ All strategies (except the ignore strategy) will apply the following properties
| **min_htlc_msat** | Minimum size (in msat) of HTLC to allow | # msat |
| **max_htlc_msat** | Maximum size (in msat) of HTLC to allow | # msat |
| **max_htlc_msat_ratio** | Maximum size of HTLC to allow as a fraction of total channel capacity | 0..1 |
| **max_htlc_proportional_slices** | Maximum size of HTLC set proportionally to local balance with slices number of steps | # slices |
| **time_lock_delta** | Time Lock Delta | # blocks |
| **min_fee_ppm_delta** | Minimum change in fees (ppm) before updating channel | ppm delta |

Expand Down
201 changes: 179 additions & 22 deletions charge_lnd/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from .config import Config
from .electrum import Electrum

edges_cache = None

def debug(message):
sys.stderr.write(message + "\n")

Expand All @@ -19,6 +21,43 @@ def call_strategy(*args, **kwargs):
return call_strategy
return register_strategy

def calculate_slices(max_value, current_value, num_slices):
# Calculate the size of each slice
slice_size = max_value // max(num_slices, 10)

# Find the slice number containing the current_value
current_slice = min(current_value // slice_size, num_slices - 1)

# Determine the upper value of the current slice without going over
slice_point = max(1, min(current_slice * slice_size, max_value))

return slice_point

def get_ratio(channel, policy, **kwargs):
if policy.getbool('sum_peer_chans', False):
lnd = kwargs['lnd']
shared_chans=lnd.get_shared_channels(channel.remote_pubkey)
local_balance = 0
remote_balance = 0
for c in (shared_chans):
# Include balance of all active channels with peer
if c.active:
local_balance += c.local_balance
remote_balance += c.remote_balance
total_balance = local_balance + remote_balance
if total_balance == 0:
# Sum inactive channels because the node is likely offline with no active channels.
# When they come back online their fees won't be changed.
for c in (shared_chans):
if not c.active:
local_balance += c.local_balance
remote_balance += c.remote_balance
total_balance = local_balance + remote_balance
ratio = local_balance/total_balance
else:
ratio = channel.local_balance/(channel.local_balance + channel.remote_balance)

return ratio

class StrategyDelegate:
STRATEGIES = {}
Expand Down Expand Up @@ -48,6 +87,11 @@ def execute(self, channel):

def effective_max_htlc_msat(self, channel):
result = self.policy.getint('max_htlc_msat')

slices = self.policy.getint('max_htlc_proportional_slices')
if slices:
result = calculate_slices(channel.capacity, channel.local_balance, slices) * 1000

ratio = self.policy.getfloat('max_htlc_msat_ratio')
if ratio:
ratio = max(0,min(1,ratio))
Expand Down Expand Up @@ -81,28 +125,7 @@ def strategy_proportional(channel, policy, **kwargs):
if ppm_min is None or ppm_max is None:
raise Exception('proportional strategy requires min_fee_ppm and max_fee_ppm properties')

if policy.getbool('sum_peer_chans', False):
lnd = kwargs['lnd']
shared_chans=lnd.get_shared_channels(channel.remote_pubkey)
local_balance = 0
remote_balance = 0
for c in (shared_chans):
# Include balance of all active channels with peer
if c.active:
local_balance += c.local_balance
remote_balance += c.remote_balance
total_balance = local_balance + remote_balance
if total_balance == 0:
# Sum inactive channels because the node is likely offline with no active channels.
# When they come back online their fees won't be changed.
for c in (shared_chans):
if not c.active:
local_balance += c.local_balance
remote_balance += c.remote_balance
total_balance = local_balance + remote_balance
ratio = local_balance/total_balance
else:
ratio = channel.local_balance/(channel.local_balance + channel.remote_balance)
ratio = get_ratio(channel, policy, **kwargs)

ppm = int(ppm_min + (1.0 - ratio) * (ppm_max - ppm_min))
# clamp to 0..inf
Expand All @@ -118,6 +141,140 @@ def strategy_match_peer(channel, policy, **kwargs):
return (policy.getint('base_fee_msat', peernode_policy.fee_base_msat),
policy.getint('fee_ppm', peernode_policy.fee_rate_milli_msat))

@strategy(name = 'proportional_peer_inbound')
def strategy_proportional_peer_inbound(channel, policy, **kwargs):
lnd = kwargs['lnd']
chan_info = lnd.get_chan_info(channel.chan_id)
my_pubkey = lnd.get_own_pubkey()

peer_node_id = chan_info.node1_pub if chan_info.node2_pub == my_pubkey else chan_info.node2_pub

# avoid having to fetch get_edges() multiple times
global edges_cache

if not edges_cache:
edges_list = lnd.get_edges()
# cache only the data we need
edges = []
for edge in edges_list:
the_edge = {
"node1_pub": edge.node1_pub,
"node2_pub": edge.node2_pub,
'capacity': edge.capacity,
"node1_policy": {
'fee_rate_milli_msat': edge.node1_policy.fee_rate_milli_msat,
'max_htlc_msat': edge.node1_policy.max_htlc_msat
},
"node2_policy": {
'fee_rate_milli_msat': edge.node2_policy.fee_rate_milli_msat,
'max_htlc_msat': edge.node2_policy.max_htlc_msat
}
}
edges.append(the_edge)
edges_cache = edges
else:
edges = edges_cache

peer_inbound = []

total_peer_capacity = 0
ppm_avg = 0

for edge in edges:
if edge['node1_pub'] == peer_node_id:
if edge['node2_pub'] == my_pubkey:
# ignore this edge if it's shared between our node and peer
continue
inbound = {
#'capacity': edge['capacity'],
# We will use the max_htlc_msat // 1000 as the estimated capacity
'capacity': edge['node2_policy']['max_htlc_msat'] // 1000,
# We will take node2_policy because we want inbound policy, not outbound
'fee_rate_milli_msat': edge['node2_policy']['fee_rate_milli_msat']
}
total_peer_capacity += inbound['capacity']
peer_inbound.append(inbound)
elif edge['node2_pub'] == peer_node_id:
if edge['node1_pub'] == my_pubkey:
# ignore this edge if it's shared between our node and peer
continue
inbound = {
#'capacity': edge['capacity'],
# We will use the max_htlc_msat // 1000 as the estimated capacity
'capacity': edge['node1_policy']['max_htlc_msat'] // 1000,
# We will take node1_policy because we want inbound policy, not outbound
'fee_rate_milli_msat': edge['node1_policy']['fee_rate_milli_msat']
}
peer_inbound.append(inbound)
total_peer_capacity += inbound['capacity']

# Calculate the weighted average inbound fee by multiplying each fee by
# the adjusted ratio and taking the sum.

fee_avg_calc_cutoff_ppm = policy.getint('fee_avg_calc_cutoff_ppm')

for inbound in peer_inbound:
if inbound['fee_rate_milli_msat'] >= fee_avg_calc_cutoff_ppm:
# ignore fee rate values over max_usable_ppm in our calculation
continue
ppm_avg += int((inbound['capacity']/total_peer_capacity)*inbound['fee_rate_milli_msat'])

# Use the same channel ratio calculation as proportional strategy
ratio = get_ratio(channel, policy, **kwargs)

ppm_min = policy.getint('min_fee_ppm')
ppm_max = policy.getint('max_fee_ppm')
avg_fee_ppm_multiplier = policy.getfloat('avg_fee_ppm_multiplier')
upper_fee_ppm_multiplier = policy.getfloat('upper_fee_ppm_multiplier')

if ppm_min is None or ppm_max is None or avg_fee_ppm_multiplier is None or upper_fee_ppm_multiplier is None:
raise Exception('proportional inbound weighted strategy requires min_fee_ppm, max_fee_ppm, avg_fee_ppm_multiplier, and upper_fee_ppm_multiplier properties')

if ppm_min >= ppm_max:
raise Exception('ppm_min should be less than ppm_max')

# The avg_fee_ppm_multiplier can tweak the ppm_avg to be slightly
# lower or higher, changing the center-point of the calculation.
# You might do this if you think the average values are all
# too cheap or too expensive. We will also make sure the average
# is not lower than ppm_min here, or higher than ppm_max.
# It's probably perfectly fine to leave this multiplier at 1.

ppm_avg = min(ppm_max, max(ppm_min, ppm_avg * avg_fee_ppm_multiplier))

# The upper_fee_ppm_multiplier sets the upper maximum when calculating
# the ppm when the local balance proportion drops below 0.5. A value
# of 2 means that the upper maximum is double the ppm_avg. If you
# wanted to increase fees more aggressively as the local balance falls,
# you could choose a higher multiplier.
# It's probably perfectly fine to leave this multiplier at 2.

ppm_upper = ppm_avg * upper_fee_ppm_multiplier

# When the ratio is near half, we want the ppm to be exactly
# the ppm_avg. When the ratio is lower, we want a higher ppm that
# is between ppm_avg and ppm_upper. When the ratio is higher,
# we want a lower ppm between ppm_avg and ppm_min. Since the range
# between can be unequal, e.g. average = 500, min = 400, max
# = 3000, we don't want to make the 0.5 ratio be the middle
# of the range 400 - 3000. It's much larger than the target
# average of 500. So we calculate the two sides separately.

if ratio < 0.5:
ppm = int(ppm_upper + 2 * (ppm_avg - ppm_upper) * ratio)
elif ratio > 0.5:
ppm = int(ppm_min + 2 * (ppm_avg - ppm_min) * (1 - ratio))
else:
ppm = ppm_avg

# Cap it to the max
ppm = min(ppm, ppm_max)

# clamp to 0..inf
ppm = max(ppm, 0)

return (policy.getint('base_fee_msat'), ppm)

@strategy(name = 'cost')
def strategy_cost(channel, policy, **kwargs):
lnd = kwargs['lnd']
Expand Down
24 changes: 24 additions & 0 deletions examples/max-htlc-proportional.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# all channels max_htlc set to a value proportional to
# local balance, with specified number of divisions
# e.g.
# slices = 10
# channel_size = 1_000_000 sats
# max_htlc values:
# 100_000, 200_000, 300_000, 400_000, 500_000
# 600_000, 700_000, 800_000, 900_000, 1_000_000
#
# slices = 2
# channel_size = 800_000_sats
# max_htlc values:
# 400_000, 800_000
#
# Rationale: avoid insufficient local balance failures
# to improve your node's reputation of successful forwarding.

[default]
strategy = ignore

[proportional_htlc]
chan.max_local_balance = 30_000_000
strategy = static
max_htlc_proportional_slices = 6
44 changes: 44 additions & 0 deletions examples/weighted-average-of-peer-incoming-rate.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Use a fee that is proportional to the channel's local balance with
# the ppm range surrounding the weighted average of the peer node's
# inbound ppm rate.
#
# Example:
# - Let's suppose you have a channel with Peer XYZ.
# - Peer XYZ has 4 channels.
# - Those 4 channels are 1M, 2M, 3M, and 4M sats capacity.
# - The inbound fee rates for those channels are 100 ppm, 200 ppm,
# 300 ppm, and 400 ppm.
# - The weighted average is the sum of fees multiplied by the
# channel's capacity. In this case, 300 ppm. Note, it's higher
# than the plain average, because more capacity is at the 400 ppm
# rate compared to the lower ppm rate.
#
# The `min_fee_ppm` and `max_fee_ppm` are absolute, and will override
# whatever calculations are made.
#
# The `fee_avg_calc_cutoff_ppm` is a cutoff maximum value when
# considering the peer's inbound channel ppm. We can exclude these
# extreme values from the calculation to get a more reasonable avg.
#
# The `avg_fee_ppm_multiplier` shifts the calculated peer's avg to
# higher or lower. This is useful if you think the avg is too low
# or too high.
#
# The `upper_fee_ppm_multiplier` determines how aggresively you want
# your ppm to rise as the local channel balance falls. A multiplier
# of 2 sets the upper ppm fee to be double as balance approaches 0.

[proportional_peer_inbound]
strategy = proportional_peer_inbound
node.min_shared_channels_active = 1
base_fee_msat = 0
min_fee_ppm_delta = 50
max_htlc_proportional_slices = 6

min_fee_ppm = 1
max_fee_ppm = 99_999
fee_avg_calc_cutoff_ppm = 5_000
avg_fee_ppm_multiplier = 1.0
upper_fee_ppm_multiplier = 2.0

time_lock_delta = 18