Skip to content

xpay disable bad nodes - #9681

Open
Lagrang3 wants to merge 5 commits into
ElementsProject:masterfrom
Lagrang3:fix/xpay-disable-bad-nodes
Open

Lagrang3 wants to merge 5 commits into
ElementsProject:masterfrom
Lagrang3:fix/xpay-disable-bad-nodes

Conversation

@Lagrang3

@Lagrang3 Lagrang3 commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

This is a follow-up PR from #9592.
We keep a record of rare errors from nodes during payment attempts.
Repeated strange errors trigger a negative bias that is applied to the node and if the errors keep coming
above a certain tolerance counter we disable the node for the current payment session.

It addresses a known issue of xpay retrying payment attempts through the same node even after
repeated unlikely error messages: #8600, #9590

@Lagrang3
Lagrang3 requested a review from cdecker as a code owner November 2, 2026 14:00
@Lagrang3
Lagrang3 requested review from cdecker and nGoline and removed request for cdecker November 2, 2026 14:01
@Lagrang3 Lagrang3 changed the title Fix/xpay disable bad nodes xpay disable bad nodes Nov 2, 2026
@Lagrang3 Lagrang3 added this to the v26.12 milestone Nov 2, 2026
@Lagrang3
Lagrang3 force-pushed the fix/xpay-disable-bad-nodes branch from 6b2ba02 to 1478850 Compare November 2, 2026 14:25

@cdecker cdecker left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two minor things that I'd like to see before merging this:

  • Blocklist any channel that we got an unknown_next_peer from, otherwise we risk immediately retrying it, causing the tolerance to run through quicker than necessary.
  • Implement the fee_insufficient error channel_update extraction, and add it to gossip_store. This could already resolve the issue of repeated fee_insufficient errors, as we now get the real feerate right away, and can try it immediately again if the MCF solver wants to use it. Also that'd remove one "weird" error

