mirror of
https://github.com/aljazceru/plugins.git
synced 2025-12-24 00:24:19 +01:00
feeadjuster: review fixes
- forcefeeadjust method becomes feeadjust - call feeadjust in plugin init - prevent calling setchannelfee multiple times with the same parameters - new optional get_ratio_hard and get_ratio_soft functions - safety check in get_ratio functions - maybe_setchannelfee is more concise thanks to @m-schmoock - new test cases by @m-schmoock
This commit is contained in:
committed by
Christian Decker
parent
ca518fd291
commit
6bbd26af18
@@ -13,12 +13,53 @@ plugin.our_node_id = None
|
||||
plugin.update_threshold = 0.05
|
||||
|
||||
|
||||
def get_ratio(plugin: Plugin, our_percentage):
|
||||
def get_ratio_soft(our_percentage):
|
||||
"""
|
||||
Basic algorithm: lesser difference than default
|
||||
"""
|
||||
our_percentage = min(1, max(0, our_percentage))
|
||||
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 plugin.ratio_base**(0.5 - our_percentage)
|
||||
our_percentage = min(1, max(0, our_percentage))
|
||||
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
|
||||
"""
|
||||
our_percentage = min(1, max(0, our_percentage))
|
||||
return 100**(0.5 - our_percentage) * (1 - our_percentage) * 2
|
||||
|
||||
|
||||
def get_chan_fees(plugin: Plugin, scid: str):
|
||||
channels = plugin.rpc.listchannels(scid)["channels"]
|
||||
for ch in channels:
|
||||
if ch["source"] == plugin.our_node_id:
|
||||
return { "base_fee_millisatoshi": ch["base_fee_millisatoshi"],
|
||||
"fee_per_millionth": ch["fee_per_millionth"] }
|
||||
|
||||
|
||||
def maybe_setchannelfee(plugin: Plugin, scid: str, base = None, ppm = None):
|
||||
base = plugin.adj_basefee if base is None else base
|
||||
ppm = plugin.adj_ppmfee if ppm is None else ppm
|
||||
fees = get_chan_fees(plugin, scid)
|
||||
|
||||
if not fees or base == fees["base_fee_millisatoshi"] and ppm == fees["fee_per_millionth"]:
|
||||
return False
|
||||
|
||||
try:
|
||||
plugin.rpc.setchannelfee(scid, base, ppm)
|
||||
return True
|
||||
except RpcError as e:
|
||||
plugin.log("Could not adjust fees for channel {}: '{}'".format(scid, e), level="warn")
|
||||
return False
|
||||
|
||||
|
||||
def maybe_adjust_fees(plugin: Plugin, scids: list):
|
||||
@@ -29,6 +70,14 @@ def maybe_adjust_fees(plugin: Plugin, scids: list):
|
||||
percentage = our / total
|
||||
last_percentage = plugin.adj_balances[scid].get("last_percentage")
|
||||
|
||||
# reset to normal fees if imbalance is not high enough
|
||||
if (percentage > plugin.imbalance and percentage < 1 - plugin.imbalance):
|
||||
if maybe_setchannelfee(plugin, scid): # applies default values
|
||||
plugin.log("Set default fees as imbalance is too low: {}".format(scid))
|
||||
plugin.adj_balances[scid]["last_percentage"] = percentage
|
||||
channels_adjusted += 1
|
||||
continue
|
||||
|
||||
# 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
|
||||
@@ -38,25 +87,13 @@ def maybe_adjust_fees(plugin: Plugin, scids: list):
|
||||
and abs(last_percentage - percentage) < update_threshold):
|
||||
continue
|
||||
|
||||
# reset to normal fees if imbalance is not high enough
|
||||
if (percentage > plugin.imbalance and percentage < 1 - plugin.imbalance):
|
||||
plugin.rpc.setchannelfee(scid) # applies default values
|
||||
plugin.log("Set default fees as imbalance is too low: {}".format(scid))
|
||||
plugin.adj_balances[scid]["last_percentage"] = percentage
|
||||
channels_adjusted += 1
|
||||
continue
|
||||
|
||||
ratio = get_ratio(plugin, percentage)
|
||||
try:
|
||||
plugin.rpc.setchannelfee(scid, int(plugin.adj_basefee * ratio),
|
||||
int(plugin.adj_ppmfee * ratio))
|
||||
ratio = plugin.get_ratio(percentage)
|
||||
if maybe_setchannelfee(plugin, scid, int(plugin.adj_basefee * ratio),
|
||||
int(plugin.adj_ppmfee * ratio)):
|
||||
plugin.log("Adjusted fees of {} with a ratio of {}"
|
||||
.format(scid, ratio))
|
||||
plugin.adj_balances[scid]["last_percentage"] = percentage
|
||||
channels_adjusted += 1
|
||||
except RpcError as e:
|
||||
plugin.log("Could not adjust fees for channel {}: '{}'"
|
||||
.format(scid, e), level="warn")
|
||||
return channels_adjusted
|
||||
|
||||
|
||||
@@ -108,24 +145,27 @@ def forward_event(plugin: Plugin, forward_event: dict, **kwargs):
|
||||
plugin.log("Adjusting fees: " + str(e), level="error")
|
||||
|
||||
|
||||
@plugin.method("forcefeeadjust")
|
||||
def forcefeeadjust(plugin: Plugin):
|
||||
@plugin.method("feeadjust")
|
||||
def feeadjust(plugin: Plugin):
|
||||
"""Adjust fees for all existing channels.
|
||||
|
||||
Can run effectively once as an initial setup, or after a successful payment. Otherwise, the plugin keeps the fees up-to-date.
|
||||
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.
|
||||
"""
|
||||
peers = plugin.rpc.listpeers()["peers"]
|
||||
channels_adjusted = 0
|
||||
for peer in peers:
|
||||
for chan in peer["channels"]:
|
||||
if chan["state"] == "CHANNELD_NORMAL":
|
||||
scid = chan['short_channel_id'];
|
||||
if scid not in plugin.adj_balances:
|
||||
plugin.adj_balances[scid] = {}
|
||||
plugin.adj_balances[scid]["our"] = int(chan["to_us_msat"])
|
||||
plugin.adj_balances[scid]["total"] = int(chan["total_msat"])
|
||||
scid = chan["short_channel_id"];
|
||||
plugin.adj_balances[scid] = {
|
||||
"our": int(chan["to_us_msat"]),
|
||||
"total": int(chan["total_msat"])
|
||||
}
|
||||
channels_adjusted += maybe_adjust_fees(plugin, [scid])
|
||||
return "%s channels adjusted" % channels_adjusted
|
||||
msg = "%s channels adjusted" % channels_adjusted
|
||||
plugin.log(msg)
|
||||
return msg
|
||||
|
||||
|
||||
@plugin.init()
|
||||
@@ -134,7 +174,11 @@ def init(options: dict, configuration: dict, plugin: Plugin, **kwargs):
|
||||
plugin.deactivate_fuzz = options.get("feeadjuster-deactivate-fuzz", False)
|
||||
plugin.update_threshold = float(options.get("feeadjuster-threshold", "0.05"))
|
||||
plugin.imbalance = float(options.get("feeadjuster-imbalance", 0.5))
|
||||
plugin.ratio_base = int(options.get("feeadjuster-ratio-base", "50"))
|
||||
plugin.get_ratio = get_ratio
|
||||
if options.get("feeadjuster-adjustment-method", "default") == "soft":
|
||||
plugin.get_ratio = get_ratio_soft
|
||||
if options.get("feeadjuster-adjustment-method", "default") == "hard":
|
||||
plugin.get_ratio = get_ratio_hard
|
||||
config = plugin.rpc.listconfigs()
|
||||
plugin.adj_basefee = config["fee-base"]
|
||||
plugin.adj_ppmfee = config["fee-per-satoshi"]
|
||||
@@ -146,13 +190,15 @@ def init(options: dict, configuration: dict, plugin: Plugin, **kwargs):
|
||||
plugin.imbalance = 1 - plugin.imbalance
|
||||
|
||||
plugin.log("Plugin feeadjuster initialized ({} base / {} ppm) with an "
|
||||
"imbalance of {}%/{}%, update_threshold: {}, deactivate_fuzz: {}, ratio_base {}".format(plugin.adj_basefee,
|
||||
plugin.adj_ppmfee,
|
||||
int(100*plugin.imbalance),
|
||||
int(100*(1-plugin.imbalance)),
|
||||
plugin.update_threshold,
|
||||
plugin.deactivate_fuzz,
|
||||
plugin.ratio_base))
|
||||
"imbalance of {}%/{}%, update_threshold: {}, deactivate_fuzz: {}, adjustment_method: {}"
|
||||
.format(plugin.adj_basefee,
|
||||
plugin.adj_ppmfee,
|
||||
int(100*plugin.imbalance),
|
||||
int(100*(1-plugin.imbalance)),
|
||||
plugin.update_threshold,
|
||||
plugin.deactivate_fuzz,
|
||||
plugin.get_ratio))
|
||||
feeadjust(plugin)
|
||||
|
||||
|
||||
plugin.add_option(
|
||||
@@ -169,10 +215,10 @@ plugin.add_option(
|
||||
"string"
|
||||
)
|
||||
plugin.add_option(
|
||||
"feeadjuster-ratio-base",
|
||||
"50",
|
||||
"Channel fees will be adjusted by the exponent of this."
|
||||
"New fee = <default fee> * feeadjuster-ratio-base**(0.5 - <our liquidity ratio>)"
|
||||
"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(
|
||||
|
||||
@@ -33,6 +33,16 @@ def get_chan_fees(l, scid):
|
||||
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")
|
||||
@@ -41,8 +51,12 @@ def pay(l, ll, amount):
|
||||
l.rpc.waitsendpay(invoice["payment_hash"])
|
||||
|
||||
|
||||
def sync_gossip(l, ll, scid):
|
||||
wait_for(lambda: l.rpc.listchannels(scid) == ll.rpc.listchannels(scid))
|
||||
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))
|
||||
|
||||
|
||||
@unittest.skipIf(not DEVELOPER, "Too slow without fast gossip")
|
||||
@@ -71,11 +85,12 @@ def test_feeadjuster_adjusts(node_factory):
|
||||
chan_B = l2.rpc.listpeers(l3.info["id"])["peers"][0]["channels"][0]
|
||||
scid_A = chan_A["short_channel_id"]
|
||||
scid_B = chan_B["short_channel_id"]
|
||||
l2_scids = [scid_A, scid_B]
|
||||
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 l2_scids])
|
||||
for scid in scids])
|
||||
|
||||
chan_total = int(chan_A["total_msat"])
|
||||
assert chan_total == int(chan_B["total_msat"])
|
||||
@@ -84,7 +99,7 @@ def test_feeadjuster_adjusts(node_factory):
|
||||
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 l2_scids]))
|
||||
for scid in scids]))
|
||||
|
||||
# Send most of the balance to the other side..
|
||||
amount = int(chan_total * 0.8)
|
||||
@@ -95,9 +110,7 @@ def test_feeadjuster_adjusts(node_factory):
|
||||
" 3.".format(scid_B)) is not None)
|
||||
|
||||
# ..And back
|
||||
for scid in l2_scids:
|
||||
sync_gossip(l3, l2, scid)
|
||||
sync_gossip(l1, l2, scid)
|
||||
sync_gossip(nodes, scids)
|
||||
pay(l3, l1, amount)
|
||||
wait_for(lambda: l2.daemon.is_in_log("Adjusted fees of {} with a ratio of"
|
||||
" 6.".format(scid_A)) is not None)
|
||||
@@ -106,16 +119,12 @@ def test_feeadjuster_adjusts(node_factory):
|
||||
|
||||
# Sending a payment worth 3% of the channel balance should not trigger
|
||||
# fee adjustment
|
||||
for scid in l2_scids:
|
||||
sync_gossip(l3, l2, scid)
|
||||
sync_gossip(l1, l2, scid)
|
||||
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)
|
||||
for scid in l2_scids:
|
||||
sync_gossip(l3, l2, scid)
|
||||
sync_gossip(l1, l2, scid)
|
||||
assert fees_before == [get_chan_fees(l2, scid) for scid in l2_scids]
|
||||
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%)
|
||||
@@ -153,37 +162,43 @@ def test_feeadjuster_imbalance(node_factory):
|
||||
chan_B = l2.rpc.listpeers(l3.info["id"])["peers"][0]["channels"][0]
|
||||
scid_A = chan_A["short_channel_id"]
|
||||
scid_B = chan_B["short_channel_id"]
|
||||
l2_scids = [scid_A, scid_B]
|
||||
nodes = [l1, l2, l3]
|
||||
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.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_log("Adjusted fees")
|
||||
l2.daemon.wait_for_log("Adjusted fees")
|
||||
log_offset = len(l2.daemon.logs)
|
||||
wait_for_not_fees(l2, scids, default_fees[0])
|
||||
|
||||
# First bring channel to somewhat of a blanance
|
||||
amount = int(chan_total * 0.5)
|
||||
pay(l1, l3, amount)
|
||||
l2.daemon.wait_for_log('Set default fees as imbalance is too low')
|
||||
for scid in l2_scids:
|
||||
sync_gossip(l3, l2, scid)
|
||||
sync_gossip(l1, l2, scid)
|
||||
fees_before = [get_chan_fees(l2, scid) for scid in [scid_A, scid_B]]
|
||||
assert fees_before == [(base_fee, ppm_fee), (base_fee, ppm_fee)]
|
||||
l2.daemon.wait_for_log('Set default fees as imbalance is too low')
|
||||
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)
|
||||
for scid in l2_scids:
|
||||
sync_gossip(l3, l2, scid)
|
||||
sync_gossip(l1, l2, scid)
|
||||
fees_before = [get_chan_fees(l2, scid) for scid in [scid_A, scid_B]]
|
||||
assert fees_before == [get_chan_fees(l2, scid) for scid in l2_scids]
|
||||
assert not l2.daemon.is_in_log("Adjusted fees")
|
||||
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_log("Adjusted fees")
|
||||
l2.daemon.wait_for_log("Adjusted fees")
|
||||
wait_for_not_fees(l2, scids, default_fees[0])
|
||||
|
||||
# Bringing it back must cause default fees
|
||||
pay(l3, l1, amount)
|
||||
l2.daemon.wait_for_log('Set default fees as imbalance is too low')
|
||||
l2.daemon.wait_for_log('Set default fees as imbalance is too low')
|
||||
wait_for_fees(l2, scids, default_fees[0])
|
||||
|
||||
Reference in New Issue
Block a user