From bf2a9abe374d3c23d851fa414238bc2907433b09 Mon Sep 17 00:00:00 2001 From: daywalker90 <8257956+daywalker90@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:54:21 +0100 Subject: [PATCH 1/2] rebalance: tests for private channels, allow private channels in rebalanceall --- rebalance/rebalance.py | 9 ++- rebalance/test_rebalance.py | 119 ++++++++++++++++++++++++++++++++++++ rebalance/uv.lock | 6 +- 3 files changed, 128 insertions(+), 6 deletions(-) diff --git a/rebalance/rebalance.py b/rebalance/rebalance.py index 519a4efd9..2c3e0e21a 100755 --- a/rebalance/rebalance.py +++ b/rebalance/rebalance.py @@ -482,7 +482,7 @@ def rebalance( "sent": msatoshi + fees, "received": msatoshi, "fee": fees, - "fee_percentage": f"{fees/msatoshi*100:.3}%", + "fee_percentage": f"{fees / msatoshi * 100:.3}%", "hops": len(route), "outgoing_scid": outgoing_scid, "incoming_scid": incoming_scid, @@ -599,12 +599,12 @@ def get_open_channels(plugin: Plugin): if plugin.listpeerchannels: channels = plugin.rpc.listpeerchannels()["channels"] for ch in channels: - if ch["state"] == "CHANNELD_NORMAL" and not ch["private"]: + if ch["state"] == "CHANNELD_NORMAL": result.append(ch) else: for peer in plugin.rpc.listpeers()["peers"]: for ch in peer["channels"]: - if ch["state"] == "CHANNELD_NORMAL" and not ch["private"]: + if ch["state"] == "CHANNELD_NORMAL": result.append(ch) return result @@ -1261,8 +1261,7 @@ def init(options: dict, configuration: dict, plugin: Plugin, **kwargs): plugin.add_option( "rebalance-erringnodes", "5", - "Exclude nodes from routing that raised N or more errors. " - "Note: Use 0 to disable.", + "Exclude nodes from routing that raised N or more errors. Note: Use 0 to disable.", "string", ) diff --git a/rebalance/test_rebalance.py b/rebalance/test_rebalance.py index 621121375..ae389a8ab 100644 --- a/rebalance/test_rebalance.py +++ b/rebalance/test_rebalance.py @@ -175,3 +175,122 @@ def test_rebalance_all(node_factory, bitcoind): report = l1.rpc.rebalancereport() assert report.get("rebalanceall_is_running") is False assert report.get("total_successful_rebalances") == 2 + + +def test_rebalance_manual_private(node_factory, bitcoind): + l1, l2, l3 = node_factory.line_graph(3, opts=plugin_opt) + l1.daemon.logsearch_start = 0 + l1.daemon.wait_for_log("Plugin rebalance initialized.*") + nodes = [l1, l2, l3] + + # form a circle so we can do rebalancing + l3.connect(l1) + l3.fundchannel(l1, announce_channel=False) + + # get scids + scid12 = l1.get_channel_scid(l2) + scid23 = l2.get_channel_scid(l3) + scid31 = l3.get_channel_scid(l1) + scids_pub = [scid12, scid23] + + # wait for each others gossip + bitcoind.generate_block(6) + for n in nodes: + for scid in scids_pub: + n.wait_channel_active(scid) + l1.wait_local_channel_active(scid31) + + result = l1.rpc.rebalance(scid12, scid31) + print(result) + assert result["status"] == "complete" + assert result["outgoing_scid"] == scid12 + assert result["incoming_scid"] == scid31 + assert result["hops"] == 3 + assert result["received"] == "500000000msat" + + # wait until listpeers is up2date + wait_for_all_htlcs(nodes) + + # check that channels are now balanced + c12 = l1.rpc.listpeerchannels(l2.info["id"])["channels"][0] + c13 = l1.rpc.listpeerchannels(l3.info["id"])["channels"][0] + assert c13["private"] is True + assert ( + abs(0.5 - (Millisatoshi(c12["to_us_msat"]) / Millisatoshi(c12["total_msat"]))) + < 0.01 + ) + assert ( + abs(0.5 - (Millisatoshi(c13["to_us_msat"]) / Millisatoshi(c13["total_msat"]))) + < 0.01 + ) + + # check we can do a manual amount rebalance in the other direction + result = l1.rpc.rebalance(scid31, scid12, "250000000msat") + assert result["status"] == "complete" + assert result["outgoing_scid"] == scid31 + assert result["incoming_scid"] == scid12 + assert result["hops"] == 3 + assert result["received"] == "250000000msat" + + # briefly check rebalancereport works + report = l1.rpc.rebalancereport() + assert report.get("rebalanceall_is_running") is False + assert report.get("total_successful_rebalances") == 2 + + +def test_rebalance_all_private(node_factory, bitcoind): + l1, l2, l3 = node_factory.line_graph(3, opts=plugin_opt) + l1.daemon.logsearch_start = 0 + l1.daemon.wait_for_log("Plugin rebalance initialized.*") + nodes = [l1, l2, l3] + + # now we form a circle so we can do actually rebalanceall + l3.connect(l1) + l3.fundchannel(l1, announce_channel=False) + + # get scids + scid12 = l1.get_channel_scid(l2) + scid23 = l2.get_channel_scid(l3) + scid31 = l3.get_channel_scid(l1) + scids_pub = [scid12, scid23] + + # wait for each others gossip + bitcoind.generate_block(6) + for n in nodes: + for scid in scids_pub: + n.wait_channel_active(scid) + l1.wait_local_channel_active(scid31) + + # check the rebalanceall starts + result = l1.rpc.rebalanceall(feeratio=5.0) # we need high fees to work + assert result["message"].startswith("Rebalance started") + l1.daemon.wait_for_logs( + [f"tries to rebalance: {scid12} -> {scid31}", "Automatic rebalance finished"] + ) + + # check additional calls to stop return 'nothing to stop' + last message + result = l1.rpc.rebalancestop()["message"] + assert result.startswith( + "No rebalance is running, nothing to stop. " + "Last 'rebalanceall' gave: Automatic rebalance finished" + ) + + # wait until listpeers is up2date + wait_for_all_htlcs(nodes) + + # check that channels are now balanced + c12 = l1.rpc.listpeerchannels(l2.info["id"])["channels"][0] + c13 = l1.rpc.listpeerchannels(l3.info["id"])["channels"][0] + assert ( + abs(0.5 - (Millisatoshi(c12["to_us_msat"]) / Millisatoshi(c12["total_msat"]))) + < 0.01 + ) + assert ( + abs(0.5 - (Millisatoshi(c13["to_us_msat"]) / Millisatoshi(c13["total_msat"]))) + < 0.01 + ) + + # briefly check rebalancereport works + report = l1.rpc.rebalancereport() + assert report.get("rebalanceall_is_running") is False + assert report.get("total_successful_rebalances") == 2 diff --git a/rebalance/uv.lock b/rebalance/uv.lock index 08f80e837..e47316519 100644 --- a/rebalance/uv.lock +++ b/rebalance/uv.lock @@ -593,7 +593,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -1053,6 +1053,9 @@ wheels = [ name = "rebalance" version = "0.1.0" source = { virtual = "." } +dependencies = [ + { name = "pyln-client" }, +] [package.dev-dependencies] dev = [ @@ -1065,6 +1068,7 @@ dev = [ ] [package.metadata] +requires-dist = [{ name = "pyln-client", specifier = ">=24.11" }] [package.metadata.requires-dev] dev = [ From 02368d140202f126b88d65f40cf5c4a400c351dc Mon Sep 17 00:00:00 2001 From: daywalker90 <8257956+daywalker90@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:01:00 +0100 Subject: [PATCH 2/2] rebalance: log errors of our own channels --- rebalance/rebalance.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rebalance/rebalance.py b/rebalance/rebalance.py index 2c3e0e21a..cb99e685c 100755 --- a/rebalance/rebalance.py +++ b/rebalance/rebalance.py @@ -535,10 +535,12 @@ def rebalance( erring_channel = e.error.get("data", {}).get("erring_channel") erring_direction = e.error.get("data", {}).get("erring_direction") if erring_channel == incoming_scid: + plugin.log(f"Error with incoming channel: {e}") raise RpcError( "rebalance", payload, {"message": "Error with incoming channel"} ) if erring_channel == outgoing_scid: + plugin.log(f"Error with outgoing channel: {e}") raise RpcError( "rebalance", payload, {"message": "Error with outgoing channel"} )