mirror of
https://github.com/ElementsProject/lightning.git
synced 2026-08-13 12:32:55 +02:00
1475 lines
64 KiB
Python
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
|