All in all I feel like the weird error mechanism can stay, but the two examples that are being used here may not be good matches (unknown_next_peer is an indication that we haven't cleaned the gossip_store using block spends, and fee_insufficient may very well be just the result of discarding the channel_update rather than persisting it).

Comment thread plugins/askrene/layer.c Resolved
Comment thread plugins/xpay/xpay.c Resolved
Comment thread plugins/xpay/xpay.c Resolved
Comment thread plugins/xpay/xpay.c
Comment on lines -1179 to -1188
/* One hit can be a stale scid, so disable that channel.
* The same node returning it again is excluded, or we
* walk every other channel through it. */
add_result_summary(attempt, LOG_DBG,
"We got %s for %s: disabling it for this payment",
errmsg,
describe_scidd(attempt, index));
maybe_exclude_unknown_next_peer(aux_cmd, attempt, index);
goto disable_channel;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to absolutely avoid this channel at all costs, the node told us it's no longer there. By biasing the node rather than banning the channel we risk reusing it and causing the node to be banned on the next attempt due to a repeated error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested a payment to Phoenix wallet:

  • got an invoice inv1 from phoenix, routehint is some LSP only scid (16Mx...x...) not revealing the onchain channel,
  • turned off the wallet,
  • called xpay on core-lightning, payment failed with "unknown_next_peer" at scid,
  • created a layer "disable-unknown-next-peer",
  • disabled scid on that layer,
  • opened up Phoenix,
  • retried xpay invoice=inv1 and layer=["disable-unknown-next-peer"], got failure "All 1 channels to the destination are disabled",
  • asked Phoenix for a second invoice inv2, same scid in the routehint,
  • retried xpay invoice=inv2 and layer=["disable-unknown-next-peer"], got failure "All 1 channels to the destination are disabled",

Lesson learned: If we permanently disable channels that have given us "unknown_next_peer" (who knows about other other permanent errors),
we will shoot ourselves in the foot.

I get the point of doing something permanent if we get a "perm" error message. But that has to be carefully crafted for every message
considering the behavior of the other implementations. Can we do that in a follow up PR?

@nGoline nGoline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is red because of the PR itself, not flakes:

  • test_pay_exclude_node fails in 4 jobs because the new "adding a global bias to channel ..." line goes through add_result_summary, so it ends up in the error string returned to the user. I would log those two bias messages with attempt_log(..., LOG_DBG, ...) instead; they are internal bookkeeping, not something a pay caller needs to read. That also keeps the second commit bisectable.
  • check-doc-examples wants xpay-failing-node-tolerance before xpay-handle-pay in listconfigs.json; make update-doc-examples should sort it (and the msggen schema).

On behaviour:

  • The default tolerance of 4 undoes the #9590 fix that is on master but not released yet: that one excluded the node on the second unknown_next_peer. The #9590 test only passes now because it sets the tolerance to 2; with the default, xpay walks all four exits of the hub again, which is what the test docstring calls the bug. I would either default to 2, or keep the #9590 test on defaults and make sure it passes.
  • Agree with @cdecker that a final-only error (incorrect_or_unknown_payment_details, mpp_timeout, final_incorrect_*) from an intermediate node should disable the node right away. The NODE-bit codes (temporary_node_failure, permanent_node_failure, required_node_feature_missing) fall in the same bucket: BOLT #4 says to remove all channels of the erring node, and today they need 4 distinct scids first.
  • The node bias threshold is hardcoded to 2 while the disable threshold is configurable, and a tolerance of 0 disables on the first error. Worth tying them together, or at least documenting it.
  • Minor: in the second commit, test_xpay_fee_insufficient_bias says "we should not need to disable the hub" but asserts that it was disabled.

On the two points in the review summary: for fee_insufficient, xpay already extracts the channel_update (process_channel_update_from_onion_error) and applies it to the payment's private layer, so only errors without a usable update reach this path. Putting it into gossip_store would go against BOLT #4, which says the sender MUST NOT apply an onion error channel_update to its local network graph. For unknown_next_peer, the channel is already disabled for the rest of the payment; your Phoenix experiment is a good argument against making that persistent, and I am fine with handling per-code permanent bans in a follow-up.

🤖 Generated with Claude Code

@Lagrang3
Lagrang3 force-pushed the fix/xpay-disable-bad-nodes branch 2 times, most recently from 880df4a to de36c81 Compare November 11, 2026 13:59
@Lagrang3

Lagrang3 commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Minor: in the second commit, test_xpay_fee_insufficient_bias says "we should not need to disable the hub" but asserts that it was disabled.

The assertion is: assert not payer.daemon.is_in_log(...), ie. that the "disabling node" line is not in the logs.

@Lagrang3

Copy link
Copy Markdown
Collaborator Author

The default tolerance of 4 undoes the #9590 fix that is on master but not released yet: that one excluded the node on the second unknown_next_peer. The #9590 test only passes now because it sets the tolerance to 2; with the default, xpay walks all four exits of the hub again, which is what the test docstring calls the bug. I would either default to 2, or keep the #9590 test on defaults and make sure it passes.

This test wants to prove that we are able to identify repeated "unknown_next_peer" errors from a hub and disable it.
The real bug is to keep trying it ad infinitum without any changes to our routing graph.
The least changes needed to make this test pass after we introduced the failure tolerance was to set it to 2 in this particular case.
Another possibility could have been to assert that we tried 4 times (with a hub that has more than 4 channels) before disabling it.

I don't like to set the default tolerance to 2 because we might be disabling a bridge node to the destination, so first adding a bias to it
would encourage the routing algorithm to search alternative routes, while not excluding the possibility that the only route is through that node.

I do agree that we could have a default tolerance of 3.

@Lagrang3

Copy link
Copy Markdown
Collaborator Author

The node bias threshold is hardcoded to 2 while the disable threshold is configurable, and a tolerance of 0 disables on the first error. Worth tying them together, or at least documenting it.

Worth tying and documenting it.

@Lagrang3

Copy link
Copy Markdown
Collaborator Author

Agree with @cdecker that a final-only error (incorrect_or_unknown_payment_details, mpp_timeout, final_incorrect_*) from an intermediate node should disable the node right away. The NODE-bit codes (temporary_node_failure, permanent_node_failure, required_node_feature_missing) fall in the same bucket: BOLT #4 says to remove all channels of the erring node, and today they need 4 distinct scids first.

Agree. Will do.

Node and channel bias add up but they never should exceed the [-100, 100] range

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
We have had reports of payment failures by xpay timeout after multiple
routes have been tried over the same node with weird errors like
unknown_next_peer and fee_insufficient.
These nodes are usually well connected hubs with hundreds of channels
and they tend to be preferred by getroutes even after the first erring
channels are disabled.  Naturally, we only disable channels for the
current payment session, so next time a second payment prefers that
route it will fail as well.
To fix this we add a negative bias on the failing channels that persist
across payments and if we detect a pattern in the failing node we
disable it completely on the current session and add a persistent bias
to it.

Changelog-Fixed: xpay: avoid channel and nodes that fail too often with weird errors

Reported-by: @blockdyor and @ErikDeSmedt
Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
@Lagrang3
Lagrang3 force-pushed the fix/xpay-disable-bad-nodes branch from de36c81 to c5c61ae Compare November 11, 2026 17:07
@Lagrang3

Copy link
Copy Markdown
Collaborator Author

@cdecker

Implement the fee_insufficient error channel_update extraction, and add it to gossip_store. This could already resolve the issue of repeated fee_insufficient errors, as we now get the real feerate right away, and can try it immediately again if the MCF solver wants to use it. Also that'd remove one "weird" error

We already have a branch that detects a channel update in the onion error. If present this channel update will be used to update the
details of the failing channel through the "askrene-update-channel" rpc instead of feeding the gossip store. There are some checks in place
to prevent a node from feeding up channel updates for a different channel than the one he controls. Renepay instead would call addgossip right away.
Therefore when we log a node that reports a fee_insufficient error it is implied that there either was not channel update or the channel update
was unusable.

@cdecker

cdecker commented Nov 11, 2026

Copy link
Copy Markdown
Member

Ah, ok, I had missed that is already being added to the gossip store, if we have one, and mixed the two cases with and without the update 👍

@Lagrang3

Copy link
Copy Markdown
Collaborator Author

A drawback could be the fact that we don't actually add the channel update to the gossip store, but to askrene instead. I think the rationale is that we will eventually learn of that gossip message.

We disable a node for the current payment session if the number of weird
failures is equal to the xpay-failing-node-tolerance.  For repeated
failing nodes we also apply a global negative bias to avoid that node on
other payments.  The bias is by default applied after the first 2
failures and it accumulates.  If xpay-failing-node-tolerance is less
than 2 we apply the bias the moment we disable the node.

Changelog-Added: xpay: add xpay-failing-node-tolerance option to set the number of weird failures that trigger a node to be disabled during the current payment session.

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Changelog-Fixed: xpay: instantly disable nodes that return plain wrong or impossible to recover error codes, eg. incorrect_or_unknown_payment_details from an intermediate node or permanent_node_failure, the disable operation applies to the current payment session and in addition a negative bias is applied to this node for all subsequent payments.

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
@Lagrang3
Lagrang3 force-pushed the fix/xpay-disable-bad-nodes branch from c5c61ae to 5b3cfb9 Compare November 11, 2026 18:24
@Lagrang3

Lagrang3 commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Ah, ok, I had missed that is already being added to the gossip store, if we have one, and mixed the two cases with and without the update 👍

Though, I am not sure if that branch is covered by the integration tests.
UPD: Yes. Just checked. There are tests to cover channel updates.

@Lagrang3
Lagrang3 requested review from cdecker and nGoline November 12, 2026 05:49

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants