From 7500a2344b6035bc55e944f92d46059015ed2eb7 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Thu, 4 Jun 2026 13:25:19 -0700 Subject: [PATCH] XMoveFunds: never disable_node our own node on a NODE-level failure In a circular rebalance the destination is ourselves, so a NODE-level onion failure (failcode & 0x2000) at the closing hop is attributed to erring_node == self. accumulate_failure_feedback's node-level branch called disable_node( erring_node) unconditionally, so it disabled OUR OWN node in the persistent clboss-xrebalance layer. askrene then treats every one of our channels as a disabled source -- getroutes fails with 205 "source has disabled N of N channels, leaving capacity only 0msat" -- and because disabled_nodes never ages, every subsequent rebalance is locked out permanently (fee budget is irrelevant). Observed on prod1: one end-of-route node failure took out all 73 source channels at once. Guard the node-level branch: if erring_node == self_id, return without disabling. Mirrors the self-loop guard FundsMover already has on the clboss layer. A node-level failure blamed on us (we are the paying destination, so we would accept) is a mis-attribution anyway and nothing about our own node should be disabled. Recovery for an already-poisoned layer (no askrene un-disable-node exists): askrene-remove-layer clboss-xrebalance ; askrene-create-layer clboss-xrebalance true Logging/behavior otherwise unchanged. --- Boss/Mod/XMoveFunds/Main.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Boss/Mod/XMoveFunds/Main.cpp b/Boss/Mod/XMoveFunds/Main.cpp index 58a865d..32e56a7 100644 --- a/Boss/Mod/XMoveFunds/Main.cpp +++ b/Boss/Mod/XMoveFunds/Main.cpp @@ -1233,8 +1233,18 @@ private: } if (fail & 0x2000) { - /* NODE-level failure: take the whole - * forwarder out of consideration. */ + /* NODE-level failure: take the whole forwarder + * out of consideration -- but NEVER our own node. + * In a circular rebalance the destination is us, + * so a node-level failure at the closing hop is + * attributed to self; disable_node(self) would + * disable ALL our channels as the source (askrene: + * "source has disabled N of N channels, capacity + * 0msat") and lock out every future rebalance -- + * permanently, since disabled_nodes does not age. + * Mirrors FundsMover's self_id guard. */ + if (std::string(enode) == std::string(self_id)) + return; actions.push_back( Boss::Mod::AskreneLayer::disable_node( *rpc,