mirror of
https://github.com/ElementsProject/lightning.git
synced 2024-11-19 01:43:36 +01:00
9d88ce3b59
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
799 lines
27 KiB
Python
799 lines
27 KiB
Python
from fixtures import * # noqa: F401,F403
|
|
from pyln.client import RpcError, Millisatoshi
|
|
from utils import (
|
|
only_one,
|
|
wait_for,
|
|
mine_funding_to_announce,
|
|
sync_blockheight,
|
|
TEST_NETWORK,
|
|
)
|
|
import pytest
|
|
import random
|
|
import time
|
|
import json
|
|
import subprocess
|
|
import os
|
|
import re
|
|
import unittest
|
|
|
|
|
|
def test_simple(node_factory):
|
|
"""Testing simply paying a peer."""
|
|
l1, l2 = node_factory.line_graph(2)
|
|
inv = l2.rpc.invoice(123000, "test_renepay", "description")["bolt11"]
|
|
details = l1.rpc.call("renepay", {"invstring": inv})
|
|
assert details["status"] == "complete"
|
|
assert details["amount_msat"] == Millisatoshi(123000)
|
|
assert details["destination"] == l2.info["id"]
|
|
|
|
|
|
def test_direction_matters(node_factory):
|
|
"""Make sure we use correct delay and fees for the direction we're going."""
|
|
l1, l2, l3 = node_factory.line_graph(
|
|
3,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{},
|
|
{"fee-base": 2000, "fee-per-satoshi": 20, "cltv-delta": 20},
|
|
{"fee-base": 3000, "fee-per-satoshi": 30, "cltv-delta": 30},
|
|
],
|
|
)
|
|
inv = l3.rpc.invoice(123000, "test_renepay", "description")["bolt11"]
|
|
details = l1.rpc.call("renepay", {"invstring": inv})
|
|
assert details["status"] == "complete"
|
|
assert details["amount_msat"] == Millisatoshi(123000)
|
|
assert details["destination"] == l3.info["id"]
|
|
|
|
|
|
def test_shadow_routing(node_factory):
|
|
"""
|
|
Test the value randomization through shadow routing
|
|
|
|
Note there is a very low (0.5**10) probability that it fails.
|
|
"""
|
|
# We need l3 for random walk
|
|
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
|
|
|
|
amount = 10000
|
|
total_amount = 0
|
|
n_payments = 10
|
|
for i in range(n_payments):
|
|
inv = l3.rpc.invoice(amount, "{}".format(i), "test")["bolt11"]
|
|
total_amount += l1.rpc.call(
|
|
"renepay", {"invstring": inv, "dev_use_shadow": True}
|
|
)["amount_sent_msat"]
|
|
|
|
assert total_amount > n_payments * amount
|
|
# Test that the added amount isn't absurd
|
|
assert total_amount < int((n_payments * amount) * (1 + 0.01))
|
|
|
|
|
|
def test_mpp(node_factory):
|
|
"""Test paying a remote node using two routes.
|
|
1----2----4
|
|
| |
|
|
3----5----6
|
|
Try paying 1.2M sats from 1 to 6.
|
|
"""
|
|
opts = [
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
]
|
|
l1, l2, l3, l4, l5, l6 = node_factory.get_nodes(6, opts=opts * 6)
|
|
node_factory.join_nodes(
|
|
[l1, l2, l4, l6], wait_for_announce=True, fundamount=1000000
|
|
)
|
|
node_factory.join_nodes(
|
|
[l1, l3, l5, l6], wait_for_announce=True, fundamount=1000000
|
|
)
|
|
|
|
send_amount = Millisatoshi("1200000sat")
|
|
inv = l6.rpc.invoice(send_amount, "test_renepay", "description")["bolt11"]
|
|
details = l1.rpc.call("renepay", {"invstring": inv})
|
|
assert details["status"] == "complete"
|
|
assert details["amount_msat"] == send_amount
|
|
assert details["destination"] == l6.info["id"]
|
|
|
|
|
|
def test_errors(node_factory, bitcoind):
|
|
opts = [
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
]
|
|
l1, l2, l3, l4, l5, l6 = node_factory.get_nodes(6, opts=opts * 6)
|
|
send_amount = Millisatoshi("21sat")
|
|
inv = l6.rpc.invoice(send_amount, "test_renepay", "description")["bolt11"]
|
|
inv_deleted = l6.rpc.invoice(send_amount, "test_renepay2", "description2")["bolt11"]
|
|
l6.rpc.delinvoice("test_renepay2", "unpaid")
|
|
|
|
failmsg = r"We don\'t have any channels"
|
|
with pytest.raises(RpcError, match=failmsg):
|
|
l1.rpc.call("renepay", {"invstring": inv})
|
|
node_factory.join_nodes([l1, l2, l4], wait_for_announce=True, fundamount=1000000)
|
|
node_factory.join_nodes([l1, l3, l5], wait_for_announce=True, fundamount=1000000)
|
|
|
|
failmsg = r"Destination is unknown in the network gossip."
|
|
with pytest.raises(RpcError, match=failmsg):
|
|
l1.rpc.call("renepay", {"invstring": inv})
|
|
|
|
l4.rpc.connect(l6.info["id"], "localhost", l6.port)
|
|
l5.rpc.connect(l6.info["id"], "localhost", l6.port)
|
|
|
|
scid46, _ = l4.fundchannel(l6, 10**6, wait_for_active=False)
|
|
scid56, _ = l5.fundchannel(l6, 10**6, wait_for_active=False)
|
|
mine_funding_to_announce(bitcoind, [l1, l2, l3, l4, l5, l6])
|
|
|
|
l1.daemon.wait_for_logs(
|
|
[
|
|
r"update for channel {}/0 now ACTIVE".format(scid46),
|
|
r"update for channel {}/1 now ACTIVE".format(scid46),
|
|
r"update for channel {}/0 now ACTIVE".format(scid56),
|
|
r"update for channel {}/1 now ACTIVE".format(scid56),
|
|
]
|
|
)
|
|
details = l1.rpc.call("renepay", {"invstring": inv})
|
|
assert details["status"] == "complete"
|
|
assert details["amount_msat"] == send_amount
|
|
assert details["destination"] == l6.info["id"]
|
|
|
|
# Test error from final node.
|
|
with pytest.raises(RpcError) as err:
|
|
l1.rpc.call("renepay", {"invstring": inv_deleted})
|
|
|
|
PAY_DESTINATION_PERM_FAIL = 203
|
|
assert err.value.error["code"] == PAY_DESTINATION_PERM_FAIL
|
|
assert "WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS" in err.value.error["message"]
|
|
|
|
|
|
@pytest.mark.openchannel("v1")
|
|
@pytest.mark.openchannel("v2")
|
|
def test_pay(node_factory):
|
|
l1, l2 = node_factory.line_graph(2)
|
|
|
|
inv = l2.rpc.invoice(123000, "test_pay", "description")["bolt11"]
|
|
before = int(time.time())
|
|
details = l1.rpc.call("renepay", {"invstring": inv, "dev_use_shadow": False})
|
|
after = time.time()
|
|
preimage = details["payment_preimage"]
|
|
assert details["status"] == "complete"
|
|
assert details["amount_msat"] == Millisatoshi(123000)
|
|
assert details["destination"] == l2.info["id"]
|
|
assert details["created_at"] >= before
|
|
assert details["created_at"] <= after
|
|
|
|
invoices = l2.rpc.listinvoices("test_pay")["invoices"]
|
|
assert len(invoices) == 1
|
|
invoice = invoices[0]
|
|
assert (
|
|
invoice["status"] == "paid"
|
|
and invoice["paid_at"] >= before
|
|
and invoice["paid_at"] <= after
|
|
)
|
|
|
|
# Repeat payments are NOPs (if valid): we can hand null.
|
|
l1.rpc.call("renepay", {"invstring": inv, "dev_use_shadow": False})
|
|
# This won't work: can't provide an amount (even if correct!)
|
|
with pytest.raises(RpcError):
|
|
l1.rpc.call("renepay", {"invstring": inv, "amount_msat": 123000})
|
|
with pytest.raises(RpcError):
|
|
l1.rpc.call("renepay", {"invstring": inv, "amount_msat": 122000})
|
|
|
|
# Check pay_index is not null
|
|
outputs = l2.db_query(
|
|
'SELECT pay_index IS NOT NULL AS q FROM invoices WHERE label="label";'
|
|
)
|
|
assert len(outputs) == 1 and outputs[0]["q"] != 0
|
|
|
|
# Check payment of any-amount invoice.
|
|
for i in range(5):
|
|
label = "any{}".format(i)
|
|
inv2 = l2.rpc.invoice("any", label, "description")["bolt11"]
|
|
# Must provide an amount!
|
|
with pytest.raises(RpcError):
|
|
l1.rpc.call("renepay", {"invstring": inv2, "dev_use_shadow": False})
|
|
|
|
l1.rpc.call(
|
|
"renepay",
|
|
{
|
|
"invstring": inv2,
|
|
"dev_use_shadow": False,
|
|
"amount_msat": random.randint(1000, 999999),
|
|
},
|
|
)
|
|
|
|
# Should see 6 completed payments
|
|
assert len(l1.rpc.listsendpays()["payments"]) == 6
|
|
|
|
# Test listsendpays indexed by bolt11.
|
|
payments = l1.rpc.listsendpays(inv)["payments"]
|
|
assert len(payments) == 1 and payments[0]["payment_preimage"] == preimage
|
|
|
|
# Make sure they're completely settled, so accounting correct.
|
|
wait_for(lambda: only_one(l1.rpc.listpeerchannels()["channels"])["htlcs"] == [])
|
|
|
|
# Check channels apy summary view of channel activity
|
|
apys_1 = l1.rpc.bkpr_channelsapy()["channels_apy"]
|
|
apys_2 = l2.rpc.bkpr_channelsapy()["channels_apy"]
|
|
|
|
assert (
|
|
apys_1[0]["channel_start_balance_msat"]
|
|
== apys_2[0]["channel_start_balance_msat"]
|
|
)
|
|
assert (
|
|
apys_1[0]["channel_start_balance_msat"] == apys_1[0]["our_start_balance_msat"]
|
|
)
|
|
assert apys_2[0]["our_start_balance_msat"] == Millisatoshi(0)
|
|
assert apys_1[0]["routed_out_msat"] == apys_2[0]["routed_in_msat"]
|
|
assert apys_1[0]["routed_in_msat"] == apys_2[0]["routed_out_msat"]
|
|
|
|
|
|
def test_amounts(node_factory):
|
|
"""
|
|
Check that the amount received matches the amount requested in the invoice.
|
|
"""
|
|
l1, l2 = node_factory.line_graph(2)
|
|
inv = l2.rpc.invoice(Millisatoshi(123456), "test_pay_amounts", "description")[
|
|
"bolt11"
|
|
]
|
|
|
|
invoice = only_one(l2.rpc.listinvoices("test_pay_amounts")["invoices"])
|
|
|
|
assert invoice["amount_msat"] == Millisatoshi(123456)
|
|
|
|
l1.rpc.call("renepay", {"invstring": inv, "dev_use_shadow": False})
|
|
|
|
invoice = only_one(l2.rpc.listinvoices("test_pay_amounts")["invoices"])
|
|
assert invoice["amount_received_msat"] >= Millisatoshi(123456)
|
|
|
|
|
|
def test_limits(node_factory):
|
|
"""
|
|
Topology:
|
|
1----2----4
|
|
| |
|
|
3----5----6
|
|
Try the error messages when paying when:
|
|
- the fees are too high,
|
|
- CLTV delay is too high,
|
|
- probability of success is too low.
|
|
"""
|
|
opts = [
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 100},
|
|
]
|
|
l1, l2, l3, l4, l5, l6 = node_factory.get_nodes(6, opts=opts * 6)
|
|
node_factory.join_nodes(
|
|
[l1, l2, l4, l6], wait_for_announce=True, fundamount=1000000
|
|
)
|
|
node_factory.join_nodes(
|
|
[l1, l3, l5, l6], wait_for_announce=True, fundamount=1000000
|
|
)
|
|
|
|
inv = l4.rpc.invoice("any", "any", "description")
|
|
l2.rpc.call("pay", {"bolt11": inv["bolt11"], "amount_msat": 500000000})
|
|
inv = l5.rpc.invoice("any", "any", "description")
|
|
l3.rpc.call("pay", {"bolt11": inv["bolt11"], "amount_msat": 500000000})
|
|
|
|
# FIXME: pylightning should define these!
|
|
# PAY_STOPPED_RETRYING = 210
|
|
PAY_ROUTE_TOO_EXPENSIVE = 206
|
|
|
|
inv = l6.rpc.invoice("any", "any", "description")
|
|
|
|
# Fee too high.
|
|
failmsg = r"Fee exceeds our fee budget"
|
|
with pytest.raises(RpcError, match=failmsg) as err:
|
|
l1.rpc.call(
|
|
"renepay", {"invstring": inv["bolt11"], "amount_msat": 1000000, "maxfee": 1}
|
|
)
|
|
assert err.value.error["code"] == PAY_ROUTE_TOO_EXPENSIVE
|
|
# TODO(eduardo): which error code shall we use here?
|
|
|
|
# TODO(eduardo): shall we list attempts in renepay?
|
|
# status = l1.rpc.call('renepaystatus', {'invstring':inv['bolt11']})['paystatus'][0]['attempts']
|
|
|
|
failmsg = r"CLTV delay exceeds our CLTV budget"
|
|
# Delay too high.
|
|
with pytest.raises(RpcError, match=failmsg) as err:
|
|
l1.rpc.call(
|
|
"renepay",
|
|
{"invstring": inv["bolt11"], "amount_msat": 1000000, "maxdelay": 0},
|
|
)
|
|
assert err.value.error["code"] == PAY_ROUTE_TOO_EXPENSIVE
|
|
|
|
inv2 = l6.rpc.invoice("800000sat", "inv2", "description")
|
|
l1.rpc.call("renepay", {"invstring": inv2["bolt11"]})
|
|
invoice = only_one(l6.rpc.listinvoices("inv2")["invoices"])
|
|
assert invoice["amount_received_msat"] >= Millisatoshi("800000sat")
|
|
|
|
|
|
def start_channels(connections):
|
|
nodes = list()
|
|
for src, dst, fundamount in connections:
|
|
nodes.append(src)
|
|
nodes.append(dst)
|
|
src.rpc.connect(dst.info["id"], "localhost", dst.port)
|
|
|
|
bitcoind = nodes[0].bitcoin
|
|
# If we got here, we want to fund channels
|
|
for src, dst, fundamount in connections:
|
|
addr = src.rpc.newaddr()["bech32"]
|
|
bitcoind.rpc.sendtoaddress(addr, (fundamount + 1000000) / 10**8)
|
|
|
|
bitcoind.generate_block(1)
|
|
sync_blockheight(bitcoind, nodes)
|
|
txids = []
|
|
for src, dst, fundamount in connections:
|
|
txids.append(
|
|
src.rpc.fundchannel(dst.info["id"], fundamount, announce=True)["txid"]
|
|
)
|
|
|
|
# Confirm all channels and wait for them to become usable
|
|
bitcoind.generate_block(1, wait_for_mempool=txids)
|
|
scids = []
|
|
for src, dst, fundamount in connections:
|
|
wait_for(lambda: src.channel_state(dst) == "CHANNELD_NORMAL")
|
|
scid = src.get_channel_scid(dst)
|
|
scids.append(scid)
|
|
|
|
# Make sure they have all seen block so they don't complain about
|
|
# the coming gossip messages
|
|
sync_blockheight(bitcoind, nodes)
|
|
|
|
bitcoind.generate_block(5)
|
|
|
|
# Make sure everyone sees all channels, all other nodes
|
|
for n in nodes:
|
|
for scid in scids:
|
|
n.wait_channel_active(scid)
|
|
|
|
# Make sure we have all node announcements, too
|
|
for n in nodes:
|
|
for n2 in nodes:
|
|
wait_for(
|
|
lambda: "alias" in only_one(n.rpc.listnodes(n2.info["id"])["nodes"])
|
|
)
|
|
|
|
|
|
def test_hardmpp(node_factory):
|
|
"""
|
|
Topology:
|
|
1----2----4
|
|
| |
|
|
3----5----6
|
|
This a payment that fails if pending HTLCs are not taken into account when
|
|
we build the network capacities.
|
|
"""
|
|
opts = [
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
]
|
|
l1, l2, l3, l4, l5, l6 = node_factory.get_nodes(6, opts=opts * 6)
|
|
start_channels(
|
|
[
|
|
(l1, l2, 10000000),
|
|
(l2, l4, 3000000),
|
|
(l4, l6, 10000000),
|
|
(l1, l3, 10000000),
|
|
(l3, l5, 1000000),
|
|
(l5, l6, 10000000),
|
|
]
|
|
)
|
|
|
|
with open("/tmp/l1-chans.txt", "w") as f:
|
|
print(json.dumps(l1.rpc.listchannels()), file=f)
|
|
|
|
inv = l4.rpc.invoice("any", "any", "description")
|
|
l2.rpc.call("pay", {"bolt11": inv["bolt11"], "amount_msat": 2000000000})
|
|
l2.wait_for_htlcs()
|
|
assert l4.rpc.listinvoices()["invoices"][0]["amount_received_msat"] == 2000000000
|
|
|
|
with open("/tmp/l2-peerchan.txt", "w") as f:
|
|
print(json.dumps(l2.rpc.listpeerchannels()), file=f)
|
|
with open("/tmp/l3-peerchan.txt", "w") as f:
|
|
print(json.dumps(l3.rpc.listpeerchannels()), file=f)
|
|
|
|
inv2 = l6.rpc.invoice("1800000sat", "inv2", "description")
|
|
|
|
out = subprocess.check_output(
|
|
[
|
|
"cli/lightning-cli",
|
|
"--network={}".format(TEST_NETWORK),
|
|
"--lightning-dir={}".format(l1.daemon.lightning_dir),
|
|
"-k",
|
|
"renepay",
|
|
"invstring={}".format(inv2["bolt11"]),
|
|
]
|
|
).decode("utf-8")
|
|
lines = out.split("\n")
|
|
# First comes commentry
|
|
assert any([l.startswith("#") for l in lines])
|
|
|
|
# Now comes JSON
|
|
json.loads("".join([l for l in lines if not l.startswith("#")]))
|
|
l1.wait_for_htlcs()
|
|
invoice = only_one(l6.rpc.listinvoices("inv2")["invoices"])
|
|
assert invoice["amount_received_msat"] >= Millisatoshi("1800000sat")
|
|
|
|
|
|
def test_self_pay(node_factory):
|
|
l1, l2 = node_factory.line_graph(2, wait_for_announce=True)
|
|
|
|
inv = l1.rpc.invoice(10000, "test", "test")["bolt11"]
|
|
l1.rpc.call("renepay", {"invstring": inv})
|
|
|
|
# We can pay twice, no problem!
|
|
l1.rpc.call("renepay", {"invstring": inv})
|
|
|
|
inv2 = l1.rpc.invoice(10000, "test2", "test2")["bolt11"]
|
|
l1.rpc.delinvoice("test2", "unpaid")
|
|
|
|
with pytest.raises(RpcError, match=r"Unknown invoice") as excinfo:
|
|
l1.rpc.call("renepay", {"invstring": inv2})
|
|
assert excinfo.value.error["code"] == 203
|
|
|
|
|
|
def test_fee_allocation(node_factory):
|
|
"""
|
|
Topology:
|
|
1----2
|
|
| |
|
|
3----4
|
|
This a payment that fails if fee is not allocated as part of the flow
|
|
constraints. The payment should be straightforward, no failures are
|
|
expected.
|
|
"""
|
|
# We set high fees at 3% and load a plugin that breaks if a sendpay_failure
|
|
# notification is received.
|
|
opts = [
|
|
{
|
|
"disable-mpp": None,
|
|
"fee-base": 1000,
|
|
"fee-per-satoshi": 30000,
|
|
"plugin": os.path.join(os.getcwd(), "tests/plugins/no_fail.py"),
|
|
},
|
|
]
|
|
l1, l2, l3, l4 = node_factory.get_nodes(4, opts=opts * 4)
|
|
start_channels(
|
|
[(l1, l2, 1000000), (l2, l4, 2000000), (l1, l3, 1000000), (l3, l4, 2000000)]
|
|
)
|
|
|
|
inv = l4.rpc.invoice("1500000sat", "inv", "description")
|
|
l1.rpc.call("renepay", {"invstring": inv["bolt11"], "maxfee": "75000sat"})
|
|
l1.wait_for_htlcs()
|
|
invoice = only_one(l4.rpc.listinvoices("inv")["invoices"])
|
|
assert invoice["amount_received_msat"] >= Millisatoshi("1500000sat")
|
|
|
|
# is the no_fail.py plugin still running?
|
|
l1.rpc.call("nofail")
|
|
|
|
|
|
def test_htlc_max(node_factory):
|
|
"""
|
|
Topology:
|
|
1----2----4
|
|
| |
|
|
3----5----6
|
|
"""
|
|
opts = [
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
{
|
|
"disable-mpp": None,
|
|
"fee-base": 0,
|
|
"fee-per-satoshi": 0,
|
|
"htlc-maximum-msat": 500000000,
|
|
},
|
|
{
|
|
"disable-mpp": None,
|
|
"fee-base": 0,
|
|
"fee-per-satoshi": 0,
|
|
"htlc-maximum-msat": 500000000,
|
|
},
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
]
|
|
l1, l2, l3, l4, l5, l6 = node_factory.get_nodes(6, opts=opts)
|
|
start_channels(
|
|
[
|
|
(l1, l2, 10000000),
|
|
(l2, l4, 1000000),
|
|
(l4, l6, 2000000),
|
|
(l1, l3, 10000000),
|
|
(l3, l5, 1000000),
|
|
(l5, l6, 2000000),
|
|
]
|
|
)
|
|
|
|
inv = l6.rpc.invoice("800000sat", "inv", "description")
|
|
|
|
l1.rpc.call("renepay", {"invstring": inv["bolt11"]})
|
|
l1.wait_for_htlcs()
|
|
invoice = only_one(l6.rpc.listinvoices("inv")["invoices"])
|
|
assert invoice["amount_received_msat"] >= Millisatoshi("800000sat")
|
|
|
|
|
|
def test_previous_sendpays(node_factory, bitcoind):
|
|
"""
|
|
Check that renepay can complete a payment that already started
|
|
"""
|
|
opts = [
|
|
{"disable-mpp": None, "fee-base": 1000, "fee-per-satoshi": 1000},
|
|
]
|
|
l1, l2, l3, l4 = node_factory.line_graph(4, wait_for_announce=True, opts=opts * 4)
|
|
|
|
# First case, do not overpay a pending MPP payment
|
|
invstr = l3.rpc.invoice("100000sat", "inv1", "description")["bolt11"]
|
|
inv = l1.rpc.decode(invstr)
|
|
route = l1.rpc.call(
|
|
"getroute", {"id": inv["payee"], "amount_msat": "50000sat", "riskfactor": 10}
|
|
)
|
|
# we start a MPP payment
|
|
l1.rpc.call(
|
|
"sendpay",
|
|
{
|
|
"route": route["route"],
|
|
"payment_hash": inv["payment_hash"],
|
|
"payment_secret": inv["payment_secret"],
|
|
"amount_msat": "100000sat",
|
|
"groupid": 1,
|
|
"partid": 1,
|
|
},
|
|
)
|
|
# while it is pending, we try to complete it with renepay
|
|
l1.rpc.call("renepay", {"invstring": invstr})
|
|
invoice = only_one(l3.rpc.listinvoices("inv1")["invoices"])
|
|
# the receive amount should be exact
|
|
assert invoice["amount_received_msat"] == Millisatoshi("100000sat")
|
|
|
|
# Second case, do not collide with failed sendpays
|
|
invstr = l3.rpc.invoice("100000sat", "inv2", "description")["bolt11"]
|
|
inv = l1.rpc.decode(invstr)
|
|
route = l1.rpc.call(
|
|
"getroute", {"id": inv["payee"], "amount_msat": "50000sat", "riskfactor": 10}
|
|
)
|
|
|
|
# load a plugin that fails all HTLCs
|
|
l2.rpc.call(
|
|
"plugin",
|
|
{
|
|
"subcommand": "start",
|
|
"plugin": os.path.join(os.getcwd(), "tests/plugins/fail_htlcs.py"),
|
|
},
|
|
)
|
|
l2.daemon.wait_for_log(r"^(?=.*plugin-manager.*fail_htlcs.py).*")
|
|
|
|
# start a MPP payment that will fail at l2
|
|
l1.rpc.call(
|
|
"sendpay",
|
|
{
|
|
"route": route["route"],
|
|
"payment_hash": inv["payment_hash"],
|
|
"payment_secret": inv["payment_secret"],
|
|
"amount_msat": "100000sat",
|
|
"groupid": 1,
|
|
"partid": 1,
|
|
},
|
|
)
|
|
l2.daemon.wait_for_log(r"Failing htlc on purpose")
|
|
l1.wait_for_htlcs()
|
|
|
|
# another payment that fails
|
|
l1.rpc.call(
|
|
"sendpay",
|
|
{
|
|
"route": route["route"],
|
|
"payment_hash": inv["payment_hash"],
|
|
"payment_secret": inv["payment_secret"],
|
|
"amount_msat": "100000sat",
|
|
"groupid": 2,
|
|
"partid": 1,
|
|
},
|
|
)
|
|
l2.daemon.wait_for_log(r"Failing htlc on purpose")
|
|
l1.wait_for_htlcs()
|
|
|
|
# unload the fail_htlcs plugin
|
|
l2.rpc.call("plugin", {"subcommand": "stop", "plugin": "fail_htlcs.py"})
|
|
l2.daemon.wait_for_log(
|
|
r"plugin-fail_htlcs.py: Killing plugin: stopped by lightningd via RPC"
|
|
)
|
|
|
|
# now renepay should be able to construct new sendpays that do not collide
|
|
# with the previously failed sendpays
|
|
l1.rpc.call("renepay", {"invstring": invstr, "dev_use_shadow": False})
|
|
invoice = only_one(l3.rpc.listinvoices("inv2")["invoices"])
|
|
assert invoice["amount_received_msat"] == Millisatoshi("100000sat")
|
|
|
|
|
|
def test_fees(node_factory):
|
|
"""
|
|
Check that fees are correctly computed.
|
|
"""
|
|
# made up some random fees for every node
|
|
opts = [
|
|
{"disable-mpp": None, "fee-base": 1000, "fee-per-satoshi": 100},
|
|
{"disable-mpp": None, "fee-base": 2222, "fee-per-satoshi": 203},
|
|
{"disable-mpp": None, "fee-base": 3333, "fee-per-satoshi": 300},
|
|
{"disable-mpp": None, "fee-base": 2012, "fee-per-satoshi": 200},
|
|
{"disable-mpp": None, "fee-base": 1010, "fee-per-satoshi": 100},
|
|
{"disable-mpp": None, "fee-base": 1050, "fee-per-satoshi": 100},
|
|
]
|
|
nodes = node_factory.line_graph(len(opts), wait_for_announce=True, opts=opts)
|
|
source = nodes[0]
|
|
dest = nodes[-1]
|
|
|
|
# check that once gossip is in sync, fees are paid correctly
|
|
invstr = dest.rpc.invoice("100000sat", "inv1", "description")["bolt11"]
|
|
source.rpc.call("renepay", {"invstring": invstr})
|
|
invoice = only_one(dest.rpc.listinvoices("inv1")["invoices"])
|
|
assert invoice["amount_received_msat"] == Millisatoshi("100000sat")
|
|
|
|
# if we update fee policy but gossip is not updated ...
|
|
nodes[2].rpc.dev_suppress_gossip()
|
|
nodes[2].rpc.setchannel(nodes[3].info["id"], 4000, 300, enforcedelay=0)
|
|
|
|
nodes[3].rpc.dev_suppress_gossip()
|
|
nodes[3].rpc.setchannel(nodes[4].info["id"], 3000, 350, enforcedelay=0)
|
|
|
|
invstr = dest.rpc.invoice("150000sat", "inv2", "description")["bolt11"]
|
|
source.rpc.call("renepay", {"invstring": invstr})
|
|
invoice = only_one(dest.rpc.listinvoices("inv2")["invoices"])
|
|
assert invoice["amount_received_msat"] == Millisatoshi("150000sat")
|
|
|
|
|
|
def test_local_htlcmax0(node_factory):
|
|
"""Testing a simple pay route when local channels have htlcmax=0."""
|
|
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
|
|
l1.rpc.setchannel(l2.info["id"], htlcmax=0)
|
|
inv = l3.rpc.invoice(123000, "test_renepay", "description")["bolt11"]
|
|
details = l1.rpc.call("renepay", {"invstring": inv})
|
|
assert details["status"] == "complete"
|
|
assert details["amount_msat"] == Millisatoshi(123000)
|
|
assert details["destination"] == l3.info["id"]
|
|
|
|
|
|
def test_htlcmax0(node_factory):
|
|
"""
|
|
Topology:
|
|
1----2----4
|
|
| |
|
|
3----5----6
|
|
Tests the plugin when some routes have htlc_max=0.
|
|
"""
|
|
opts = [
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
{
|
|
"disable-mpp": None,
|
|
"fee-base": 0,
|
|
"fee-per-satoshi": 0,
|
|
"htlc-maximum-msat": 0,
|
|
},
|
|
{
|
|
"disable-mpp": None,
|
|
"fee-base": 0,
|
|
"fee-per-satoshi": 0,
|
|
"htlc-maximum-msat": 800000000,
|
|
},
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
]
|
|
l1, l2, l3, l4, l5, l6 = node_factory.get_nodes(6, opts=opts)
|
|
start_channels(
|
|
[
|
|
(l1, l2, 10000000),
|
|
(l2, l4, 1000000),
|
|
(l4, l6, 1000000),
|
|
(l1, l3, 10000000),
|
|
(l3, l5, 1000000),
|
|
(l5, l6, 1000000),
|
|
]
|
|
)
|
|
|
|
inv = l6.rpc.invoice("600000sat", "inv", "description")
|
|
|
|
l1.rpc.call("renepay", {"invstring": inv["bolt11"]})
|
|
l1.wait_for_htlcs()
|
|
invoice = only_one(l6.rpc.listinvoices("inv")["invoices"])
|
|
assert invoice["amount_received_msat"] >= Millisatoshi("600000sat")
|
|
|
|
|
|
def test_concurrency(node_factory):
|
|
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True, opts=[{}, {}, {}])
|
|
inv = l3.rpc.invoice("1000sat", "test_renepay", "description")["bolt11"]
|
|
p1 = subprocess.Popen(
|
|
[
|
|
"cli/lightning-cli",
|
|
"--network={}".format(TEST_NETWORK),
|
|
"--lightning-dir={}".format(l1.daemon.lightning_dir),
|
|
"-k",
|
|
"renepay",
|
|
"invstring={}".format(inv),
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
# make several other spurious requests
|
|
for i in range(4):
|
|
subprocess.Popen(
|
|
[
|
|
"cli/lightning-cli",
|
|
"--network={}".format(TEST_NETWORK),
|
|
"--lightning-dir={}".format(l1.daemon.lightning_dir),
|
|
"-k",
|
|
"renepay",
|
|
"invstring={}".format(inv),
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
p1.wait(timeout=60)
|
|
# remove comments from the output before parsing the json
|
|
out1 = json.loads(re.sub("#.*?\n", "", p1.stdout.read().decode()))
|
|
assert out1["status"] == "complete"
|
|
assert out1["amount_msat"] == Millisatoshi("1000sat")
|
|
invoice = only_one(l3.rpc.listinvoices("test_renepay")["invoices"])
|
|
assert invoice["amount_received_msat"] >= Millisatoshi("1000sat")
|
|
|
|
|
|
def test_privatechan(node_factory, bitcoind):
|
|
"""
|
|
Topology:
|
|
1----2----3----4
|
|
Tests if a payment can get through a private channel.
|
|
"""
|
|
opts = [
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 100},
|
|
{"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},
|
|
]
|
|
l1, l2, l3, l4 = node_factory.get_nodes(4, opts=opts)
|
|
|
|
l1.rpc.connect(l2.info["id"], "localhost", l2.port)
|
|
l2.rpc.connect(l3.info["id"], "localhost", l3.port)
|
|
l3.rpc.connect(l4.info["id"], "localhost", l4.port)
|
|
|
|
c12, _ = l1.fundchannel(l2, 10**6)
|
|
c23, _ = l2.fundchannel(l3, 10**6)
|
|
c34, _ = l3.fundchannel(l4, 10**6, announce_channel=False)
|
|
|
|
mine_funding_to_announce(bitcoind, [l1, l2, l3, l4])
|
|
l1.wait_channel_active(c12)
|
|
l2.wait_channel_active(c23)
|
|
l3.wait_local_channel_active(c34)
|
|
|
|
wait_for(lambda: len(l1.rpc.listchannels()["channels"]) == 4)
|
|
wait_for(lambda: len(l2.rpc.listchannels()["channels"]) == 4)
|
|
wait_for(lambda: len(l3.rpc.listchannels()["channels"]) == 4)
|
|
wait_for(lambda: len(l4.rpc.listchannels()["channels"]) == 4)
|
|
|
|
inv = l4.rpc.invoice("1000sat", "inv", "description")
|
|
|
|
l1.rpc.call("renepay", {"invstring": inv["bolt11"]})
|
|
l1.wait_for_htlcs()
|
|
invoice = only_one(l4.rpc.listinvoices("inv")["invoices"])
|
|
assert invoice["amount_received_msat"] >= Millisatoshi("1000sat")
|
|
|
|
|
|
@unittest.skipIf(TEST_NETWORK == 'liquid-regtest', "broken for some reason")
|
|
def test_hardmpp2(node_factory, bitcoind):
|
|
"""Credits to @daywalker90 for this test case."""
|
|
opts = {"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 10}
|
|
l1, l2, l3 = node_factory.get_nodes(3, opts=opts)
|
|
start_channels(
|
|
[
|
|
(l1, l2, 100_000),
|
|
(l1, l2, 200_000),
|
|
(l1, l2, 300_000),
|
|
(l1, l2, 400_000),
|
|
(l2, l3, 100_000),
|
|
(l2, l3, 200_000),
|
|
(l2, l3, 300_000),
|
|
(l2, l3, 600_000),
|
|
]
|
|
)
|
|
# FIXME: changing the last channel from 600k to 400k will fail the test due
|
|
# to l2 not accepting to forward any amount above 200k with error:
|
|
# CHANNEL_ERR_CHANNEL_CAPACITY_EXCEEDED, still investigating
|
|
inv = l3.rpc.invoice("800000sat", "inv", "description")
|
|
l1.rpc.call("renepay", {"invstring": inv["bolt11"]})
|
|
l1.wait_for_htlcs()
|
|
receipt = only_one(l3.rpc.listinvoices("inv")["invoices"])
|
|
assert receipt["amount_received_msat"] == Millisatoshi("800000sat")
|