From 14dc1c37ab5213b9c32349ccf49f24e6e6e8a743 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 27 Aug 2018 14:43:57 +0930 Subject: [PATCH] fundchannel / withdraw: allow explicit feerate setting. These are the two cases where we'll refuse without a fee estimate. Signed-off-by: Rusty Russell --- CHANGELOG.md | 1 + contrib/pylightning/lightning/lightning.py | 12 ++++++--- doc/lightning-fundchannel.7.txt | 7 ++++- doc/lightning-withdraw.7.txt | 6 ++++- lightningd/chaintopology.c | 30 ++++++++++++++++++++++ lightningd/chaintopology.h | 8 ++++++ lightningd/opening_control.c | 14 ++++++---- tests/test_connection.py | 13 ++++++++++ wallet/walletrpc.c | 17 +++++++----- 9 files changed, 90 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef19273b..ac59b36e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This release named by ZmnSCPxj. - JSON API: `ping` command to send a ping to a connected peer. - JSON API: `feerates` command to inject fee estimates manually, and retrieve current estimates. +- JSON API: `withdraw` and `fundchannel` can be given manual feerate. - Config: `--conf` option to set config file. - Documentation: Added CHANGELOG.md - pylightning: RpcError now has `method` and `payload` fields. diff --git a/contrib/pylightning/lightning/lightning.py b/contrib/pylightning/lightning/lightning.py index 548e16882..d90c2b3e2 100644 --- a/contrib/pylightning/lightning/lightning.py +++ b/contrib/pylightning/lightning/lightning.py @@ -331,13 +331,15 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("listpeers", payload) - def fundchannel(self, node_id, satoshi): + def fundchannel(self, node_id, satoshi, feerate=None, feeratestyle=None): """ Fund channel with {id} using {satoshi} satoshis" """ payload = { "id": node_id, - "satoshi": satoshi + "satoshi": satoshi, + "feerate": feerate, + "feeratestyle": feeratestyle } return self.call("fundchannel", payload) @@ -404,14 +406,16 @@ class LightningRpc(UnixDomainSocketRpc): """ return self.call("dev-memleak") - def withdraw(self, destination, satoshi): + def withdraw(self, destination, satoshi, feerate=None, feeratestyle=None): """ Send to {destination} address {satoshi} (or "all") amount via Bitcoin transaction """ payload = { "destination": destination, - "satoshi": satoshi + "satoshi": satoshi, + "feerate": feerate, + "feeratestyle": feeratestyle } return self.call("withdraw", payload) diff --git a/doc/lightning-fundchannel.7.txt b/doc/lightning-fundchannel.7.txt index d9c8b1db7..1df784a8d 100644 --- a/doc/lightning-fundchannel.7.txt +++ b/doc/lightning-fundchannel.7.txt @@ -8,7 +8,7 @@ lightning-fundchannel - Command for establishing a lightning channel. SYNOPSIS -------- -*fundchannel* 'id' 'satoshi' +*fundchannel* 'id' 'satoshi' ['feerate' 'feeratestyle'] DESCRIPTION ----------- @@ -27,6 +27,11 @@ The string 'all' can be used to specify all available funds (or 16777215 satoshi The value cannot be less than the dust limit, currently set to 546, nor more than 16777215 satoshi. +'feerate' is an optional feerate to use, overriding lightningd's +internal estimate. If specified, 'feeratestyle' must be either +'"perkw"' for if 'feerate' is in satoshi-per-kilosipa (weight), +or '"perkb"' for if 'feerate' is in bitcoind-style satoshi-per-kilobyte. + RETURN VALUE ------------ On success, the 'tx' and 'txid' of the transaction is returned, as well as the diff --git a/doc/lightning-withdraw.7.txt b/doc/lightning-withdraw.7.txt index 7701e5ecc..87ba84d79 100644 --- a/doc/lightning-withdraw.7.txt +++ b/doc/lightning-withdraw.7.txt @@ -9,7 +9,7 @@ internal wallet. SYNOPSIS -------- -*withdraw* 'destination' 'satoshi' +*withdraw* 'destination' 'satoshi' ['feerate' 'feeratestyle'] DESCRIPTION ----------- @@ -24,6 +24,10 @@ wallet (expressed, as name suggests, in satoshi). The string 'all' can be used to specify withdrawal of all available funds. +'feerate' is an optional feerate to use, overriding lightningd's +internal estimate. If specified, 'feeratestyle' must be either +'"perkw"' for if 'feerate' is in satoshi-per-kilosipa (weight), +or '"perkb"' for if 'feerate' is in bitcoind-style satoshi-per-kilobyte. RETURN VALUE ------------ diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index 60f83e8bc..1c5957fd4 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -447,6 +447,36 @@ u32 feerate_to_style(u32 feerate_perkw, enum feerate_style style) abort(); } +/* If we have both feerate and style, use that, otherwise use inbuilt if avail. + * Return false if we failed command, otherwise fills in feerate_perkw. */ +bool json_feerate_and_style(struct command *cmd, + const u32 *feerate, enum feerate_style *style, + u32 fallback_feerate_per_kw, + u32 *feerate_per_kw) +{ + if (feerate) { + if (!style) { + command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "'feerate' requires 'feeratestyle'"); + return false; + } + *feerate_per_kw = feerate_from_style(*feerate, *style); + return true; + } else { + if (style) { + command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "'feeratestyle' requires 'feerate'"); + return false; + } + *feerate_per_kw = fallback_feerate_per_kw; + if (!*feerate_per_kw) { + command_fail(cmd, LIGHTNINGD, "Cannot estimate fees"); + return false; + } + return true; + } +} + static void json_feerates(struct command *cmd, const char *buffer, const jsmntok_t *params) { diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index ced494752..c785dd528 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -150,6 +150,14 @@ u32 unilateral_feerate(struct chain_topology *topo); u32 feerate_from_style(u32 feerate, enum feerate_style style); u32 feerate_to_style(u32 feerate_perkw, enum feerate_style style); +/* If we have both feerate and style, use that, otherwise use fallback + * if nonzero. Return false if we failed command, otherwise fills in + * feerate_per_kw. */ +bool json_feerate_and_style(struct command *cmd, + const u32 *feerate, enum feerate_style *style, + u32 fallback_feerate_per_kw, + u32 *feerate_per_kw); + /* Broadcast a single tx, and rebroadcast as reqd (copies tx). * If failed is non-NULL, call that and don't rebroadcast. */ void broadcast_tx(struct chain_topology *topo, diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index 0f77cb3cf..8a4b1344d 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -764,7 +764,9 @@ static void json_fund_channel(struct command *cmd, struct pubkey *id; struct peer *peer; struct channel *channel; - u32 feerate_per_kw = opening_feerate(cmd->ld->topology); + unsigned int *feerate; + enum feerate_style *style; + u32 feerate_per_kw; u8 *msg; fc->cmd = cmd; @@ -773,16 +775,18 @@ static void json_fund_channel(struct command *cmd, if (!param(fc->cmd, buffer, params, p_req("id", json_tok_pubkey, &id), p_req("satoshi", json_tok_tok, &sattok), + p_opt("feerate", json_tok_number, &feerate), + p_opt("feeratestyle", json_tok_feerate_style, &style), NULL)) return; if (!json_tok_wtx(&fc->wtx, buffer, sattok, MAX_FUNDING_SATOSHI)) return; - if (!feerate_per_kw) { - command_fail(cmd, LIGHTNINGD, "Cannot estimate fees"); + if (!json_feerate_and_style(cmd, feerate, style, + opening_feerate(cmd->ld->topology), + &feerate_per_kw)) return; - } peer = peer_by_id(cmd->ld, id); if (!peer) { @@ -839,6 +843,6 @@ static void json_fund_channel(struct command *cmd, static const struct json_command fund_channel_command = { "fundchannel", json_fund_channel, - "Fund channel with {id} using {satoshi} (or 'all') satoshis" + "Fund channel with {id} using {satoshi} (or 'all') satoshis, at optional {feerate}" }; AUTODATA(json_command, &fund_channel_command); diff --git a/tests/test_connection.py b/tests/test_connection.py index b1e26694f..6061023a8 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1124,7 +1124,20 @@ def test_no_fee_estimate(node_factory, bitcoind, executor): with pytest.raises(RpcError, match=r'Cannot estimate fees'): l1.rpc.withdraw(l2.rpc.newaddr()['address'], 'all') + # Can with manual feerate. + l1.rpc.withdraw(l2.rpc.newaddr()['address'], 10000, 1500, 'perkb') + l1.rpc.fundchannel(l2.info['id'], 10**6, 2000, 'perkw') + + # Make sure we clean up cahnnel for later attempt. + l1.daemon.wait_for_log('sendrawtx exit 0') + l1.rpc.dev_fail(l2.info['id']) + l1.daemon.wait_for_log('sendrawtx exit 0') + bitcoind.generate_block(6) + wait_for(lambda: only_one(l1.rpc.getpeer(l2.info['id'])['channels'])['state'] == 'ONCHAIN') + wait_for(lambda: only_one(l2.rpc.getpeer(l1.info['id'])['channels'])['state'] == 'ONCHAIN') + # But can accept incoming connections. + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) l2.fund_channel(l1, 10**6) # Can do HTLCs. diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index 8bbf969ce..8dbeb3bdd 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -89,10 +89,10 @@ static void json_withdraw(struct command *cmd, { const jsmntok_t *desttok, *sattok; struct withdrawal *withdraw = tal(cmd, struct withdrawal); - - u32 feerate_per_kw = try_get_feerate(cmd->ld->topology, FEERATE_NORMAL); + u32 feerate_per_kw; + unsigned int *feerate; + enum feerate_style *style; struct bitcoin_tx *tx; - enum address_parse_result addr_parse; withdraw->cmd = cmd; @@ -101,16 +101,19 @@ static void json_withdraw(struct command *cmd, if (!param(cmd, buffer, params, p_req("destination", json_tok_tok, &desttok), p_req("satoshi", json_tok_tok, &sattok), + p_opt("feerate", json_tok_number, &feerate), + p_opt("feeratestyle", json_tok_feerate_style, &style), NULL)) return; if (!json_tok_wtx(&withdraw->wtx, buffer, sattok, -1ULL)) return; - if (!feerate_per_kw) { - command_fail(cmd, LIGHTNINGD, "Cannot estimate fees"); + if (!json_feerate_and_style(cmd, feerate, style, + try_get_feerate(cmd->ld->topology, + FEERATE_NORMAL), + &feerate_per_kw)) return; - } /* Parse address. */ addr_parse = json_tok_address_scriptpubkey(cmd, @@ -163,7 +166,7 @@ static void json_withdraw(struct command *cmd, static const struct json_command withdraw_command = { "withdraw", json_withdraw, - "Send to {destination} address {satoshi} (or 'all') amount via Bitcoin transaction", + "Send to {destination} address {satoshi} (or 'all') amount via Bitcoin transaction, at optional {feerate}", false, "Send funds from the internal wallet to the specified address. Either specify a number of satoshis to send or 'all' to sweep all funds in the internal wallet to the address." }; AUTODATA(json_command, &withdraw_command);