From 7fac5517ebdade5d2fa741d00889e8e3f36cfafd Mon Sep 17 00:00:00 2001 From: niftynei Date: Sat, 15 Aug 2026 17:15:59 -0500 Subject: [PATCH] askrene: exclude stubchannels from routing My node has some recovered channels, which were causing a crash in askrene, due to the fact that they have duplicate scids. Easy fix is simply to skip recovered stubs when planning routes. Aug 15 16:34:14 citrine lightningd[1976574]: cln-askrene: ./plugins/askrene/child/additional_costs.h:26: additional_cost_htable_add: Assertion `!additional_c ost_htable_getmatch_(ht, k, h, v, &i)' failed. Changelog-Fixed: Crash when trying to pay and you have recovered channel stubs in your peers list --- common/gossmods_listpeerchannels.c | 6 ++++- tests/test_askrene.py | 35 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/common/gossmods_listpeerchannels.c b/common/gossmods_listpeerchannels.c index 256bc3090f79..87387fffc15c 100644 --- a/common/gossmods_listpeerchannels.c +++ b/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; } - diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 1032cf8b092b..6afa3decf932 100644 --- a/tests/test_askrene.py +++ b/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)