Skip to content

Commit 61e1d64

Browse files
rustyrussellniftynei
authored andcommitted
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 <[email protected]>
1 parent f657146 commit 61e1d64

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

lightningd/channel_control.c

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,3 +742,51 @@ struct command_result *cancel_channel_before_broadcast(struct command *cmd,
742742
notleak(cc));
743743
return command_still_pending(cmd);
744744
}
745+
746+
#if DEVELOPER
747+
static struct command_result *json_dev_feerate(struct command *cmd,
748+
const char *buffer,
749+
const jsmntok_t *obj UNNEEDED,
750+
const jsmntok_t *params)
751+
{
752+
u32 *feerate;
753+
struct node_id *id;
754+
struct peer *peer;
755+
struct json_stream *response;
756+
struct channel *channel;
757+
const u8 *msg;
758+
759+
if (!param(cmd, buffer, params,
760+
p_req("id", param_node_id, &id),
761+
p_req("feerate", param_number, &feerate),
762+
NULL))
763+
return command_param_failed();
764+
765+
peer = peer_by_id(cmd->ld, id);
766+
if (!peer)
767+
return command_fail(cmd, LIGHTNINGD, "Peer not connected");
768+
769+
channel = peer_active_channel(peer);
770+
if (!channel || !channel->owner || channel->state != CHANNELD_NORMAL)
771+
return command_fail(cmd, LIGHTNINGD, "Peer bad state");
772+
773+
msg = towire_channel_feerates(NULL, *feerate,
774+
feerate_min(cmd->ld, NULL),
775+
feerate_max(cmd->ld, NULL));
776+
subd_send_msg(channel->owner, take(msg));
777+
778+
response = json_stream_success(cmd);
779+
json_add_node_id(response, "id", id);
780+
json_add_u32(response, "feerate", *feerate);
781+
782+
return command_success(cmd, response);
783+
}
784+
785+
static const struct json_command dev_feerate_command = {
786+
"dev-feerate",
787+
"developer",
788+
json_dev_feerate,
789+
"Set feerate for {id} to {feerate}"
790+
};
791+
AUTODATA(json_command, &dev_feerate_command);
792+
#endif /* DEVELOPER */

tests/test_connection.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2146,3 +2146,54 @@ def test_feerate_spam(node_factory, chainparams):
21462146
# But it won't do it again once it's at max.
21472147
with pytest.raises(TimeoutError):
21482148
l1.daemon.wait_for_log('peer_out WIRE_UPDATE_FEE', timeout=5)
2149+
2150+
2151+
@pytest.mark.xfail(strict=True)
2152+
@unittest.skipIf(not DEVELOPER, "need dev-feerate")
2153+
def test_feerate_stress(node_factory, executor):
2154+
# Third node makes HTLC traffic less predictable.
2155+
l1, l2, l3 = node_factory.line_graph(3, opts={'commit-time': 100,
2156+
'may_reconnect': True})
2157+
2158+
l1.pay(l2, 10**9 // 2)
2159+
scid12 = l1.get_channel_scid(l2)
2160+
scid23 = l2.get_channel_scid(l3)
2161+
2162+
routel1l3 = [{'msatoshi': '10002msat', 'id': l2.info['id'], 'delay': 11, 'channel': scid12},
2163+
{'msatoshi': '10000msat', 'id': l3.info['id'], 'delay': 5, 'channel': scid23}]
2164+
routel2l1 = [{'msatoshi': '10000msat', 'id': l1.info['id'], 'delay': 5, 'channel': scid12}]
2165+
2166+
rate = 1875
2167+
NUM_ATTEMPTS = 25
2168+
l1done = 0
2169+
l2done = 0
2170+
prev_log = 0
2171+
while l1done < NUM_ATTEMPTS and l2done < NUM_ATTEMPTS:
2172+
try:
2173+
r = random.randrange(6)
2174+
if r == 5:
2175+
l1.rpc.sendpay(routel1l3, "{:064x}".format(l1done))
2176+
l1done += 1
2177+
elif r == 4:
2178+
l2.rpc.sendpay(routel2l1, "{:064x}".format(l2done))
2179+
l2done += 1
2180+
elif r > 0:
2181+
l1.rpc.call('dev-feerate', [l2.info['id'], rate])
2182+
rate += 5
2183+
else:
2184+
l2.rpc.disconnect(l1.info['id'], True)
2185+
time.sleep(1)
2186+
except RpcError:
2187+
time.sleep(0.01)
2188+
assert not l1.daemon.is_in_log('Bad.*signature', start=prev_log)
2189+
prev_log = len(l1.daemon.logs)
2190+
2191+
# Make sure it's reconnected, and wait for last payment.
2192+
wait_for(lambda: l1.rpc.getpeer(l2.info['id'])['connected'])
2193+
with pytest.raises(RpcError, match='WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS'):
2194+
l1.rpc.waitsendpay("{:064x}".format(l1done - 1))
2195+
with pytest.raises(RpcError, match='WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS'):
2196+
l2.rpc.waitsendpay("{:064x}".format(l2done - 1))
2197+
l1.rpc.call('dev-feerate', [l2.info['id'], rate - 5])
2198+
assert not l1.daemon.is_in_log('Bad.*signature')
2199+
assert not l2.daemon.is_in_log('Bad.*signature')

0 commit comments

Comments
 (0)