From 70a79e3998ab129ca565c023ce4f264d87c33c46 Mon Sep 17 00:00:00 2001 From: darosior Date: Thu, 2 Jan 2020 21:04:03 +0100 Subject: [PATCH] plugins/bcli: a new plugin for gathering Bitcoin data Most is taken from lightningd/bitcoind and adapted. This currently exposes 5 commands: - `getchaininfo`, currently called at startup to check the network and whether we are on IBD. - `getrawblockbyheight`, which basically does the `getblockhash` + `getblock` trick. - `getfeerate` - `sendrawtransaction` - `getutxout`, used to gather infos about an output and currently used by `getfilteredblock` in `lightningd/bitcoind`. --- Makefile | 2 +- common/jsonrpc_errors.h | 3 + plugins/Makefile | 11 +- plugins/bcli.c | 755 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 767 insertions(+), 4 deletions(-) create mode 100644 plugins/bcli.c diff --git a/Makefile b/Makefile index 749e6ddab..7f49817de 100644 --- a/Makefile +++ b/Makefile @@ -503,7 +503,7 @@ PKGLIBEXEC_PROGRAMS = \ lightningd/lightning_hsmd \ lightningd/lightning_onchaind \ lightningd/lightning_openingd -PLUGINS=plugins/pay plugins/autoclean plugins/fundchannel +PLUGINS=plugins/pay plugins/autoclean plugins/fundchannel plugins/bcli install-program: installdirs $(BIN_PROGRAMS) $(PKGLIBEXEC_PROGRAMS) $(PLUGINS) @$(NORMAL_INSTALL) diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h index 791cdf3fc..0e4f206f8 100644 --- a/common/jsonrpc_errors.h +++ b/common/jsonrpc_errors.h @@ -51,6 +51,9 @@ static const errcode_t FUNDING_UNKNOWN_PEER = 306; static const errcode_t CONNECT_NO_KNOWN_ADDRESS = 400; static const errcode_t CONNECT_ALL_ADDRESSES_FAILED = 401; +/* bitcoin-cli plugin errors */ +#define BCLI_ERROR 400 + /* Errors from `invoice` command */ static const errcode_t INVOICE_LABEL_ALREADY_EXISTS = 900; static const errcode_t INVOICE_PREIMAGE_ALREADY_EXISTS = 901; diff --git a/plugins/Makefile b/plugins/Makefile index fae47b922..441438262 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -7,6 +7,9 @@ PLUGIN_AUTOCLEAN_OBJS := $(PLUGIN_AUTOCLEAN_SRC:.c=.o) PLUGIN_FUNDCHANNEL_SRC := plugins/fundchannel.c PLUGIN_FUNDCHANNEL_OBJS := $(PLUGIN_FUNDCHANNEL_SRC:.c=.o) +PLUGIN_BCLI_SRC := plugins/bcli.c +PLUGIN_BCLI_OBJS := $(PLUGIN_BCLI_SRC:.c=.o) + PLUGIN_LIB_SRC := plugins/libplugin.c PLUGIN_LIB_HEADER := plugins/libplugin.h PLUGIN_LIB_OBJS := $(PLUGIN_LIB_SRC:.c=.o) @@ -51,11 +54,13 @@ plugins/autoclean: bitcoin/chainparams.o $(PLUGIN_AUTOCLEAN_OBJS) $(PLUGIN_LIB_O plugins/fundchannel: common/addr.o $(PLUGIN_FUNDCHANNEL_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) -$(PLUGIN_PAY_OBJS) $(PLUGIN_AUTOCLEAN_OBJS) $(PLUGIN_FUNDCHANNEL_OBJS) $(PLUGIN_LIB_OBJS): $(PLUGIN_LIB_HEADER) +plugins/bcli: bitcoin/chainparams.o $(PLUGIN_BCLI_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) + +$(PLUGIN_PAY_OBJS) $(PLUGIN_AUTOCLEAN_OBJS) $(PLUGIN_FUNDCHANNEL_OBJS) $(PLUGIN_BCLI_OBJS) $(PLUGIN_LIB_OBJS): $(PLUGIN_LIB_HEADER) # Make sure these depend on everything. -ALL_PROGRAMS += plugins/pay plugins/autoclean plugins/fundchannel -ALL_OBJS += $(PLUGIN_PAY_OBJS) $(PLUGIN_AUTOCLEAN_OBJS) $(PLUGIN_FUNDCHANNEL_OBJS) $(PLUGIN_LIB_OBJS) +ALL_PROGRAMS += plugins/pay plugins/autoclean plugins/fundchannel plugins/bcli +ALL_OBJS += $(PLUGIN_PAY_OBJS) $(PLUGIN_AUTOCLEAN_OBJS) $(PLUGIN_FUNDCHANNEL_OBJS) $(PLUGIN_BCLI_OBJS) $(PLUGIN_LIB_OBJS) check-source: $(PLUGIN_PAY_SRC:%=check-src-include-order/%) $(PLUGIN_AUTOCLEAN_SRC:%=check-src-include-order/%) $(PLUGIN_FUNDCHANNEL_SRC:%=check-src-include-order/%) check-source-bolt: $(PLUGIN_PAY_SRC:%=bolt-check/%) $(PLUGIN_AUTOCLEAN_SRC:%=bolt-check/%) $(PLUGIN_FUNDCHANNEL_SRC:%=bolt-check/%) diff --git a/plugins/bcli.c b/plugins/bcli.c new file mode 100644 index 000000000..34019035f --- /dev/null +++ b/plugins/bcli.c @@ -0,0 +1,755 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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 + +enum bitcoind_prio { + BITCOIND_LOW_PRIO, + BITCOIND_HIGH_PRIO +}; +#define BITCOIND_NUM_PRIO (BITCOIND_HIGH_PRIO+1) + +struct bitcoind { + /* eg. "bitcoin-cli" */ + char *cli; + + /* -datadir arg for bitcoin-cli. */ + char *datadir; + + /* Is bitcoind synced? If not, we retry. */ + bool synced; + + /* How many high/low prio requests are we running (it's ratelimited) */ + size_t num_requests[BITCOIND_NUM_PRIO]; + + /* Pending requests (high and low prio). */ + struct list_head pending[BITCOIND_NUM_PRIO]; + + /* If non-zero, time we first hit a bitcoind error. */ + unsigned int error_count; + struct timemono first_error_time; + + /* How long to keep trying to contact bitcoind + * before fatally exiting. */ + u64 retry_timeout; + + /* Passthrough parameters for bitcoin-cli */ + char *rpcuser, *rpcpass, *rpcconnect, *rpcport; +}; + +static struct bitcoind *bitcoind; + +struct bitcoin_cli { + struct list_node list; + 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; + struct command_result *(*process)(struct bitcoin_cli *); + struct command *cmd; + /* Used to stash content between multiple calls */ + void *stash; +}; + +/* 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) +{ + tal_arr_expand(args, arg); +} + +static const char **gather_args(const tal_t *ctx, const char *cmd, const char **cmd_args) +{ + const char **args = tal_arr(ctx, const char *, 1); + + 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); + for (size_t i = 0; i < tal_count(cmd_args); i++) + add_arg(&args, cmd_args[i]); + add_arg(&args, NULL); + + return args; +} + +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(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(struct bitcoin_cli *bcli) +{ + return args_string(bcli, bcli->args); +} + +static void retry_bcli(void *cb_arg) +{ + struct bitcoin_cli *bcli = cb_arg; + list_add_tail(&bitcoind->pending[bcli->prio], &bcli->list); + next_bcli(bcli->prio); +} + +/* We allow 60 seconds of spurious errors, eg. reorg. */ +static void bcli_failure(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))) + plugin_err(bcli->cmd->plugin, + "%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(bcli), + exitstatus, + bitcoind->error_count, + (int)bcli->output_bytes, + bcli->output, + bitcoind->retry_timeout); + + plugin_log(bcli->cmd->plugin, LOG_UNUSUAL, "%s exited with status %u", + bcli_args(bcli), exitstatus); + bitcoind->error_count++; + + /* Retry in 1 second (not a leak!) */ + plugin_timer(bcli->cmd->plugin, time_from_sec(1), retry_bcli, bcli); +} + +static void bcli_finished(struct io_conn *conn UNUSED, struct bitcoin_cli *bcli) +{ + int ret, status; + struct command_result *res; + enum bitcoind_prio prio = bcli->prio; + u64 msec = time_to_msec(time_between(time_now(), bcli->start)); + + /* If it took over 10 seconds, that's rather strange. */ + if (msec > 10000) + plugin_log(bcli->cmd->plugin, LOG_UNUSUAL, + "bitcoin-cli: finished %s (%"PRIu64" ms)", + bcli_args(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) + plugin_err(bcli->cmd->plugin, "%s %s", bcli_args(bcli), + ret == 0 ? "not exited?" : strerror(errno)); + + if (!WIFEXITED(status)) + plugin_err(bcli->cmd->plugin, "%s died with signal %i", + bcli_args(bcli), + WTERMSIG(status)); + + /* Implicit nonzero_exit_ok == false */ + if (!bcli->exitstatus) { + if (WEXITSTATUS(status) != 0) { + bcli_failure(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]--; + + res = bcli->process(bcli); + if (!res) + bcli_failure(bcli, WEXITSTATUS(status)); + else + tal_free(bcli); + +done: + next_bcli(prio); +} + +static void next_bcli(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) + plugin_err(bcli->cmd->plugin, "%s exec failed: %s", + bcli->args[0], strerror(errno)); + + bcli->start = time_now(); + + bitcoind->num_requests[prio]++; + + conn = io_new_conn(bcli, bcli->fd, output_init, bcli); + io_set_finish(conn, bcli_finished, bcli); +} + +/* 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(const tal_t *ctx, + struct command *cmd, + struct command_result *(*process)(struct bitcoin_cli *), + bool nonzero_exit_ok, + enum bitcoind_prio prio, + char *method, const char **method_args, + void *stash) +{ + struct bitcoin_cli *bcli = tal(bitcoind, struct bitcoin_cli); + + bcli->process = process; + bcli->cmd = cmd; + bcli->prio = prio; + + if (nonzero_exit_ok) + bcli->exitstatus = tal(bcli, int); + else + bcli->exitstatus = NULL; + + bcli->args = gather_args(bcli, method, method_args); + bcli->stash = stash; + + list_add_tail(&bitcoind->pending[bcli->prio], &bcli->list); + next_bcli(bcli->prio); +} + +static struct command_result *process_getutxout(struct bitcoin_cli *bcli) +{ + const jsmntok_t *tokens, *valuetok, *scriptpubkeytok, *hextok; + struct json_stream *response; + bool valid; + struct bitcoin_tx_output output; + char *err; + + /* As of at least v0.15.1.0, bitcoind returns "success" but an empty + string on a spent txout. */ + if (*bcli->exitstatus != 0 || bcli->output_bytes == 0) { + response = jsonrpc_stream_success(bcli->cmd); + json_add_null(response, "amount"); + json_add_null(response, "script"); + + return command_finished(bcli->cmd, response); + } + + tokens = json_parse_input(bcli->output, bcli->output, + bcli->output_bytes, &valid); + if (!tokens) { + err = tal_fmt(bcli, "%s: %s response", bcli_args(bcli), + valid ? "partial" : "invalid"); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + if (tokens[0].type != JSMN_OBJECT) { + err = tal_fmt(bcli, "%s: gave non-object (%.*s)?", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + valuetok = json_get_member(bcli->output, tokens, "value"); + if (!valuetok) { + err = tal_fmt(bcli,"%s: had no value member (%.*s)?", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + if (!json_to_bitcoin_amount(bcli->output, valuetok, &output.amount.satoshis)) {/* Raw: talking to bitcoind */ + err = tal_fmt(bcli, "%s: had bad value (%.*s)?", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + scriptpubkeytok = json_get_member(bcli->output, tokens, "scriptPubKey"); + if (!scriptpubkeytok) { + err = tal_fmt(bcli, "%s: had no scriptPubKey member (%.*s)?", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + hextok = json_get_member(bcli->output, scriptpubkeytok, "hex"); + if (!hextok) { + err = tal_fmt(bcli, "%s: had no scriptPubKey->hex member (%.*s)?", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + output.script = tal_hexdata(bcli, bcli->output + hextok->start, + hextok->end - hextok->start); + if (!output.script) { + err = tal_fmt(bcli, "%s: scriptPubKey->hex invalid hex (%.*s)?", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + response = jsonrpc_stream_success(bcli->cmd); + json_add_amount_sat_only(response, "amount", output.amount); + json_add_string(response, "script", + tal_hexstr(response, output.script, sizeof(output.script))); + + return command_finished(bcli->cmd, response); +} + +static struct command_result *process_getblockchaininfo(struct bitcoin_cli *bcli) +{ + const jsmntok_t *tokens, *chaintok, *headerstok, *blockstok, *ibdtok; + struct json_stream *response; + bool valid, ibd; + u32 headers, blocks; + char *err; + + tokens = json_parse_input(bcli, bcli->output, bcli->output_bytes, + &valid); + if (!tokens) { + err = tal_fmt(bcli->cmd, "%s: %s response (%.*s)", + bcli_args(bcli), valid ? "partial" : "invalid", + (int)bcli->output_bytes, bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + if (tokens[0].type != JSMN_OBJECT) { + err = tal_fmt(bcli->cmd, "%s: gave non-object (%.*s)", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + chaintok = json_get_member(bcli->output, tokens, "chain"); + if (!chaintok) { + err = tal_fmt(bcli->cmd, "%s: bad 'chain' field (%.*s)", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + headerstok = json_get_member(bcli->output, tokens, "headers"); + if (!headerstok || !json_to_number(bcli->output, headerstok, &headers)) { + err = tal_fmt(bcli->cmd, "%s: bad 'headers' field (%.*s)", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + blockstok = json_get_member(bcli->output, tokens, "blocks"); + if (!blockstok || !json_to_number(bcli->output, blockstok, &blocks)) { + err = tal_fmt(bcli->cmd, "%s: bad 'blocks' field (%.*s)", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + ibdtok = json_get_member(bcli->output, tokens, "initialblockdownload"); + if (!ibdtok || !json_to_bool(bcli->output, ibdtok, &ibd)) { + err = tal_fmt(bcli->cmd, "%s: bad 'initialblockdownload' field (%.*s)", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + response = jsonrpc_stream_success(bcli->cmd); + json_add_string(response, "chain", + json_strdup(response, bcli->output, chaintok)); + json_add_u32(response, "headercount", headers); + json_add_u32(response, "blockcount", blocks); + json_add_bool(response, "ibd", ibd); + + return command_finished(bcli->cmd, response); +} + +static struct command_result *process_estimatefee(struct bitcoin_cli *bcli) +{ + const jsmntok_t *tokens, *feeratetok = NULL; + struct json_stream *response; + bool valid; + u64 feerate; + char *err; + + if (*bcli->exitstatus != 0) + goto end; + + tokens = json_parse_input(bcli->output, bcli->output, + (int)bcli->output_bytes, &valid); + if (!tokens) { + err = tal_fmt(bcli->cmd, "%s: %s response (%.*s)", + bcli_args(bcli), valid ? "partial" : "invalid", + (int)bcli->output_bytes, bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + if (tokens[0].type != JSMN_OBJECT) { + err = tal_fmt(bcli->cmd, "%s: gave non-object (%.*s)", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + feeratetok = json_get_member(bcli->output, tokens, "feerate"); + if (feeratetok && + !json_to_bitcoin_amount(bcli->output, feeratetok, &feerate)) { + err = tal_fmt(bcli->cmd, "%s: bad 'feerate' field (%.*s)", + bcli_args(bcli), (int)bcli->output_bytes, + bcli->output); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + +end: + response = jsonrpc_stream_success(bcli->cmd); + if (feeratetok) + json_add_u64(response, "feerate", feerate); + else + json_add_null(response, "feerate"); + + return command_finished(bcli->cmd, response); +} + +static struct command_result *process_sendrawtransaction(struct bitcoin_cli *bcli) +{ + struct json_stream *response; + + plugin_log(bcli->cmd->plugin, LOG_DBG, "sendrawtx exit %i (%s)", + *bcli->exitstatus, bcli_args(bcli)); + + response = jsonrpc_stream_success(bcli->cmd); + json_add_bool(response, "success", *bcli->exitstatus == 0); + json_add_string(response, "errmsg", + bcli->exitstatus ? tal_strndup(bcli, bcli->output, + bcli->output_bytes-1) : ""); + + return command_finished(bcli->cmd, response); +} + +struct getrawblock_stash { + const char *block_hash; + u32 block_height; + const char *block_hex; +}; + +static struct command_result *process_getrawblock(struct bitcoin_cli *bcli) +{ + struct json_stream *response; + struct getrawblock_stash *stash = bcli->stash; + + /* -1 to strip \n. */ + stash->block_hex = tal_fmt(stash, "%.*s", + (int)bcli->output_bytes-1, bcli->output); + + response = jsonrpc_stream_success(bcli->cmd); + json_add_string(response, "blockhash", stash->block_hash); + json_add_string(response, "block", stash->block_hex); + + return command_finished(bcli->cmd, response); +} + +static struct command_result * +getrawblockbyheight_notfound(struct bitcoin_cli *bcli) +{ + struct json_stream *response; + + response = jsonrpc_stream_success(bcli->cmd); + json_add_null(response, "blockhash"); + json_add_null(response, "block"); + + return command_finished(bcli->cmd, response); +} + +static struct command_result *process_getblockhash(struct bitcoin_cli *bcli) +{ + const char *err, **params; + struct getrawblock_stash *stash = bcli->stash; + + /* If it failed with error 8, give an empty response. */ + if (bcli->exitstatus && *bcli->exitstatus != 0) { + /* Other error means we have to retry. */ + if (*bcli->exitstatus != 8) + return NULL; + return getrawblockbyheight_notfound(bcli); + } + + /* `-1` to strip the newline character. */ + stash->block_hash = tal_strndup(stash, bcli->output, + bcli->output_bytes-1); + if (!stash->block_hash || strlen(stash->block_hash) != 64) { + err = tal_fmt(bcli->cmd, "%s: bad blockhash '%s'", + bcli_args(bcli), stash->block_hash); + return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); + } + + params = tal_arr(bcli->cmd, const char *, 2); + params[0] = stash->block_hash; + /* Non-verbose: raw block. */ + params[1] = "0"; + start_bitcoin_cli(NULL, bcli->cmd, process_getrawblock, false, + BITCOIND_HIGH_PRIO, "getblock", params, stash); + + return command_still_pending(bcli->cmd); +} + +/* Get a raw block given its height. + * Calls `getblockhash` then `getblock` to retrieve it from bitcoin_cli. + * Will return early with null fields if block isn't known (yet). + */ +static struct command_result *getrawblockbyheight(struct command *cmd, + const char *buf, + const jsmntok_t *toks) +{ + struct getrawblock_stash *stash; + u32 *height; + const char **params; + + /* bitcoin-cli wants a string. */ + if (!param(cmd, buf, toks, + p_req("height", param_number, &height), + NULL)) + return command_param_failed(); + + stash = tal(cmd, struct getrawblock_stash); + stash->block_height = *height; + + params = tal_arr(cmd, const char *, 1); + params[0] = tal_fmt(params, "%u", *height); + start_bitcoin_cli(NULL, cmd, process_getblockhash, true, + BITCOIND_LOW_PRIO, "getblockhash", params, stash); + + return command_still_pending(cmd); +} + +/* Get infos about the block chain. + * Calls `getblockchaininfo` and returns headers count, blocks count, + * the chain id, and whether this is initialblockdownload. + */ +static struct command_result *getchaininfo(struct command *cmd, + const char *buf UNUSED, + const jsmntok_t *toks UNUSED) +{ + if (!param(cmd, buf, toks, NULL)) + return command_param_failed(); + + start_bitcoin_cli(NULL, cmd, process_getblockchaininfo, false, + BITCOIND_HIGH_PRIO, "getblockchaininfo", NULL, NULL); + + return command_still_pending(cmd); +} + +/* Get current feerate. + * Calls `estimatesmartfee` and returns the feerate as btc/k*VBYTE*. + */ +static struct command_result *getfeerate(struct command *cmd, + const char *buf UNUSED, + const jsmntok_t *toks UNUSED) +{ + u32 *blocks; + const char **params = tal_arr(cmd, const char *, 2); + + if (!param(cmd, buf, toks, + p_req("blocks", param_number, &blocks), + p_req("mode", param_string, ¶ms[1]), + NULL)) + return command_param_failed(); + + params[0] = tal_fmt(params, "%u", *blocks); + start_bitcoin_cli(NULL, cmd, process_estimatefee, true, + BITCOIND_LOW_PRIO, "estimatesmartfee", params, NULL); + + return command_still_pending(cmd); +} + +/* Send a transaction to the Bitcoin network. + * Calls `sendrawtransaction` using the first parameter as the raw tx. + */ +static struct command_result *sendrawtransaction(struct command *cmd, + const char *buf, + const jsmntok_t *toks) +{ + const char **params = tal_arr(cmd, const char *, 1); + + /* bitcoin-cli wants strings. */ + if (!param(cmd, buf, toks, + p_req("tx", param_string, ¶ms[0]), + NULL)) + return command_param_failed(); + + start_bitcoin_cli(NULL, cmd, process_sendrawtransaction, true, + BITCOIND_HIGH_PRIO, "sendrawtransaction", params, NULL); + + return command_still_pending(cmd); +} + +static struct command_result *getutxout(struct command *cmd, + const char *buf, + const jsmntok_t *toks) +{ + const char **params = tal_arr(cmd, const char *, 2); + + /* bitcoin-cli wants strings. */ + if (!param(cmd, buf, toks, + p_req("txid", param_string, ¶ms[0]), + p_req("vout", param_string, ¶ms[1]), + NULL)) + return command_param_failed(); + + start_bitcoin_cli(NULL, cmd, process_getutxout, true, + BITCOIND_HIGH_PRIO, "gettxout", params, NULL); + + return command_still_pending(cmd); +} + +/* Initialize the global context when handshake is done. */ +static void init(struct plugin *p UNUSED, const char *buffer UNUSED, + const jsmntok_t *config UNUSED) +{ + bitcoind = tal(NULL, struct bitcoind); + + bitcoind->cli = NULL; + bitcoind->datadir = NULL; + for (size_t i = 0; i < BITCOIND_NUM_PRIO; i++) { + bitcoind->num_requests[i] = 0; + list_head_init(&bitcoind->pending[i]); + } + bitcoind->error_count = 0; + bitcoind->retry_timeout = 60; + bitcoind->rpcuser = NULL; + bitcoind->rpcpass = NULL; + bitcoind->rpcconnect = NULL; + bitcoind->rpcport = NULL; +} + +static const struct plugin_command commands[] = { + { + "getrawblockbyheight", + "bitcoin", + "Get the bitcoin block at a given height", + "", + getrawblockbyheight + }, + { + "getchaininfo", + "bitcoin", + "Get the chain id, the header count, the block count," + " and whether this is IBD.", + "", + getchaininfo + }, + { + "getfeerate", + "bitcoin", + "Get the Bitcoin feerate in btc/kilo-vbyte.", + "", + getfeerate + }, + { + "sendrawtransaction", + "bitcoin", + "Send a raw transaction to the Bitcoin network.", + "", + sendrawtransaction + }, + { + "getutxout", + "bitcoin", + "Get informations about an output, identified by a {txid} an a {vout}", + "", + getutxout + }, +}; + +int main(int argc, char *argv[]) +{ + setup_locale(); + + /* FIXME: handle bitcoind options at init */ + plugin_main(argv, init, PLUGIN_STATIC, commands, ARRAY_SIZE(commands), + NULL, 0, NULL, 0, NULL); +}