tests: lnpeer: test_reject_multiple_payments_of_same_invoice
What changed, and why it matters
This commit only adds a new automated test to Electrum's Lightning code. It checks that the software rejects attempts to pay the same invoice twice. There is no change to the actual wallet or Lightning logic, so by itself this commit does not fix or introduce a security issue. It may be related to a prior bug fix, but the commit message and diff do not describe one.
No action required for this commit alone. If reviewing a series, verify that the production code change this test exercises is already present in a preceding commit and that the duplicate-payment rejection is robust against race conditions and trampoline routing.
Security signals we found
Test-only commit with no production code changes
Test asserts duplicate invoice payments are rejected, which is a desirable security property
No patch, mitigation, or vulnerability description present in commit
Evidence from the diff
The diff adds a single unit test, test_reject_multiple_payments_of_same_invoice, in tests/test_lnpeer.py. The test creates two simulated Lightning peers, has one pay an invoice, resets the payer’s local payment status, then attempts to pay the same invoice again and asserts that the payee rejects the duplicate HTLC. The test runs once with direct payment and once with trampoline routing. No production code is modified.
Changed components
tests/test_lnpeer.pyInspect captured patch +41 / −0
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index e5f98ec..7cf778f 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -1046,6 +1046,47 @@ class TestPeerDirect(TestPeer):
for _test_trampoline in [False, True]:
await run_test(_test_trampoline)
+ async def test_reject_multiple_payments_of_same_invoice(self):
+ """Tests that new htlcs paying an invoice that has already been paid will get rejected."""
+ async def run_test(test_trampoline):
+ alice_channel, bob_channel = create_test_channels()
+ p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel)
+
+ lnaddr, _pay_req = self.prepare_invoice(w2)
+
+ async def try_pay_invoice_twice(pay_req: Invoice, w1=w1):
+ result, log = await w1.pay_invoice(pay_req)
+ assert result is True
+ # now pay the same invoice again, the payment should be rejected by w2
+ w1.set_payment_status(pay_req._lnaddr.paymenthash, PR_UNPAID)
+ result, log = await w1.pay_invoice(pay_req)
+ if not result:
+ # w1.pay_invoice returned a payment failure as the payment got rejected by w2
+ raise SuccessfulTest()
+ raise PaymentDone()
+
+ if test_trampoline:
+ await self._activate_trampoline(w1)
+ # declare bob as trampoline node
+ electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {
+ 'bob': LNPeerAddr(host="127.0.0.1", port=9735, pubkey=w2.node_keypair.pubkey),
+ }
+
+ async def f():
+ async with OldTaskGroup() as group:
+ await group.spawn(p1._message_loop())
+ await group.spawn(p1.htlc_switch())
+ await group.spawn(p2._message_loop())
+ await group.spawn(p2.htlc_switch())
+ await asyncio.sleep(0.01)
+ await group.spawn(try_pay_invoice_twice(_pay_req))
+
+ with self.assertRaises(SuccessfulTest):
+ await f()
+
+ for _test_trampoline in [False, True]:
+ await run_test(_test_trampoline)
+
async def test_payment_race(self):
"""Alice and Bob pay each other simultaneously.
They both send 'update_add_htlc' and receive each other's update
Why this scored 12/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.