XMoveFunds: never disable_node our own node on a NODE-level failure
Some checks are pending
Code Base Sanity Check / tests (push) Waiting to run
Code Base Sanity Check / coverage (push) Waiting to run
Code Base Sanity Check / build-clang (push) Waiting to run

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.
This commit is contained in:
Ken Sedgwick 2026-06-04 13:25:19 -07:00
parent 118a722b32
commit 7500a2344b
No known key found for this signature in database
GPG key ID: DBD2AF0849D711A9

View file

@ -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,