From 1142c44c2932b93c94d6da51f62f5c9b0b0655f4 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 2 Nov 2017 10:24:00 +1030 Subject: [PATCH] lightningd: fail htlcs we offer if peer unresponsive after deadline. Signed-off-by: Rusty Russell --- lightningd/peer_control.c | 2 +- lightningd/peer_control.h | 2 ++ lightningd/peer_htlcs.c | 54 +++++++++++++++++++++++++++++++++++++-- tests/test_lightningd.py | 40 +++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index c1799ffc3..22a9d3070 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -176,7 +176,7 @@ void peer_fail_permanent(struct peer *peer, const u8 *msg TAKES) return; } -static void peer_fail_permanent_str(struct peer *peer, const char *str TAKES) +void peer_fail_permanent_str(struct peer *peer, const char *str TAKES) { /* Don't use tal_strdup, since we need tal_len */ u8 *msg = tal_dup_arr(peer, u8, (const u8 *)str, strlen(str) + 1, 0); diff --git a/lightningd/peer_control.h b/lightningd/peer_control.h index f22911657..4025d06e6 100644 --- a/lightningd/peer_control.h +++ b/lightningd/peer_control.h @@ -201,6 +201,8 @@ u8 *get_supported_local_features(const tal_t *ctx); PRINTF_FMT(2,3) void peer_fail_transient(struct peer *peer, const char *fmt,...); /* Peer has failed, give up on it. */ void peer_fail_permanent(struct peer *peer, const u8 *msg TAKES); +/* Version where we supply the reason string. */ +void peer_fail_permanent_str(struct peer *peer, const char *str TAKES); /* Permanent error, but due to internal problems, not peer. */ void peer_internal_error(struct peer *peer, const char *fmt, ...); diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 249614372..00bf77bbb 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -1426,8 +1426,58 @@ void peer_htlcs(const tal_t *ctx, } } -void notify_new_block(struct lightningd *ld, u32 height) +/* BOLT #2: + * + * For HTLCs we offer: the timeout deadline when we have to fail the channel + * and time it out on-chain. This is `G` blocks after the HTLC + * `cltv_expiry`; 1 block is reasonable. + */ +static u32 htlc_out_deadline(const struct htlc_out *hout) { - /* FIXME */ + return hout->cltv_expiry + 1; } +void notify_new_block(struct lightningd *ld, u32 height) +{ + bool removed; + + /* BOLT #2: + * + * A node ... MUST fail the channel if an HTLC which it offered is in + * either node's current commitment transaction past this timeout + * deadline. + */ + /* FIXME: use db to look this up in one go (earliest deadline per-peer) */ + do { + struct htlc_out *hout; + struct htlc_out_map_iter outi; + + removed = false; + + for (hout = htlc_out_map_first(&ld->htlcs_out, &outi); + hout; + hout = htlc_out_map_next(&ld->htlcs_out, &outi)) { + /* Not timed out yet? */ + if (height < htlc_out_deadline(hout)) + continue; + + /* Peer on chain already? */ + if (peer_on_chain(hout->key.peer)) + continue; + + /* Peer already failed, or we hit it? */ + if (hout->key.peer->error) + continue; + + peer_fail_permanent_str(hout->key.peer, + take(tal_fmt(hout, + "Offered HTLC %"PRIu64 + " %s cltv %u hit deadline", + hout->key.id, + htlc_state_name(hout->hstate), + hout->cltv_expiry))); + removed = true; + } + /* Iteration while removing is safe, but can skip entries! */ + } while (removed); +} diff --git a/tests/test_lightningd.py b/tests/test_lightningd.py index bf2ca90c6..6f11c415b 100644 --- a/tests/test_lightningd.py +++ b/tests/test_lightningd.py @@ -1595,6 +1595,46 @@ class LightningDTests(BaseLightningDTests): l1.rpc.sendpay(to_json(route), rhash) assert l3.rpc.listinvoice('test_forward_pad_fees_and_cltv')[0]['complete'] == True + @unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1") + def test_htlc_out_timeout(self): + """Test that we drop onchain if the peer doesn't time out HTLC""" + + # HTLC 1->2, 1 fails after it's irrevocably committed, can't reconnect + disconnects = ['@WIRE_REVOKE_AND_ACK'] + l1 = self.node_factory.get_node(disconnect=disconnects, + options=['--no-reconnect']) + l2 = self.node_factory.get_node() + + l1.rpc.connect(l2.info['id'], 'localhost:{}'.format(l2.info['port'])) + chanid = self.fund_channel(l1, l2, 10**6) + + # Wait for route propagation. + bitcoind.rpc.generate(5) + l1.daemon.wait_for_logs(['Received channel_update for channel {}\(0\)' + .format(chanid), + 'Received channel_update for channel {}\(1\)' + .format(chanid)]) + + amt = 200000000 + inv = l2.rpc.invoice(amt, 'test_htlc_out_timeout', 'desc')['bolt11'] + assert l2.rpc.listinvoice('test_htlc_out_timeout')[0]['complete'] == False + + payfuture = self.executor.submit(l1.rpc.pay, inv); + + # l1 will drop to chain, not reconnect. + l1.daemon.wait_for_log('dev_disconnect: @WIRE_REVOKE_AND_ACK') + + # Takes 6 blocks to timeout (cltv-final + 1), but we also give grace period of 1 block. + bitcoind.rpc.generate(5 + 1) + assert not l1.daemon.is_in_log('hit deadline') + bitcoind.rpc.generate(1) + + l1.daemon.wait_for_log('Offered HTLC 0 SENT_ADD_ACK_REVOCATION cltv {} hit deadline'.format(bitcoind.rpc.getblockcount()-1)) + l1.daemon.wait_for_log('sendrawtx exit 0') + l1.bitcoin.rpc.generate(1) + l1.daemon.wait_for_log('-> ONCHAIND_OUR_UNILATERAL') + l2.daemon.wait_for_log('-> ONCHAIND_THEIR_UNILATERAL') + @unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1") def test_disconnect(self): # These should all make us fail, and retry.