cln/tests/test_xpay.py
Lagrang3 0b67601f0a xpay: tests should expect the new error messages
Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
2026-08-04 18:51:43 -03:00

1475 lines
64 KiB
Python

from fixtures import * # noqa: F401,F403
from fixtures import TEST_NETWORK
from pyln.client import RpcError
from pyln.testing.utils import FUNDAMOUNT, only_one
from utils import (
TIMEOUT, first_scid, first_scidd, GenChannel, generate_gossip_store, wait_for,
sync_blockheight,
)
import os
import pytest
import re
import subprocess
import sys
from hashlib import sha256
from pathlib import Path
import tempfile
import time
import unittest
def test_pay_fakenet(node_factory):
hash1 = sha256(bytes.fromhex('00' + '00' * 31)).hexdigest()
hash2 = sha256(bytes.fromhex('01' + '00' * 31)).hexdigest()
failhash = '00' * 32
# Create gossip map of channels from l2 (aka nodemap[0])
gsfile, nodemap = generate_gossip_store([GenChannel(0, 1, capacity_sats=100_000),
GenChannel(1, 2, capacity_sats=100_000),
GenChannel(2, 3, capacity_sats=200_000)],
nodemap={0: '033845802d25b4e074ccfd7cd8b339a41dc75bf9978a034800444b51d42b07799a'})
# l2 will warn l1 about its invalid gossip: ignore.
l1, l2 = node_factory.line_graph(2,
opts=[{'gossip_store_file': gsfile.name,
'subdaemon': 'channeld:../tests/plugins/channeld_fakenet',
'allow_warning': True}, {}])
# l1 needs to know l2's shaseed for the channel so it can make revocations
hsmfile = os.path.join(l2.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")
# Needs peer node id and channel dbid (1, it's the first channel), prints out:
# "shaseed: xxxxxxx\n"
shaseed = subprocess.check_output(["tools/lightning-hsmtool", "dumpcommitments", l1.info['id'], "1", "0", hsmfile]).decode('utf-8').strip().partition(": ")[2]
l1.rpc.dev_peer_shachain(l2.info['id'], shaseed)
# Failure from final (unknown payment hash)
l1.rpc.sendpay(route=[{'id': l2.info['id'],
'channel': first_scid(l1, l2),
'delay': 18 + 6,
'amount_msat': 1000001},
{'id': nodemap[1],
'channel': '0x1x0',
'delay': 18,
'amount_msat': 100000}],
payment_hash=failhash)
with pytest.raises(RpcError, match="WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS") as err:
l1.rpc.waitsendpay(payment_hash=failhash, timeout=TIMEOUT)
assert err.value.error['data']['erring_node'] == nodemap[1]
# Success from final (known payment hash)
l1.rpc.sendpay(route=[{'id': l2.info['id'],
'channel': first_scid(l1, l2),
'delay': 18 + 6,
'amount_msat': 1000001},
{'id': nodemap[1],
'channel': '0x1x0',
'delay': 18,
'amount_msat': 100000}],
payment_hash=hash1)
l1.rpc.waitsendpay(payment_hash=hash1, timeout=TIMEOUT)
# Failure from node 2 (unknown scid)
l1.rpc.sendpay(route=[{'id': l2.info['id'],
'channel': first_scid(l1, l2),
'delay': 18 + 6 + 6,
'amount_msat': 1000002},
{'id': nodemap[1],
'channel': '0x1x0',
'delay': 18 + 6,
'amount_msat': 1000001},
{'id': nodemap[2],
'channel': '1x1x0',
'delay': 18,
'amount_msat': 1000000},
{'id': nodemap[3],
'channel': '0x1x0',
'delay': 18,
'amount_msat': 100000}],
payment_hash=failhash)
with pytest.raises(RpcError, match="WIRE_UNKNOWN_NEXT_PEER"):
l1.rpc.waitsendpay(payment_hash=failhash, timeout=TIMEOUT)
# MPP test
l1.rpc.sendpay(partid=1,
amount_msat=200000,
route=[{'id': l2.info['id'],
'channel': first_scid(l1, l2),
'delay': 18 + 6,
'amount_msat': 1000001},
{'id': nodemap[1],
'channel': '0x1x0',
'delay': 18,
'amount_msat': 100000}],
payment_hash=hash2,
payment_secret=hash2)
with pytest.raises(RpcError, match="WIRE_MPP_TIMEOUT"):
l1.rpc.waitsendpay(payment_hash=hash2, timeout=60 + TIMEOUT, partid=1)
# This one will actually work.
l1.rpc.sendpay(partid=2,
groupid=2,
amount_msat=200000,
route=[{'id': l2.info['id'],
'channel': first_scid(l1, l2),
'delay': 18 + 6,
'amount_msat': 1000001},
{'id': nodemap[1],
'channel': '0x1x0',
'delay': 18,
'amount_msat': 100000}],
payment_hash=hash2,
payment_secret=hash2)
l1.rpc.sendpay(partid=3,
groupid=2,
amount_msat=200000,
route=[{'id': l2.info['id'],
'channel': first_scid(l1, l2),
'delay': 18 + 6,
'amount_msat': 1000001},
{'id': nodemap[1],
'channel': '0x1x0',
'delay': 18,
'amount_msat': 100000}],
payment_hash=hash2,
payment_secret=hash2)
l1.rpc.waitsendpay(payment_hash=hash2, timeout=TIMEOUT, partid=2)
l1.rpc.waitsendpay(payment_hash=hash2, timeout=TIMEOUT, partid=3)
def test_xpay_simple(node_factory):
l1, l2, l3, l4 = node_factory.get_nodes(4, opts={'may_reconnect': True})
node_factory.join_nodes([l1, l2, l3], wait_for_announce=True)
node_factory.join_nodes([l3, l4], announce_channels=False)
# BOLT-11, direct peer
b11 = l2.rpc.invoice('10000msat', 'test_xpay_simple', 'test_xpay_simple bolt11')['bolt11']
ret = l1.rpc.xpay(b11)
assert ret['failed_parts'] == 0
assert ret['successful_parts'] == 1
assert ret['amount_msat'] == 10000
assert ret['amount_sent_msat'] == 10000
PAY_INJECTPAYMENTONION_ALREADY_PAID = 219
# Fails if we try to pay again
b11_paid = b11
with pytest.raises(RpcError, match="Already paid") as err:
l1.rpc.xpay(b11_paid)
assert err.value.error['code'] == PAY_INJECTPAYMENTONION_ALREADY_PAID
# BOLT-11, indirect peer
b11 = l3.rpc.invoice('10000msat', 'test_xpay_simple', 'test_xpay_simple bolt11')['bolt11']
ret = l1.rpc.xpay(b11)
assert ret['failed_parts'] == 0
assert ret['successful_parts'] == 1
assert ret['amount_msat'] == 10000
assert ret['amount_sent_msat'] == 10001
# BOLT-11, routehint
b11 = l4.rpc.invoice('10000msat', 'test_xpay_simple', 'test_xpay_simple bolt11')['bolt11']
l1.rpc.xpay(b11)
# BOLT-12 (with payer_note specified).
offer = l3.rpc.offer('any')['bolt12']
b12 = l1.rpc.fetchinvoice(offer, '100000msat')['invoice']
l1.rpc.xpay(invstring=b12, payer_note="Payment for a cup of coffee")
# BOLT-12, direct peer
offer = l2.rpc.offer('any')['bolt12']
b12 = l1.rpc.fetchinvoice(offer, '10000msat')['invoice']
ret = l1.rpc.xpay(invstring=b12)
assert ret['failed_parts'] == 0
assert ret['successful_parts'] == 1
assert ret['amount_msat'] == 10000
assert ret['amount_sent_msat'] == 10000
# Failure from l4.
b11 = l4.rpc.invoice('10000msat', 'test_xpay_simple2', 'test_xpay_simple2 bolt11')['bolt11']
l4.rpc.delinvoice('test_xpay_simple2', 'unpaid')
with pytest.raises(RpcError, match="Destination said it doesn't know invoice"):
l1.rpc.xpay(b11)
offer = l4.rpc.offer('any')['bolt12']
b12 = l1.rpc.fetchinvoice(offer, '100000msat')['invoice']
# Failure from l3 (with routehint)
l4.stop()
with pytest.raises(
RpcError,
match=(
r"Failed after 1 attempts\. "
r"We got temporary_channel_failure for the invoice's route hint \([0-9x]*/[01]\), "
r"assuming it can't carry 10000msat\. "
r"Then routing failed: We could not find a usable set of paths\. "
r"We know from xpay-15 that destination has maximum "
r"capacity 9999msat \(in 1 channels\)\."
),
):
l1.rpc.xpay(b11)
# Failure from l3 (with blinded path)
with pytest.raises(
RpcError,
match=(
r"Failed after 1 attempts\. "
r"We got an error from inside the blinded path 0x0x0/1: "
r"we assume it means insufficient capacity\. "
r"Then routing failed: "
r"We could not find a usable set of paths\. "
r"We know from xpay-17 that destination has maximum "
r"capacity 99999msat \(in 1 channels\)\."
),
):
l1.rpc.xpay(b12)
# Restart, try pay already paid one again.
l1.restart()
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
with pytest.raises(RpcError, match="Already paid"):
l1.rpc.xpay(b11_paid)
def test_xpay_selfpay(node_factory):
l1 = node_factory.get_node()
b11 = l1.rpc.invoice(1000, "test_xpay_selfpay1", "test_xpay_selfpay1")['bolt11']
offer = l1.rpc.offer('any')
b12 = l1.rpc.fetchinvoice(offer['bolt12'], '1000msat')['invoice']
l1.rpc.xpay(b11)
l1.rpc.xpay(b12)
# These were obviously having a bad day at the time of the snapshot:
canned_gossmap_badnodes = [19, 53, 69, 72, 86]
@pytest.mark.skip(reason="channeld_fakenet needs updating")
@pytest.mark.slow_test
@unittest.skipIf(TEST_NETWORK != 'regtest', '29-way split for node 17 is too dusty on elements')
@pytest.mark.parametrize("slow_mode", [False, True])
def test_xpay_fake_channeld(node_factory, bitcoind, chainparams, slow_mode):
outfile = tempfile.NamedTemporaryFile(prefix='gossip-store-')
nodeids = subprocess.check_output(['devtools/gossmap-compress',
'decompress',
'--node-map=2134=033845802d25b4e074ccfd7cd8b339a41dc75bf9978a034800444b51d42b07799a',
'tests/data/gossip-store-2026-02-03.compressed',
outfile.name]).decode('utf-8').splitlines()
# 100,000sat gave no failures at all, which is not very interesting!
AMOUNT = 500_000_000
# l2 will warn l1 about its invalid gossip: ignore.
# We throttle l1's gossip to avoid massive log spam.
# Suppress debug and below because logs are huge
l1, l2 = node_factory.line_graph(2,
# This is in sats, so 1000x amount we send.
fundamount=AMOUNT,
opts=[{'gossip_store_file': outfile.name,
'subdaemon': 'channeld:../tests/plugins/channeld_fakenet',
'allow_warning': True,
'dev-throttle-gossip': None,
'log-level': 'info',
'broken_log': 'Throttling incoming peer',
# xpay gets upset if it's aging when we remove cln-askrene!
'dev-xpay-no-age': None,
},
{'allow_bad_gossip': True,
'log-level': 'info',
}])
# l1 needs to know l2's shaseed for the channel so it can make revocations
hsmfile = os.path.join(l2.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")
# Needs peer node id and channel dbid (1, it's the first channel), prints out:
# "shaseed: xxxxxxx\n"
shaseed = subprocess.check_output(["tools/lightning-hsmtool", "dumpcommitments", l1.info['id'], "1", "0", hsmfile]).decode('utf-8').strip().partition(": ")[2]
l1.rpc.dev_peer_shachain(l2.info['id'], shaseed)
# Toggle whether we wait for all the parts to finish.
l1.rpc.setconfig('xpay-slow-mode', slow_mode)
failed_parts = []
for n in range(0, 100):
if n in canned_gossmap_badnodes:
continue
print(f"PAYING Node #{n}")
preimage_hex = bytes([n]).hex() + '00' * 31
hash_hex = sha256(bytes.fromhex(preimage_hex)).hexdigest()
inv = subprocess.check_output(["devtools/bolt11-cli",
"encode",
n.to_bytes(length=8, byteorder=sys.byteorder).hex() + '01' * 24,
f"currency={chainparams['bip173_prefix']}",
f"p={hash_hex}",
f"s={'00' * 32}",
"9=020000", # option_basic_mpp
f"d=Paying node {n}",
f"amount={AMOUNT}msat"]).decode('utf-8').strip()
assert l1.rpc.decode(inv)['payee'] == nodeids[n]
failed_parts.append(l1.rpc.xpay(inv)['failed_parts'])
# FIXME: We fail on #10, due mainly to a buildup of usage on 0x2134x0/0:
# Failed: We could not find a usable set of paths. The shortest path is 103x1x0->0x2134x0->1725x11x1725, but 0x2134x0/0 exceeds htlc_maximum_msat ~1000448msat
# So we "age" the xpay layer to forget old successful payments.
l1.rpc.askrene_age('xpay', 1)
# Should be no reservations left (clean up happens after return though)
wait_for(lambda: l1.rpc.askrene_listreservations() == {'reservations': []})
# Wait for temporary layers to be gone too.
wait_for(lambda: len(l1.rpc.askrene_listlayers()['layers']) == 1)
# It should remember the information it learned across restarts!
# FIXME: channeld_fakenet doesn't restart properly, so just redo xpay.
layers = l1.rpc.askrene_listlayers()
l1.rpc.plugin_stop("cln-askrene")
l1.rpc.plugin_start(os.path.join(os.getcwd(), 'plugins/cln-askrene'))
layers_after = l1.rpc.askrene_listlayers()
assert layers == layers_after
failed_parts_retry = []
for n in range(0, 100):
if n in canned_gossmap_badnodes:
continue
print(f"PAYING Node #{n}")
preimage_hex = bytes([n + 100]).hex() + '00' * 31
hash_hex = sha256(bytes.fromhex(preimage_hex)).hexdigest()
inv = subprocess.check_output(["devtools/bolt11-cli",
"encode",
n.to_bytes(length=8, byteorder=sys.byteorder).hex() + '01' * 24,
f"p={hash_hex}",
"9=020000", # option_basic_mpp
f"s={'00' * 32}",
f"d=Paying node {n}",
f"amount={AMOUNT}msat"]).decode('utf-8').strip()
assert l1.rpc.decode(inv)['payee'] == nodeids[n]
failed_parts_retry.append(l1.rpc.xpay(inv)['failed_parts'])
# At least some will have improved!
assert failed_parts_retry != failed_parts
# Now, we should be as good *or better* than the first time, since we remembered!
for p in zip(failed_parts_retry, failed_parts):
assert p[0] <= p[1]
def test_xpay_timeout(node_factory, executor):
# ->l3->
# l1->l2< >l4
# ->l5->
l1, l2, l3, l4, l5 = node_factory.get_nodes(5, opts={'dev-no-reconnect': None})
node_factory.join_nodes([l1, l2, l3, l4], wait_for_announce=True)
node_factory.join_nodes([l2, l5, l4], fundamount=FUNDAMOUNT // 2, wait_for_announce=True)
# Make sure l1 sees both paths.
wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 5 * 2)
# Break l3->l4
l3.rpc.disconnect(l4.info['id'], force=True)
b11 = l4.rpc.invoice('100000sat', 'test_xpay_timeout', 'test_xpay_timeout')['bolt11']
fut = executor.submit(l1.rpc.xpay, invstring=b11, retry_for=0)
with pytest.raises(RpcError, match=r"Timed out after after 1 attempts"):
fut.result(TIMEOUT)
@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
def test_xpay_partial_msat(node_factory, executor):
l1, l2, l3 = node_factory.line_graph(3)
inv = l3.rpc.invoice(100000000, "inv", "inv")
with pytest.raises(RpcError, match="partial_msat must be less or equal to total amount 10000000"):
l2.rpc.xpay(invstring=inv['bolt11'], partial_msat=100000001)
# This will fail with an MPP timeout.
with pytest.raises(RpcError, match=r"Timed out after after 1 attempts\. Payment of 90000000msat reached destination, but timed out before the rest arrived\."):
l2.rpc.xpay(invstring=inv['bolt11'], partial_msat=90000000)
# This will work like normal.
l2.rpc.xpay(invstring=inv['bolt11'], partial_msat=100000000)
# Make sure l3 can pay to l2 now.
wait_for(lambda: only_one(l3.rpc.listpeerchannels()['channels'])['spendable_msat'] > 1001)
# Now we can combine together to pay l2:
inv = l2.rpc.invoice('any', "inv", "inv")
# If we specify different totals, this *won't work*
l1pay = executor.submit(l1.rpc.xpay, invstring=inv['bolt11'], amount_msat=10000, partial_msat=9000)
l3pay = executor.submit(l3.rpc.xpay, invstring=inv['bolt11'], amount_msat=10001, partial_msat=1001)
# BOLT #4:
# - SHOULD fail the entire HTLC set if `total_msat` is not
# the same for all HTLCs in the set.
with pytest.raises(RpcError, match=r"Unexpected error \(final_incorrect_htlc_amount\) from final node"):
l3pay.result(TIMEOUT)
with pytest.raises(RpcError, match=r"Unexpected error \(final_incorrect_htlc_amount\) from final node"):
l1pay.result(TIMEOUT)
# But same amount, will combine forces!
l1pay = executor.submit(l1.rpc.xpay, invstring=inv['bolt11'], amount_msat=10000, partial_msat=9000)
l3pay = executor.submit(l3.rpc.xpay, invstring=inv['bolt11'], amount_msat=10000, partial_msat=1000)
l1pay.result(TIMEOUT)
l3pay.result(TIMEOUT)
def test_xpay_takeover(node_factory, executor):
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True,
opts={'xpay-handle-pay': True,
'allow-deprecated-apis': True})
# Simple bolt11/bolt12 payment.
inv = l3.rpc.invoice(100000, "test_xpay_takeover1", "test_xpay_takeover1")['bolt11']
l1.rpc.pay(inv)
l1.daemon.wait_for_log('Calling rpc_command hook of plugin cln-xpay')
l1.daemon.wait_for_log('Redirecting pay->xpay')
# Quickly test that xpay does NOT receive other commands now.
l1.rpc.help()
assert not l1.daemon.is_in_log('Calling rpc_command hook of plugin cln-xpay',
start=l1.daemon.logsearch_start)
# Array version
inv = l3.rpc.invoice(100000, "test_xpay_takeover2", "test_xpay_takeover2")['bolt11']
subprocess.check_output(['cli/lightning-cli',
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'pay',
inv])
l1.daemon.wait_for_log('Redirecting pay->xpay')
offer = l3.rpc.offer(100000, "test_xpay_takeover2")['bolt12']
b12 = l1.rpc.fetchinvoice(offer)['invoice']
l1.rpc.pay(b12)
l1.daemon.wait_for_log('Redirecting pay->xpay')
# BOLT-11 with amount.
inv = l3.rpc.invoice('any', "test_xpay_takeover3", "test_xpay_takeover3")['bolt11']
l1.rpc.pay(inv, amount_msat=10000)
l1.daemon.wait_for_log('Redirecting pay->xpay')
# Array version
inv = l3.rpc.invoice('any', "test_xpay_takeover4", "test_xpay_takeover4")['bolt11']
subprocess.check_output(['cli/lightning-cli',
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'pay',
inv, "10000"])
l1.daemon.wait_for_log('Redirecting pay->xpay')
# retry_for, maxfee and partial_msat all work
inv = l3.rpc.invoice('any', "test_xpay_takeover5", "test_xpay_takeover5")['bolt11']
fut1 = executor.submit(l1.rpc.pay, bolt11=inv, amount_msat=2000, retry_for=0, maxfee=100, partial_msat=1000)
l1.daemon.wait_for_log('Redirecting pay->xpay')
fut2 = executor.submit(l2.rpc.pay, bolt11=inv, amount_msat=2000, retry_for=0, maxfee=0, partial_msat=1000)
l2.daemon.wait_for_log('Redirecting pay->xpay')
fut1.result(TIMEOUT)
fut2.result(TIMEOUT)
# Three-array-arg replacements don't work.
inv = l3.rpc.invoice('any', "test_xpay_takeover6", "test_xpay_takeover6")['bolt11']
subprocess.check_output(['cli/lightning-cli',
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'pay',
inv, "10000", 'label'])
l1.daemon.wait_for_log(r'Not redirecting pay \(only handle 1 or 2 args\): ')
# Label gets maintained.
inv = l3.rpc.invoice('any', "test_xpay_takeover7", "test_xpay_takeover7")
l1.rpc.pay(inv['bolt11'], amount_msat=10000, label='test_xpay_takeover7')
l1.daemon.wait_for_log('Redirecting pay->xpay')
assert any(p.get('label') == 'test_xpay_takeover7' for p in l1.rpc.listsendpays()['payments'])
# Other args are ignored.
inv = l3.rpc.invoice('any', "test_xpay_takeover8", "test_xpay_takeover8")
l1.rpc.pay(inv['bolt11'], amount_msat=10000, riskfactor=1)
l1.daemon.wait_for_log('Unknown arg "riskfactor", xpay will ignore it.')
l1.daemon.wait_for_log('Redirecting pay->xpay')
# Test that it's really dynamic.
l1.rpc.setconfig('xpay-handle-pay', False)
# There's no log for this though!
inv = l3.rpc.invoice(100000, "test_xpay_takeover12", "test_xpay_takeover12")['bolt11']
realpay = l1.rpc.pay(inv)
assert not l1.daemon.is_in_log('Redirecting pay->xpay',
start=l1.daemon.logsearch_start)
l1.rpc.setconfig('xpay-handle-pay', True)
inv = l3.rpc.invoice(100000, "test_xpay_takeover13", "test_xpay_takeover13")['bolt11']
xpay = l1.rpc.pay(inv)
l1.daemon.wait_for_log('Redirecting pay->xpay')
# They should look the same! Same keys, same types
assert {k: type(v) for k, v in realpay.items()} == {k: type(v) for k, v in xpay.items()}
for f in ('created_at', 'payment_hash', 'payment_preimage'):
del realpay[f]
del xpay[f]
assert xpay == {'amount_msat': 100000,
'amount_sent_msat': 100002,
'destination': l3.info['id'],
'parts': 1,
'status': 'complete'}
assert realpay == xpay
# We get destination and amount_msat in listsendpays and listpays.
ret = only_one(l1.rpc.listsendpays(inv)['payments'])
assert ret['destination'] == l3.info['id']
assert ret['amount_msat'] == 100000
assert ret['amount_sent_msat'] > 100000
ret = only_one(l1.rpc.listpays(inv)['pays'])
assert ret['destination'] == l3.info['id']
assert ret['amount_msat'] == 100000
assert ret['amount_sent_msat'] > 100000
# Test maxfeepercent.
inv = l3.rpc.invoice(100000, "test_xpay_takeover14", "test_xpay_takeover14")['bolt11']
with pytest.raises(RpcError, match=r"Could not find route without excessive cost"):
l1.rpc.pay(bolt11=inv, maxfeepercent=0.001, exemptfee=0)
l1.daemon.wait_for_log('plugin-cln-xpay: Converted maxfeepercent=0.001, exemptfee=0 to maxfee 1msat')
# Exemptfee default more than covers it.
l1.rpc.pay(bolt11=inv, maxfeepercent=0.25)
l1.daemon.wait_for_log('Converted maxfeepercent=0.25, exemptfee=UNSET to maxfee 5000msat')
def test_xpay_takeover_null_parms(node_factory, executor):
"""Test passing through RPC a list of parameters some of which have null
json value."""
l1, l2, l3 = node_factory.line_graph(
3, wait_for_announce=True, opts={"xpay-handle-pay": True}
)
# Amount argument is null.
inv = l3.rpc.invoice(100000, "test_xpay_takeover1", "test_xpay_takeover1")["bolt11"]
l1.rpc.call("pay", [inv, None])
l1.daemon.wait_for_log("Redirecting pay->xpay")
# Amount argument is given
inv = l3.rpc.invoice("any", "test_xpay_takeover2", "test_xpay_takeover2")["bolt11"]
l1.rpc.call("pay", [inv, "100sat"])
l1.daemon.wait_for_log("Redirecting pay->xpay")
# bolt11 invoice cannot be NULL
with pytest.raises(RpcError, match=r"missing required parameter: bolt11"):
l1.rpc.call("pay", [None, "100sat"])
l1.daemon.wait_for_log(r"Not redirecting pay \(missing bolt11 parameter\)")
def test_xpay_preapprove(node_factory):
l1, l2 = node_factory.line_graph(2, opts={'dev-hsmd-fail-preapprove': None})
inv = l2.rpc.invoice(100000, "test_xpay_preapprove", "test_xpay_preapprove")['bolt11']
with pytest.raises(RpcError, match=r"invoice was declined"):
l1.rpc.check('xpay', invstring=inv)
with pytest.raises(RpcError, match=r"invoice was declined"):
l1.rpc.xpay(inv)
def test_xpay_maxdelay(node_factory):
l1, l2 = node_factory.line_graph(2, wait_for_announce=True)
inv = l2.rpc.invoice('10000msat', 'test_xpay_maxdelay', 'test_xpay_maxdelay')["bolt11"]
with pytest.raises(RpcError, match=r"Could not find route without excessive delays"):
l1.rpc.xpay(invstring=inv, maxdelay=1)
def test_xpay_unannounced(node_factory):
l1, l2 = node_factory.line_graph(2, announce_channels=False)
# BOLT-11, direct peer
b11 = l2.rpc.invoice('10000msat', 'test_xpay_unannounced', 'test_xpay_unannounced bolt11')['bolt11']
ret = l1.rpc.xpay(b11)
assert ret['failed_parts'] == 0
assert ret['successful_parts'] == 1
assert ret['amount_msat'] == 10000
assert ret['amount_sent_msat'] == 10000
# BOLT-12, direct peer
offer = l2.rpc.offer('any')['bolt12']
b12 = l1.rpc.fetchinvoice(offer, '100000msat')['invoice']
l1.rpc.xpay(b12)
def test_xpay_zeroconf(node_factory):
zeroconf_plugin = Path(__file__).parent / "plugins" / "zeroconf-selective.py"
l1, l2 = node_factory.get_nodes(2,
opts=[{},
{'plugin': zeroconf_plugin,
'zeroconf_allow': 'any'}])
l1.fundwallet(FUNDAMOUNT * 2)
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
l1.rpc.fundchannel(l2.info['id'], amount=FUNDAMOUNT, announce=False, mindepth=0)
wait_for(lambda: all([c['state'] == 'CHANNELD_NORMAL' for c in l1.rpc.listpeerchannels()['channels'] + l2.rpc.listpeerchannels()['channels']]))
# BOLT-11, direct peer
b11 = l2.rpc.invoice('10000msat', 'test_xpay_unannounced', 'test_xpay_unannounced bolt11')['bolt11']
ret = l1.rpc.xpay(b11)
assert ret['failed_parts'] == 0
assert ret['successful_parts'] == 1
assert ret['amount_msat'] == 10000
assert ret['amount_sent_msat'] == 10000
# BOLT-12, direct peer
offer = l2.rpc.offer('any')['bolt12']
b12 = l1.rpc.fetchinvoice(offer, '100000msat')['invoice']
l1.rpc.xpay(b12)
def test_xpay_no_mpp(node_factory, chainparams):
"""Suppress mpp, resulting in a single payment part"""
l1, l2, l3, l4 = node_factory.get_nodes(4)
node_factory.join_nodes([l1, l2, l3], wait_for_announce=True)
node_factory.join_nodes([l1, l4, l3], wait_for_announce=True)
# Amount needs to be enought that it bothers splitting, but not
# so much that it can't pay without mpp.
AMOUNT = 500000000
# We create a version of this which doesn't support MPP
no_mpp = l3.rpc.invoice(AMOUNT, 'test_xpay_no_mpp2', 'test_xpay_no_mpp without mpp')
b11_no_mpp = subprocess.check_output(["devtools/bolt11-cli",
"encode",
# secret for l3
"79893b45d1e57cf2ebf302af91aa52c9e573f638a61a83c6e603a331b53f452c",
f"currency={chainparams['bip173_prefix']}",
f"p={no_mpp['payment_hash']}",
f"s={no_mpp['payment_secret']}",
f"d=Paying l3 without mpp",
f"amount={AMOUNT}"]).decode('utf-8').strip()
# This should not mpp!
ret = l1.rpc.xpay(b11_no_mpp)
assert ret['failed_parts'] == 0
assert ret['successful_parts'] == 1
assert ret['amount_msat'] == AMOUNT
assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1
def test_xpay_bolt12_no_mpp(node_factory, chainparams):
"""If we force it, we use MPP even if BOLT12 invoice doesn't say we should"""
# l4 needs dev-allow-localhost so it considers itself to have an advertized address, and doesn't create a blinded path from l2/l4.
opts = [{}, {}, {'dev-force-features': -17, 'dev-allow-localhost': None}, {}]
l1, l2, l3, l4 = node_factory.get_nodes(4, opts=opts)
node_factory.join_nodes([l1, l2, l3], wait_for_announce=True)
node_factory.join_nodes([l1, l4, l3], wait_for_announce=True)
# Amount needs to be enought that it bothers splitting, but not
# so much that it can't pay without mpp.
AMOUNT = 800000000
# l2 will advertize mpp, l3 won't.
l2offer = l2.rpc.offer(AMOUNT, 'test_xpay_bolt12_no_mpp')
invl2 = l1.rpc.fetchinvoice(l2offer['bolt12'])
l3offer = l3.rpc.offer(AMOUNT, 'test_xpay_bolt12_no_mpp')
invl3 = l1.rpc.fetchinvoice(l3offer['bolt12'])
assert l1.rpc.decode(invl2['invoice'])['invoice_features'] == "020000"
assert l1.rpc.decode(invl3['invoice'])['invoice_features'] == ""
ret = l1.rpc.xpay(invl3['invoice'])
assert ret['failed_parts'] == 0
assert ret['successful_parts'] == 1
assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1
assert ret['amount_msat'] == AMOUNT
def test_xpay_slow_mode(node_factory, bitcoind):
# l1 -> l2 -> l3 -> l5
# \-> l4 -/^
l1, l2, l3, l4, l5 = node_factory.get_nodes(5, opts=[{'xpay-slow-mode': True},
{}, {}, {}, {}])
node_factory.join_nodes([l2, l3, l5])
node_factory.join_nodes([l2, l4, l5])
# Make sure l1 can see all paths.
node_factory.join_nodes([l1, l2])
bitcoind.generate_block(5)
wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 10)
# First try an MPP which fails
inv = l5.rpc.invoice(800000000, 'test_xpay_slow_mode_fail', 'test_xpay_slow_mode_fail', preimage='01' * 32)['bolt11']
l5.rpc.delinvoice('test_xpay_slow_mode_fail', status='unpaid')
with pytest.raises(RpcError, match=r"Destination said it doesn't know invoice: incorrect_or_unknown_payment_details"):
l1.rpc.xpay(inv)
# Now a successful one
inv = l5.rpc.invoice(800000000, 'test_xpay_slow_mode', 'test_xpay_slow_mode', preimage='00' * 32)['bolt11']
assert l1.rpc.xpay(inv) == {'payment_preimage': '00' * 32,
'amount_msat': 800000000,
'amount_sent_msat': 800016004,
'failed_parts': 0,
'successful_parts': 2}
@pytest.mark.parametrize("slow_mode", [False, True])
def test_fail_after_success(node_factory, bitcoind, executor, slow_mode):
# l1 -> l2 -> l3 -> l5
# \-> l4 -/^
l1, l2, l3, l4, l5 = node_factory.get_nodes(5, opts=[{'xpay-slow-mode': slow_mode},
{},
{},
{'disconnect': ['-WIRE_UPDATE_FULFILL_HTLC']},
{}])
node_factory.join_nodes([l2, l3, l5])
node_factory.join_nodes([l2, l4, l5])
# Make sure l1 can see all paths.
node_factory.join_nodes([l1, l2])
bitcoind.generate_block(5)
wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 10)
inv = l5.rpc.invoice(800000000, 'test_xpay_slow_mode', 'test_xpay_slow_mode', preimage='00' * 32)['bolt11']
fut = executor.submit(l1.rpc.xpay, invstring=inv, retry_for=0, dev_use_shadow=False)
# Part via l3 is fine. Part via l4 is stuck, so we kill l4 and mine
# blocks to make l2 force close.
l4.daemon.wait_for_log('dev_disconnect: -WIRE_UPDATE_FULFILL_HTLC')
l4.stop()
# Normally, we return as soon as first part succeeds.
if slow_mode is False:
assert fut.result(TIMEOUT) == {'payment_preimage': '00' * 32,
'amount_msat': 800000000,
'amount_sent_msat': 800016004,
'failed_parts': 0,
'successful_parts': 2}
# Time it out, l2 will collect it.
bitcoind.generate_block(13)
l2.daemon.wait_for_log('Peer permanent failure in CHANNELD_NORMAL: Offered HTLC 0 SENT_ADD_ACK_REVOCATION cltv 124 hit deadline')
bitcoind.generate_block(3, wait_for_mempool=1)
l1.daemon.wait_for_log(r"UNUSUAL.*Destination accepted partial payment, failed a part \(Error permanent_channel_failure for path ->033845802d25b4e074ccfd7cd8b339a41dc75bf9978a034800444b51d42b07799a->02287bfac8b99b35477ebe9334eede1e32b189e24644eb701c079614712331cec0->0258f3ff3e0853ccc09f6fe89823056d7c0c55c95fab97674df5e1ad97a72f6265, from 033845802d25b4e074ccfd7cd8b339a41dc75bf9978a034800444b51d42b07799a\)")
# Could be either way around, check both
line = l1.daemon.is_in_log(r"UNUSUAL.*Destination accepted partial payment, failed a part")
assert re.search(r'but accepted only .* of 800000000msat\. Winning\?!', line)
if slow_mode is True:
# Now it succeeds, but notes that it only sent one part!
res = fut.result(TIMEOUT)
# Some variation due to floating point.
assert res['amount_sent_msat'] < 800000000
assert res == {'payment_preimage': '00' * 32,
'amount_msat': 800000000,
'amount_sent_msat': res['amount_sent_msat'],
'failed_parts': 1,
'successful_parts': 1}
def test_xpay_twohop_bug(node_factory, bitcoind):
"""From https://github.com/ElementsProject/lightning/issues/8119:
Oh, interesting! I tried again and got a two-hop blinded path. xpay returned the same error you saw while pay was successful.
lightning-cli xpay lni1qqgv5nalmz08ukj4av074kyk6pepq93pqvvhnlnvurnfanndnxjtcjnmxrkj92xtsupa6lwjm7hkr8s8zflqk5sz82v9gqzcyypu03lyetn3ayp8p5798mz4der4xexkxxxu8ck0m25gmjaaj3n5scaql5q0uquxfmcztl0gldv8mxy3sm8x5jscdz27u39fy6luxu8zcdn9j73l3upaypvngk89elemj7cvu9v57r28k65e4jtlsr0vd66yzwrw2uzyvjczq0sd8fk6vazvrxvnks7hdqkl3lar4ff4a2ccjpltacz8z6tw6lunvqzrlrvyu3rkyylgd0splzr0xs3cccmzmfllyu7k06gclf9wx5n463z5arlwz2frk9a6lnfrvjdh3znsppxc4v8ahdy7e0y3us5rww0lcdxqj6psx87tgwvm3u260gc25frw9c4t368cecy3f5flll87tgk2uva09mncqqe9qqq0vdm023df0eetknvgcxk5yuupg8j5a5jtdmpj0u5unp8f3g2gskpma8l53sd5vmsfcrlr4rm7y9n2y8qqqqp7sqqqq8espgsqqqqqqqqqqqqqqqqqqqqq73qqqqq2gpr8hefjt2pqdgsfqcr3r5kcckf6sgtmkwuds8wp5uc7j0zcj0d9lsgl47vl95y25q36nzhqxqsqqzczzqce08lxec8xnm8xmxdyh398kv8dy25vhpcrm47a9ha0vx0qwyn7p0cyqt925k662c9kelq545l944k3j9gdvmkfm2ev2pqpnslmx8qvuwx3jqg4u8ful39aq0tlzujjd2yagjndcd2q42r5hvydx0lxe58mdrgs
{
"code": 209,
"message": "Failed after 1 attempts. We got an error from inside the blinded path 0x0x0/1: we assume it means insufficient capacity. Then routing failed: We could not find a usable set of paths. The shortest path is 858132x1647x1->861005x2291x1->0x0x0, but 0x0x0/1 layer xpay-0 says max is 14999msat"
}
lightning-cli pay lni1qqgv5nalmz08ukj4av074kyk6pepq93pqvvhnlnvurnfanndnxjtcjnmxrkj92xtsupa6lwjm7hkr8s8zflqk5sz82v9gqzcyypu03lyetn3ayp8p5798mz4der4xexkxxxu8ck0m25gmjaaj3n5scaql5q0uquxfmcztl0gldv8mxy3sm8x5jscdz27u39fy6luxu8zcdn9j73l3upaypvngk89elemj7cvu9v57r28k65e4jtlsr0vd66yzwrw2uzyvjczq0sd8fk6vazvrxvnks7hdqkl3lar4ff4a2ccjpltacz8z6tw6lunvqzrlrvyu3rkyylgd0splzr0xs3cccmzmfllyu7k06gclf9wx5n463z5arlwz2frk9a6lnfrvjdh3znsppxc4v8ahdy7e0y3us5rww0lcdxqj6psx87tgwvm3u260gc25frw9c4t368cecy3f5flll87tgk2uva09mncqqe9qqq0vdm023df0eetknvgcxk5yuupg8j5a5jtdmpj0u5unp8f3g2gskpma8l53sd5vmsfcrlr4rm7y9n2y8qqqqp7sqqqq8espgsqqqqqqqqqqqqqqqqqqqqq73qqqqq2gpr8hefjt2pqdgsfqcr3r5kcckf6sgtmkwuds8wp5uc7j0zcj0d9lsgl47vl95y25q36nzhqxqsqqzczzqce08lxec8xnm8xmxdyh398kv8dy25vhpcrm47a9ha0vx0qwyn7p0cyqt925k662c9kelq545l944k3j9gdvmkfm2ev2pqpnslmx8qvuwx3jqg4u8ful39aq0tlzujjd2yagjndcd2q42r5hvydx0lxe58mdrgs
{
"destination": "031979fe6ce0e69ece6d99a4bc4a7b30ed22a8cb8703dd7dd2dfaf619e07127e0b",
"payment_hash": "6a209060711d2d8c593a8217bb3b8d81dc1a731e93c5893da5fc11faf99f2d08",
"created_at": 1740526413.632475833,
"parts": 1,
"amount_msat": 16007,
"amount_sent_msat": 16010,
"payment_preimage": "c6c75b93dbbdbc5082350a4396afec0232ff4400ea0c9f053b977f4389f501bf",
"status": "complete"
}
"""
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True,
opts=[{'cltv-delta': 50},
{'cltv-delta': 100},
{'cltv-delta': 200}])
# Connect l3->l4 via private channel, to force blinded path
l4 = node_factory.get_node(options={'cltv-final': 400})
node_factory.join_nodes([l3, l4], announce_channels=False)
# Make sure l4 sees all the gossip
wait_for(lambda: len(l4.rpc.listchannels()['channels']) == 2 * 2)
offer = l4.rpc.offer('any')
inv = l1.rpc.fetchinvoice(offer['bolt12'], '15000msat')['invoice']
# Inserts a blinded path
path = only_one(l1.rpc.decode(inv)['invoice_paths'])
assert path['first_node_id'] == l3.info['id']
assert len(path['path']) == 2
assert path['payinfo']['cltv_expiry_delta'] == 200 + 400
# Make sure l1 is on correct height, so CLTV is as expected.
sync_blockheight(bitcoind, [l1])
# This works.
l1.rpc.pay(inv)
# CLTV is blockheight (110) + 1 + 100 + 200 + 400
l1.daemon.wait_for_log(f'Adding HTLC 0 amount=15002msat cltv={110 + 1 + 100 + 200 + 400}')
inv = l1.rpc.fetchinvoice(offer['bolt12'], '15000msat')['invoice']
# This doesn't!
l1.rpc.xpay(inv)
l1.daemon.wait_for_log(f'Adding HTLC 1 amount=15002msat cltv={110 + 1 + 100 + 200 + 400}')
def test_attempt_notifications(node_factory):
def zero_fields(obj, fieldnames):
if isinstance(obj, dict):
for k, v in obj.items():
if k in fieldnames:
obj[k] = 0
else:
zero_fields(v, fieldnames)
elif isinstance(obj, list):
for item in obj:
zero_fields(item, fieldnames)
# other types are ignored
return obj
part_starts = []
part_ends = []
def setup(plugin):
@plugin.subscribe("pay_part_start")
def on_pay_part_start(origin, **kwargs):
part_starts.append(kwargs)
@plugin.subscribe("pay_part_end")
def on_pay_part_end(origin, **kwargs):
part_ends.append(kwargs)
@plugin.subscribe("pay_success")
def on_pay_success(origin, pay_success, **kwargs):
pass
@plugin.subscribe("custom")
def on_custom_notification(origin, message, **kwargs):
pass
@plugin.subscribe("ididntannouncethis")
def on_faulty_emit(origin, payload, **kwargs):
pass
@plugin.method("emit")
def emit(plugin):
"""Emit a simple string notification to topic "custom" """
plugin.notify("custom", {'message': "Hello world"})
@plugin.method("faulty-emit")
def faulty_emit(plugin):
"""Emit a simple string notification to topic "custom" """
plugin.notify("ididntannouncethis", {'message': "Hello world"})
plugin.add_notification_topic("custom")
l1 = node_factory.get_node(inline_plugin=setup)
l2 = node_factory.get_node()
l3 = node_factory.get_node()
node_factory.join_nodes([l1, l2, l3], wait_for_announce=True)
scid12 = only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['short_channel_id']
scid12_dir = only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['direction']
scid23 = only_one(l2.rpc.listpeerchannels(l3.info['id'])['channels'])['short_channel_id']
scid23_dir = only_one(l2.rpc.listpeerchannels(l3.info['id'])['channels'])['direction']
inv1 = l3.rpc.invoice(5000000, 'test_attempt_notifications1', 'test_attempt_notifications1')
l1.rpc.xpay(inv1['bolt11'])
wait_for(lambda: len(part_starts) >= 1)
data = zero_fields(part_starts.pop(0), ['groupid'])
expected = {'pay_part_start':
{'payment_hash': inv1['payment_hash'],
'groupid': 0,
'partid': 1,
'total_payment_msat': 5000000,
'attempt_msat': 5000000,
'hops': [{'next_node': l2.info['id'],
'short_channel_id': scid12,
'direction': scid12_dir,
'channel_in_msat': 5000051,
'channel_out_msat': 5000051},
{'next_node': l3.info['id'],
'short_channel_id': scid23,
'direction': scid23_dir,
'channel_in_msat': 5000051,
'channel_out_msat': 5000000}]}}
assert data == expected
wait_for(lambda: len(part_ends) >= 1)
data = zero_fields(part_ends.pop(0), ('duration', 'groupid'))
expected = {'pay_part_end':
{'payment_hash': inv1['payment_hash'],
'status': 'success',
'duration': 0,
'groupid': 0,
'partid': 1}}
assert data == expected
inv2 = l3.rpc.invoice(10000000, 'test_attempt_notifications2', 'test_attempt_notifications2')
l3.rpc.delinvoice('test_attempt_notifications2', "unpaid")
# Final node failure
with pytest.raises(RpcError, match=r"Destination said it doesn't know invoice: incorrect_or_unknown_payment_details"):
l1.rpc.xpay(inv2['bolt11'])
wait_for(lambda: len(part_starts) >= 1)
data = zero_fields(part_starts.pop(0), ['groupid'])
expected = {'pay_part_start':
{'payment_hash': inv2['payment_hash'],
'groupid': 0,
'partid': 1,
'total_payment_msat': 10000000,
'attempt_msat': 10000000,
'hops': [{'next_node': l2.info['id'],
'short_channel_id': scid12,
'direction': scid12_dir,
'channel_in_msat': 10000101,
'channel_out_msat': 10000101},
{'next_node': l3.info['id'],
'short_channel_id': scid23,
'direction': scid23_dir,
'channel_in_msat': 10000101,
'channel_out_msat': 10000000}]}}
assert data == expected
wait_for(lambda: len(part_ends) >= 1)
data = zero_fields(part_ends.pop(0), ('duration', 'groupid'))
expected = {'pay_part_end':
{'payment_hash': inv2['payment_hash'],
'status': 'failure',
'duration': 0,
'groupid': 0,
'partid': 1,
'failed_msg': '400f00000000009896800000006c',
'failed_node_id': l3.info['id'],
'error_code': 16399,
'error_message': 'incorrect_or_unknown_payment_details'}}
assert data == expected
# Intermediary node failure
l3.stop()
with pytest.raises(RpcError, match=r"Failed after 1 attempts"):
l1.rpc.xpay(inv2['bolt11'])
wait_for(lambda: len(part_starts) >= 1)
data = zero_fields(part_starts.pop(0), ['groupid'])
expected = {'pay_part_start':
{'payment_hash': inv2['payment_hash'],
'groupid': 0,
'partid': 1,
'total_payment_msat': 10000000,
'attempt_msat': 10000000,
'hops': [{'next_node': l2.info['id'],
'short_channel_id': scid12,
'direction': scid12_dir,
'channel_in_msat': 10000101,
'channel_out_msat': 10000101},
{'next_node': l3.info['id'],
'short_channel_id': scid23,
'direction': scid23_dir,
'channel_in_msat': 10000101,
'channel_out_msat': 10000000}]}}
assert data == expected
wait_for(lambda: len(part_ends) >= 1)
data = zero_fields(part_ends.pop(0), ('duration', 'groupid', 'failed_msg'))
expected = {'pay_part_end':
{'payment_hash': inv2['payment_hash'],
'status': 'failure',
'duration': 0,
'groupid': 0,
'partid': 1,
# This includes the channel update: just zero it out
'failed_msg': 0,
'failed_direction': 0,
'failed_node_id': l2.info['id'],
'failed_short_channel_id': scid23,
'error_code': 4103,
'error_message': 'temporary_channel_failure'}}
assert data == expected
def test_xpay_offer(node_factory):
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
offer1 = l3.rpc.offer('any')['bolt12']
offer2 = l3.rpc.offer('5sat', "5sat donation")['bolt12']
with pytest.raises(RpcError, match=r"Must specify amount for this offer"):
l1.rpc.xpay(offer1)
l1.rpc.xpay(offer1, 100)
with pytest.raises(RpcError, match=r"Offer amount is 5000msat, you tried to pay 1000msat"):
l1.rpc.xpay(offer2, 1000)
l1.rpc.xpay(offer2)
l1.rpc.xpay(offer2, 5000)
def test_xpay_circular_routehint(node_factory):
"""Test that xpay gracefully skips a circular bolt11 routehint (src == dst)."""
l1, l2 = node_factory.line_graph(2)
# A routehint containing a same-node channel (example is l3 in this case)
circular_hint = [{'id': '03cecbfdc68544cc596223b68ce0710c9e5d2c9cb317ee07822d95079acc703d31',
'short_channel_id': '1x2x3',
'fee_base_msat': 0,
'fee_proportional_millionths': 0,
'cltv_expiry_delta': 6},
{'id': '03cecbfdc68544cc596223b68ce0710c9e5d2c9cb317ee07822d95079acc703d31',
'short_channel_id': '1x2x4',
'fee_base_msat': 0,
'fee_proportional_millionths': 0,
'cltv_expiry_delta': 6}]
inv = l2.dev_invoice(amount_msat=10000,
label='circular_hint',
description='circular hint test',
dev_routes=[circular_hint])
# Payment should still succeed via the direct channel.
ret = l1.rpc.xpay(inv['bolt11'])
assert ret['successful_parts'] == 1
l1.daemon.wait_for_log('Invoice gave bad self-node route')
def test_xpay_currency_offer(node_factory):
"""Test that xpay and sendamount correctly report the currency name when rejecting non-msat offers."""
plugin = Path(__file__).parent / "plugins" / "currencyUSDAUD5000.py"
l1, l2 = node_factory.line_graph(2, wait_for_announce=True,
opts=[{}, {'plugin': str(plugin)}])
offerusd = l2.rpc.offer('10USD', 'USD test')['bolt12']
with pytest.raises(RpcError, match=r"Cannot pay offer in different currency USD"):
l1.rpc.xpay(offerusd)
with pytest.raises(RpcError, match=r"Cannot pay offer in different currency USD"):
l1.rpc.sendamount(offerusd, '100sat')
def test_xpay_bip353(node_factory):
fakebip353_plugin = Path(__file__).parent / "plugins" / "fakebip353.py"
l1 = node_factory.get_node()
offer = l1.rpc.offer('any')['bolt12']
l2 = node_factory.get_node(options={'disable-plugin': 'cln-bip353',
'plugin': fakebip353_plugin,
'bip353offer': offer})
node_factory.join_nodes([l2, l1])
l2.rpc.xpay('fake@fake.com', 100)
def test_xpay_shadow_cltv(node_factory, bitcoind):
"""Shadow CLTV: dev_use_shadow=False gives exact CLTV; errors for offer/BIP353."""
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
scidd_21 = first_scidd(l2, l1)
scidd_23 = first_scidd(l2, l3)
scidd_32 = first_scidd(l3, l2)
# One in 4 chance that it adds two shadow routes...
i = 0
while True:
inv = l3.rpc.invoice('10000msat', f'with_shadow {i}', f'with shadow {i}')['bolt11']
l1.rpc.xpay(invstring=inv)
if l1.daemon.is_in_log(r'shadow #2: stopping'):
break
i += 1
# First one must be the one to l2.
assert l1.daemon.is_in_log(f"shadow #0: adding {scidd_32} to {l2.info['id']}")
# Second one could be back to l3 or to l1.
assert l1.daemon.is_in_log(f"shadow #1: adding {scidd_23} to {l3.info['id']}") or l1.daemon.is_in_log(f"shadow #1: adding {scidd_21} to {l1.info['id']}")
# Final cltv is 5, then two shadow hops and one padding.
expected_cltv = bitcoind.rpc.getblockchaininfo()['blocks'] + 5 + 1 + 6 * 2
assert l2.daemon.is_in_log(f"Adding HTLC .* amount=10000msat cltv={expected_cltv} gave CHANNEL_ERR_ADD_OK")
def test_xpay_limited_max_accepted_htlcs(node_factory):
"""xpay should try to reduce flows to 6 if there is an unannounced channel, and only try more if that fails"""
CHANNEL_SIZE_SATS = 10**6
l1, l2 = node_factory.line_graph(2,
fundamount=CHANNEL_SIZE_SATS * 20,
opts=[{}, {'max-concurrent-htlcs': 6}],
announce_channels=False)
# We want 10 paths between l3 and l1.
l3 = node_factory.get_node()
nodes = node_factory.get_nodes(10)
for n in nodes:
node_factory.join_nodes([l3, n, l1], fundamount=CHANNEL_SIZE_SATS)
# We don't want to use up capacity, so we make payment fail.
inv1 = l1.rpc.invoice(f"{CHANNEL_SIZE_SATS * 5}sat",
'test_xpay_limited_max_accepted_htlcs',
'test_xpay_limited_max_accepted_htlcs')['bolt11']
l1.rpc.delinvoice('test_xpay_limited_max_accepted_htlcs', 'unpaid')
with pytest.raises(RpcError, match="Destination said it doesn't know invoice"):
l3.rpc.xpay(inv1)
# 7 flows.
l3.daemon.wait_for_log('Final answer has 7 flows')
# Make sure xpay has completely finished!
wait_for(lambda: l3.rpc.askrene_listreservations() == {'reservations': []})
# If we have a routehint, it will squeeze into 6.
inv2 = l2.rpc.invoice(f"{CHANNEL_SIZE_SATS * 5}sat",
'test_xpay_limited_max_accepted_htlcs',
'test_xpay_limited_max_accepted_htlcs')['bolt11']
l2.rpc.delinvoice('test_xpay_limited_max_accepted_htlcs', 'unpaid')
with pytest.raises(RpcError, match="Destination said it doesn't know invoice"):
l3.rpc.xpay(inv2)
# 6 flows.
l3.daemon.wait_for_log('Final answer has 6 flows')
# Make sure xpay has completely finished!
wait_for(lambda: l3.rpc.askrene_listreservations() == {'reservations': []})
# If we force it, it will use more flows. And fail on 7th part!
inv2 = l2.rpc.invoice(f"{CHANNEL_SIZE_SATS * 6}sat",
'test_xpay_limited_max_accepted_htlcs2',
'test_xpay_limited_max_accepted_htlcs2')['bolt11']
with pytest.raises(RpcError, match="We got temporary_channel_failure"):
l3.rpc.xpay(inv2)
l3.daemon.wait_for_log('Final answer has 7 flows')
def test_xpay_blockheight_mismatch(node_factory, bitcoind, executor):
"""We should wait a (reasonable) amount if the final node gives us a blockheight that would explain our failure."""
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
sync_blockheight(bitcoind, [l1, l2, l3])
# Pin `send` at the current height. by not returning the next
# blockhash. This error is special-cased not to count as the
# backend failing since it is used to poll for the next block.
def mock_getblockhash(req):
return {
"id": req['id'],
"error": {
"code": -8,
"message": "Block height out of range"
}
}
l1.daemon.rpcproxy.mock_rpc('getblockhash', mock_getblockhash)
bitcoind.generate_block(4)
sync_blockheight(bitcoind, [l2, l3])
l1_height = l1.rpc.getinfo()['blockheight']
l3_height = l3.rpc.getinfo()['blockheight']
inv = l3.rpc.invoice(42, 'lbl', 'desc')['bolt11']
# This will wait, then fail.
with pytest.raises(RpcError, match=f'Timed out waiting for blockheight {l3_height}'):
l1.rpc.xpay(invstring=inv, retry_for=10, dev_use_shadow=False)
# This will succeed, because we wait for the blocks.
fut = executor.submit(l1.rpc.xpay, invstring=inv, retry_for=60, dev_use_shadow=False)
l1.daemon.wait_for_log(fr"Our blockheight may be too low: waiting .* seconds for height {l3_height} \(we are at {l1_height}\)")
# Now let it catch up, and it will retry, and succeed.
l1.daemon.rpcproxy.mock_rpc('getblockhash')
fut.result(TIMEOUT)
def test_xpay_user_layers(node_factory):
l1, l2, l3, l4 = node_factory.get_nodes(
4, opts={"may_reconnect": True, "xpay-handle-pay": True}
)
node_factory.join_nodes([l1, l2, l3], wait_for_announce=True)
node_factory.join_nodes([l2, l4], wait_for_announce=True)
layer = "disable-chan23"
l1.rpc.askrene_create_layer(layer=layer, persistent=True)
scid = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]["short_channel_id"]
direction = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]["direction"]
l1.rpc.askrene_update_channel(
layer=layer, enabled=False, short_channel_id_dir=f"{scid}/{direction}"
)
layer = "disable-chan24"
l1.rpc.askrene_create_layer(layer=layer, persistent=True)
scid = l2.rpc.listpeerchannels(l4.info["id"])["channels"][0]["short_channel_id"]
direction = l2.rpc.listpeerchannels(l4.info["id"])["channels"][0]["direction"]
l1.rpc.askrene_update_channel(
layer=layer, enabled=False, short_channel_id_dir=f"{scid}/{direction}"
)
# Let us load these layers as user layers in xpay. Both payments should fail
l1.stop()
l1.daemon.opts["xpay-user-layer"] = ["disable-chan23", "disable-chan24"]
l1.start()
l1.rpc.connect(l2.info["id"], "localhost", l2.port)
l1.daemon.wait_for_log(f"channeld.*: billboard: Channel ready for use")
inv3 = l3.rpc.invoice(1000, "test-xpay-user-layer", "test-xpay-user-layer")[
"bolt11"
]
with pytest.raises(
RpcError,
match="We could not find a usable set of paths. All 1 channels to the destination are disabled.",
):
l1.rpc.pay(inv3)
inv4 = l4.rpc.invoice(1000, "test-xpay-user-layer", "test-xpay-user-layer")[
"bolt11"
]
with pytest.raises(
RpcError,
match="We could not find a usable set of paths. All 1 channels to the destination are disabled.",
):
l1.rpc.pay(inv4)
# Without those layers, the same payments should go through
l1.stop()
del l1.daemon.opts["xpay-user-layer"]
l1.start()
l1.rpc.connect(l2.info["id"], "localhost", l2.port)
l1.daemon.wait_for_log(f"channeld.*: billboard: Channel ready for use")
l1.rpc.pay(inv3)
l1.rpc.pay(inv4)
def test_xpay_get_error_with_update(node_factory):
"""We should process an update inside a temporary_channel_failure"""
l1, l2, l3 = node_factory.line_graph(3, opts={'log-level': 'io'}, fundchannel=True, wait_for_announce=True)
chanid2 = l2.get_channel_scid(l3)
inv = l3.rpc.invoice(123000, 'test_xpay_get_error_with_update', 'description')
# Make sure it's not doing startup any more (where it doesn't disable channels!)
l2.daemon.wait_for_log("channel_gossip: no longer in startup mode", timeout=70)
# Make sure l2 doesn't tell l1 directly that channel is disabled.
l2.rpc.dev_suppress_gossip()
l3.stop()
# Make sure that l2 has seen disconnect, considers channel disabled.
wait_for(lambda: only_one(l2.rpc.listpeerchannels(l3.info['id'])['channels'])['peer_connected'] is False)
assert(l1.is_channel_active(chanid2))
with pytest.raises(RpcError, match=r'temporary_channel_failure'):
l1.rpc.xpay(inv['bolt11'])
# Make sure we get an onionreply, without the type prefix of the nested
# channel_update, and it should patch it to include a type prefix. The
# prefix 0x0102 should be in the channel_update, but not in the
# onionreply (negation of 0x0102 in the RE)
l1.daemon.wait_for_log(rf'Got channel_update from error for {chanid2}/0: 0102')
# But this update is only for this one, not future ones!
time.sleep(5)
assert l1.is_channel_active(chanid2)
def test_xpay_error_update_fees(node_factory):
"""We should process an update inside a temporary_channel_failure"""
l1, l2, l3 = node_factory.line_graph(3, fundchannel=True, wait_for_announce=True)
# Don't include any routehints in first invoice.
inv1 = l3.dev_invoice(amount_msat=123000,
label='test_xpay_error_update_fees',
description='description',
dev_routes=[])
inv2 = l3.rpc.invoice(123000, 'test_xpay_error_update_fees2', 'desc')
assert 'routes' not in l1.rpc.decode(inv1['bolt11'])
assert 'routes' in l1.rpc.decode(inv2['bolt11'])
# Make sure l2 doesn't tell l1 directly that channel fee is changed.
l2.rpc.dev_suppress_gossip()
l2.rpc.setchannel(l3.info['id'], 1337, 137, enforcedelay=0)
# Should bounce off and retry...
ret = l1.rpc.xpay(inv1['bolt11'])
assert ret["failed_parts"] == 1
assert ret["successful_parts"] == 1
l1.daemon.wait_for_log('We got fee_insufficient for .*, containing a channel_update: updating our map')
# This will have to do the same, since we don't remember such updates. It will
# even fix the routehint which is (now) wrong.
ret = l1.rpc.xpay(inv2['bolt11'])
assert ret["failed_parts"] == 1
assert ret["successful_parts"] == 1
l1.daemon.wait_for_log('We got fee_insufficient for .*, containing a channel_update: updating our map')
def test_error_messages(node_factory):
"""Nicer error messages when we disable the only channel to the destination."""
plugin = os.path.join(os.path.dirname(__file__), 'plugins/replace_payload.py')
l1, l2 = node_factory.line_graph(
2,
opts=[{}, {"plugin": plugin}],
wait_for_announce=True
)
# Replace with an invalid payload.
l2.rpc.call('setpayload', ['0000'])
inv = l2.rpc.invoice(123, 'test_error_messages', 'test_error_messages')['bolt11']
scidd = first_scidd(l1, l2)
with pytest.raises(RpcError, match=rf"Failed after 1 attempts\. Unexpected error \(invalid_onion_payload\) from final node: disabling {scidd} for this payment\. Then routing failed: We could not find a usable set of paths\. All 1 channels to the source are disabled."):
l1.rpc.xpay(inv)
def test_blinded_path_fees(node_factory):
"""Test that we don't send the amount+fees to our direct peer (we should
only send the required amount) when the sending node is the entry point in
the blinded path."""
AMT_MSAT = 10000
FEES_MSAT = 5000
l1, l2 = node_factory.get_nodes(
2, opts={"may_reconnect": True, "fee-base": FEES_MSAT, "fee-per-satoshi": 0}
)
node_factory.join_nodes([l1, l2], wait_for_announce=True)
offer = l2.rpc.offer(amount="any", fronting_nodes=[l1.info["id"]])["bolt12"]
b12 = l1.rpc.fetchinvoice(offer, AMT_MSAT)["invoice"]
b12_decode = l1.rpc.decode(b12)
assert b12_decode["invoice_amount_msat"] == AMT_MSAT
assert len(b12_decode["invoice_paths"]) == 1
assert b12_decode["invoice_paths"][0]["first_node_id"] == l1.info["id"]
assert b12_decode["invoice_paths"][0]["payinfo"]["fee_base_msat"] == FEES_MSAT
assert b12_decode["invoice_paths"][0]["payinfo"]["fee_proportional_millionths"] == 0
ret = l1.rpc.xpay(invstring=b12)
assert ret["failed_parts"] == 0
assert ret["successful_parts"] == 1
assert ret["amount_msat"] == AMT_MSAT
assert ret["amount_sent_msat"] == AMT_MSAT
htlcs = l1.rpc.listhtlcs()["htlcs"]
assert len(htlcs) == 1
assert htlcs[0]["payment_hash"] == b12_decode["invoice_payment_hash"]
assert htlcs[0]["amount_msat"] == AMT_MSAT
def test_sendamount(node_factory):
l1, l2, l3, l4 = node_factory.get_nodes(4)
node_factory.join_nodes([l1, l2, l3, l4], wait_for_announce=True)
# pay bolt11
b11 = l4.rpc.invoice("any", "test_sendamount_bolt11", "test_sendamount bolt11")[
"bolt11"
]
ret = l1.rpc.sendamount(b11, "100sat")
assert ret["successful_parts"] == 1
assert ret["amount_sent_msat"] == 100000
# pay bolt12
b12 = l4.rpc.offer("any")["bolt12"]
ret = l1.rpc.sendamount(b12, "100sat")
assert ret["successful_parts"] == 1
assert ret["amount_sent_msat"] == 100000
# pay to self bolt11
b11 = l4.rpc.invoice(
"any", "test_sendamount_bolt11_self", "test_sendamount_bolt11_self"
)["bolt11"]
ret = l4.rpc.sendamount(b11, "100sat")
assert ret["successful_parts"] == 1
assert ret["amount_sent_msat"] == 100000
# pay to self bolt12
b12 = l4.rpc.offer("any")["bolt12"]
ret = l4.rpc.sendamount(b12, "100sat")
assert ret["successful_parts"] == 1
assert ret["amount_sent_msat"] == 100000
# pay bolt11 direct peer
b11 = l4.rpc.invoice(
"any", "test_sendamount_bolt11_direct", "test_sendamount bolt11_direct"
)["bolt11"]
ret = l3.rpc.sendamount(b11, "100sat")
assert ret["successful_parts"] == 1
assert ret["amount_sent_msat"] == 100000
# pay bolt12 direct peer
b12 = l4.rpc.offer("any")["bolt12"]
ret = l3.rpc.sendamount(b12, "100sat")
assert ret["successful_parts"] == 1
assert ret["amount_sent_msat"] == 100000
# cannot sendamount to bolt11 with fixed amount
b11 = l4.rpc.invoice(
"100sat", "test_sendamount_bolt11_fail", "test_sendamount bolt11_fail"
)["bolt11"]
with pytest.raises(RpcError, match=r"Expecting a Bolt11 invoice with no amount."):
ret = l1.rpc.sendamount(b11, "100sat")
# cannot sendamount to bolt12 offer with fixed amount
b12 = l4.rpc.offer("100sat", "test sendamount bolt12 offer fail")["bolt12"]
with pytest.raises(RpcError, match=r"Expecting an offer with no amount"):
ret = l1.rpc.sendamount(b12, "100sat")
# cannot sendamount to bolt12 invoice
offer = l4.rpc.offer("any")["bolt12"]
b12 = l1.rpc.fetchinvoice(offer, "100sat")["invoice"]
with pytest.raises(RpcError, match=r"Invalid bolt12 offer: unexpected prefix lni"):
ret = l1.rpc.sendamount(b12, "100sat")
def test_xpay_informs_askrene_on_success(node_factory):
"""After a successful payment, xpay should add an impression to the xpay askrene layer."""
l1, l2, l3 = node_factory.get_nodes(3)
node_factory.join_nodes([l1, l2, l3], wait_for_announce=True)
inv = l3.rpc.invoice(100000, "test-inform", "test inform")["bolt11"]
l1.rpc.xpay(inv)
# xpay skips the local channel (hops[0]), so only the l2->l3 hop gets an impression
scid23dir = first_scidd(l2, l3)
layers = l1.rpc.askrene_listlayers('xpay')['layers']
impressions = only_one(layers)['impressions']
assert len(impressions) == 1
assert impressions[0]['short_channel_id_dir'] == scid23dir
assert impressions[0]['amount_msat'] == 100000
def test_sendamount_bip353(node_factory):
fakebip353_plugin = Path(__file__).parent / "plugins" / "fakebip353.py"
l1 = node_factory.get_node()
offer = l1.rpc.offer("any")["bolt12"]
l2 = node_factory.get_node(
options={
"disable-plugin": "cln-bip353",
"plugin": fakebip353_plugin,
"bip353offer": offer,
}
)
node_factory.join_nodes([l2, l1])
ret = l2.rpc.sendamount("fake@fake.com", "100sat")
assert ret["successful_parts"] == 1
assert ret["amount_sent_msat"] == 100000