diff --git a/tests/test_pay.py b/tests/test_pay.py index 4130d12e5..dae4051ad 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -6,6 +6,7 @@ from utils import DEVELOPER, wait_for, only_one, sync_blockheight, SLOW_MACHINE import copy import pytest import random +import re import string import time import unittest @@ -1425,3 +1426,333 @@ def test_pay_direct(node_factory, bitcoind): # have changed. l1l2msat = only_one(l1.rpc.getpeer(l2.info['id'])['channels'])['msatoshi_to_us'] assert l1l2msat == l1l2msatreference + + +def test_setchannelfee_usage(node_factory, bitcoind): + # TEST SETUP + # + # [l1] ---> [l2] (channel funded) + # | + # o - - > [l3] (only connected) + # + # - check initial SQL values + # - check setchannelfee can be used + # - checks command's return object format + # - check custom SQL fee values + # - check values in local nodes listchannels output + # - json throws exception on negative values + # - checks if peer id can be used instead of scid + DEF_BASE = 10 + DEF_PPM = 100 + + l1 = node_factory.get_node(options={'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM}) + l2 = node_factory.get_node(options={'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM}) + l3 = node_factory.get_node(options={'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM}) + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l1.rpc.connect(l3.info['id'], 'localhost', l3.port) + l1.fund_channel(l2, 1000000) + + # get short channel id + scid = l1.get_channel_scid(l2) + scid_hex = scid.encode('utf-8').hex() + + # feerates should be init with global config + db_fees = l1.db_query('SELECT feerate_base, feerate_ppm FROM channels;') + assert(db_fees[0]['feerate_base'] == DEF_BASE) + assert(db_fees[0]['feerate_ppm'] == DEF_PPM) + + # custom setchannelfee scid + result = l1.rpc.setchannelfee(scid, 1337, 137) + + # check result format + assert(re.match('^[0-9a-f]{64}$', result['channel_id'])) + assert(result['peer_id'] == l2.info['id']) + assert(result['short_channel_id'] == scid) + assert(result['base'] == 1337) + assert(result['ppm'] == 137) + + # check if custom values made it into the database + db_fees = l1.db_query( + 'SELECT feerate_base, feerate_ppm FROM channels ' + 'WHERE hex(short_channel_id)="' + scid_hex + '";') + assert(db_fees[0]['feerate_base'] == 1337) + assert(db_fees[0]['feerate_ppm'] == 137) + + # wait for gossip and check if l1 sees new fees in listchannels + wait_for(lambda: [c['base_fee_millisatoshi'] for c in l1.rpc.listchannels(scid)['channels']] == [DEF_BASE, 1337]) + wait_for(lambda: [c['fee_per_millionth'] for c in l1.rpc.listchannels(scid)['channels']] == [DEF_PPM, 137]) + + # also test with named and missing paramters + result = l1.rpc.setchannelfee(ppm=42, id=scid) + assert(re.match('^[0-9a-f]{64}$', result['channel_id'])) + assert(result['short_channel_id'] == scid) + assert(result['base'] == DEF_BASE) + assert(result['ppm'] == 42) + result = l1.rpc.setchannelfee(base=42, id=scid) + assert(re.match('^[0-9a-f]{64}$', result['channel_id'])) + assert(result['short_channel_id'] == scid) + assert(result['base'] == 42) + assert(result['ppm'] == DEF_PPM) + + # check if negative fees raise error and DB keeps values + # JSONRPC2_INVALID_PARAMS := -32602 + with pytest.raises(RpcError, match=r'-32602'): + l1.rpc.setchannelfee(scid, -1, -1) + + # test if zero fees is possible + result = l1.rpc.setchannelfee(scid, 0, 0) + assert(result['base'] == 0) + assert(result['ppm'] == 0) + db_fees = l1.db_query( + 'SELECT feerate_base, feerate_ppm FROM channels ' + 'WHERE hex(short_channel_id)="' + scid_hex + '";') + assert(db_fees[0]['feerate_base'] == 0) + assert(db_fees[0]['feerate_ppm'] == 0) + + # disable and check for global values to be returned + result = l1.rpc.setchannelfee(scid) + assert(result['base'] == DEF_BASE) + assert(result['ppm'] == DEF_PPM) + # check default values in DB + db_fees = l1.db_query( + 'SELECT feerate_base, feerate_ppm FROM channels ' + 'WHERE hex(short_channel_id)="' + scid_hex + '";') + assert(db_fees[0]['feerate_base'] == DEF_BASE) + assert(db_fees[0]['feerate_ppm'] == DEF_PPM) + + # check also peer id can be used + result = l1.rpc.setchannelfee(l2.info['id'], 42, 43) + assert(result['peer_id'] == l2.info['id']) + assert(result['short_channel_id'] == scid) + assert(result['base'] == 42) + assert(result['ppm'] == 43) + db_fees = l1.db_query( + 'SELECT feerate_base, feerate_ppm FROM channels ' + 'WHERE hex(short_channel_id)="' + scid_hex + '";') + assert(db_fees[0]['feerate_base'] == 42) + assert(db_fees[0]['feerate_ppm'] == 43) + + # check if invalid scid raises proper error + with pytest.raises(RpcError, match=r'-1.*Could not find active channel of peer with that id'): + result = l1.rpc.setchannelfee(l3.info['id'], 42, 43) + with pytest.raises(RpcError, match=r'-32602.*Given id is not a channel ID or short channel ID'): + result = l1.rpc.setchannelfee('f42' + scid[3:], 42, 43) + + # check if 'base' unit can be modified to satoshi + result = l1.rpc.setchannelfee(scid, '1sat') + assert(result['base'] == 1000) + db_fees = l1.db_query( + 'SELECT feerate_base, feerate_ppm FROM channels ' + 'WHERE hex(short_channel_id)="' + scid_hex + '";') + assert(db_fees[0]['feerate_base'] == 1000) + + # check if 'ppm' values greater than u32_max fail + with pytest.raises(RpcError, match=r'-32602.*should be an integer, not'): + l1.rpc.setchannelfee(scid, 0, 2**32) + + # check if 'ppm' values greater than u32_max fail + with pytest.raises(RpcError, match=r'-32602.*exceeds u32 max'): + l1.rpc.setchannelfee(scid, 2**32) + + +@unittest.skipIf(not DEVELOPER, "gossip without DEVELOPER=1 is slow") +def test_setchannelfee_state(node_factory, bitcoind): + # TEST SETUP + # + # [l0] --> [l1] --> [l2] + # + # Initiate channel [l1,l2] and try to set feerates other states than + # CHANNELD_NORMAL or CHANNELD_AWAITING_LOCKIN. Should raise error. + # Use l0 to make a forward through l1/l2 for testing. + DEF_BASE = 0 + DEF_PPM = 0 + + l0 = node_factory.get_node(options={'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM}) + l1 = node_factory.get_node(options={'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM}) + l2 = node_factory.get_node(options={'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM}) + + # connection and funding + l0.rpc.connect(l1.info['id'], 'localhost', l1.port) + l0.fund_channel(l1, 1000000, wait_for_active=True) + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + scid = l1.fund_channel(l2, 1000000, wait_for_active=False) + + # try setting the fee in state AWAITING_LOCKIN should be possible + # assert(l1.channel_state(l2) == "CHANNELD_AWAITING_LOCKIN") + result = l1.rpc.setchannelfee(l2.info['id'], 42, 0) + assert(result['peer_id'] == l2.info['id']) + # cid = result['channel_id'] + + # test routing correct new fees once routing is established + bitcoind.generate_block(6) + l0.wait_for_route(l2) + inv = l2.rpc.invoice(100000, 'test_setchannelfee_state', 'desc')['bolt11'] + result = l0.rpc.pay(inv) + assert result['status'] == 'complete' + assert result['msatoshi_sent'] == 100042 + + # Disconnect and unilaterally close from l2 to l1 + l2.rpc.disconnect(l1.info['id'], force=True) + l1.rpc.disconnect(l2.info['id'], force=True) + result = l2.rpc.close(scid, True, 0) + assert result['type'] == 'unilateral' + + # wait for l1 to see unilateral close via bitcoin network + while l1.channel_state(l2) == "CHANNELD_NORMAL": + bitcoind.generate_block(1) + # assert l1.channel_state(l2) == "FUNDING_SPEND_SEEN" + + # Try to setchannelfee in order to raise expected error. + # To reduce false positive flakes, only test if state is not NORMAL anymore. + with pytest.raises(RpcError, match=r'-1.*'): + # l1.rpc.setchannelfee(l2.info['id'], 10, 1) + l1.rpc.setchannelfee(l2.info['id'], 10, 1) + + +@unittest.skipIf(not DEVELOPER, "gossip without DEVELOPER=1 is slow") +def test_setchannelfee_routing(node_factory, bitcoind): + # TEST SETUP + # + # [l1] <--default_fees--> [l2] <--specific_fees--> [l3] + # + # - json listchannels is able to see the new values in foreign node + # - routing calculates fees correctly + # - payment can be done using specific fees + # - channel specific fees can be disabled again + # - payment can be done using global fees + DEF_BASE = 1 + DEF_PPM = 10 + + l1, l2, l3 = node_factory.line_graph( + 3, announce_channels=True, wait_for_announce=True, + opts={'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM}) + + # get short channel id for 2->3 + scid = l2.get_channel_scid(l3) + + # TEST CUSTOM VALUES + l2.rpc.setchannelfee(scid, 1337, 137) + + # wait for l1 to see updated channel via gossip + wait_for(lambda: [c['base_fee_millisatoshi'] for c in l1.rpc.listchannels(scid)['channels']] == [1337, DEF_BASE]) + wait_for(lambda: [c['fee_per_millionth'] for c in l1.rpc.listchannels(scid)['channels']] == [137, DEF_PPM]) + + # test fees are applied to HTLC forwards + # + # BOLT #7: + # If l1 were to send 4,999,999 millisatoshi to l3 via l2, it needs to + # pay l2 the fee it specified in the l2->l3 `channel_update`, calculated as + # per [HTLC Fees](#htlc_fees): base + amt * pm / 10**6 + # + # 1337 + 4999999 * 137 / 1000000 = 2021.999 (2021) + route = l1.rpc.getroute(l3.info['id'], 4999999, 1)["route"] + assert len(route) == 2 + assert route[0]['msatoshi'] == 5002020 + assert route[1]['msatoshi'] == 4999999 + + # do and check actual payment + inv = l3.rpc.invoice(4999999, 'test_setchannelfee_1', 'desc')['bolt11'] + result = l1.rpc.pay(inv) + assert result['status'] == 'complete' + assert result['msatoshi_sent'] == 5002020 + + # TEST DISABLE and check global fee routing + l2.rpc.setchannelfee(scid) + + # wait for l1 to see default values again via gossip + wait_for(lambda: [c['base_fee_millisatoshi'] for c in l1.rpc.listchannels(scid)['channels']] == [DEF_BASE, DEF_BASE]) + wait_for(lambda: [c['fee_per_millionth'] for c in l1.rpc.listchannels(scid)['channels']] == [DEF_PPM, DEF_PPM]) + + # test if global fees are applied again (base 1 ppm 10) + # 1 + 4999999 * 10 / 1000000 = 50.999 (50) + route = l1.rpc.getroute(l3.info['id'], 4999999, 1)["route"] + assert len(route) == 2 + assert route[0]['msatoshi'] == 5000049 + assert route[1]['msatoshi'] == 4999999 + + # do and check actual payment + inv = l3.rpc.invoice(4999999, 'test_setchannelfee_2', 'desc')['bolt11'] + result = l1.rpc.pay(inv) + assert result['status'] == 'complete' + assert result['msatoshi_sent'] == 5000049 + + +@unittest.skipIf(not DEVELOPER, "gossip without DEVELOPER=1 is slow") +def test_setchannelfee_zero(node_factory, bitcoind): + # TEST SETUP + # + # [l1] <--default_fees--> [l2] <--specific_fees--> [l3] + # + # - json listchannels is able to see the new values in foreign node + # - routing calculates fees correctly + # - payment can be done using zero fees + DEF_BASE = 1 + DEF_PPM = 10 + + l1, l2, l3 = node_factory.line_graph( + 3, announce_channels=True, wait_for_announce=True, + opts={'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM}) + + # get short channel id for 2->3 + scid = l2.get_channel_scid(l3) + + # TEST ZERO fees possible + l2.rpc.setchannelfee(scid, 0, 0) + wait_for(lambda: [c['base_fee_millisatoshi'] for c in l1.rpc.listchannels(scid)['channels']] == [0, DEF_BASE]) + wait_for(lambda: [c['fee_per_millionth'] for c in l1.rpc.listchannels(scid)['channels']] == [0, DEF_PPM]) + + # test if zero fees are applied + route = l1.rpc.getroute(l3.info['id'], 4999999, 1)["route"] + assert len(route) == 2 + assert route[0]['msatoshi'] == 4999999 + assert route[1]['msatoshi'] == 4999999 + + # do and check actual payment + inv = l3.rpc.invoice(4999999, 'test_setchannelfee_3', 'desc')['bolt11'] + result = l1.rpc.pay(inv) + assert result['status'] == 'complete' + assert result['msatoshi_sent'] == 4999999 + + +@unittest.skipIf(not DEVELOPER, "gossip without DEVELOPER=1 is slow") +def test_setchannelfee_restart(node_factory, bitcoind): + # TEST SETUP + # + # [l1] <--default_fees--> [l2] <--specific_fees--> [l3] + # + # - l2 sets fees to custom values and restarts + # - l1 routing can be made with the custom fees + # - l2 sets fees to UIN32_MAX (db update default) and restarts + # - l1 routing can be made to l3 and global (1 10) fees are applied + DEF_BASE = 1 + DEF_PPM = 10 + OPTS = {'may_reconnect': True, 'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM} + + l1, l2, l3 = node_factory.line_graph(3, announce_channels=True, wait_for_announce=True, opts=OPTS) + + # get short channel id for 2->3 + scid = l2.get_channel_scid(l3) + + # l2 set custom fees + l2.rpc.setchannelfee(scid, 1337, 137) + + # restart l2 and reconnect + l2.restart() + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l2.rpc.connect(l3.info['id'], 'localhost', l3.port) + + # l1 wait for channel update + wait_for(lambda: [c['base_fee_millisatoshi'] for c in l1.rpc.listchannels(scid)['channels']] == [1337, DEF_BASE]) + wait_for(lambda: [c['fee_per_millionth'] for c in l1.rpc.listchannels(scid)['channels']] == [137, DEF_PPM]) + + # for slow travis: wait for everyone to see all channels + 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) + + # l1 can make payment to l3 with custom fees being applied + # Note: BOLT #7 math works out to 2021 msat fees + inv = l3.rpc.invoice(4999999, 'test_setchannelfee_1', 'desc')['bolt11'] + result = l1.rpc.pay(inv) + assert result['status'] == 'complete' + assert result['msatoshi_sent'] == 5002020