diff --git a/contrib/pylightning/lightning/lightning.py b/contrib/pylightning/lightning/lightning.py index c57a3dec1..2e89d3f0a 100644 --- a/contrib/pylightning/lightning/lightning.py +++ b/contrib/pylightning/lightning/lightning.py @@ -333,12 +333,16 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("fundchannel", payload) - def close(self, peer_id): + def close(self, peer_id, force=None, timeout=None): """ - Close the channel with peer {id} + Close the channel with peer {id}, forcing a unilateral + close if {force} is True, and timing out with {timeout} + seconds. """ payload = { - "id": peer_id + "id": peer_id, + "force": force, + "timeout": timeout } return self.call("close", payload) diff --git a/doc/Makefile b/doc/Makefile index 788793e0b..9acb59785 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -6,6 +6,7 @@ doc-wrongdir: MANPAGES := doc/lightning-cli.1 \ doc/lightning-autocleaninvoice.7 \ + doc/lightning-close.7 \ doc/lightning-decodepay.7 \ doc/lightning-delexpiredinvoice.7 \ doc/lightning-delinvoice.7 \ diff --git a/doc/lightning-close.7 b/doc/lightning-close.7 new file mode 100644 index 000000000..275f11074 --- /dev/null +++ b/doc/lightning-close.7 @@ -0,0 +1,59 @@ +'\" t +.\" Title: lightning-close +.\" Author: [see the "AUTHOR" section] +.\" Generator: DocBook XSL Stylesheets v1.79.1 +.\" Date: 04/15/2018 +.\" Manual: \ \& +.\" Source: \ \& +.\" Language: English +.\" +.TH "LIGHTNING\-CLOSE" "7" "04/15/2018" "\ \&" "\ \&" +.\" ----------------------------------------------------------------- +.\" * Define some portability stuff +.\" ----------------------------------------------------------------- +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.\" http://bugs.debian.org/507673 +.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" ----------------------------------------------------------------- +.\" * set default formatting +.\" ----------------------------------------------------------------- +.\" disable hyphenation +.nh +.\" disable justification (adjust text to left margin only) +.ad l +.\" ----------------------------------------------------------------- +.\" * MAIN CONTENT STARTS HERE * +.\" ----------------------------------------------------------------- +.SH "NAME" +lightning-close \- Protocol for closing channels with direct peers +.SH "SYNOPSIS" +.sp +\fBclose\fR \fIid\fR [\fIforce\fR] [\fItimeout\fR] +.SH "DESCRIPTION" +.sp +The \fBclose\fR RPC command attempts to close the channel cooperatively with the peer\&. It applies to the active channel of the direct peer corresponding to the given peer \fIid\fR\&. +.sp +The \fBclose\fR command will time out and return with an error when the number of seconds specified in \fItimeout\fR is reached\&. If unspecified, it times out in 30 seconds\&. +.sp +The \fIforce\fR argument, if the JSON value \fItrue\fR, will cause the channel to be unilaterally closed when the timeout is reached\&. If so, timeout will not cause an error, but instead cause the channel to be failed and put onchain unilaterally\&. Unilateral closes will lead to your funds getting locked according to the \fIto_self_delay\fR parameter of the peer\&. +.sp +Normally the peer needs to be live and connected in order to negotiate a mutual close\&. Forcing a unilateral close can be used if you suspect you can no longer contact the peer\&. +.SH "RETURN VALUE" +.sp +On success, an object with fields \fItx\fR and \fItxid\fR containing the closing transaction are returned\&. It will also have a field \fItype\fR which is either the JSON string \fImutual\fR or the JSON string \fIunilateral\fR\&. A \fImutual\fR close means that we could negotiate a close with the peer, while a \fIunilateral\fR close means that the \fIforce\fR flag was set and we had to close the channel without waiting for the counterparty\&. +.sp +A unilateral close may still occur with \fIforce\fR set to \fIfalse\fR if the peer did not behave correctly during the close negotiation\&. +.sp +Unilateral closes will return your funds after a delay\&. The delay will vary based on the peer \fIto_self_delay\fR setting, not your own setting\&. +.sp +On failure, if \fBclose\fR failed due to timing out with \fIforce\fR argument \fIfalse\fR, the channel will still eventually close once we have contacted the peer\&. +.SH "AUTHOR" +.sp +ZmnSCPxj is mainly responsible\&. +.SH "SEE ALSO" +.SH "RESOURCES" +.sp +Main web site: https://github\&.com/ElementsProject/lightning diff --git a/doc/lightning-close.7.txt b/doc/lightning-close.7.txt new file mode 100644 index 000000000..a3ef5170e --- /dev/null +++ b/doc/lightning-close.7.txt @@ -0,0 +1,70 @@ +LIGHTNING-CLOSE(7) +================== +:doctype: manpage + +NAME +---- +lightning-close - Protocol for closing channels with direct peers + +SYNOPSIS +-------- +*close* 'id' ['force'] ['timeout'] + +DESCRIPTION +----------- + +The *close* RPC command attempts to close the channel cooperatively +with the peer. +It applies to the active channel of the direct peer corresponding to +the given peer 'id'. + +The *close* command will time out and return with an error when the +number of seconds specified in 'timeout' is reached. +If unspecified, it times out in 30 seconds. + +The 'force' argument, if the JSON value 'true', will cause the +channel to be unilaterally closed when the timeout is reached. +If so, timeout will not cause an error, but instead cause the +channel to be failed and put onchain unilaterally. +Unilateral closes will lead to your funds getting locked according +to the 'to_self_delay' parameter of the peer. + +Normally the peer needs to be live and connected in order to negotiate +a mutual close. +Forcing a unilateral close can be used if you suspect you can no longer +contact the peer. + +RETURN VALUE +------------ + +On success, an object with fields 'tx' and 'txid' containing the +closing transaction are returned. +It will also have a field 'type' which is either the JSON string +'mutual' or the JSON string 'unilateral'. +A 'mutual' close means that we could negotiate a close with the +peer, while a 'unilateral' close means that the 'force' flag was +set and we had to close the channel without waiting for the +counterparty. + +A unilateral close may still occur with 'force' set to 'false' if +the peer did not behave correctly during the close negotiation. + +Unilateral closes will return your funds after a delay. +The delay will vary based on the peer 'to_self_delay' setting, not +your own setting. + +On failure, if *close* failed due to timing out with 'force' +argument 'false', the channel will still eventually close once +we have contacted the peer. + +AUTHOR +------ +ZmnSCPxj is mainly responsible. + +SEE ALSO +-------- + + +RESOURCES +--------- +Main web site: https://github.com/ElementsProject/lightning diff --git a/lightningd/channel.c b/lightningd/channel.c index 87325a885..a87bc1d9d 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -321,7 +321,8 @@ void channel_fail_permanent(struct channel *channel, const char *fmt, ...) } channel_set_owner(channel, NULL); - drop_to_chain(ld, channel); + /* Drop non-cooperatively (unilateral) to chain. */ + drop_to_chain(ld, channel, false); tal_free(why); } diff --git a/lightningd/closing_control.c b/lightningd/closing_control.c index d0f9fa113..e6296ec35 100644 --- a/lightningd/closing_control.c +++ b/lightningd/closing_control.c @@ -93,7 +93,8 @@ static void peer_closing_complete(struct channel *channel, const u8 *msg) if (channel->state == CLOSINGD_COMPLETE) return; - drop_to_chain(channel->peer->ld, channel); + /* Channel gets dropped to chain cooperatively. */ + drop_to_chain(channel->peer->ld, channel, true); channel_set_state(channel, CLOSINGD_SIGEXCHANGE, CLOSINGD_COMPLETE); } diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index d671cf712..189762818 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -68,6 +68,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx) list_head_init(&ld->connects); list_head_init(&ld->waitsendpay_commands); list_head_init(&ld->sendpay_commands); + list_head_init(&ld->close_commands); ld->wireaddrs = tal_arr(ld, struct wireaddr, 0); ld->portnum = DEFAULT_PORT; timers_init(&ld->timers, time_mono()); diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h index f1e9ebce9..2ff947202 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -138,6 +138,8 @@ struct lightningd { struct list_head waitsendpay_commands; /* Outstanding sendpay commands. */ struct list_head sendpay_commands; + /* Outstanding close commands. */ + struct list_head close_commands; /* Maintained by invoices.c */ struct invoices *invoices; diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 820b4d3be..984811f64 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -44,6 +44,17 @@ #include #include +struct close_command { + /* Inside struct lightningd close_commands. */ + struct list_node list; + /* Command structure. This is the parent of the close command. */ + struct command *cmd; + /* Channel being closed. */ + struct channel *channel; + /* Should we force the close on timeout? */ + bool force; +}; + static void destroy_peer(struct peer *peer) { list_del_from(&peer->ld->peers, &peer->list); @@ -209,13 +220,126 @@ static void remove_sig(struct bitcoin_tx *signed_tx) signed_tx->input[0].witness = tal_free(signed_tx->input[0].witness); } -void drop_to_chain(struct lightningd *ld, struct channel *channel) +/* Resolve a single close command. */ +static void +resolve_one_close_command(struct close_command *cc, bool cooperative) +{ + struct json_result *result = new_json_result(cc); + u8 *tx = linearize_tx(result, cc->channel->last_tx); + struct bitcoin_txid txid; + + bitcoin_txid(cc->channel->last_tx, &txid); + + json_object_start(result, NULL); + json_add_hex(result, "tx", tx, tal_len(tx)); + json_add_txid(result, "txid", &txid); + if (cooperative) + json_add_string(result, "type", "mutual"); + else + json_add_string(result, "type", "unilateral"); + json_object_end(result); + + command_success(cc->cmd, result); +} + +/* Resolve a close command for a channel that will be closed soon. */ +static void +resolve_close_command(struct lightningd *ld, struct channel *channel, + bool cooperative) +{ + struct close_command *cc; + struct close_command *n; + + list_for_each_safe (&ld->close_commands, cc, n, list) { + if (cc->channel != channel) + continue; + resolve_one_close_command(cc, cooperative); + } +} + +/* Destroy the close command structure in reaction to the + * channel being destroyed. */ +static void +destroy_close_command_on_channel_destroy(struct channel *_ UNUSED, + struct close_command *cc) +{ + /* The cc has the command as parent, so resolving the + * command destroys the cc and triggers destroy_close_command. + * Clear the cc->channel first so that we will not try to + * remove a destructor. */ + cc->channel = NULL; + command_fail(cc->cmd, "Channel forgotten before proper close."); +} + +/* Destroy the close command structure. */ +static void +destroy_close_command(struct close_command *cc) +{ + list_del(&cc->list); + /* If destroy_close_command_on_channel_destroy was + * triggered beforehand, it will have cleared + * the channel field, preventing us from removing it + * from an already-destroyed channel. */ + if (!cc->channel) + return; + tal_del_destructor2(cc->channel, + &destroy_close_command_on_channel_destroy, + cc); +} + +/* Handle timeout. */ +static void +close_command_timeout(struct close_command *cc) +{ + if (cc->force) + /* This will trigger drop_to_chain, which will trigger + * resolution of the command and destruction of the + * close_command. */ + channel_fail_permanent(cc->channel, + "Forcibly closed by 'close' command timeout"); + else + /* Fail the command directly, which will resolve the + * command and destroy the close_command. */ + command_fail(cc->cmd, + "Channel close negotiation not finished " + "before timeout"); +} + +/* Construct a close command structure and add to ld. */ +static void +register_close_command(struct lightningd *ld, + struct command *cmd, + struct channel *channel, + unsigned int timeout, + bool force) +{ + struct close_command *cc; + assert(channel); + + cc = tal(cmd, struct close_command); + list_add_tail(&ld->close_commands, &cc->list); + cc->cmd = cmd; + cc->channel = channel; + cc->force = force; + tal_add_destructor(cc, &destroy_close_command); + tal_add_destructor2(channel, + &destroy_close_command_on_channel_destroy, + cc); + new_reltimer(&ld->timers, cc, time_from_sec(timeout), + &close_command_timeout, cc); +} + +void drop_to_chain(struct lightningd *ld, struct channel *channel, + bool cooperative) { sign_last_tx(channel); /* Keep broadcasting until we say stop (can fail due to dup, * if they beat us to the broadcast). */ broadcast_tx(ld->topology, channel, channel->last_tx, NULL); + + resolve_close_command(ld, channel, cooperative); + remove_sig(channel->last_tx); } @@ -854,11 +978,17 @@ static void json_close(struct command *cmd, const char *buffer, const jsmntok_t *params) { jsmntok_t *peertok; + jsmntok_t *timeouttok; + jsmntok_t *forcetok; struct peer *peer; struct channel *channel; + unsigned int timeout = 30; + bool force = false; if (!json_get_params(cmd, buffer, params, "id", &peertok, + "?force", &forcetok, + "?timeout", &timeouttok, NULL)) { return; } @@ -868,6 +998,18 @@ static void json_close(struct command *cmd, command_fail(cmd, "Could not find peer with that id"); return; } + if (forcetok && !json_tok_bool(buffer, forcetok, &force)) { + command_fail(cmd, "Force '%.*s' must be true or false", + forcetok->end - forcetok->start, + buffer + forcetok->start); + return; + } + if (timeouttok && !json_tok_number(buffer, timeouttok, &timeout)) { + command_fail(cmd, "Timeout '%.*s' is not a number", + timeouttok->end - timeouttok->start, + buffer + timeouttok->start); + return; + } channel = peer_active_channel(peer); if (!channel) { @@ -883,7 +1025,22 @@ static void json_close(struct command *cmd, return; } - /* Normal case. */ + /* Normal case. + * We allow states shutting down and sigexchange; a previous + * close command may have timed out, and this current command + * will continue waiting for the effects of the previous + * close command. */ + if (channel->state != CHANNELD_NORMAL && + channel->state != CHANNELD_AWAITING_LOCKIN && + channel->state != CHANNELD_SHUTTING_DOWN && + channel->state != CLOSINGD_SIGEXCHANGE) + command_fail(cmd, "Peer is in state %s", + channel_state_name(channel)); + + /* If normal or locking in, transition to shutting down + * state. + * (if already shutting down or sigexchange, just keep + * waiting) */ if (channel->state == CHANNELD_NORMAL || channel->state == CHANNELD_AWAITING_LOCKIN) { channel_set_state(channel, channel->state, CHANNELD_SHUTTING_DOWN); @@ -891,11 +1048,20 @@ static void json_close(struct command *cmd, if (channel->owner) subd_send_msg(channel->owner, take(towire_channel_send_shutdown(channel))); + } + /* If channel has no owner, it means the peer is disconnected, + * so make a nominal effort to contact it now. + */ + if (!channel->owner) + subd_send_msg(cmd->ld->gossip, + take(towire_gossipctl_reach_peer(NULL, + &channel->peer->id))); - command_success(cmd, null_response(cmd)); - } else - command_fail(cmd, "Peer is in state %s", - channel_state_name(channel)); + /* Register this command for later handling. */ + register_close_command(cmd->ld, cmd, channel, timeout, force); + + /* Wait until close drops down to chain. */ + command_still_pending(cmd); } static const struct json_command close_command = { diff --git a/lightningd/peer_control.h b/lightningd/peer_control.h index 44adc202a..dd8b524f1 100644 --- a/lightningd/peer_control.h +++ b/lightningd/peer_control.h @@ -102,7 +102,7 @@ u8 *p2wpkh_for_keyidx(const tal_t *ctx, struct lightningd *ld, u64 keyidx); /* We've loaded peers from database, set them going. */ void activate_peers(struct lightningd *ld); -void drop_to_chain(struct lightningd *ld, struct channel *channel); +void drop_to_chain(struct lightningd *ld, struct channel *channel, bool cooperative); /* Get range of feerates to insist other side abide by for normal channels. */ u32 feerate_min(struct lightningd *ld); diff --git a/tests/test_lightningd.py b/tests/test_lightningd.py index 34cd0c920..043a37555 100644 --- a/tests/test_lightningd.py +++ b/tests/test_lightningd.py @@ -1180,8 +1180,10 @@ class LightningDTests(BaseLightningDTests): billboard = l1.rpc.listpeers(l2.info['id'])['peers'][0]['channels'][0]['status'] assert billboard == ['CHANNELD_NORMAL:Funding transaction locked. Channel announced.'] - # This should return, then close. - l1.rpc.close(l2.info['id']) + # This should return with an error, then close. + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l2.info['id'], False, 0) l1.daemon.wait_for_log(' to CHANNELD_SHUTTING_DOWN') l2.daemon.wait_for_log(' to CHANNELD_SHUTTING_DOWN') @@ -1306,7 +1308,9 @@ class LightningDTests(BaseLightningDTests): # Now close for p in peers: - l1.rpc.close(p.info['id']) + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, p.info['id'], False, 0) for p in peers: p.daemon.wait_for_log(' to CLOSINGD_COMPLETE') @@ -3118,8 +3122,10 @@ class LightningDTests(BaseLightningDTests): assert l1.bitcoin.rpc.getmempoolinfo()['size'] == 0 - # This should return, then close. - l1.rpc.close(l2.info['id']) + # This should return with an error, then close. + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l2.info['id'], False, 0) l1.daemon.wait_for_log(' to CHANNELD_SHUTTING_DOWN') l2.daemon.wait_for_log(' to CHANNELD_SHUTTING_DOWN') @@ -3144,7 +3150,10 @@ class LightningDTests(BaseLightningDTests): l1.daemon.wait_for_log('sendrawtx exit 0') bitcoind.generate_block(1) - l1.rpc.close(l2.info['id']) + # This should return with an error, then close. + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l2.info['id'], False, 0) l1.daemon.wait_for_log('CHANNELD_AWAITING_LOCKIN to CHANNELD_SHUTTING_DOWN') l2.daemon.wait_for_log('CHANNELD_AWAITING_LOCKIN to CHANNELD_SHUTTING_DOWN') @@ -3179,8 +3188,10 @@ class LightningDTests(BaseLightningDTests): assert l1.bitcoin.rpc.getmempoolinfo()['size'] == 0 - # This should return, then close. - l1.rpc.close(l2.info['id']) + # This should return with an error, then close. + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l2.info['id'], False, 0) l1.daemon.wait_for_log(' to CHANNELD_SHUTTING_DOWN') l2.daemon.wait_for_log(' to CHANNELD_SHUTTING_DOWN') @@ -3852,7 +3863,9 @@ class LightningDTests(BaseLightningDTests): self.pay(l2, l1, 100000000) # Now shutdown cleanly. - l1.rpc.close(l2.info['id']) + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l2.info['id'], False, 0) l1.daemon.wait_for_log(' to CLOSINGD_COMPLETE') l2.daemon.wait_for_log(' to CLOSINGD_COMPLETE') @@ -3896,8 +3909,10 @@ class LightningDTests(BaseLightningDTests): l1.rpc.dev_setfees() l1.daemon.wait_for_log('dev-setfees: fees now 21098/7654/321') - # Now shutdown cleanly. - l1.rpc.close(l2.info['id']) + # This should return with an error, then close. + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l2.info['id'], False, 0) l1.daemon.wait_for_log(' to CLOSINGD_COMPLETE') l2.daemon.wait_for_log(' to CLOSINGD_COMPLETE') @@ -3936,8 +3951,10 @@ class LightningDTests(BaseLightningDTests): # 15sat/byte fee l1.daemon.wait_for_log('peer_out WIRE_REVOKE_AND_ACK') - # Now shutdown cleanly. - l1.rpc.close(l3.info['id']) + # This should return with an error, then close. + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l3.info['id'], False, 0) l1.daemon.wait_for_log(' to CLOSINGD_COMPLETE') l3.daemon.wait_for_log(' to CLOSINGD_COMPLETE') @@ -3968,7 +3985,9 @@ class LightningDTests(BaseLightningDTests): assert l2.daemon.is_in_log('got commitsig [0-9]*: feerate 14000') # Now shutdown cleanly. - l1.rpc.close(l2.info['id']) + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l2.info['id'], False, 0) l1.daemon.wait_for_log(' to CLOSINGD_COMPLETE') l2.daemon.wait_for_log(' to CLOSINGD_COMPLETE') @@ -4104,7 +4123,9 @@ class LightningDTests(BaseLightningDTests): l2.daemon.wait_for_log('Handing back peer .* to master') self.fund_channel(l1, l2, 10**6) - l1.rpc.close(l2.info['id']) + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l2.info['id'], False, 0) l1.daemon.wait_for_log(' to CLOSINGD_COMPLETE') l2.daemon.wait_for_log(' to CLOSINGD_COMPLETE') @@ -4249,7 +4270,9 @@ class LightningDTests(BaseLightningDTests): assert l1.rpc.getpeer(l2.info['id'])['color'] == l1.rpc.listnodes(l2.info['id'])['nodes'][0]['color'] # Close the channel to forget the peer - l1.rpc.close(l2.info['id']) + self.assertRaisesRegex(ValueError, + "Channel close negotiation not finished", + l1.rpc.close, l2.info['id'], False, 0) l1.daemon.wait_for_log('Forgetting remote peer') bitcoind.generate_block(100) l1.daemon.wait_for_log('WIRE_ONCHAIN_ALL_IRREVOCABLY_RESOLVED') diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index e132ea611..8a4b5eb45 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -253,6 +253,10 @@ bool json_tok_bool(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, b bool json_tok_loglevel(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, enum log_level *level UNNEEDED) { fprintf(stderr, "json_tok_loglevel called!\n"); abort(); } +/* Generated stub for json_tok_number */ +bool json_tok_number(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + unsigned int *num UNNEEDED) +{ fprintf(stderr, "json_tok_number called!\n"); abort(); } /* Generated stub for json_tok_pubkey */ bool json_tok_pubkey(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct pubkey *pubkey UNNEEDED)