From 61e1d6431c6ec010ac6708fa61f2cdd14c122053 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 28 Oct 2019 14:03:42 +1030 Subject: [PATCH] pytest: stress fee_update code, trigger bug. A 'Bad commit_sig signature' was reported by @Javier on Telegram and @DarthCoin. This was between two c-lightning peers, so definitely our fault. Analysis of this message revealed the signature was using the wrong feerate. I finally managed to make a test case which triggered this. Signed-off-by: Rusty Russell --- lightningd/channel_control.c | 48 +++++++++++++++++++++++++++++++++ tests/test_connection.py | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index ec9850c4e..05d715ec3 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -742,3 +742,51 @@ struct command_result *cancel_channel_before_broadcast(struct command *cmd, notleak(cc)); return command_still_pending(cmd); } + +#if DEVELOPER +static struct command_result *json_dev_feerate(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + u32 *feerate; + struct node_id *id; + struct peer *peer; + struct json_stream *response; + struct channel *channel; + const u8 *msg; + + if (!param(cmd, buffer, params, + p_req("id", param_node_id, &id), + p_req("feerate", param_number, &feerate), + NULL)) + return command_param_failed(); + + peer = peer_by_id(cmd->ld, id); + if (!peer) + return command_fail(cmd, LIGHTNINGD, "Peer not connected"); + + channel = peer_active_channel(peer); + if (!channel || !channel->owner || channel->state != CHANNELD_NORMAL) + return command_fail(cmd, LIGHTNINGD, "Peer bad state"); + + msg = towire_channel_feerates(NULL, *feerate, + feerate_min(cmd->ld, NULL), + feerate_max(cmd->ld, NULL)); + subd_send_msg(channel->owner, take(msg)); + + response = json_stream_success(cmd); + json_add_node_id(response, "id", id); + json_add_u32(response, "feerate", *feerate); + + return command_success(cmd, response); +} + +static const struct json_command dev_feerate_command = { + "dev-feerate", + "developer", + json_dev_feerate, + "Set feerate for {id} to {feerate}" +}; +AUTODATA(json_command, &dev_feerate_command); +#endif /* DEVELOPER */ diff --git a/tests/test_connection.py b/tests/test_connection.py index 509776859..9af104564 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -2146,3 +2146,54 @@ def test_feerate_spam(node_factory, chainparams): # But it won't do it again once it's at max. with pytest.raises(TimeoutError): l1.daemon.wait_for_log('peer_out WIRE_UPDATE_FEE', timeout=5) + + +@pytest.mark.xfail(strict=True) +@unittest.skipIf(not DEVELOPER, "need dev-feerate") +def test_feerate_stress(node_factory, executor): + # Third node makes HTLC traffic less predictable. + l1, l2, l3 = node_factory.line_graph(3, opts={'commit-time': 100, + 'may_reconnect': True}) + + l1.pay(l2, 10**9 // 2) + scid12 = l1.get_channel_scid(l2) + scid23 = l2.get_channel_scid(l3) + + routel1l3 = [{'msatoshi': '10002msat', 'id': l2.info['id'], 'delay': 11, 'channel': scid12}, + {'msatoshi': '10000msat', 'id': l3.info['id'], 'delay': 5, 'channel': scid23}] + routel2l1 = [{'msatoshi': '10000msat', 'id': l1.info['id'], 'delay': 5, 'channel': scid12}] + + rate = 1875 + NUM_ATTEMPTS = 25 + l1done = 0 + l2done = 0 + prev_log = 0 + while l1done < NUM_ATTEMPTS and l2done < NUM_ATTEMPTS: + try: + r = random.randrange(6) + if r == 5: + l1.rpc.sendpay(routel1l3, "{:064x}".format(l1done)) + l1done += 1 + elif r == 4: + l2.rpc.sendpay(routel2l1, "{:064x}".format(l2done)) + l2done += 1 + elif r > 0: + l1.rpc.call('dev-feerate', [l2.info['id'], rate]) + rate += 5 + else: + l2.rpc.disconnect(l1.info['id'], True) + time.sleep(1) + except RpcError: + time.sleep(0.01) + assert not l1.daemon.is_in_log('Bad.*signature', start=prev_log) + prev_log = len(l1.daemon.logs) + + # Make sure it's reconnected, and wait for last payment. + wait_for(lambda: l1.rpc.getpeer(l2.info['id'])['connected']) + with pytest.raises(RpcError, match='WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS'): + l1.rpc.waitsendpay("{:064x}".format(l1done - 1)) + with pytest.raises(RpcError, match='WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS'): + l2.rpc.waitsendpay("{:064x}".format(l2done - 1)) + l1.rpc.call('dev-feerate', [l2.info['id'], rate - 5]) + assert not l1.daemon.is_in_log('Bad.*signature') + assert not l2.daemon.is_in_log('Bad.*signature')