askrene: added reservations leak test under load
What changed, and why it matters
This commit only adds a new automated test to the Core Lightning project. It does not change any production code, fix a bug, or introduce a feature. The test checks whether a routing-reservation cleanup bug exists under heavy payment load, but the commit itself is purely a test addition with no patch.
No action required for this commit. Treat it as a regression test. If the test fails, investigate askrene reservation cleanup logic separately.
Security signals we found
Test-only change: no production code altered
Test targets potential resource leak in askrene reservation cleanup under concurrency
Assertion checks for stale reservations and failed reservation-removal log messages after load
Evidence from the diff
The diff adds test_reservations_leak_under_load to tests/test_askrene.py. The test creates a six-node topology with two paths sharing a bottleneck relay, submits 300 concurrent xpay calls, verifies that reservations are non-empty and contended during execution, and then asserts that the reservation table is empty and no reserve_remove failed log line appears after all payments settle. No source code in lightningd, plugins/askrene, or elsewhere is modified.
Changed components
tests/test_askrene.pyInspect captured patch +43 / −0
diff --git a/tests/test_askrene.py b/tests/test_askrene.py
index eb3e6936..5e8f0512 100644
--- a/tests/test_askrene.py
+++ b/tests/test_askrene.py
@@ -8,10 +8,12 @@ from utils import (
)
import os
import pytest
+import random
import subprocess
import time
import tempfile
import unittest
+from concurrent import futures as concurrent_futures
def direction(src, dst):
@@ -1958,6 +1960,47 @@ def test_unreserve_all(node_factory):
assert l1.rpc.askrene_listreservations() == {"reservations": []}
+def test_reservations_leak_under_load(node_factory, executor):
+ """Stress-test reservation cleanup: concurrent payments over shared channels
+ must leave zero stale reservations after all payments settle."""
+ # Topology: two paths share l4 as a bottleneck relay.
+ # Path A: l1 -> l2 -> l4 -> l5
+ # Path B: l1 -> l3 -> l4 -> l6
+ # join_nodes([l1, l2, l4, l5]) creates channels: l1-l2, l2-l4, l4-l5
+ # join_nodes([l1, l3, l4, l6]) creates channels: l1-l3, l3-l4, l4-l6
+ zero_fee = {"fee-base": 0, "fee-per-satoshi": 0}
+ l1, l2, l3, l4, l5, l6 = node_factory.get_nodes(
+ 6,
+ opts=[zero_fee] * 6,
+ )
+ node_factory.join_nodes([l1, l2, l4, l5], wait_for_announce=True) # creates channels: l1-l2, l2-l4, l4-l5
+ node_factory.join_nodes([l1, l3, l4, l6], wait_for_announce=True) # creates channels: l1-l3, l3-l4, l4-l6
+
+ NUM = 300
+ invoices = [l5.rpc.invoice(1000, f"inv-a-{i}", "x")["bolt11"] for i in range(NUM // 2)]
+ invoices += [l6.rpc.invoice(1000, f"inv-b-{i}", "x")["bolt11"] for i in range(NUM // 2)]
+ random.shuffle(invoices)
+
+ futs = [executor.submit(l1.rpc.xpay, inv) for inv in invoices]
+
+ # While payments are in flight, reservations must be non-empty: this
+ # checks the test isn't trivially passing on an empty table.
+ wait_for(lambda: l1.rpc.askrene_listreservations()["reservations"] != [])
+
+ # Make sure that we have channel contention by looking for repeating scids
+ def has_channel_contention():
+ res = l1.rpc.askrene_listreservations()["reservations"]
+ scids = [r["short_channel_id_dir"] for r in res]
+ return len(scids) != len(set(scids))
+ wait_for(has_channel_contention)
+
+ for f in concurrent_futures.as_completed(futs, timeout=TIMEOUT):
+ f.result() # raise on any payment failure
+
+ assert l1.rpc.askrene_listreservations() == {"reservations": []}
+ assert l1.daemon.is_in_log("reserve_remove failed") is None
+
+
def test_askrene_reserve_clash(node_factory, bitcoind):
"""Reserves get (erroneously) counted globally by scid, even for fake scids."""
l1 = node_factory.get_node()
Why this scored 15/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.