From d06024cef7cbadcebf5f221f7c24c2c94c7caade Mon Sep 17 00:00:00 2001 From: dovgopoly Date: Tue, 6 Jan 2026 10:16:12 +0200 Subject: [PATCH] bcli: convert `estimatefees` to synchronous execution Add `command_err_badjson` helper for sync error handling, mirroring the async `command_err_bcli_badjson`. Store args string in `bcli_result` for consistent error messages. --- plugins/bcli.c | 184 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 134 insertions(+), 50 deletions(-) diff --git a/plugins/bcli.c b/plugins/bcli.c index ce0d6c123..5174012be 100644 --- a/plugins/bcli.c +++ b/plugins/bcli.c @@ -96,6 +96,8 @@ struct bcli_result { char *output; size_t output_len; int exitstatus; + /* Command args string for error messages */ + const char *args; }; /* Add the n'th arg to *args, incrementing n and keeping args of size n+1 */ @@ -248,13 +250,14 @@ run_bitcoin_cliv(const tal_t *ctx, res = tal(ctx, struct bcli_result); res->output = grab_fd_str(res, from); res->output_len = strlen(res->output); + res->args = args_string(res, cmd, stdinargs); /* Wait for child to exit */ while (waitpid(child, &status, 0) < 0 && errno == EINTR); if (!WIFEXITED(status)) plugin_err(plugin, "%s died with signal %i", - args_string(tmpctx, cmd, stdinargs), WTERMSIG(status)); + res->args, WTERMSIG(status)); res->exitstatus = WEXITSTATUS(status); @@ -490,6 +493,15 @@ static struct command_result *command_err_bcli_badjson(struct bitcoin_cli *bcli, return command_done_err(bcli->cmd, BCLI_ERROR, err, NULL); } +static struct command_result *command_err_badjson(struct command *cmd, + struct bcli_result *res, + const char *errmsg) +{ + char *err = tal_fmt(cmd, "%s: bad JSON: %s (%.*s)", + res->args, errmsg, (int)res->output_len, res->output); + return command_done_err(cmd, BCLI_ERROR, err, NULL); +} + /* Don't use this in general: it's better to omit fields. */ static void json_add_null(struct json_stream *stream, const char *fieldname) { @@ -591,19 +603,19 @@ struct estimatefees_stash { }; static struct command_result * -estimatefees_null_response(struct bitcoin_cli *bcli) +estimatefees_null_response(struct command *cmd) { - struct json_stream *response = jsonrpc_stream_success(bcli->cmd); + struct json_stream *response = jsonrpc_stream_success(cmd); /* We give a floor, which is the standard minimum */ json_array_start(response, "feerates"); json_array_end(response); json_add_u32(response, "feerate_floor", 1000); - return command_finished(bcli->cmd, response); + return command_finished(cmd, response); } -static struct command_result * +static UNNEEDED struct command_result * estimatefees_parse_feerate(struct bitcoin_cli *bcli, u64 *feerate) { const jsmntok_t *tokens; @@ -626,7 +638,7 @@ estimatefees_parse_feerate(struct bitcoin_cli *bcli, u64 *feerate) } /* We return null if estimation failed, and bitcoin-cli will * exit with 0 but no feerate field on failure. */ - return estimatefees_null_response(bcli); + return estimatefees_null_response(bcli->cmd); } return NULL; @@ -923,13 +935,9 @@ static struct command_result *getchaininfo(struct command *cmd, NULL); } - tokens = json_parse_simple(cmd, res->output, res->output_len); - if (!tokens) { - return command_done_err(cmd, BCLI_ERROR, - tal_fmt(cmd, "getblockchaininfo: bad JSON: cannot parse (%s)", - res->output), - NULL); - } + tokens = json_parse_simple(res->output, res->output, res->output_len); + if (!tokens) + return command_err_badjson(cmd, res, "cannot parse"); err = json_scan(tmpctx, res->output, tokens, "{chain:%,headers:%,blocks:%,initialblockdownload:%}", @@ -937,12 +945,8 @@ static struct command_result *getchaininfo(struct command *cmd, JSON_SCAN(json_to_number, &headers), JSON_SCAN(json_to_number, &blocks), JSON_SCAN(json_to_bool, &ibd)); - if (err) { - return command_done_err(cmd, BCLI_ERROR, - tal_fmt(cmd, "getblockchaininfo: bad JSON: %s (%s)", - err, res->output), - NULL); - } + if (err) + return command_err_badjson(cmd, res, err); if (bitcoind->dev_ignore_ibd) ibd = false; @@ -962,30 +966,30 @@ static struct command_result *estimatefees_done(struct bitcoin_cli *bcli); /* Add a feerate, but don't publish one that bitcoind won't accept. */ static void json_add_feerate(struct json_stream *result, const char *fieldname, struct command *cmd, - const struct estimatefees_stash *stash, - uint64_t value) + u64 perkb_floor, + u64 value) { /* Anthony Towns reported signet had a 900kbtc fee block, and then * CLN got upset scanning feerate. It expects a u32. */ if (value > 0xFFFFFFFF) { plugin_log(cmd->plugin, LOG_UNUSUAL, - "Feerate %"PRIu64" is ridiculous: trimming to 32 bites", + "Feerate %"PRIu64" is ridiculous: trimming to 32 bits", value); value = 0xFFFFFFFF; } /* 0 is special, it means "unknown" */ - if (value && value < stash->perkb_floor) { + if (value && value < perkb_floor) { plugin_log(cmd->plugin, LOG_DBG, "Feerate %s raised from %"PRIu64 " perkb to floor of %"PRIu64, - fieldname, value, stash->perkb_floor); - json_add_u64(result, fieldname, stash->perkb_floor); + fieldname, value, perkb_floor); + json_add_u64(result, fieldname, perkb_floor); } else { json_add_u64(result, fieldname, value); } } -static struct command_result *estimatefees_next(struct command *cmd, +static UNNEEDED struct command_result *estimatefees_next(struct command *cmd, struct estimatefees_stash *stash) { struct json_stream *response; @@ -1010,7 +1014,7 @@ static struct command_result *estimatefees_next(struct command *cmd, continue; json_object_start(response, NULL); json_add_u32(response, "blocks", estimatefee_params[i].blocks); - json_add_feerate(response, "feerate", cmd, stash, stash->perkb[i]); + json_add_feerate(response, "feerate", cmd, stash->perkb_floor, stash->perkb[i]); json_object_end(response); } json_array_end(response); @@ -1018,7 +1022,7 @@ static struct command_result *estimatefees_next(struct command *cmd, return command_finished(cmd, response); } -static struct command_result *getminfees_done(struct bitcoin_cli *bcli) +static UNNEEDED struct command_result *getminfees_done(struct bitcoin_cli *bcli) { const jsmntok_t *tokens; const char *err; @@ -1026,7 +1030,7 @@ static struct command_result *getminfees_done(struct bitcoin_cli *bcli) struct estimatefees_stash *stash = bcli->stash; if (*bcli->exitstatus != 0) - return estimatefees_null_response(bcli); + return estimatefees_null_response(bcli->cmd); tokens = json_parse_simple(bcli->output, bcli->output, bcli->output_bytes); @@ -1048,6 +1052,72 @@ static struct command_result *getminfees_done(struct bitcoin_cli *bcli) return estimatefees_next(bcli->cmd, stash); } +/* Get the feerate floor from getmempoolinfo. + * Returns NULL on success (floor stored in *perkb_floor), or error response. */ +static struct command_result *get_feerate_floor(struct command *cmd, + u64 *perkb_floor) +{ + struct bcli_result *res; + const jsmntok_t *tokens; + const char *err; + u64 mempoolfee, relayfee; + + res = run_bitcoin_cli(cmd, cmd->plugin, "getmempoolinfo", NULL); + if (res->exitstatus != 0) + return estimatefees_null_response(cmd); + + tokens = json_parse_simple(res->output, res->output, res->output_len); + if (!tokens) + return command_err_badjson(cmd, res, "cannot parse"); + + err = json_scan(tmpctx, res->output, tokens, + "{mempoolminfee:%,minrelaytxfee:%}", + JSON_SCAN(json_to_bitcoin_amount, &mempoolfee), + JSON_SCAN(json_to_bitcoin_amount, &relayfee)); + if (err) + return command_err_badjson(cmd, res, err); + + *perkb_floor = max_u64(mempoolfee, relayfee); + return NULL; +} + +/* Get a single feerate from estimatesmartfee. + * Returns NULL on success (feerate stored in *perkb), or error response. */ +static struct command_result *get_feerate(struct command *cmd, + u32 blocks, + const char *style, + u64 *perkb) +{ + struct bcli_result *res; + const jsmntok_t *tokens; + + res = run_bitcoin_cli(cmd, cmd->plugin, "estimatesmartfee", + tal_fmt(tmpctx, "%u", blocks), style, NULL); + + if (res->exitstatus != 0) + return estimatefees_null_response(cmd); + + tokens = json_parse_simple(res->output, res->output, res->output_len); + if (!tokens) + return command_err_badjson(cmd, res, "cannot parse"); + + if (json_scan(tmpctx, res->output, tokens, "{feerate:%}", + JSON_SCAN(json_to_bitcoin_amount, perkb)) != NULL) { + /* Paranoia: if it had a feerate, but was malformed: */ + if (json_get_member(res->output, tokens, "feerate")) + return command_err_badjson(cmd, res, "cannot scan"); + /* Regtest fee estimation is generally awful: Fake it at min. */ + if (bitcoind->fake_fees) + *perkb = 1000; + else + /* We return null if estimation failed, and bitcoin-cli will + * exit with 0 but no feerate field on failure. */ + return estimatefees_null_response(cmd); + } + + return NULL; +} + /* Get the current feerates. We use an urgent feerate for unilateral_close and max, * a slightly less urgent feerate for htlc_resolution and penalty transactions, * a slow feerate for min, and a normal one for all others. @@ -1056,26 +1126,48 @@ static struct command_result *estimatefees(struct command *cmd, const char *buf UNUSED, const jsmntok_t *toks UNUSED) { - struct estimatefees_stash *stash = tal(cmd, struct estimatefees_stash); + struct command_result *err; + u64 perkb_floor; + u64 perkb[ARRAY_SIZE(estimatefee_params)]; + struct json_stream *response; if (!param(cmd, buf, toks, NULL)) return command_param_failed(); - start_bitcoin_cli(NULL, cmd, getminfees_done, true, - BITCOIND_LOW_PRIO, stash, - "getmempoolinfo", - NULL); - return command_still_pending(cmd); + err = get_feerate_floor(cmd, &perkb_floor); + if (err) + return err; + + for (size_t i = 0; i < ARRAY_SIZE(estimatefee_params); i++) { + err = get_feerate(cmd, estimatefee_params[i].blocks, + estimatefee_params[i].style, &perkb[i]); + if (err) + return err; + } + + response = jsonrpc_stream_success(cmd); + json_array_start(response, "feerates"); + for (size_t i = 0; i < ARRAY_SIZE(perkb); i++) { + if (!perkb[i]) + continue; + json_object_start(response, NULL); + json_add_u32(response, "blocks", estimatefee_params[i].blocks); + json_add_feerate(response, "feerate", cmd, perkb_floor, perkb[i]); + json_object_end(response); + } + json_array_end(response); + json_add_u64(response, "feerate_floor", perkb_floor); + return command_finished(cmd, response); } -static struct command_result *estimatefees_done(struct bitcoin_cli *bcli) +static UNNEEDED struct command_result *estimatefees_done(struct bitcoin_cli *bcli) { struct command_result *err; struct estimatefees_stash *stash = bcli->stash; /* If we cannot estimate fees, no need to continue bothering bitcoind. */ if (*bcli->exitstatus != 0) - return estimatefees_null_response(bcli); + return estimatefees_null_response(bcli->cmd); err = estimatefees_parse_feerate(bcli, &stash->perkb[stash->cursor]); if (err) @@ -1160,13 +1252,9 @@ static struct command_result *getutxout(struct command *cmd, return command_finished(cmd, response); } - tokens = json_parse_simple(cmd, res->output, res->output_len); - if (!tokens) { - return command_done_err(cmd, BCLI_ERROR, - tal_fmt(cmd, "gettxout: bad JSON: cannot parse (%s)", - res->output), - NULL); - } + tokens = json_parse_simple(res->output, res->output, res->output_len); + if (!tokens) + return command_err_badjson(cmd, res, "cannot parse"); err = json_scan(tmpctx, res->output, tokens, "{value:%,scriptPubKey:{hex:%}}", @@ -1174,12 +1262,8 @@ static struct command_result *getutxout(struct command *cmd, &output.amount.satoshis), /* Raw: bitcoind */ JSON_SCAN_TAL(cmd, json_tok_bin_from_hex, &output.script)); - if (err) { - return command_done_err(cmd, BCLI_ERROR, - tal_fmt(cmd, "gettxout: bad JSON: %s (%s)", - err, res->output), - NULL); - } + if (err) + return command_err_badjson(cmd, res, err); response = jsonrpc_stream_success(cmd); json_add_sats(response, "amount", output.amount);