Move jitrebalance, feeadjuster, noise to Unmaintained directory

This commit is contained in:
fmhoeger
2024-02-03 21:06:41 -06:00
committed by mergify[bot]
parent 7bf1ab844b
commit 08c2c24e46
19 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
# Fee Adjuster
This plugin dynamically adjusts fees according to channel balances. The default behaviour is to automatically adjust fees at startup
and following forwarding events. There is a threshold for balance deltas that must be crossed before an update is triggered. It can
also set the max htlc for channels according to available liquidity. This may reduce transaction failures but it will also reveal
information about what the current channel balance is.
## Options
- `feeadjuster-deactivate-fuzz` boolean (default `False`) deactivates update threshold randomization and hysterisis
- `feeadjuster-deactivate-fee-update` boolean (default `False`) deactivates automatic fee updates for forward events
- `feeadjuster-threshold` default 0.05 - Relative channel balance delta at which to trigger an update. Default 0.05 means 5%. Note: it's also fuzzed by 1.5%.
- `feeadjuster-threshold-abs` default 0.001btc - Absolute channel balance delta at which to always trigger an update. Note: it's also fuzzed by 1.5%.
- `feeadjuster-enough-liquidity` default 0msat (turned off) - Beyond this liquidity do not adjust fees.
This also modifies the fee curve to achieve having this amount of liquidity.
- `feeadjuster-adjustment-method` Adjustment method to calculate channel fee. Can be 'default', 'soft' for less difference or 'hard' for higher difference.
- `feeadjuster-imbalance` default 0.5 (always acts) - Ratio at which channel imbalance the feeadjuster should start acting. Set higher or lower values to
limit feeadjuster's activity to more imbalanced channels. E.g. 0.3 for '70/30'% or 0.6 for '40/60'%.
- `feeadjuster-feestrategy` Sets the per channel fee selection strategy. Can be 'global' (default) to use global config or default values, or 'median' to use
the median fees from peers of peer.
- `feeadjuster-median-multiplier` Sets the factor with which the median fee is multiplied if using the fee strategy
'median'. This allows over- or underbidding other nodes by a constant factor (default: 1.0).
- `feeadjuster-max-htlc-steps` Default 0 (turned off). Sets the number of max htlc adjustment steps. If our local channel balance drops below a step level
it will reduce the max htlc to that level, which can reduce local routing channel failures. A value of 0 disables the stepping.

View File

@@ -0,0 +1,23 @@
import re
def cln_parse_rpcversion(string):
"""
Parse cln version string to determine RPC version.
cln switched from 'semver' alike `major.minor.sub[rcX][-mod]`
to ubuntu style with version 22.11 `yy.mm[.patch][-mod]`
make sure we can read all of them for (the next 80 years).
"""
rpcversion = string
if rpcversion.startswith('v'): # strip leading 'v'
rpcversion = rpcversion[1:]
if rpcversion.find('-') != -1: # strip mods
rpcversion = rpcversion[:rpcversion.find('-')]
if re.search('.*(rc[\\d]*)$', rpcversion): # strip release candidates
rpcversion = rpcversion[:rpcversion.find('rc')]
if rpcversion.count('.') == 1: # imply patch version 0 if not given
rpcversion = rpcversion + '.0'
# split and convert numeric string parts to actual integers
return list(map(int, rpcversion.split('.')))

View File

