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