Files
plugins/feeadjuster/test_feeadjuster.py
2022-12-26 13:52:00 +01:00

295 lines
10 KiB
Python

import os
import random
import string
import unittest
from pyln.testing.fixtures import * # noqa: F401,F403
from pyln.testing.utils import DEVELOPER, 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.listpeers(
l1.info["id"])["peers"][0]["channels"][0]["short_channel_id"]
scid_B = l2.rpc.listpeers(
l3.info["id"])["peers"][0]["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))
@unittest.skipIf(not DEVELOPER, "Too slow without fast gossip")
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.listpeers(l1.info["id"])["peers"][0]["channels"][0]
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"]
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'])
@unittest.skipIf(not DEVELOPER, "Too slow without fast gossip")
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.listpeers(l1.info["id"])["peers"][0]["channels"][0]
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"]
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: {scid_A}',
f'Set default fees as imbalance is too low: {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: {scid_A}',
f'Set default fees as imbalance is too low: {scid_B}'
])
wait_for_fees(l2, scids, default_fees[0])
@unittest.skipIf(not DEVELOPER, "Too slow without fast gossip")
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.listpeers(l1.info["id"])["peers"][0]["channels"][0]
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"]
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])