diff --git a/feeadjuster/feeadjuster.py b/feeadjuster/feeadjuster.py index 51d06c3..fbfe1b1 100755 --- a/feeadjuster/feeadjuster.py +++ b/feeadjuster/feeadjuster.py @@ -28,6 +28,12 @@ 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): + plugin.rpc.setchannelfee(scid) # applies default values + plugin.log("Set default fees as imbalance is too low: {}".format(scid)) + return + # 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 @@ -101,14 +107,22 @@ def init(options: dict, configuration: dict, plugin: Plugin, **kwargs): plugin.our_node_id = plugin.rpc.getinfo()["id"] 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)) config = plugin.rpc.listconfigs() plugin.adj_basefee = config["fee-base"] plugin.adj_ppmfee = config["fee-per-satoshi"] - plugin.log("Plugin feeadjuster initialized ({} base / {} ppm) with a " - "threshold of {}" - .format(plugin.adj_basefee, plugin.adj_ppmfee, - plugin.update_threshold)) + # 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 + + plugin.log("Plugin feeadjuster initialized ({} base / {} ppm) with an " + "imbalance of {}%/{}%".format(plugin.adj_basefee, + plugin.adj_ppmfee, + int(100*plugin.imbalance), + int(100*(1-plugin.imbalance)))) plugin.add_option( @@ -124,4 +138,13 @@ plugin.add_option( "Note: it's also fuzzed by 1.5%", "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.run() diff --git a/feeadjuster/test_feeadjuster.py b/feeadjuster/test_feeadjuster.py index c056c78..e33971e 100644 --- a/feeadjuster/test_feeadjuster.py +++ b/feeadjuster/test_feeadjuster.py @@ -124,3 +124,66 @@ def test_feeadjuster_adjusts(node_factory): " 4.".format(scid_A)) is not None) wait_for(lambda: l2.daemon.is_in_log("Adjusted fees of {} with a ratio of" " 0.2".format(scid_B)) is not None) + + +@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"] + l2_scids = [scid_A, scid_B] + + chan_total = int(chan_A["total_msat"]) + assert chan_total == int(chan_B["total_msat"]) + l2.daemon.wait_for_log('imbalance of 30%/70%') + + # 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)] + + # 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") + + # Sending another 20% must now trigger because the imbalance + pay(l1, l3, amount) + l2.daemon.wait_for_log("Adjusted fees") + + # Bringing it back must cause default fees + pay(l3, l1, amount) + l2.daemon.wait_for_log('Set default fees as imbalance is too low')