topology: in deterministic mode, only return one best candidate for listincoming.
What changed, and why it matters
This change makes a Lightning node return a single, predictable incoming channel when running in deterministic mode (used for reproducible testing and invoice route hints). Previously, multiple candidate channels could be returned in a non-deterministic order, which could cause the same invoice to produce different route hints across runs. The patch selects the highest-capacity channel as the one and only candidate. This is primarily a reliability/consistency fix; it does not appear to be a direct security vulnerability, but non-deterministic route hints could theoretically leak extra topology information or cause payment routing inconsistencies.
Treat as a normal code-quality/reliability patch. No urgent security action required. Reviewers may want to confirm that deterministic mode cannot be enabled in production configurations and that selecting a single highest-capacity channel does not degrade payment reliability for nodes with multiple usable incoming channels.
Security signals we found
Non-deterministic route hint selection could expose different subsets of a node's channels across invoice generations
Deterministic mode is typically test-only, limiting production exposure
No input validation or memory safety issues visible in the diff
No cryptographic or authorization changes
Evidence from the diff
In plugins/topology.c, the listincoming RPC handler now checks if the random byte generator has been overridden (randbytes_overridden()). When in deterministic mode, it pre-selects the single best incoming channel candidate by capacity using best_candidate(), and skips all other channels in the output loop. This ensures bolt11/bolt12 route hints and blinded paths are stable across runs. The change adds a helper that iterates the node’s channels, skips those without a valid channel update in the opposite direction, computes peer_capacity(), and tracks the index with the greatest capacity.
Changed components
plugins/topology.clistincoming RPC handlerbolt11/bolt12 route hint and blinded path generationInspect captured patch +36 / −0
diff --git a/plugins/topology.c b/plugins/topology.c
index 944180de..5990bbe7 100644
--- a/plugins/topology.c
+++ b/plugins/topology.c
@@ -8,6 +8,7 @@
#include <common/json_param.h>
#include <common/json_stream.h>
#include <common/memleak.h>
+#include <common/randbytes.h>
#include <common/route.h>
#include <errno.h>
#include <inttypes.h>
@@ -459,6 +460,33 @@ static struct amount_msat peer_capacity(const struct gossmap *gossmap,
return capacity;
}
+/* For deterministic results with bolt12/11 routes, we only return a
+ * single candidate: choose the one with most capacity */
+static size_t best_candidate(const struct gossmap *gossmap,
+ const struct gossmap_node *me)
+{
+ struct amount_msat best_cap = AMOUNT_MSAT(0);
+ size_t best_num = 0;
+ for (size_t i = 0; i < me->num_chans; i++) {
+ int dir;
+ struct gossmap_chan *ourchan;
+ struct amount_msat cap;
+ struct gossmap_node *peer;
+
+ ourchan = gossmap_nth_chan(gossmap, me, i, &dir);
+ if (ourchan->cupdate_off[!dir] == 0)
+ continue;
+
+ peer = gossmap_nth_node(gossmap, ourchan, !dir);
+ cap = peer_capacity(gossmap, me, peer, ourchan);
+ if (amount_msat_greater(cap, best_cap)) {
+ best_num = i;
+ best_cap = cap;
+ }
+ }
+ return best_num;
+}
+
static struct command_result *
listpeerchannels_listincoming_done(struct command *cmd,
const char *method,
@@ -470,6 +498,7 @@ listpeerchannels_listincoming_done(struct command *cmd,
struct gossmap_node *me;
struct gossmap *gossmap;
struct gossmap_localmods *mods;
+ size_t deterministic_candidate = 0;
/* Get local knowledge */
mods = gossmods_from_listpeerchannels(tmpctx, &local_id,
@@ -487,6 +516,9 @@ listpeerchannels_listincoming_done(struct command *cmd,
if (!me)
goto done;
+ if (randbytes_overridden())
+ deterministic_candidate = best_candidate(gossmap, me);
+
for (size_t i = 0; i < me->num_chans; i++) {
struct node_id peer_id;
int dir;
@@ -499,6 +531,10 @@ listpeerchannels_listincoming_done(struct command *cmd,
/* Entirely missing? Ignore. */
if (ourchan->cupdate_off[!dir] == 0)
continue;
+
+ if (randbytes_overridden() && i != deterministic_candidate)
+ continue;
+
/* We used to ignore if the peer said it was disabled,
* but we have a report of LND telling us our unannounced
* channel is disabled, so we still use them. */
Why this scored 26/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.