@@ -0,0 +1,443 @@
#!/usr/bin/env python3
import random
import statistics
import time
import math
from clnutils import cln_parse_rpcversion
from pyln.client import Plugin, Millisatoshi, RpcError
from threading import Lock
plugin = Plugin()
# Our amount and the total amount in each of our channel, indexed by scid
plugin.adj_balances = {}
# Cache to avoid loads of RPC calls
plugin.our_node_id = None
plugin.peerchannels = None
plugin.channels = None
# Users can configure this
plugin.update_threshold = 0.05
# forward_event must wait for init
plugin.mutex = Lock()
plugin.mutex.acquire()
def get_adjusted_percentage(plugin: Plugin, scid: str):
"""
For big channels, there may be a wide range where the liquidity is just okay.
Note: if big_enough_liquidity is greater than {total} * 2
then percentage is actually {our} / {total}, as it was before
"""
channel = plugin.adj_balances[scid]
if plugin.big_enough_liquidity == Millisatoshi(0):
return channel["our"] / channel["total"]
min_liquidity = min(channel["total"] / 2, int(plugin.big_enough_liquidity))
theirs = channel["total"] - channel["our"]
if channel["our"] >= min_liquidity and theirs >= min_liquidity:
# the liquidity is just okay
return 0.5
if channel["our"] < min_liquidity:
# our liquidity is too low
return channel["our"] / min_liquidity / 2
# their liquidity is too low
return (min_liquidity - theirs) / min_liquidity / 2 + 0.5
def get_ratio_soft(our_percentage):
"""
Basic algorithm: lesser difference than default
"""
return 10**(0.5 - our_percentage)
def get_ratio(our_percentage):
"""
Basic algorithm: the farther we are from the optimal case, the more we
bump/lower.
"""
return 50**(0.5 - our_percentage)
def get_ratio_hard(our_percentage):
"""
Return value is between 0 and 20: 0 -> 20; 0.5 -> 1; 1 -> 0
"""
return 100**(0.5 - our_percentage) * (1 - our_percentage) * 2
def get_peerchannels(plugin: Plugin):
""" Helper to reconstruct `listpeerchannels` for older CLN versions """
# first the good case
if plugin.rpcversion[0] > 23 or plugin.rpcversion[0] == 23 and plugin.rpcversion[1] >= 2:
return plugin.rpc.listpeerchannels()["channels"]
# now the workaround
channels = []
peers = plugin.rpc.listpeers()['peers']
for peer in peers:
newchans = peer['channels']
for ch in newchans:
ch['peer_id'] = peer['id'] # all we need is to set the 'peer_id'
channels.extend(newchans)
return channels
def get_peer_id_for_scid(plugin: Plugin, scid: str):
for ch in plugin.peerchannels:
if ch.get('short_channel_id') == scid:
return ch['peer_id']
return None
def get_peerchannel(plugin: Plugin, scid: str):
for ch in plugin.peerchannels:
if ch.get("short_channel_id") == scid:
return ch
return None
def get_chan_fees(plugin: Plugin, scid: str):
channel = get_peerchannel(plugin, scid)
assert channel is not None
return {"base": channel["fee_base_msat"], "ppm": channel["fee_proportional_millionths"]}
def get_fees_global(plugin: Plugin, scid: str):
return {"base": plugin.adj_basefee, "ppm": plugin.adj_ppmfee}
def get_fees_median(plugin: Plugin, scid: str):
""" Median fees from peers or peer.
The assumption is that our node competes in fees to other peers of a peer.
"""
peer_id = get_peer_id_for_scid(plugin, scid)
assert peer_id is not None
if plugin.listchannels_by_dst:
plugin.channels = plugin.rpc.call("listchannels",
{"destination": peer_id})['channels']
channels_to_peer = [ch for ch in plugin.channels
if ch['destination'] == peer_id
and ch['source'] != plugin.our_node_id]
if len(channels_to_peer) == 0:
return None
# fees > ~5000 (base and ppm) are currently about top 2% of network fee extremists
fees_ppm = [ch['fee_per_millionth'] for ch in channels_to_peer if 0 < ch['fee_per_millionth'] < 5000]
fees_base = [ch['base_fee_millisatoshi'] for ch in channels_to_peer if 0 < ch['base_fee_millisatoshi'] < 5000]
# if lists are emtpy use default values, otherwise statistics.median will fail.
if len(fees_ppm) == 0:
fees_ppm = [int(plugin.adj_ppmfee / plugin.median_multiplier)]
if len(fees_base) == 0:
fees_base = [int(plugin.adj_basefee / plugin.median_multiplier)]
return {"base": statistics.median(fees_base) * plugin.median_multiplier,
"ppm": statistics.median(fees_ppm) * plugin.median_multiplier}
def setchannelfee(plugin: Plugin, scid: str, base: int, ppm: int, min_htlc: int = None, max_htlc: int = None):
fees = get_chan_fees(plugin, scid)
if fees is None or base == fees['base'] and ppm == fees['ppm']:
return False
try:
plugin.rpc.setchannel(scid, base, ppm, min_htlc, max_htlc)
return True
except RpcError as e:
plugin.log(f"Could not adjust fees for channel {scid}: '{e}'", level="error")
return False
def significant_update(plugin: Plugin, scid: str):
channel = plugin.adj_balances[scid]
last_liquidity = channel.get("last_liquidity")
if last_liquidity is None:
return True
# Only update on substantial balance moves to avoid flooding, and add
# some pseudo-randomness to avoid too easy channel balance probing
update_threshold = plugin.update_threshold
update_threshold_abs = int(plugin.update_threshold_abs)
if not plugin.deactivate_fuzz:
update_threshold += random.uniform(-0.015, 0.015)
update_threshold_abs += update_threshold_abs * random.uniform(-0.015, 0.015)
last_percentage = last_liquidity / channel["total"]
percentage = channel["our"] / channel["total"]
if (abs(last_percentage - percentage) > update_threshold
or abs(last_liquidity - channel["our"]) > update_threshold_abs):
return True
return False
def maybe_adjust_fees(plugin: Plugin, scids: list):
channels_adjusted = 0
for scid in scids:
our = plugin.adj_balances[scid]["our"]
total = plugin.adj_balances[scid]["total"]
percentage = our / total
base = int(plugin.adj_basefee)
ppm = int(plugin.adj_ppmfee)
# select ideal values per channel
fees = plugin.fee_strategy(plugin, scid)
if fees is not None:
base = int(fees['base'])
ppm = int(fees['ppm'])
# reset to normal fees if imbalance is not high enough
if (percentage > plugin.imbalance and percentage < 1 - plugin.imbalance):
if setchannelfee(plugin, scid, base, ppm):
plugin.log(f"Set default fees as imbalance is too low for {scid}: ppm {ppm} base {base}msat")
plugin.adj_balances[scid]["last_liquidity"] = our
channels_adjusted += 1
continue
if not significant_update(plugin, scid):
continue
percentage = get_adjusted_percentage(plugin, scid)
assert 0 <= percentage and percentage <= 1
ratio = plugin.get_ratio(percentage)
if plugin.max_htlc_steps >= 1:
max_htlc = int(total * math.ceil(plugin.max_htlc_steps * percentage) / plugin.max_htlc_steps)
else:
max_htlc = None
if setchannelfee(plugin, scid, base, int(ppm * ratio), None, max_htlc):
plugin.log(f"Adjusted fees of {scid} with a ratio of {ratio}: ppm {int(ppm * ratio)} base {base}msat max_htlc {max_htlc}")
plugin.adj_balances[scid]["last_liquidity"] = our
channels_adjusted += 1
return channels_adjusted
def maybe_add_new_balances(plugin: Plugin, scids: list):
for scid in scids:
if scid not in plugin.adj_balances:
chan = get_peerchannel(plugin, scid)
assert chan is not None
plugin.adj_balances[scid] = {
"our": int(chan["to_us_msat"]),
"total": int(chan["total_msat"])
}
@plugin.subscribe("forward_event")
def forward_event(plugin: Plugin, forward_event: dict, **kwargs):
if not plugin.forward_event_subscription:
return
plugin.mutex.acquire(blocking=True)
plugin.peerchannels = get_peerchannels(plugin)
if plugin.fee_strategy == get_fees_median and not plugin.listchannels_by_dst:
plugin.channels = plugin.rpc.listchannels()['channels']
if forward_event["status"] == "settled":
in_scid = forward_event["in_channel"]
out_scid = forward_event["out_channel"]
maybe_add_new_balances(plugin, [in_scid, out_scid])
if plugin.rpcversion[0] == 0 and plugin.rpcversion[1] < 12:
plugin.adj_balances[in_scid]["our"] += int(Millisatoshi(forward_event["in_msatoshi"]))
plugin.adj_balances[out_scid]["our"] -= int(Millisatoshi(forward_event["out_msatoshi"]))
else:
plugin.adj_balances[in_scid]["our"] += int(Millisatoshi(forward_event["in_msat"]))
plugin.adj_balances[out_scid]["our"] -= int(Millisatoshi(forward_event["out_msat"]))
try:
# Pseudo-randomly add some hysterisis to the update
if not plugin.deactivate_fuzz and random.randint(0, 9) == 9:
time.sleep(random.randint(0, 5))
maybe_adjust_fees(plugin, [in_scid, out_scid])
except Exception as e:
plugin.log("Adjusting fees: " + str(e), level="error")
plugin.mutex.release()
@plugin.method("feeadjust")
def feeadjust(plugin: Plugin, scid: str = None):
"""Adjust fees for all channels (default) or just a given `scid`.
This method is automatically called in plugin init, or can be called manually after a successful payment.
Otherwise, the plugin keeps the fees up-to-date.
To stop setting the channels with a list of nodes place a file called `feeadjuster-exclude.list` in the
lightningd data directory with a simple line-by-line list of pubkeys.
"""
plugin.mutex.acquire(blocking=True)
plugin.peerchannels = get_peerchannels(plugin)
if plugin.fee_strategy == get_fees_median and not plugin.listchannels_by_dst:
plugin.channels = plugin.rpc.listchannels()['channels']
channels_adjusted = 0
try:
with open('feeadjuster-exclude.list') as file:
exclude_list = [l.rstrip("\n") for l in file]
print("Excluding the channels with the nodes:", exclude_list)
except FileNotFoundError:
exclude_list = []
print("There is no feeadjuster-exclude.list given, applying the options to the channels with all peers.")
for chan in plugin.peerchannels:
if chan["peer_id"] in exclude_list:
continue
if chan["state"] == "CHANNELD_NORMAL":
_scid = chan.get("short_channel_id")
if scid is not None and scid != _scid:
continue
plugin.adj_balances[_scid] = {
"our": int(chan["to_us_msat"]),
"total": int(chan["total_msat"])
}
channels_adjusted += maybe_adjust_fees(plugin, [_scid])
msg = f"{channels_adjusted} channel(s) adjusted"
plugin.log(msg)
plugin.mutex.release()
return msg
@plugin.method("feeadjuster-toggle")
def feeadjuster_toggle(plugin: Plugin, value: bool = None):
"""Activates/Deactivates automatic fee updates for forward events.
The status will be set to value.
"""
msg = {"forward_event_subscription": {"previous": plugin.forward_event_subscription}}
if value is None:
plugin.forward_event_subscription = not plugin.forward_event_subscription
else:
plugin.forward_event_subscription = bool(value)
msg["forward_event_subscription"]["current"] = plugin.forward_event_subscription
return msg
@plugin.init()
def init(options: dict, configuration: dict, plugin: Plugin, **kwargs):
# do all the stuff that needs to be done just once ...
plugin.getinfo = plugin.rpc.getinfo()
plugin.rpcversion = cln_parse_rpcversion(plugin.getinfo.get('version'))
plugin.our_node_id = plugin.getinfo["id"]
plugin.deactivate_fuzz = options.get("feeadjuster-deactivate-fuzz")
plugin.forward_event_subscription = not options.get("feeadjuster-deactivate-fee-update")
plugin.update_threshold = float(options.get("feeadjuster-threshold"))
plugin.update_threshold_abs = Millisatoshi(options.get("feeadjuster-threshold-abs"))
plugin.big_enough_liquidity = Millisatoshi(options.get("feeadjuster-enough-liquidity"))
plugin.imbalance = float(options.get("feeadjuster-imbalance"))
plugin.max_htlc_steps = int(options.get("feeadjuster-max-htlc-steps"))
adjustment_switch = {
"soft": get_ratio_soft,
"hard": get_ratio_hard,
"default": get_ratio
}
plugin.get_ratio = adjustment_switch.get(options.get("feeadjuster-adjustment-method"), get_ratio)
fee_strategy_switch = {
"global": get_fees_global,
"median": get_fees_median
}
plugin.fee_strategy = fee_strategy_switch.get(options.get("feeadjuster-feestrategy"), get_fees_global)
plugin.median_multiplier = float(options.get("feeadjuster-median-multiplier"))
config = plugin.rpc.listconfigs()
plugin.adj_basefee = config["fee-base"]
plugin.adj_ppmfee = config["fee-per-satoshi"]
# normalize the imbalance percentage value to 0%-50%
if plugin.imbalance < 0 or plugin.imbalance > 1:
raise ValueError("feeadjuster-imbalance must be between 0 and 1.")
if plugin.imbalance > 0.5:
plugin.imbalance = 1 - plugin.imbalance
# detect if server supports the new listchannels by `destination` (#4614)
plugin.listchannels_by_dst = False
rpchelp = plugin.rpc.help().get('help')
if len([c for c in rpchelp if c["command"].startswith("listchannels ")
and "destination" in c["command"]]) == 1:
plugin.listchannels_by_dst = True
# Detect if server supports new 'setchannel' command over setchannelfee.
# If not, make plugin.rpc.setchannel a 'symlink' to setchannelfee
if len([c for c in rpchelp if c["command"].startswith("setchannel ")]) == 0:
plugin.rpc.setchannel = plugin.rpc.setchannelfee
plugin.log(f"Plugin feeadjuster initialized "
f"({plugin.adj_basefee} base / {plugin.adj_ppmfee} ppm) with an "
f"imbalance of {int(100 * plugin.imbalance)}%/{int(100 * ( 1 - plugin.imbalance))}%, "
f"update_threshold: {int(100 * plugin.update_threshold)}%, "
f"update_threshold_abs: {plugin.update_threshold_abs}, "
f"enough_liquidity: {plugin.big_enough_liquidity}, "
f"deactivate_fuzz: {plugin.deactivate_fuzz}, "
f"forward_event_subscription: {plugin.forward_event_subscription}, "
f"adjustment_method: {plugin.get_ratio.__name__}, "
f"fee_strategy: {plugin.fee_strategy.__name__}, "
f"listchannels_by_dst: {plugin.listchannels_by_dst},"
f"max_htlc_steps: {plugin.max_htlc_steps}")
plugin.mutex.release()
feeadjust(plugin)
plugin.add_option(
"feeadjuster-deactivate-fuzz",
False,
"Deactivate update threshold randomization and hysterisis.",
"flag"
)
plugin.add_option(
"feeadjuster-deactivate-fee-update",
False,
"Deactivate automatic fee updates for forward events.",
"flag"
)
plugin.add_option(
"feeadjuster-threshold",
"0.05",
"Relative channel balance delta at which to trigger an update. Default 0.05 means 5%. "
"Note: it's also fuzzed by 1.5%",
"string"
)
plugin.add_option(
"feeadjuster-threshold-abs",
"0.001btc",
"Absolute channel balance delta at which to always trigger an update. "
"Note: it's also fuzzed by 1.5%",
"string"
)
plugin.add_option(
"feeadjuster-enough-liquidity",
"0msat",
"Beyond this liquidity do not adjust fees. "
"This also modifies the fee curve to achieve having this amount of liquidity. "
"Default: '0msat' (turned off).",
"string"
)
plugin.add_option(
"feeadjuster-adjustment-method",
"default",
"Adjustment method to calculate channel fee"
"Can be 'default', 'soft' for less difference or 'hard' for higher difference"
"string"
)
plugin.add_option(
"feeadjuster-imbalance",
"0.5",
"Ratio at which channel imbalance the feeadjuster should start acting. "
"Default: 0.5 (always). Set higher or lower values to limit feeadjuster's "
"activity to more imbalanced channels. "
"E.g. 0.3 for '70/30'% or 0.6 for '40/60'%.",
"string"
)
plugin.add_option(
"feeadjuster-feestrategy",
"global",
"Sets the per channel fee selection strategy. "
"Can be 'global' to use global config or default values, "
"or 'median' to use the median fees from peers of peer "
"Default: 'global'.",
"string"
)
plugin.add_option(
"feeadjuster-median-multiplier",
"1.0",
"Sets the factor with which the median fee is multiplied if using the fee strategy 'median'. "
"This allows over or underbidding other nodes by a constant factor"
"Default: '1.0'.",
"string"
)
plugin.add_option(
"feeadjuster-max-htlc-steps",
"0",
"Sets the number of max htlc adjustment steps. "
"This will reduce the max htlc according to available "
"liquidity, which can reduce local routing channel failures."
"A value of 0 disables the stepping.",
"string"
)
plugin.run()

