Browse Source

peer_control: Make close wait for complete closure, with timeout.

Also report tx and txid, and whether we closed unilaterally or
bilaterally, if we could close the channel.

Also make a manpage.

Fixes: #1207
Fixes: #714
Fixes: #622
ppa-0.6.1
ZmnSCPxj 7 years ago
committed by Rusty Russell
parent
commit
2cee1ab20f
  1. 10
      contrib/pylightning/lightning/lightning.py
  2. 1
      doc/Makefile
  3. 59
      doc/lightning-close.7
  4. 70
      doc/lightning-close.7.txt
  5. 3
      lightningd/channel.c
  6. 3
      lightningd/closing_control.c
  7. 1
      lightningd/lightningd.c
  8. 2
      lightningd/lightningd.h
  9. 178
      lightningd/peer_control.c
  10. 2
      lightningd/peer_control.h
  11. 55
      tests/test_lightningd.py
  12. 4
      wallet/test/run-wallet.c

10
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)

1
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 \

59
doc/lightning-close.7

@ -0,0 +1,59 @@
'\" t
.\" Title: lightning-close
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
.\" 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 <ZmnSCPxj@protonmail\&.com> is mainly responsible\&.
.SH "SEE ALSO"
.SH "RESOURCES"
.sp
Main web site: https://github\&.com/ElementsProject/lightning

70
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 <ZmnSCPxj@protonmail.com> is mainly responsible.
SEE ALSO
--------
RESOURCES
---------
Main web site: https://github.com/ElementsProject/lightning

3
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);
}

3
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);
}

1
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());

2
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;

178
lightningd/peer_control.c

@ -44,6 +44,17 @@
#include <wally_bip32.h>
#include <wire/gen_onion_wire.h>
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 = {

2
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);

55
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')

4
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)

Loading…
Cancel
Save