askrene: exclude stubchannels from routing
What changed, and why it matters
This commit fixes a crash in Core Lightning's routing plugin (askrene) that occurred when a user had 'recovered channel stubs'—placeholder records left behind after restoring a node from a backup. These stubs all share the same placeholder channel ID (1x1x1), which caused an internal table that expects unique IDs to fail an assertion and crash the plugin. The fix makes askrene skip these stubs when planning routes. The crash is triggered by normal user actions (trying to pay with recovered stubs present), not by a remote attacker, and the plugin can be restarted.
Apply the patch and run the new regression test. Users who have performed static-backup recovery and still see askrene crashes should upgrade. No immediate remote mitigation is needed because the crash requires local recovered stubs.
Security signals we found
Denial-of-service condition: local askrene plugin aborts on assertion failure
Root cause: non-unique placeholder SCIDs in recovered channel stubs
Fix: filter stub SCIDs before routing
Regression test added to prevent reintroduction
Evidence from the diff
In common/gossmods_listpeerchannels.c, the function gossmods_from_listpeerchannels_ now calls is_stub_scid(scidd.scid) and skips any channel whose short channel ID is the recovery stub placeholder (1x1x1). Previously, these stubs were fed into askrene’s additional_cost_htable, which asserts uniqueness of SCIDs; because every recovery stub uses the same SCID, adding a second one triggered an assertion failure in additional_cost_htable_add. A regression test in tests/test_askrene.py creates three stub channels by deleting the database and using recoverchannel, then verifies that getroutes no longer crashes askrene.
Changed components
plugins/askrenecommon/gossmods_listpeerchannels.ctests/test_askrene.pyInspect captured patch +40 / −1
### common/gossmods_listpeerchannels.c
@@ -143,6 +143,11 @@ gossmods_from_listpeerchannels_(const tal_t *ctx,
if (scidd.scid.u64 == 0)
continue;
+ /* Recovery stubs all use the placeholder 1x1x1 SCID. They are
+ * deliberately not unique and cannot be used for routing. */
+ if (is_stub_scid(scidd.scid))
+ continue;
+
/* Disable if in bad state (it's already false if not connected) */
if (!streq(state, "CHANNELD_NORMAL")
&& !streq(state, "CHANNELD_AWAITING_SPLICE"))
@@ -178,4 +183,3 @@ gossmods_from_listpeerchannels_(const tal_t *ctx,
return mods;
}
-
### tests/test_askrene.py
@@ -1215,6 +1215,41 @@ def test_getroutes_auto_localchans(node_factory):
{'short_channel_id_dir': f'2x2x1/{dir12}', 'amount_in_msat': 101000, 'cltv_in': 99 + 6}]])
+@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3',
+ "deletes database, which is assumed sqlite3")
+def test_getroutes_ignores_recovery_stubs(node_factory):
+ l1, l2, l3, l4 = node_factory.get_nodes(4)
+
+ # Three stubs guarantee that at least two have the same direction. Since
+ # every recovery stub has SCID 1x1x1, askrene used to abort while adding
+ # the second such channel to its no-duplicates additional-cost table.
+ l1.fundchannel(l2, 100000)
+ l1.fundchannel(l3, 100000)
+ l1.fundchannel(l4, 100000)
+ scb = l1.rpc.staticbackup()['scb']
+
+ l2.stop()
+ l3.stop()
+ l4.stop()
+ l1.stop()
+ os.unlink(os.path.join(l1.daemon.lightning_dir,
+ TEST_NETWORK,
+ 'lightningd.sqlite3'))
+ l1.start()
+ assert len(l1.rpc.recoverchannel(scb)['stubs']) == 3
+
+ with pytest.raises(RpcError):
+ l1.rpc.getroutes(source=l1.info['id'],
+ destination=l2.info['id'],
+ amount_msat=1000,
+ layers=['auto.localchans'],
+ maxfee_msat=1000,
+ final_cltv=9)
+
+ # A route cannot be found, but the recovery stubs must not crash askrene.
+ assert l1.rpc.getinfo()['id'] == l1.info['id']
+
+
def test_fees_dont_exceed_constraints(node_factory):
msat = 100000000
max_msat = int(msat * 0.45)Why this scored 60/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.