diff --git a/closingd/closingd.c b/closingd/closingd.c index d6edad5711e0..d8fbe3037aa3 100644 --- a/closingd/closingd.c +++ b/closingd/closingd.c @@ -610,6 +610,7 @@ static void calc_fee_bounds(size_t expected_weight, /* option_anchor_outputs sets commitment_fee to max, so this * doesn't do anything */ if (amount_sat_greater(*maxfee, commitment_fee)) { + /* FIXME: would be nice to notify close cmd here! */ status_unusual("Maximum feerate %u would give fee %s:" " we must limit it to the final commitment fee %s", *max_feerate, diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index a2bcf4ad5764..752f202c1913 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -515,7 +515,7 @@ def check(self, command_to_check, **kwargs): payload.update({k: v for k, v in kwargs.items()}) return self.call("check", payload) - def close(self, peer_id, unilateraltimeout=None, destination=None, fee_negotiation_step=None): + def close(self, peer_id, unilateraltimeout=None, destination=None, fee_negotiation_step=None, feerange=None): """ Close the channel with peer {id}, forcing a unilateral close after {unilateraltimeout} seconds if non-zero, and @@ -525,7 +525,8 @@ def close(self, peer_id, unilateraltimeout=None, destination=None, fee_negotiati "id": peer_id, "unilateraltimeout": unilateraltimeout, "destination": destination, - "fee_negotiation_step": fee_negotiation_step + "fee_negotiation_step": fee_negotiation_step, + "feerange": feerange, } return self.call("close", payload) diff --git a/doc/lightning-close.7 b/doc/lightning-close.7 index 40a770203762..8d05457284eb 100644 --- a/doc/lightning-close.7 +++ b/doc/lightning-close.7 @@ -3,7 +3,7 @@ lightning-close - Command for closing channels with direct peers .SH SYNOPSIS -\fBclose\fR \fIid\fR [\fIunilateraltimeout\fR] [\fIdestination\fR] [\fIfee_negotiation_step\fR] [\fIwrong_funding\\\fR] +\fBclose\fR \fIid\fR [\fIunilateraltimeout\fR] [\fIdestination\fR] [\fIfee_negotiation_step\fR] [\fIwrong_funding\\\fR] [*feerange*] .SH DESCRIPTION @@ -35,7 +35,12 @@ friends to upgrade! The \fIfee_negotiation_step\fR parameter controls how closing fee negotiation is performed assuming the peer proposes a fee that is -different than our estimate\. On every negotiation step we must give up +different than our estimate\. (Note that using this option +prevents \fBexperimental-quick-close\fR, as the quick-close protocol +does not allow negotiation)\. + + +On every negotiation step we must give up some amount from our proposal towards the peer's proposal\. This parameter can be an integer in which case it is interpreted as number of satoshis to step at a time\. Or it can be an integer followed by "%" to designate @@ -67,6 +72,26 @@ allowed if this peer opened the channel and the channel is unused: it can rescue openings which have been manually miscreated\. +\fIfeerange\fR is an optional array [ \fImin\fR, \fImax\fR ], indicating the +minimum and maximum feerates to offer\. \fIslow\fR and \fIunilateral_close\fR +are the defaults\. + + +Rates are one of the strings \fIurgent\fR (aim for next block), \fInormal\fR +(next 4 blocks or so) or \fIslow\fR (next 100 blocks or so) to use +lightningd’s internal estimates, or one of the names from +\fBlightning-feerates\fR(7)\. Otherwise, they can be numbers with +an optional suffix: \fIperkw\fR means the number is interpreted as +satoshi-per-kilosipa (weight), and \fIperkb\fR means it is interpreted +bitcoind-style as satoshi-per-kilobyte\. Omitting the suffix is +equivalent to \fIperkb\fR\. + + +Note that the maximum fee will be capped at the final commitment +transaction fee (unless the experimental anchor-outputs option is +negotiated)\. + + The peer needs to be live and connected in order to negotiate a mutual close\. The default of unilaterally closing after 48 hours is usually a reasonable indication that you can no longer contact the peer\. @@ -123,4 +148,4 @@ ZmnSCPxj \fI is mainly responsible\. Main web site: \fIhttps://github.com/ElementsProject/lightning\fR -\" SHA256STAMP:03f1e6937a88aad4bdcd29d010da9ced148e3498ea19b388e8cbfde25276482d +\" SHA256STAMP:2bc147c28f9dd20328ef778d815342ee22941ca08b2e86218dc68a90b4d6129d diff --git a/doc/lightning-close.7.md b/doc/lightning-close.7.md index 2e42891015a0..716410bcab9a 100644 --- a/doc/lightning-close.7.md +++ b/doc/lightning-close.7.md @@ -4,7 +4,7 @@ lightning-close -- Command for closing channels with direct peers SYNOPSIS -------- -**close** *id* \[*unilateraltimeout*\] \[*destination*\] \[*fee_negotiation_step*\] \[*wrong_funding\*] +**close** *id* \[*unilateraltimeout*\] \[*destination*\] \[*fee_negotiation_step*\] \[*wrong_funding\*] [\*feerange\*] DESCRIPTION ----------- @@ -33,7 +33,11 @@ friends to upgrade! The *fee_negotiation_step* parameter controls how closing fee negotiation is performed assuming the peer proposes a fee that is -different than our estimate. On every negotiation step we must give up +different than our estimate. (Note that using this option +prevents **experimental-quick-close**, as the quick-close protocol +does not allow negotiation). + +On every negotiation step we must give up some amount from our proposal towards the peer's proposal. This parameter can be an integer in which case it is interpreted as number of satoshis to step at a time. Or it can be an integer followed by "%" to designate @@ -56,6 +60,23 @@ shutdown transaction will spend this output instead. This is only allowed if this peer opened the channel and the channel is unused: it can rescue openings which have been manually miscreated. +*feerange* is an optional array [ *min*, *max* ], indicating the +minimum and maximum feerates to offer. *slow* and *unilateral_close* +are the defaults. + +Rates are one of the strings *urgent* (aim for next block), *normal* +(next 4 blocks or so) or *slow* (next 100 blocks or so) to use +lightningd’s internal estimates, or one of the names from +lightning-feerates(7). Otherwise, they can be numbers with +an optional suffix: *perkw* means the number is interpreted as +satoshi-per-kilosipa (weight), and *perkb* means it is interpreted +bitcoind-style as satoshi-per-kilobyte. Omitting the suffix is +equivalent to *perkb*. + +Note that the maximum fee will be capped at the final commitment +transaction fee (unless the experimental anchor-outputs option is +negotiated). + The peer needs to be live and connected in order to negotiate a mutual close. The default of unilaterally closing after 48 hours is usually a reasonable indication that you can no longer contact the peer. diff --git a/lightningd/channel.c b/lightningd/channel.c index 074a058efb18..3367d192bbed 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -245,6 +245,7 @@ struct channel *new_unsaved_channel(struct peer *peer, channel->closing_fee_negotiation_step_unit = CLOSING_FEE_NEGOTIATION_STEP_UNIT_PERCENTAGE; channel->shutdown_wrong_funding = NULL; + channel->closing_feerate_range = NULL; /* Channel is connected! */ channel->connected = true; @@ -404,6 +405,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid, = CLOSING_FEE_NEGOTIATION_STEP_UNIT_PERCENTAGE; channel->shutdown_wrong_funding = tal_steal(channel, shutdown_wrong_funding); + channel->closing_feerate_range = NULL; if (local_shutdown_scriptpubkey) channel->shutdown_scriptpubkey[LOCAL] = tal_steal(channel, local_shutdown_scriptpubkey); diff --git a/lightningd/channel.h b/lightningd/channel.h index d3f4f8dc2b99..f8d6b5c4958b 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -164,6 +164,9 @@ struct channel { /* optional wrong_funding for mutual close */ const struct bitcoin_outpoint *shutdown_wrong_funding; + /* optional feerate min/max for mutual close */ + u32 *closing_feerate_range; + /* Reestablishment stuff: last sent commit and revocation details. */ bool last_was_revoke; struct changed_htlc *last_sent_commit; diff --git a/lightningd/closing_control.c b/lightningd/closing_control.c index d8ddb8173aab..de8679b34310 100644 --- a/lightningd/closing_control.c +++ b/lightningd/closing_control.c @@ -197,7 +197,7 @@ void peer_start_closingd(struct channel *channel, struct per_peer_state *pps) { u8 *initmsg; - u32 feerate, *max_feerate; + u32 min_feerate, feerate, *max_feerate; struct amount_msat their_msat; struct amount_sat feelimit; int hsmfd; @@ -269,6 +269,14 @@ void peer_start_closingd(struct channel *channel, } else max_feerate = NULL; + min_feerate = feerate_min(ld, NULL); + + /* If they specified feerates in `close`, they apply now! */ + if (channel->closing_feerate_range) { + min_feerate = channel->closing_feerate_range[0]; + max_feerate = &channel->closing_feerate_range[1]; + } + /* BOLT #3: * * Each node offering a signature: @@ -301,8 +309,7 @@ void peer_start_closingd(struct channel *channel, amount_msat_to_sat_round_down(channel->our_msat), amount_msat_to_sat_round_down(their_msat), channel->our_config.dust_limit, - feerate_min(ld, NULL), feerate, - max_feerate, + min_feerate, feerate, max_feerate, feelimit, channel->shutdown_scriptpubkey[LOCAL], channel->shutdown_scriptpubkey[REMOTE], diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 77a235a6c3f7..e3ed65cb9416 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -1648,6 +1648,31 @@ static struct command_result *param_outpoint(struct command *cmd, "should be a txid:outnum"); } +static struct command_result *param_feerate_range(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + u32 **feerate_range) +{ + struct command_result *ret; + u32 *rate; + + *feerate_range = tal_arr(cmd, u32, 2); + if (tok->type != JSMN_ARRAY || tok->size != 2) + return command_fail_badparam(cmd, name, buffer, tok, + "should be an array of 2 entries"); + + ret = param_feerate(cmd, name, buffer, tok+1, &rate); + if (ret) + return ret; + (*feerate_range)[0] = *rate; + ret = param_feerate(cmd, name, buffer, tok+2, &rate); + if (ret) + return ret; + (*feerate_range)[1] = *rate; + return NULL; +} + static struct command_result *json_close(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, @@ -1661,6 +1686,7 @@ static struct command_result *json_close(struct command *cmd, bool close_script_set, wrong_funding_changed; const char *fee_negotiation_step_str; struct bitcoin_outpoint *wrong_funding; + u32 *feerate_range; char* end; bool anysegwit; @@ -1672,6 +1698,7 @@ static struct command_result *json_close(struct command *cmd, p_opt("fee_negotiation_step", param_string, &fee_negotiation_step_str), p_opt("wrong_funding", param_outpoint, &wrong_funding), + p_opt("feerange", param_feerate_range, &feerate_range), NULL)) return command_param_failed(); @@ -1821,6 +1848,9 @@ static struct command_result *json_close(struct command *cmd, wrong_funding_changed = false; } + /* Works fine if feerate_range is NULL */ + channel->closing_feerate_range = tal_steal(channel, feerate_range); + /* Normal case. * We allow states shutting down and sigexchange; a previous * close command may have timed out, and this current command diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 1fd3a3dfa60d..6b16bedcd78b 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -524,6 +524,11 @@ struct command_result *param_escaped_string(struct command *cmd UNNEEDED, const jsmntok_t *tok UNNEEDED, const char **str UNNEEDED) { fprintf(stderr, "param_escaped_string called!\n"); abort(); } +/* Generated stub for param_feerate */ +struct command_result *param_feerate(struct command *cmd UNNEEDED, const char *name UNNEEDED, + const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + u32 **feerate UNNEEDED) +{ fprintf(stderr, "param_feerate called!\n"); abort(); } /* Generated stub for param_label */ struct command_result *param_label(struct command *cmd UNNEEDED, const char *name UNNEEDED, const char * buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, diff --git a/tests/test_closing.py b/tests/test_closing.py index dc93ff336c4c..20289d5a5e2f 100644 --- a/tests/test_closing.py +++ b/tests/test_closing.py @@ -2937,3 +2937,30 @@ def test_anysegwit_close_needs_feature(node_factory, bitcoind): l1.rpc.close(l2.info['id'], destination='bcrt1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k0ylj56') wait_for(lambda: only_one(only_one(l1.rpc.listpeers()['peers'])['channels'])['state'] == 'CLOSINGD_COMPLETE') bitcoind.generate_block(1, wait_for_mempool=1) + + +def test_close_feerate_range(node_factory, bitcoind, chainparams): + """Test the quick-close fee range negotiation""" + l1, l2 = node_factory.line_graph(2, opts={'experimental-quick-close': None}) + + # Lowball the range here. + l1.rpc.close(l2.info['id'], feerange=['253perkw', 'normal']) + + if not chainparams['elements']: + l1_range = [138, 4110] + if EXPERIMENTAL_FEATURES: + # This doesn't base max on last commitment tx, because anchors + l2_range = [1027, 6028] + else: + # This does + l2_range = [1027, 7964] + else: + # That fee output is a little chunky. + l1_range = [175, 5212] + l2_range = [1303, 13134] + + l1.daemon.wait_for_log('Negotiating closing fee between {}sat and {}sat satoshi'.format(l1_range[0], l1_range[1])) + l2.daemon.wait_for_log('Negotiating closing fee between {}sat and {}sat satoshi'.format(l2_range[0], l2_range[1])) + + overlap = [max(l1_range[0], l2_range[0]), min(l1_range[1], l2_range[1])] + l1.daemon.wait_for_log('performing quickclose in range {}sat-{}sat'.format(overlap[0], overlap[1])) diff --git a/wallet/db_postgres_sqlgen.c b/wallet/db_postgres_sqlgen.c index 6d69b9e801df..51cb412fb53a 100644 --- a/wallet/db_postgres_sqlgen.c +++ b/wallet/db_postgres_sqlgen.c @@ -1924,4 +1924,4 @@ struct db_query db_postgres_queries[] = { #endif /* LIGHTNINGD_WALLET_GEN_DB_POSTGRES */ -// SHA256STAMP:3c0924d3409c1d356d9f90291ae54895d85f34c6a3a42388ad4fb6bfbf13820d +// SHA256STAMP:e007757d76b68fe7ac8ebffb64ba88f0b32ed762602fdf6efd14aafe286c1bb5 diff --git a/wallet/db_sqlite3_sqlgen.c b/wallet/db_sqlite3_sqlgen.c index 945e4408592a..b919e2ab3f77 100644 --- a/wallet/db_sqlite3_sqlgen.c +++ b/wallet/db_sqlite3_sqlgen.c @@ -1924,4 +1924,4 @@ struct db_query db_sqlite3_queries[] = { #endif /* LIGHTNINGD_WALLET_GEN_DB_SQLITE3 */ -// SHA256STAMP:3c0924d3409c1d356d9f90291ae54895d85f34c6a3a42388ad4fb6bfbf13820d +// SHA256STAMP:e007757d76b68fe7ac8ebffb64ba88f0b32ed762602fdf6efd14aafe286c1bb5 diff --git a/wallet/statements_gettextgen.po b/wallet/statements_gettextgen.po index f750b00a6e2a..cfc0c159cbec 100644 --- a/wallet/statements_gettextgen.po +++ b/wallet/statements_gettextgen.po @@ -1262,11 +1262,11 @@ msgstr "" msgid "not a valid SQL statement" msgstr "" -#: wallet/test/run-wallet.c:1451 +#: wallet/test/run-wallet.c:1456 msgid "SELECT COUNT(1) FROM channel_funding_inflights WHERE channel_id = ?;" msgstr "" -#: wallet/test/run-wallet.c:1649 +#: wallet/test/run-wallet.c:1654 msgid "INSERT INTO channels (id) VALUES (1);" msgstr "" -# SHA256STAMP:913e7b10afc4160e8b437a2c54839d996a94667fc0c00a22f3ca97856491ac72 +# SHA256STAMP:ff01a174921bb944901b3942ca70654852fedf045b0cc929dd89f956f3c7b654 diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 1ad0c83ac3c7..9fb5ba5ce5a5 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -575,6 +575,11 @@ struct command_result *param_channel_id(struct command *cmd UNNEEDED, const jsmntok_t *tok UNNEEDED, struct channel_id **cid UNNEEDED) { fprintf(stderr, "param_channel_id called!\n"); abort(); } +/* Generated stub for param_feerate */ +struct command_result *param_feerate(struct command *cmd UNNEEDED, const char *name UNNEEDED, + const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + u32 **feerate UNNEEDED) +{ fprintf(stderr, "param_feerate called!\n"); abort(); } /* Generated stub for param_loglevel */ struct command_result *param_loglevel(struct command *cmd UNNEEDED, const char *name UNNEEDED,