pytest: test the askrene doesn't use local dying channels.
What changed, and why it matters
This commit adds a test showing that Core Lightning's routing helper (askrene) may incorrectly consider both an old 'pre-splice' channel and a new 'post-splice' channel as usable at the same time, even though only the new one is valid. The test demonstrates that a local node correctly ignores the dying old channel, but a remote peer (l3) still sees both as active and could try to route payments through the stale channel. This could lead to failed or stuck payments for remote nodes relying on outdated gossip.
Treat as a bug report in test form. A follow-up fix should make askrene (and possibly broader gossip routing) recognize when a channel splice has replaced an older scid and exclude the pre-splice/dying channel from route selection. Reviewers should confirm whether the failing assertion is intentional and whether an accompanying fix is planned.
Security signals we found
Routing over stale/dying channel after splice
Remote node sees both pre-splice and post-splice channels as active
Potential payment failure or channel liquidity misestimation
Test-only commit, no production code change
Evidence from the diff
The patch is a pytest-only addition in tests/test_askrene.py (plus a helper first_scidd in tests/utils.py). It constructs a 3-node line graph with experimental splicing enabled, performs a splice on the l1-l2 channel, mines blocks, and verifies: (1) l1’s local askrene routes using only the post-splice scid and respects the new capacity; (2) l3, which learned about both pre- and post-splice channel announcements via gossip, currently returns routes over both scids because it does not eliminate the ‘dying’ pre-splice channel. The commit does not include a fix; it only adds a failing/regression test documenting the bug.
Changed components
tests/test_askrene.pytests/utils.pyplugins/askrene (implied target of test)gossip/channel announcement handling (implied)Inspect captured patch +50 / −1
diff --git a/tests/test_askrene.py b/tests/test_askrene.py
index bf87287e..613cfa1b 100644
--- a/tests/test_askrene.py
+++ b/tests/test_askrene.py
@@ -3,7 +3,7 @@ from hashlib import sha256
from pyln.client import RpcError
from pyln.testing.utils import SLOW_MACHINE
from utils import (
- only_one, first_scid, GenChannel, generate_gossip_store,
+ only_one, first_scid, first_scidd, GenChannel, generate_gossip_store,
sync_blockheight, wait_for, TEST_NETWORK, TIMEOUT, mine_funding_to_announce
)
import os
@@ -11,6 +11,7 @@ import pytest
import subprocess
import time
import tempfile
+import unittest
def direction(src, dst):
@@ -1915,3 +1916,46 @@ def test_askrene_reserve_clash(node_factory, bitcoind):
layers=['layer2'],
maxfee_msat=1000,
final_cltv=5)
+
+
+@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
+@pytest.mark.openchannel('v1')
+@pytest.mark.openchannel('v2')
+def test_splice_dying_channel(node_factory, bitcoind):
+ """We should NOT try to use the pre-splice channel here"""
+ l1, l2, l3 = node_factory.line_graph(3,
+ wait_for_announce=True,
+ fundamount=200000,
+ opts={'experimental-splicing': None})
+
+ chan_id = l1.get_channel_id(l2)
+ funds_result = l1.rpc.addpsbtoutput(100000)
+ pre_splice_scidd = first_scidd(l1, l2)
+
+ # Pay with fee by subjtracting 5000 from channel balance
+ result = l1.rpc.splice_init(chan_id, -105000, funds_result['psbt'])
+ result = l1.rpc.splice_update(chan_id, result['psbt'])
+ assert(result['commitments_secured'] is False)
+ result = l1.rpc.splice_update(chan_id, result['psbt'])
+ assert(result['commitments_secured'] is True)
+ result = l1.rpc.splice_signed(chan_id, result['psbt'])
+
+ mine_funding_to_announce(bitcoind,
+ [l1, l2, l3],
+ num_blocks=6, wait_for_mempool=1)
+
+ wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['state'] == 'CHANNELD_NORMAL')
+ post_splice_scidd = first_scidd(l1, l2)
+
+ # You will use the new scid
+ route = only_one(l1.rpc.getroutes(l1.info['id'], l2.info['id'], '50000sat', ['auto.localchans'], 100000, 6)['routes'])
+ assert only_one(route['path'])['short_channel_id_dir'] == post_splice_scidd
+
+ # And you will not be able to route 100001 sats:
+ with pytest.raises(RpcError, match="We could not find a usable set of paths"):
+ l1.rpc.getroutes(l1.info['id'], l2.info['id'], '100001sat', ['auto.localchans'], 100000, 6)
+
+ # But l3 would think it can use both, since it doesn't eliminate dying channel!
+ wait_for(lambda: [c['active'] for c in l3.rpc.listchannels()['channels']] == [True] * 6)
+ routes = l3.rpc.getroutes(l1.info['id'], l2.info['id'], '200001sat', [], 100000, 6)['routes']
+ assert set([only_one(r['path'])['short_channel_id_dir'] for r in routes]) == set([pre_splice_scidd, post_splice_scidd])
diff --git a/tests/utils.py b/tests/utils.py
index fc3f18b1..d879bf3d 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -458,6 +458,11 @@ def first_scid(n1, n2):
return only_one(n1.rpc.listpeerchannels(n2.info['id'])['channels'])['short_channel_id']
+def first_scidd(n1, n2):
+ c = only_one(n1.rpc.listpeerchannels(n2.info['id'])['channels'])
+ return c['short_channel_id'] + '/' + str(c['direction'])
+
+
def basic_fee(feerate, anchor_expected):
if anchor_expected:
# option_anchor_outputs / option_anchors_zero_fee_htlc_tx
Why this scored 33/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.