View File

@@ -0,0 +1 @@
pyln-client>=0.12

View File

@@ -0,0 +1,43 @@
from clnutils import cln_parse_rpcversion
def test_rpcversion():
foo = cln_parse_rpcversion("0.11.2")
assert(foo[0] == 0)
assert(foo[1] == 11)
assert(foo[2] == 2)
foo = cln_parse_rpcversion("0.11.2rc2-modded")
assert(foo[0] == 0)
assert(foo[1] == 11)
assert(foo[2] == 2)
foo = cln_parse_rpcversion("22.11")
assert(foo[0] == 22)
assert(foo[1] == 11)
assert(foo[2] == 0)
foo = cln_parse_rpcversion("22.11rc1")
assert(foo[0] == 22)
assert(foo[1] == 11)
assert(foo[2] == 0)
foo = cln_parse_rpcversion("22.11rc1-modded")
assert(foo[0] == 22)
assert(foo[1] == 11)
assert(foo[2] == 0)
foo = cln_parse_rpcversion("22.11-modded")
assert(foo[0] == 22)
assert(foo[1] == 11)
assert(foo[2] == 0)
foo = cln_parse_rpcversion("22.11.0")
assert(foo[0] == 22)
assert(foo[1] == 11)
assert(foo[2] == 0)
foo = cln_parse_rpcversion("22.11.1")
assert(foo[0] == 22)
assert(foo[1] == 11)
assert(foo[2] == 1)

