From c7c5affa3fe4eaa3bc046f439f8190705a89694a Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 24 Aug 2018 11:52:02 +0930 Subject: [PATCH] feerates: new command to inject/query fee estimates. This is useful mainly in the case where bitcoind is not giving estimates, but can also be used to bias results if you want. Signed-off-by: Rusty Russell --- CHANGELOG.md | 5 +- contrib/pylightning/lightning/lightning.py | 12 ++++ lightningd/chaintopology.c | 79 ++++++++++++++++++++++ tests/test_misc.py | 40 +++++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c979f9716..dd1b4222f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Sending lightningd a SIGHUP will make it reopen its `log-file`, if any. - Protocol: `option_data_loss_protect` now supported to protect peers against being out-of-date. +- JSON API: `feerates` command to inject fee estimates manually. ### Changed @@ -42,6 +43,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - We now try to connect to all known addresses for a peer, not just the one given or the first one announced. - Crash logs are now placed one-per file like `crash.log.20180822233752` +- We will no longer allow withdrawing funds or funding channels if we + do not have a fee estimate (eg. bitcoind not synced). ### Deprecated @@ -56,7 +59,7 @@ changes. used to exist and set to `GOSSIPING` before we opened a channel). `connected` will indicate if we're connected, and the `channels` array indicates individual channel states (if any). -- Options: `default-fee-rate` is no longer available. +- Options: `default-fee-rate` is no longer available; use `feerates`. - Removed all Deprecated options from 0.6. ### Fixed diff --git a/contrib/pylightning/lightning/lightning.py b/contrib/pylightning/lightning/lightning.py index 985b92027..6a27e3ed5 100644 --- a/contrib/pylightning/lightning/lightning.py +++ b/contrib/pylightning/lightning/lightning.py @@ -448,3 +448,15 @@ class LightningRpc(UnixDomainSocketRpc): "id": peer_id, } return self.call("disconnect", payload) + + def feerates(self, style, urgent=None, normal=None, slow=None): + """ + Supply feerate estimates manually. + """ + payload = { + "style": style, + "urgent": urgent, + "normal": normal, + "slow": slow + } + return self.call("feerates", payload) diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index f32dc4e31..b29433312 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -20,6 +20,7 @@ #include #include #include +#include #include /* Mutual recursion via timer. */ @@ -388,6 +389,84 @@ static void start_fee_estimate(struct chain_topology *topo) update_feerates, topo); } +static void json_feerates(struct command *cmd, + const char *buffer, const jsmntok_t *params) +{ + struct chain_topology *topo = cmd->ld->topology; + struct json_result *response; + u32 *urgent, *normal, *slow, feerates[NUM_FEERATES]; + bool missing; + const jsmntok_t *style; + bool bitcoind_style; + + if (!param(cmd, buffer, params, + p_req("style", json_tok_tok, &style), + p_opt("urgent", json_tok_number, &urgent), + p_opt("normal", json_tok_number, &normal), + p_opt("slow", json_tok_number, &slow), + NULL)) + return; + + /* update_feerates uses 0 as "don't know" */ + feerates[FEERATE_URGENT] = urgent ? *urgent : 0; + feerates[FEERATE_NORMAL] = normal ? *normal : 0; + feerates[FEERATE_SLOW] = slow ? *slow : 0; + + if (json_tok_streq(buffer, style, "sipa")) + bitcoind_style = false; + else if (json_tok_streq(buffer, style, "bitcoind")) { + /* Everyone uses satoshi per kbyte, but we use satoshi per ksipa + * (don't round down to zero though)! */ + for (size_t i = 0; i < ARRAY_SIZE(feerates); i++) + feerates[i] = (feerates[i] + 3) / 4; + bitcoind_style = true; + } else { + command_fail(cmd, JSONRPC2_INVALID_PARAMS, "invalid style"); + return; + } + + log_info(topo->log, + "feerates: inserting feerates in sipa/kb %u/%u/%u", + feerates[FEERATE_URGENT], + feerates[FEERATE_NORMAL], + feerates[FEERATE_SLOW]); + + update_feerates(topo->bitcoind, feerates, topo); + + missing = false; + for (size_t i = 0; i < ARRAY_SIZE(feerates); i++) { + feerates[i] = try_get_feerate(topo, i); + if (!feerates[i]) + missing = true; + if (bitcoind_style) + feerates[i] *= 4; + } + + response = new_json_result(cmd); + json_object_start(response, NULL); + json_object_start(response, bitcoind_style ? "bitcoind" : "sipa"); + for (size_t i = 0; i < ARRAY_SIZE(feerates); i++) { + if (!feerates[i]) + continue; + json_add_num(response, feerate_name(i), feerates[i]); + } + json_object_end(response); + + if (missing) + json_add_string(response, "warning", + "Some fee estimates unavailable: bitcoind startup?"); + json_object_end(response); + + command_success(cmd, response); +} + +static const struct json_command feerates_command = { + "feerates", + json_feerates, + "Add/query feerate estimates, either satoshi-per-kw ({style} sipa) or satoshi-per-kb ({style} bitcoind) for {urgent}, {normal} and {slow}." +}; +AUTODATA(json_command, &feerates_command); + static void next_updatefee_timer(struct chain_topology *topo) { /* This takes care of its own lifetime. */ diff --git a/tests/test_misc.py b/tests/test_misc.py index 42d4e67a4..6b87fd128 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -858,6 +858,46 @@ def test_ipv4_and_ipv6(node_factory): assert int(bind[0]['port']) == port +def test_feerates(node_factory): + l1 = node_factory.get_node(options={'log-level': 'io'}, start=False) + l1.bitcoind_cmd_override(cmd='estimatesmartfee', + failscript="""echo '{ "errors": [ "Insufficient data or no feerate found" ], "blocks": 0 }'; exit 0""") + l1.start() + + # Query feerates (shouldn't give any!) + feerates = l1.rpc.feerates('sipa') + assert len(feerates['sipa']) == 0 + assert feerates['warning'] == 'Some fee estimates unavailable: bitcoind startup?' + assert 'bitcoind' not in feerates + + feerates = l1.rpc.feerates('bitcoind') + assert len(feerates['bitcoind']) == 0 + assert feerates['warning'] == 'Some fee estimates unavailable: bitcoind startup?' + assert 'sipa' not in feerates + + # Now try setting them, one at a time. + feerates = l1.rpc.feerates('sipa', 15000) + assert len(feerates['sipa']) == 1 + assert feerates['sipa']['urgent'] == 15000 + assert feerates['warning'] == 'Some fee estimates unavailable: bitcoind startup?' + assert 'bitcoind' not in feerates + + feerates = l1.rpc.feerates('bitcoind', normal=25000) + assert len(feerates['bitcoind']) == 2 + assert feerates['bitcoind']['urgent'] == 15000 * 4 + assert feerates['bitcoind']['normal'] == 25000 + assert feerates['warning'] == 'Some fee estimates unavailable: bitcoind startup?' + assert 'sipa' not in feerates + + feerates = l1.rpc.feerates('sipa', None, None, 5000) + assert len(feerates['sipa']) == 3 + assert feerates['sipa']['urgent'] == 15000 + assert feerates['sipa']['normal'] == 25000 // 4 + assert feerates['sipa']['slow'] == 5000 + assert 'warning' not in feerates + assert 'bitcoind' not in feerates + + def test_logging(node_factory): # Since we redirect, node.start() will fail: do manually. l1 = node_factory.get_node(options={'log-file': 'logfile'}, may_fail=True, start=False)