rgb-cln/tests/test_invoices.py

435 lines
16 KiB
Python

from fixtures import * # noqa: F401,F403
from lightning import RpcError
from utils import only_one, DEVELOPER, wait_for, wait_channel_quiescent
import pytest
import time
import unittest
def test_invoice(node_factory):
l1, l2 = node_factory.line_graph(2, fundchannel=False)
addr1 = l2.rpc.newaddr('bech32')['address']
addr2 = l2.rpc.newaddr('p2sh-segwit')['address']
before = int(time.time())
inv = l1.rpc.invoice(123000, 'label', 'description', '3700', [addr1, addr2])
after = int(time.time())
b11 = l1.rpc.decodepay(inv['bolt11'])
assert b11['currency'] == 'bcrt'
assert b11['created_at'] >= before
assert b11['created_at'] <= after
assert b11['payment_hash'] == inv['payment_hash']
assert b11['description'] == 'description'
assert b11['expiry'] == 3700
assert b11['payee'] == l1.info['id']
assert len(b11['fallbacks']) == 2
assert b11['fallbacks'][0]['addr'] == addr1
assert b11['fallbacks'][0]['type'] == 'P2WPKH'
assert b11['fallbacks'][1]['addr'] == addr2
assert b11['fallbacks'][1]['type'] == 'P2SH'
# There's no incoming channel, so no routeboost
assert 'routes' not in b11
assert 'warning_capacity' in inv
# Check pay_index is null
outputs = l1.db_query('SELECT pay_index IS NULL AS q FROM invoices WHERE label="label";')
assert len(outputs) == 1 and outputs[0]['q'] != 0
# Check any-amount invoice
inv = l1.rpc.invoice("any", 'label2', 'description2')
b11 = inv['bolt11']
# Amount usually comes after currency (bcrt in our case),
# but an any-amount invoices will have no amount
assert b11.startswith("lnbcrt1")
# By bech32 rules, the last '1' digit is the separator
# between the human-readable and data parts. We want
# to match the "lnbcrt1" above with the '1' digit as the
# separator, and not for example "lnbcrt1m1....".
assert b11.count('1') == 1
# There's no incoming channel, so no routeboost
assert 'routes' not in b11
assert 'warning_capacity' in inv
def test_invoice_weirdstring(node_factory):
l1 = node_factory.get_node()
weird_label = 'label \\ " \t \n'
weird_desc = 'description \\ " \t \n'
l1.rpc.invoice(123000, weird_label, weird_desc)
# FIXME: invoice RPC should return label!
# Can find by this label.
inv = only_one(l1.rpc.listinvoices(weird_label)['invoices'])
assert inv['label'] == weird_label
# Can find this in list.
inv = only_one(l1.rpc.listinvoices()['invoices'])
assert inv['label'] == weird_label
b11 = l1.rpc.decodepay(inv['bolt11'])
assert b11['description'] == weird_desc
# Can delete by weird label.
l1.rpc.delinvoice(weird_label, "unpaid")
# We can also use numbers as labels.
weird_label = 25
weird_desc = '"'
l1.rpc.invoice(123000, weird_label, weird_desc)
# FIXME: invoice RPC should return label!
# Can find by this label.
inv = only_one(l1.rpc.listinvoices(weird_label)['invoices'])
assert inv['label'] == str(weird_label)
# Can find this in list.
inv = only_one(l1.rpc.listinvoices()['invoices'])
assert inv['label'] == str(weird_label)
b11 = l1.rpc.decodepay(inv['bolt11'])
assert b11['description'] == weird_desc
# Can delete by weird label.
l1.rpc.delinvoice(weird_label, "unpaid")
def test_invoice_preimage(node_factory):
"""Test explicit invoice 'preimage'.
"""
l1, l2 = node_factory.line_graph(2, wait_for_announce=True)
# I promise the below number is randomly generated
invoice_preimage = "17b08f669513b7379728fc1abcea5eaf3448bc1eba55a68ca2cd1843409cdc04"
# Make invoice and pay it
inv = l2.rpc.invoice(msatoshi=123456, label="inv", description="?", preimage=invoice_preimage)
payment = l1.rpc.pay(inv['bolt11'])
# Check preimage was given.
payment_preimage = payment['payment_preimage']
assert invoice_preimage == payment_preimage
# Creating a new invoice with same preimage should error.
with pytest.raises(RpcError, match=r'preimage already used'):
l2.rpc.invoice(123456, 'inv2', '?', preimage=invoice_preimage)
def test_invoice_routeboost(node_factory, bitcoind):
"""Test routeboost 'r' hint in bolt11 invoice.
"""
l0, l1, l2 = node_factory.line_graph(3, fundamount=2 * (10**4), wait_for_announce=True)
# Check routeboost.
# Make invoice and pay it
inv = l2.rpc.invoice(msatoshi=123456, label="inv1", description="?")
# Check routeboost.
assert 'warning_capacity' not in inv
assert 'warning_offline' not in inv
# Route array has single route with single element.
r = only_one(only_one(l1.rpc.decodepay(inv['bolt11'])['routes']))
assert r['pubkey'] == l1.info['id']
assert r['short_channel_id'] == l2.rpc.listpeers(l1.info['id'])['peers'][0]['channels'][0]['short_channel_id']
assert r['fee_base_msat'] == 1
assert r['fee_proportional_millionths'] == 10
assert r['cltv_expiry_delta'] == 6
# Pay it (and make sure it's fully resolved before we take l1 offline!)
l1.rpc.pay(inv['bolt11'])
wait_channel_quiescent(l1, l2)
# Due to reserve & fees, l1 doesn't have capacity to pay this.
inv = l2.rpc.invoice(msatoshi=2 * (10**7) - 123456, label="inv2", description="?")
# Check warning
assert 'warning_capacity' in inv
assert 'warning_offline' not in inv
l1.rpc.disconnect(l2.info['id'], True)
wait_for(lambda: not only_one(l2.rpc.listpeers(l1.info['id'])['peers'])['connected'])
inv = l2.rpc.invoice(123456, label="inv3", description="?")
# Check warning.
assert 'warning_capacity' not in inv
assert 'warning_offline' in inv
# Close l0, l2 will not use l1 at all.
l0.rpc.close(l1.info['id'])
l0.wait_for_channel_onchain(l1.info['id'])
bitcoind.generate_block(100)
# l2 has to notice channel is gone.
wait_for(lambda: len(l2.rpc.listchannels()['channels']) == 2)
inv = l2.rpc.invoice(123456, label="inv4", description="?")
# Check warning.
assert 'warning_capacity' in inv
assert 'warning_offline' not in inv
def test_invoice_routeboost_private(node_factory, bitcoind):
"""Test routeboost 'r' hint in bolt11 invoice for private channels
"""
l1, l2 = node_factory.line_graph(2, fundamount=10**6, announce_channels=False)
# Attach public channel to l1 so it doesn't look like a dead-end.
l0 = node_factory.get_node()
l0.rpc.connect(l1.info['id'], 'localhost', l1.port)
scid = l0.fund_channel(l1, 2 * (10**4))
bitcoind.generate_block(5)
# Make sure channel is totally public.
wait_for(lambda: [c['public'] for c in l2.rpc.listchannels(scid)['channels']] == [True, True])
# Since there's only one route, it will reluctantly hint that even
# though it's private
inv = l2.rpc.invoice(msatoshi=123456, label="inv0", description="?")
assert 'warning_capacity' not in inv
assert 'warning_offline' not in inv
# Route array has single route with single element.
r = only_one(only_one(l1.rpc.decodepay(inv['bolt11'])['routes']))
assert r['pubkey'] == l1.info['id']
assert r['short_channel_id'] == l1.rpc.listchannels()['channels'][0]['short_channel_id']
assert r['fee_base_msat'] == 1
assert r['fee_proportional_millionths'] == 10
assert r['cltv_expiry_delta'] == 6
# If we explicitly say not to, it won't expose.
inv = l2.rpc.invoice(msatoshi=123456, label="inv1", description="?", exposeprivatechannels=False)
assert 'warning_capacity' in inv
assert 'routes' not in l1.rpc.decodepay(inv['bolt11'])
# The existence of a public channel, even without capacity, will suppress
# the exposure of private channels.
l3 = node_factory.get_node()
l3.rpc.connect(l2.info['id'], 'localhost', l2.port)
scid = l3.fund_channel(l2, (10**4))
bitcoind.generate_block(5)
# Make sure channel is totally public.
wait_for(lambda: [c['public'] for c in l3.rpc.listchannels(scid)['channels']] == [True, True])
inv = l2.rpc.invoice(msatoshi=10**7, label="inv2", description="?")
assert 'warning_capacity' in inv
# Unless we tell it to include it.
inv = l2.rpc.invoice(msatoshi=10**7, label="inv3", description="?", exposeprivatechannels=True)
assert 'warning_capacity' not in inv
assert 'warning_offline' not in inv
# Route array has single route with single element.
r = only_one(only_one(l1.rpc.decodepay(inv['bolt11'])['routes']))
assert r['pubkey'] == l1.info['id']
assert r['short_channel_id'] == l1.rpc.listchannels()['channels'][0]['short_channel_id']
assert r['fee_base_msat'] == 1
assert r['fee_proportional_millionths'] == 10
assert r['cltv_expiry_delta'] == 6
def test_invoice_expiry(node_factory, executor):
l1, l2 = node_factory.line_graph(2, fundchannel=True)
inv = l2.rpc.invoice(msatoshi=123000, label='test_pay', description='description', expiry=1)['bolt11']
time.sleep(2)
with pytest.raises(RpcError):
l1.rpc.pay(inv)
invoices = l2.rpc.listinvoices('test_pay')['invoices']
assert len(invoices) == 1
assert invoices[0]['status'] == 'expired' and invoices[0]['expires_at'] < time.time()
# Try deleting it.
with pytest.raises(RpcError, match=r'Invoice status is expired not unpaid'):
l2.rpc.delinvoice('test_pay', 'unpaid')
with pytest.raises(RpcError, match=r'Invoice status is expired not paid'):
l2.rpc.delinvoice('test_pay', 'paid')
l2.rpc.delinvoice('test_pay', 'expired')
with pytest.raises(RpcError, match=r'Unknown invoice'):
l2.rpc.delinvoice('test_pay', 'expired')
# Test expiration waiting.
# The second invoice created expires first.
l2.rpc.invoice('any', 'inv1', 'description', 10)
l2.rpc.invoice('any', 'inv2', 'description', 4)
l2.rpc.invoice('any', 'inv3', 'description', 16)
creation = int(time.time())
# Check waitinvoice correctly waits
w1 = executor.submit(l2.rpc.waitinvoice, 'inv1')
w2 = executor.submit(l2.rpc.waitinvoice, 'inv2')
w3 = executor.submit(l2.rpc.waitinvoice, 'inv3')
time.sleep(2) # total 2
assert not w1.done()
assert not w2.done()
assert not w3.done()
time.sleep(4) # total 6
assert not w1.done()
with pytest.raises(RpcError):
w2.result()
assert not w3.done()
time.sleep(6) # total 12
with pytest.raises(RpcError):
w1.result()
assert not w3.done()
time.sleep(8) # total 20
with pytest.raises(RpcError):
w3.result()
# Test delexpiredinvoice
l2.rpc.delexpiredinvoice(maxexpirytime=creation + 8)
# only inv2 should have been deleted
assert len(l2.rpc.listinvoices()['invoices']) == 2
assert len(l2.rpc.listinvoices('inv2')['invoices']) == 0
# Test delexpiredinvoice all
l2.rpc.delexpiredinvoice()
# all invoices are expired and should be deleted
assert len(l2.rpc.listinvoices()['invoices']) == 0
@unittest.skipIf(not DEVELOPER, "Too slow without --dev-bitcoind-poll")
def test_waitinvoice(node_factory, executor):
"""Test waiting for one invoice will not return if another invoice is paid.
"""
# Setup
l1, l2 = node_factory.line_graph(2)
# Create invoices
inv1 = l2.rpc.invoice(1000, 'inv1', 'inv1')
inv2 = l2.rpc.invoice(1000, 'inv2', 'inv2')
l2.rpc.invoice(1000, 'inv3', 'inv3')
# Start waiting on invoice 3
f3 = executor.submit(l2.rpc.waitinvoice, 'inv3')
# Start waiting on invoice 1, should block
f = executor.submit(l2.rpc.waitinvoice, 'inv1')
time.sleep(1)
assert not f.done()
# Pay invoice 2
l1.rpc.pay(inv2['bolt11'])
# Waiter should stil be blocked
time.sleep(1)
assert not f.done()
# Waiting on invoice 2 should return immediately
r = executor.submit(l2.rpc.waitinvoice, 'inv2').result(timeout=5)
assert r['label'] == 'inv2'
# Pay invoice 1
l1.rpc.pay(inv1['bolt11'])
# Waiter for invoice 1 should now finish
r = f.result(timeout=5)
assert r['label'] == 'inv1'
# Waiter for invoice 3 should still be waiting
time.sleep(1)
assert not f3.done()
@unittest.skipIf(not DEVELOPER, "Too slow without --dev-bitcoind-poll")
def test_waitanyinvoice(node_factory, executor):
"""Test various variants of waiting for the next invoice to complete.
"""
l1, l2 = node_factory.line_graph(2)
inv1 = l2.rpc.invoice(1000, 'inv1', 'inv1')
inv2 = l2.rpc.invoice(1000, 'inv2', 'inv2')
inv3 = l2.rpc.invoice(1000, 'inv3', 'inv3')
# Attempt to wait for the first invoice
f = executor.submit(l2.rpc.waitanyinvoice)
time.sleep(1)
# The call to waitanyinvoice should not have returned just yet
assert not f.done()
# Now pay the first two invoices and make sure we notice
l1.rpc.pay(inv1['bolt11'])
l1.rpc.pay(inv2['bolt11'])
r = f.result(timeout=5)
assert r['label'] == 'inv1'
pay_index = r['pay_index']
# This one should return immediately with inv2
r = executor.submit(l2.rpc.waitanyinvoice, pay_index).result(timeout=5)
assert r['label'] == 'inv2'
pay_index = r['pay_index']
# Now spawn the next waiter
f = executor.submit(l2.rpc.waitanyinvoice, pay_index)
time.sleep(1)
assert not f.done()
l1.rpc.pay(inv3['bolt11'])
r = f.result(timeout=5)
assert r['label'] == 'inv3'
with pytest.raises(RpcError):
l2.rpc.waitanyinvoice('non-number')
def test_waitanyinvoice_reversed(node_factory, executor):
"""Test waiting for invoices, where they are paid in reverse order
to when they are created.
"""
# Setup
l1, l2 = node_factory.line_graph(2)
# Create invoices
inv1 = l2.rpc.invoice(1000, 'inv1', 'inv1')
inv2 = l2.rpc.invoice(1000, 'inv2', 'inv2')
# Pay inv2, wait, pay inv1, wait
# Pay inv2
l1.rpc.pay(inv2['bolt11'])
# Wait - should not block, should return inv2
r = executor.submit(l2.rpc.waitanyinvoice).result(timeout=5)
assert r['label'] == 'inv2'
pay_index = r['pay_index']
# Pay inv1
l1.rpc.pay(inv1['bolt11'])
# Wait inv2 - should not block, should return inv1
r = executor.submit(l2.rpc.waitanyinvoice, pay_index).result(timeout=5)
assert r['label'] == 'inv1'
def test_autocleaninvoice(node_factory):
l1 = node_factory.get_node()
l1.rpc.invoice(msatoshi=12300, label='inv1', description='description1', expiry=4)
l1.rpc.invoice(msatoshi=12300, label='inv2', description='description2', expiry=12)
l1.rpc.autocleaninvoice(cycle_seconds=8, expired_by=2)
start_time = time.time()
# time 0
# Both should still be there.
assert len(l1.rpc.listinvoices('inv1')['invoices']) == 1
assert len(l1.rpc.listinvoices('inv2')['invoices']) == 1
assert l1.rpc.listinvoices('inv1')['invoices'][0]['description'] == 'description1'
time.sleep(start_time - time.time() + 6) # total 6
# Both should still be there - auto clean cycle not started.
# inv1 should be expired
assert len(l1.rpc.listinvoices('inv1')['invoices']) == 1
assert only_one(l1.rpc.listinvoices('inv1')['invoices'])['status'] == 'expired'
assert len(l1.rpc.listinvoices('inv2')['invoices']) == 1
assert only_one(l1.rpc.listinvoices('inv2')['invoices'])['status'] != 'expired'
time.sleep(start_time - time.time() + 10) # total 10
# inv1 should have deleted, inv2 still there and unexpired.
assert len(l1.rpc.listinvoices('inv1')['invoices']) == 0
assert len(l1.rpc.listinvoices('inv2')['invoices']) == 1
assert only_one(l1.rpc.listinvoices('inv2')['invoices'])['status'] != 'expired'
time.sleep(start_time - time.time() + 14) # total 14
# inv2 should still be there, but expired
assert len(l1.rpc.listinvoices('inv1')['invoices']) == 0
assert len(l1.rpc.listinvoices('inv2')['invoices']) == 1
assert only_one(l1.rpc.listinvoices('inv2')['invoices'])['status'] == 'expired'
time.sleep(start_time - time.time() + 18) # total 18
# Everything deleted
assert len(l1.rpc.listinvoices('inv1')['invoices']) == 0
assert len(l1.rpc.listinvoices('inv2')['invoices']) == 0