View File

@@ -0,0 +1,331 @@
import os
import random
import string
import unittest
from pyln.testing.fixtures import * # noqa: F401,F403
from pyln.testing.utils import wait_for
plugin_path = os.path.join(os.path.dirname(__file__), "feeadjuster.py")
def test_feeadjuster_starts(node_factory):
l1 = node_factory.get_node()
# Test dynamically
l1.rpc.plugin_start(plugin_path)
l1.daemon.wait_for_log("Plugin feeadjuster initialized.*")
l1.rpc.plugin_stop(plugin_path)
l1.rpc.plugin_start(plugin_path)
l1.daemon.wait_for_log("Plugin feeadjuster initialized.*")
l1.stop()
# Then statically
l1.daemon.opts["plugin"] = plugin_path
l1.start()
# Start at 0 and 're-await' the two inits above. Otherwise this is flaky.
l1.daemon.logsearch_start = 0
l1.daemon.wait_for_logs(["Plugin feeadjuster initialized.*",
"Plugin feeadjuster initialized.*",
"Plugin feeadjuster initialized.*"])
l1.rpc.plugin_stop(plugin_path)
# We adjust fees in init
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
scid_A = l2.rpc.listpeerchannels(l1.info["id"])["channels"][0]["short_channel_id"]
scid_B = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]["short_channel_id"]
l2.rpc.plugin_start(plugin_path)
l2.daemon.wait_for_logs([f"Adjusted fees of {scid_A}.*",
f"Adjusted fees of {scid_B}.*"])
def get_chan_fees(l, scid):
for half in l.rpc.listchannels(scid)["channels"]:
if l.info["id"] == half["source"]:
return (half["base_fee_millisatoshi"], half["fee_per_millionth"])
def wait_for_fees(l, scids, fees):
for scid in scids:
wait_for(lambda: get_chan_fees(l, scid) == fees)
def wait_for_not_fees(l, scids, fees):
for scid in scids:
wait_for(lambda: not get_chan_fees(l, scid) == fees)
def pay(l, ll, amount):
label = ''.join(random.choices(string.ascii_letters, k=20))
invoice = ll.rpc.invoice(amount, label, "desc")
route = l.rpc.getroute(ll.info["id"], amount, riskfactor=0, fuzzpercent=0)
l.rpc.sendpay(route["route"], invoice["payment_hash"], payment_secret=invoice.get('payment_secret'))
l.rpc.waitsendpay(invoice["payment_hash"])
def sync_gossip(nodes, scids):
node = nodes[0]
nodes = nodes[1:]
for scid in scids:
for n in nodes:
wait_for(lambda: node.rpc.listchannels(scid) == n.rpc.listchannels(scid))
def test_feeadjuster_adjusts(node_factory):
"""
A rather simple network:
A B
l1 <========> l2 <=========> l3
l2 will adjust its configuration-set base and proportional fees for
channels A and B as l1 and l3 exchange payments.
"""
base_fee = 5000
ppm_fee = 300
l2_opts = {
"fee-base": base_fee,
"fee-per-satoshi": ppm_fee,
"plugin": plugin_path,
"feeadjuster-deactivate-fuzz": None,
}
l1, l2, l3 = node_factory.line_graph(3, opts=[{}, l2_opts, {}],
wait_for_announce=True)
chan_A = l2.rpc.listpeerchannels(l1.info["id"])["channels"][0]
chan_B = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]
scid_A = chan_A["short_channel_id"]
scid_B = chan_B["short_channel_id"]
nodes = [l1, l2, l3]
scids = [scid_A, scid_B]
# Fees don't get updated until there is a forwarding event!
assert all([get_chan_fees(l2, scid) == (base_fee, ppm_fee)
for scid in scids])
chan_total = int(chan_A["total_msat"])
assert chan_total == int(chan_B["total_msat"])
# The first payment will trigger fee adjustment, no matter its value
amount = int(chan_total * 0.04)
pay(l1, l3, amount)
wait_for(lambda: all([get_chan_fees(l2, scid) != (base_fee, ppm_fee)
for scid in scids]))
# Send most of the balance to the other side..
amount = int(chan_total * 0.8)
pay(l1, l3, amount)
l2.daemon.wait_for_logs([f'Adjusted fees of {scid_A} with a ratio of 0.2',
f'Adjusted fees of {scid_B} with a ratio of 3.'])
# ..And back
sync_gossip(nodes, scids)
pay(l3, l1, amount)
l2.daemon.wait_for_logs([f'Adjusted fees of {scid_A} with a ratio of 6.',
f'Adjusted fees of {scid_B} with a ratio of 0.1'])
# Sending a payment worth 3% of the channel balance should not trigger
# fee adjustment
sync_gossip(nodes, scids)
fees_before = [get_chan_fees(l2, scid) for scid in [scid_A, scid_B]]
amount = int(chan_total * 0.03)
pay(l1, l3, amount)
sync_gossip(nodes, scids)
assert fees_before == [get_chan_fees(l2, scid) for scid in scids]
# But sending another 3%-worth payment does trigger adjustment (total sent
# since last adjustment is >5%)
pay(l1, l3, amount)
l2.daemon.wait_for_logs([f'Adjusted fees of {scid_A} with a ratio of 4.',
f'Adjusted fees of {scid_B} with a ratio of 0.2'])
def test_feeadjuster_imbalance(node_factory):
"""
A rather simple network:
A B
l1 <========> l2 <=========> l3
l2 will adjust its configuration-set base and proportional fees for
channels A and B as l1 and l3 exchange payments.
"""
base_fee = 5000
ppm_fee = 300
l2_opts = {
"fee-base": base_fee,
"fee-per-satoshi": ppm_fee,
"plugin": plugin_path,
"feeadjuster-deactivate-fuzz": None,
"feeadjuster-imbalance": 0.7, # should be normalized to 30/70
}
l1, l2, l3 = node_factory.line_graph(3, opts=[{}, l2_opts, {}],
wait_for_announce=True)
chan_A = l2.rpc.listpeerchannels(l1.info["id"])["channels"][0]
chan_B = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]
scid_A = chan_A["short_channel_id"]
scid_B = chan_B["short_channel_id"]
scids = [scid_A, scid_B]
default_fees = [(base_fee, ppm_fee), (base_fee, ppm_fee)]
chan_total = int(chan_A["total_msat"])
assert chan_total == int(chan_B["total_msat"])
l2.daemon.logsearch_start = 0
l2.daemon.wait_for_log('imbalance of 30%/70%')
# we force feeadjust initially to test this method and check if it applies
# default fees when balancing the channel below
l2.rpc.feeadjust()
l2.daemon.wait_for_logs([
f"Adjusted fees.*{scid_A}",
f"Adjusted fees.*{scid_B}"
])
log_offset = len(l2.daemon.logs)
wait_for_not_fees(l2, scids, default_fees[0])
# First bring channel to somewhat of a balance
amount = int(chan_total * 0.5)
pay(l1, l3, amount)
l2.daemon.wait_for_logs([
f'Set default fees as imbalance is too low for {scid_A}',
f'Set default fees as imbalance is too low for {scid_B}'
])
wait_for_fees(l2, scids, default_fees[0])
# Because of the 70/30 imbalance limiter, a 15% payment must not yet trigger
# 50% + 15% = 65% .. which is < 70%
amount = int(chan_total * 0.15)
pay(l1, l3, amount)
assert not l2.daemon.is_in_log("Adjusted fees", log_offset)
# Sending another 20% must now trigger because the imbalance
pay(l1, l3, amount)
l2.daemon.wait_for_logs([
f"Adjusted fees.*{scid_A}",
f"Adjusted fees.*{scid_B}"
])
wait_for_not_fees(l2, scids, default_fees[0])
# Bringing it back must cause default fees
pay(l3, l1, amount)
l2.daemon.wait_for_logs([
f'Set default fees as imbalance is too low for {scid_A}',
f'Set default fees as imbalance is too low for {scid_B}'
])
wait_for_fees(l2, scids, default_fees[0])
def test_feeadjuster_big_enough_liquidity(node_factory):
"""
A rather simple network:
A B
l1 <========> l2 <=========> l3
l2 will adjust its configuration-set base and proportional fees for
channels A and B as l1 and l3 exchange payments.
"""
base_fee = 5000
ppm_fee = 300
l2_opts = {
"fee-base": base_fee,
"fee-per-satoshi": ppm_fee,
"plugin": plugin_path,
"feeadjuster-deactivate-fuzz": None,
"feeadjuster-imbalance": 0.5,
"feeadjuster-enough-liquidity": "0.001btc",
"feeadjuster-threshold-abs": "0.0001btc",
}
# channels' size: 0.01btc
# between 0.001btc and 0.009btc the liquidity is big enough
l1, l2, l3 = node_factory.line_graph(3, fundamount=10**6, opts=[{}, l2_opts, {}],
wait_for_announce=True)
chan_A = l2.rpc.listpeerchannels(l1.info["id"])["channels"][0]
chan_B = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]
scid_A = chan_A["short_channel_id"]
scid_B = chan_B["short_channel_id"]
scids = [scid_A, scid_B]
default_fees = [(base_fee, ppm_fee), (base_fee, ppm_fee)]
chan_total = int(chan_A["total_msat"])
assert chan_total == int(chan_B["total_msat"])
l2.daemon.logsearch_start = 0
l2.daemon.wait_for_log('enough_liquidity: 100000000msat')
# we force feeadjust initially to test this method and check if it applies
# default fees when balancing the channel below
l2.rpc.feeadjust()
l2.daemon.wait_for_logs([
f"Adjusted fees.*{scid_A}",
f"Adjusted fees.*{scid_B}"
])
wait_for_not_fees(l2, scids, default_fees[0])
# Bring channels to beyond big enough liquidity with 0.003btc
amount = 300000000
pay(l1, l3, amount)
l2.daemon.wait_for_logs([
f"Adjusted fees of {scid_A} with a ratio of 1.0",
f"Adjusted fees of {scid_B} with a ratio of 1.0"
])
log_offset = len(l2.daemon.logs)
wait_for_fees(l2, scids, default_fees[0])
# Let's move another 0.003btc -> the channels will be at 0.006btc
amount = 300000000
pay(l1, l3, amount)
l2.wait_for_htlcs()
assert not l2.daemon.is_in_log("Adjusted fees", log_offset)
# Sending another 0.0033btc will result in a channel balance of 0.0093btc
# It must trigger because the remaining liquidity is not big enough
amount = 330000000
pay(l1, l3, amount)
l2.daemon.wait_for_logs([
f"Adjusted fees.*{scid_A}",
f"Adjusted fees.*{scid_B}"
])
wait_for_not_fees(l2, scids, default_fees[0])
def test_feeadjuster_median(node_factory):
"""
A rather simple network:
a b c
l1 <=======> l2 <=======> l3 <=======> l4
l2 will adjust its configuration-set base and proportional fees for
channels A and B as l1 and l3 exchange payments.
l4 is needed so l2 can make a median peers-of-peer calculation on l3.
"""
opts = {
"fee-base": 1337,
"fee-per-satoshi": 42,
}
l2_opts = {
"fee-base": 1000,
"fee-per-satoshi": 100,
"plugin": plugin_path,
"feeadjuster-deactivate-fuzz": None,
"feeadjuster-imbalance": 0.5,
"feeadjuster-feestrategy": "median"
}
l1, l2, l3, _ = node_factory.line_graph(4, opts=[opts, l2_opts, opts, opts],
wait_for_announce=True)
scid_a = l2.rpc.listpeerchannels(l1.info["id"])["channels"][0]["short_channel_id"]
scid_b = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]["short_channel_id"]
# we do a manual feeadjust
l2.rpc.feeadjust()
l2.daemon.wait_for_logs([
f"Adjusted fees.*{scid_a}",
f"Adjusted fees.*{scid_b}"
])
# since there is only l4 with channel c towards l3, l2 should take that value
chan_b = l2.rpc.listpeerchannels(l3.info['id'])['channels'][0]
assert chan_b['fee_base_msat'] == 1337
assert chan_b['fee_proportional_millionths'] < 42 # we could do the actual ratio math, but meh