Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Peer inbound weighted average fee strategy + Local balance proportional max_htlc_msat rule #104

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,21 @@ Available strategies:
|**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|
|**match_peer_inbound_weighted_average** | sets the fee rate to the peer's inbound weighted average fee rate||
|**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|
|**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**|

Strategy for match peer inbound weighted average will apply the following properties if defined:

|Property|Description|Values|
|:--|:--|:--|
| **inbound_skip_fee_rate_above_ppm** | Inbound rate of peer to ignore exceeds this value | ppm |
microlancer marked this conversation as resolved.
Show resolved Hide resolved
| **inbound_weighted_average_fee_rate_premium_percent** | Premium added to the final peer inbound weighted average | percent |


All strategies (except the ignore strategy) will apply the following properties if defined:

|Property|Description|Values|
Expand All @@ -203,6 +212,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
91 changes: 91 additions & 0 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,17 @@ 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 // num_slices

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably best to have a hardcoded upper limit for num_slices here, so we don't spam the network (e.g. 10)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternatively, we could define a 'minimum slice size', so that large channels can use more slices than small channels (e.g. 200_000)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For choosing the number of slices for large channels, would the system automatically choose from multiple policies if we set a chan.min_capacity etc. and so we could define different slice counts for different sizes?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I was thinking along the lines of

min_slice_size = 200_000
min_slices = 10
max_slices = max(min_slices, max_value // min_slice_size)
slice_size = max_value // max_slices

so the number of slices is capped at 10 for small channels, or higher if the channel capacity allows for more.

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

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

return slice_point

class StrategyDelegate:
STRATEGIES = {}
Expand Down Expand Up @@ -48,6 +61,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 @@ -118,6 +136,79 @@ 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 = 'match_peer_inbound_weighted_average')
def strategy_match_peer_inbound_weighted_average(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
},
"node2_policy": {
'fee_rate_milli_msat': edge.node2_policy.fee_rate_milli_msat
}
}
edges.append(the_edge)
edges_cache = edges
else:
edges = edges_cache

peer_inbound = []

total_peer_capacity = 0
weighted_average_inbound_fee = 0

for edge in edges:
if edge['node1_pub'] == peer_node_id:
# We will take node2_policy because we want inbound policy, not outbound
inbound = {
'capacity': edge['capacity'],
'fee_rate_milli_msat': edge['node2_policy']['fee_rate_milli_msat']
}
total_peer_capacity += edge['capacity']
peer_inbound.append(inbound)
elif edge['node2_pub'] == peer_node_id:
inbound = {
'capacity': edge['capacity'],
'fee_rate_milli_msat': edge['node1_policy']['fee_rate_milli_msat']
}
peer_inbound.append(inbound)
total_peer_capacity += edge['capacity']

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

max_usable_ppm = policy.getint('inbound_skip_fee_rate_above_ppm')

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

premium_pct = policy.getint('inbound_weighted_average_fee_rate_premium_percent')

if premium_pct:
premium = int(weighted_average_inbound_fee * (premium_pct/100))
weighted_average_inbound_fee += premium

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

@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
36 changes: 36 additions & 0 deletions examples/weighted-average-of-peer-incoming-rate.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[default]
# 'default' is special, it is used if no other policy matches a channel
strategy = static
base_fee_msat = 0
fee_ppm = 50_000
min_fee_ppm_delta = 50
max_htlc_proportional_slices = 6

# Ignore inactive nodes
[inactive-nodes]
node.max_shared_channels_active = 0
strategy = ignore

# Use the weighted average of the peer node's channels inbound 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 inbound_weighted_average_fee_rate_premium_percent value adds a
# multiple of the weighted average fee. For example, a value of 25
# means that a 200 ppm weighted average will be set to 250 ppm.

[peer-adjusted]
strategy = match_peer_inbound_weighted_average
base_fee_msat = 0
min_fee_ppm_delta = 50
max_htlc_proportional_slices = 6
microlancer marked this conversation as resolved.
Show resolved Hide resolved
inbound_skip_fee_rate_above_ppm = 10000
inbound_weighted_average_fee_rate_premium_percent = 100