lightningd/bitcoind: use the Bitcoin plugin for fee estimates

And remove bitcoin-cli interaction code, now unused.
This commit is contained in:
darosior 2020-01-09 18:41:07 +01:00 committed by Rusty Russell
parent c79ab0f1b0
commit ced444a605
1 changed files with 60 additions and 356 deletions

View File

@ -26,13 +26,6 @@
#include <lightningd/chaintopology.h>
#include <lightningd/plugin.h>
/* Bitcoind's web server has a default of 4 threads, with queue depth 16.
* It will *fail* rather than queue beyond that, so we must not stress it!
*
* This is how many request for each priority level we have.
*/
#define BITCOIND_MAX_PARALLEL 4
/* The names of the request we can make to our Bitcoin backend. */
static const char *methods[] = {"getchaininfo", "getrawblockbyheight",
"sendrawtransaction", "getutxout",
@ -91,324 +84,30 @@ void bitcoind_check_commands(struct bitcoind *bitcoind)
}
}
/* Add the n'th arg to *args, incrementing n and keeping args of size n+1 */
static void add_arg(const char ***args, const char *arg)
/* Our Bitcoin backend plugin gave us a bad response. We can't recover. */
static void bitcoin_plugin_error(struct bitcoind *bitcoind, const char *buf,
const jsmntok_t *toks, const char *method,
const char *reason)
{
tal_arr_expand(args, arg);
struct plugin *p = strmap_get(&bitcoind->pluginsmap, method);
fatal("%s error: bad response to %s (%s), response was %.*s",
p->cmd, method, reason,
toks->end - toks->start, buf + toks->start);
}
static const char **gather_args(const struct bitcoind *bitcoind,
const tal_t *ctx, const char *cmd, va_list ap)
{
const char **args = tal_arr(ctx, const char *, 1);
const char *arg;
/* `getfeerate`
*
* Gather feerate from our Bitcoin backend. Will set the feerate to `null`
* if estimation failed.
*
* Plugin response:
* {
* "feerate": <fee per KB>,
* }
*/
args[0] = bitcoind->cli ? bitcoind->cli : chainparams->cli;
if (chainparams->cli_args)
add_arg(&args, chainparams->cli_args);
if (bitcoind->datadir)
add_arg(&args, tal_fmt(args, "-datadir=%s", bitcoind->datadir));
if (bitcoind->rpcconnect)
add_arg(&args,
tal_fmt(args, "-rpcconnect=%s", bitcoind->rpcconnect));
if (bitcoind->rpcport)
add_arg(&args,
tal_fmt(args, "-rpcport=%s", bitcoind->rpcport));
if (bitcoind->rpcuser)
add_arg(&args, tal_fmt(args, "-rpcuser=%s", bitcoind->rpcuser));
if (bitcoind->rpcpass)
add_arg(&args,
tal_fmt(args, "-rpcpassword=%s", bitcoind->rpcpass));
add_arg(&args, cmd);
while ((arg = va_arg(ap, const char *)) != NULL)
add_arg(&args, tal_strdup(args, arg));
add_arg(&args, NULL);
return args;
}
struct bitcoin_cli {
struct list_node list;
struct estimatefee_call {
struct bitcoind *bitcoind;
int fd;
int *exitstatus;
pid_t pid;
const char **args;
struct timeabs start;
enum bitcoind_prio prio;
char *output;
size_t output_bytes;
size_t new_output;
bool (*process)(struct bitcoin_cli *);
void *cb;
void *cb_arg;
struct bitcoin_cli **stopper;
};
static struct io_plan *read_more(struct io_conn *conn, struct bitcoin_cli *bcli)
{
bcli->output_bytes += bcli->new_output;
if (bcli->output_bytes == tal_count(bcli->output))
tal_resize(&bcli->output, bcli->output_bytes * 2);
return io_read_partial(conn, bcli->output + bcli->output_bytes,
tal_count(bcli->output) - bcli->output_bytes,
&bcli->new_output, read_more, bcli);
}
static struct io_plan *output_init(struct io_conn *conn, struct bitcoin_cli *bcli)
{
bcli->output_bytes = bcli->new_output = 0;
bcli->output = tal_arr(bcli, char, 100);
return read_more(conn, bcli);
}
static void next_bcli(struct bitcoind *bitcoind, enum bitcoind_prio prio);
/* For printing: simple string of args (no secrets!) */
static char *args_string(const tal_t *ctx, const char **args)
{
size_t i;
char *ret = tal_strdup(ctx, args[0]);
for (i = 1; args[i]; i++) {
ret = tal_strcat(ctx, take(ret), " ");
if (strstarts(args[i], "-rpcpassword")) {
ret = tal_strcat(ctx, take(ret), "-rpcpassword=...");
} else if (strstarts(args[i], "-rpcuser")) {
ret = tal_strcat(ctx, take(ret), "-rpcuser=...");
} else {
ret = tal_strcat(ctx, take(ret), args[i]);
}
}
return ret;
}
static char *bcli_args(const tal_t *ctx, struct bitcoin_cli *bcli)
{
return args_string(ctx, bcli->args);
}
static void retry_bcli(struct bitcoin_cli *bcli)
{
list_add_tail(&bcli->bitcoind->pending[bcli->prio], &bcli->list);
next_bcli(bcli->bitcoind, bcli->prio);
}
/* We allow 60 seconds of spurious errors, eg. reorg. */
static void bcli_failure(struct bitcoind *bitcoind,
struct bitcoin_cli *bcli,
int exitstatus)
{
struct timerel t;
if (!bitcoind->error_count)
bitcoind->first_error_time = time_mono();
t = timemono_between(time_mono(), bitcoind->first_error_time);
if (time_greater(t, time_from_sec(bitcoind->retry_timeout)))
fatal("%s exited %u (after %u other errors) '%.*s'; "
"we have been retrying command for "
"--bitcoin-retry-timeout=%"PRIu64" seconds; "
"bitcoind setup or our --bitcoin-* configs broken?",
bcli_args(tmpctx, bcli),
exitstatus,
bitcoind->error_count,
(int)bcli->output_bytes,
bcli->output,
bitcoind->retry_timeout);
log_unusual(bitcoind->log,
"%s exited with status %u",
bcli_args(tmpctx, bcli), exitstatus);
bitcoind->error_count++;
/* Retry in 1 second (not a leak!) */
notleak(new_reltimer(bitcoind->ld->timers, notleak(bcli),
time_from_sec(1),
retry_bcli, bcli));
}
static void bcli_finished(struct io_conn *conn UNUSED, struct bitcoin_cli *bcli)
{
int ret, status;
struct bitcoind *bitcoind = bcli->bitcoind;
enum bitcoind_prio prio = bcli->prio;
bool ok;
u64 msec = time_to_msec(time_between(time_now(), bcli->start));
/* If it took over 10 seconds, that's rather strange. */
if (msec > 10000)
log_unusual(bitcoind->log,
"bitcoin-cli: finished %s (%"PRIu64" ms)",
bcli_args(tmpctx, bcli), msec);
assert(bitcoind->num_requests[prio] > 0);
/* FIXME: If we waited for SIGCHILD, this could never hang! */
while ((ret = waitpid(bcli->pid, &status, 0)) < 0 && errno == EINTR);
if (ret != bcli->pid)
fatal("%s %s", bcli_args(tmpctx, bcli),
ret == 0 ? "not exited?" : strerror(errno));
if (!WIFEXITED(status))
fatal("%s died with signal %i",
bcli_args(tmpctx, bcli),
WTERMSIG(status));
if (!bcli->exitstatus) {
if (WEXITSTATUS(status) != 0) {
bcli_failure(bitcoind, bcli, WEXITSTATUS(status));
bitcoind->num_requests[prio]--;
goto done;
}
} else
*bcli->exitstatus = WEXITSTATUS(status);
if (WEXITSTATUS(status) == 0)
bitcoind->error_count = 0;
bitcoind->num_requests[bcli->prio]--;
/* Don't continue if were only here because we were freed for shutdown */
if (bitcoind->shutdown)
return;
db_begin_transaction(bitcoind->ld->wallet->db);
ok = bcli->process(bcli);
db_commit_transaction(bitcoind->ld->wallet->db);
if (!ok)
bcli_failure(bitcoind, bcli, WEXITSTATUS(status));
else
tal_free(bcli);
done:
next_bcli(bitcoind, prio);
}
static void next_bcli(struct bitcoind *bitcoind, enum bitcoind_prio prio)
{
struct bitcoin_cli *bcli;
struct io_conn *conn;
if (bitcoind->num_requests[prio] >= BITCOIND_MAX_PARALLEL)
return;
bcli = list_pop(&bitcoind->pending[prio], struct bitcoin_cli, list);
if (!bcli)
return;
bcli->pid = pipecmdarr(NULL, &bcli->fd, &bcli->fd,
cast_const2(char **, bcli->args));
if (bcli->pid < 0)
fatal("%s exec failed: %s", bcli->args[0], strerror(errno));
bcli->start = time_now();
bitcoind->num_requests[prio]++;
/* This lifetime is attached to bitcoind command fd */
conn = notleak(io_new_conn(bitcoind, bcli->fd, output_init, bcli));
io_set_finish(conn, bcli_finished, bcli);
}
static bool process_donothing(struct bitcoin_cli *bcli UNUSED)
{
return true;
}
/* If stopper gets freed first, set process() to a noop. */
static void stop_process_bcli(struct bitcoin_cli **stopper)
{
(*stopper)->process = process_donothing;
(*stopper)->stopper = NULL;
}
/* It command finishes first, free stopper. */
static void remove_stopper(struct bitcoin_cli *bcli)
{
/* Calls stop_process_bcli, but we don't care. */
tal_free(bcli->stopper);
}
/* If ctx is non-NULL, and is freed before we return, we don't call process().
* process returns false() if it's a spurious error, and we should retry. */
static void
start_bitcoin_cli(struct bitcoind *bitcoind,
const tal_t *ctx,
bool (*process)(struct bitcoin_cli *),
bool nonzero_exit_ok,
enum bitcoind_prio prio,
void *cb, void *cb_arg,
char *cmd, ...)
{
va_list ap;
struct bitcoin_cli *bcli = tal(bitcoind, struct bitcoin_cli);
bcli->bitcoind = bitcoind;
bcli->process = process;
bcli->prio = prio;
bcli->cb = cb;
bcli->cb_arg = cb_arg;
if (ctx) {
/* Create child whose destructor will stop us calling */
bcli->stopper = tal(ctx, struct bitcoin_cli *);
*bcli->stopper = bcli;
tal_add_destructor(bcli->stopper, stop_process_bcli);
tal_add_destructor(bcli, remove_stopper);
} else
bcli->stopper = NULL;
if (nonzero_exit_ok)
bcli->exitstatus = tal(bcli, int);
else
bcli->exitstatus = NULL;
va_start(ap, cmd);
bcli->args = gather_args(bitcoind, bcli, cmd, ap);
va_end(ap);
list_add_tail(&bitcoind->pending[bcli->prio], &bcli->list);
next_bcli(bitcoind, bcli->prio);
}
static bool extract_feerate(struct bitcoin_cli *bcli,
const char *output, size_t output_bytes,
u64 *feerate)
{
const jsmntok_t *tokens, *feeratetok;
bool valid;
tokens = json_parse_input(output, output, output_bytes, &valid);
if (!tokens)
fatal("%s: %s response",
bcli_args(tmpctx, bcli),
valid ? "partial" : "invalid");
if (tokens[0].type != JSMN_OBJECT) {
log_unusual(bcli->bitcoind->log,
"%s: gave non-object (%.*s)?",
bcli_args(tmpctx, bcli),
(int)output_bytes, output);
return false;
}
feeratetok = json_get_member(output, tokens, "feerate");
if (!feeratetok)
return false;
return json_to_bitcoin_amount(output, feeratetok, feerate);
}
struct estimatefee {
size_t i;
const u32 *blocks;
const char **estmode;
@ -420,59 +119,74 @@ struct estimatefee {
};
static void do_one_estimatefee(struct bitcoind *bitcoind,
struct estimatefee *efee);
struct estimatefee_call *call);
static bool process_estimatefee(struct bitcoin_cli *bcli)
static void getfeerate_callback(const char *buf, const jsmntok_t *toks,
const jsmntok_t *idtok,
struct estimatefee_call *call)
{
const jsmntok_t *resulttok, *feeratetok;
u64 feerate;
struct estimatefee *efee = bcli->cb_arg;
resulttok = json_get_member(buf, toks, "result");
if (!resulttok)
bitcoin_plugin_error(call->bitcoind, buf, toks,
"getfeerate",
"bad 'result' field");
feeratetok = json_get_member(buf, resulttok, "feerate");
if (!feeratetok)
bitcoin_plugin_error(call->bitcoind, buf, toks,
"getfeerate",
"bad 'feerate' field");
/* FIXME: We could trawl recent blocks for median fee... */
if (!extract_feerate(bcli, bcli->output, bcli->output_bytes, &feerate)) {
log_unusual(bcli->bitcoind->log, "Unable to estimate %s/%u fee",
efee->estmode[efee->i], efee->blocks[efee->i]);
if (!json_to_u64(buf, feeratetok, &feerate)) {
log_unusual(call->bitcoind->log, "Unable to estimate %s/%u fee",
call->estmode[call->i], call->blocks[call->i]);
#if DEVELOPER
/* This is needed to test for failed feerate estimates
* in DEVELOPER mode */
efee->satoshi_per_kw[efee->i] = 0;
call->satoshi_per_kw[call->i] = 0;
#else
/* If we are in testnet mode we want to allow payments
* with the minimal fee even if the estimate didn't
* work out. This is less disruptive than erring out
* all the time. */
if (chainparams->testnet)
efee->satoshi_per_kw[efee->i] = FEERATE_FLOOR;
call->satoshi_per_kw[call->i] = FEERATE_FLOOR;
else
efee->satoshi_per_kw[efee->i] = 0;
call->satoshi_per_kw[call->i] = 0;
#endif
} else
/* Rate in satoshi per kw. */
efee->satoshi_per_kw[efee->i]
call->satoshi_per_kw[call->i]
= feerate_from_style(feerate, FEERATE_PER_KBYTE);
efee->i++;
if (efee->i == tal_count(efee->satoshi_per_kw)) {
efee->cb(bcli->bitcoind, efee->satoshi_per_kw, efee->arg);
tal_free(efee);
call->i++;
if (call->i == tal_count(call->satoshi_per_kw)) {
call->cb(call->bitcoind, call->satoshi_per_kw, call->arg);
tal_free(call);
} else {
/* Next */
do_one_estimatefee(bcli->bitcoind, efee);
do_one_estimatefee(call->bitcoind, call);
}
return true;
}
static void do_one_estimatefee(struct bitcoind *bitcoind,
struct estimatefee *efee)
struct estimatefee_call *call)
{
char blockstr[STR_MAX_CHARS(u32)];
struct jsonrpc_request *req;
snprintf(blockstr, sizeof(blockstr), "%u", efee->blocks[efee->i]);
start_bitcoin_cli(bitcoind, NULL, process_estimatefee, false,
BITCOIND_LOW_PRIO,
NULL, efee,
"estimatesmartfee", blockstr, efee->estmode[efee->i],
NULL);
req = jsonrpc_request_start(bitcoind, "getfeerate",
bitcoind->log, getfeerate_callback,
call);
json_add_num(req->stream, "blocks", call->blocks[call->i]);
json_add_string(req->stream, "mode", call->estmode[call->i]);
jsonrpc_request_end(req);
plugin_request_send(strmap_get(&bitcoind->pluginsmap,
"getfeerate"), req);
}
void bitcoind_estimate_fees_(struct bitcoind *bitcoind,
@ -482,8 +196,9 @@ void bitcoind_estimate_fees_(struct bitcoind *bitcoind,
const u32 satoshi_per_kw[], void *),
void *arg)
{
struct estimatefee *efee = tal(bitcoind, struct estimatefee);
struct estimatefee_call *efee = tal(bitcoind, struct estimatefee_call);
efee->bitcoind = bitcoind;
efee->i = 0;
efee->blocks = tal_dup_arr(efee, u32, blocks, num_estimates, 0);
efee->estmode = tal_dup_arr(efee, const char *, estmode, num_estimates,
@ -495,17 +210,6 @@ void bitcoind_estimate_fees_(struct bitcoind *bitcoind,
do_one_estimatefee(bitcoind, efee);
}
/* Our Bitcoin backend plugin gave us a bad response. We can't recover. */
static void bitcoin_plugin_error(struct bitcoind *bitcoind, const char *buf,
const jsmntok_t *toks, const char *method,
const char *reason)
{
struct plugin *p = strmap_get(&bitcoind->pluginsmap, method);
fatal("%s error: bad response to %s (%s), response was %.*s",
p->cmd, method, reason,
toks->end - toks->start, buf + toks->start);
}
/* `sendrawtransaction`
*
* Send a transaction to the Bitcoin backend plugin. If the broadcast was