diff --git a/README.md b/README.md index aa71729..a1273ee 100644 --- a/README.md +++ b/README.md @@ -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
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_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**
**max_fee_ppm**
**sum_peer_chans** consider all channels with peer for balance calculations
**fee_avg_calc_cutoff_ppm** Ignore peer inbound ppm above this value
**avg_fee_ppm_multiplier** Tweak avg ppm by this multiplier (0..1)
**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**| @@ -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 | diff --git a/charge_lnd/strategy.py b/charge_lnd/strategy.py index 2b2e1fc..d3f1e8c 100644 --- a/charge_lnd/strategy.py +++ b/charge_lnd/strategy.py @@ -6,6 +6,8 @@ from .config import Config from .electrum import Electrum +edges_cache = None + def debug(message): sys.stderr.write(message + "\n") @@ -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 = {} @@ -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)) @@ -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 @@ -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'] diff --git a/examples/max-htlc-proportional.config b/examples/max-htlc-proportional.config new file mode 100644 index 0000000..936dd7c --- /dev/null +++ b/examples/max-htlc-proportional.config @@ -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 \ No newline at end of file diff --git a/examples/weighted-average-of-peer-incoming-rate.config b/examples/weighted-average-of-peer-incoming-rate.config new file mode 100644 index 0000000..dcd41c0 --- /dev/null +++ b/examples/weighted-average-of-peer-incoming-rate.config @@ -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 \ No newline at end of file