bkpr: add `bkpr-currency` option to record currency rates on each event.
What changed, and why it matters
This commit adds an optional bookkeeping feature to Core Lightning that lets users specify a currency (like USD). The bookkeeper plugin then asks another plugin for the exchange rate at the time of each financial event and records it. It is a feature addition, not a fix for a known security bug. The main security-relevant observation is that the new code makes JSON-RPC calls to a 'currencyrate' plugin and stores the returned rate, but it does not validate the currency name or the returned rate beyond checking that the rate is a valid JSON double. A malicious or compromised 'currencyrate' plugin could return extreme or malformed values, but this only affects optional accounting records, not funds or consensus.
Treat as a routine feature commit. If reviewing for hardening, consider adding validation of the currency option (e.g., ISO-4217 code format) and sanity bounds on the returned rate before storing it. Users relying on this feature should ensure the currencyrate plugin is trustworthy, since its output is recorded as accounting data.
Security signals we found
New optional plugin option added: bkpr-currency
Plugin makes outbound JSON-RPC call to currencyrate with user-controlled currency string
Returned rate parsed as double via json_to_double without range validation
Rate stored in uintmap keyed by timestamp; duplicate timestamps ignored
Error handling logs once and continues; no abort or fallback rate
No input sanitization visible for ISO-4217 name beyond documentation note
Evidence from the diff
The patch introduces a new ‘bkpr-currency’ string option in the bookkeeper plugin. When set, parse_and_log_chain_move and parse_and_log_channel_move call lookup_currency(), which issues a jsonrpc ‘currencyrate’ request with the user-supplied currency string. The response is scanned for a ‘rate’ field as a double and stored in a UINTMAP keyed by event timestamp. Errors are logged once via warned_currency_fail. The currency string is passed directly to currencyrate without length or charset validation, and the returned double is not range-checked. There is no evidence in the commit of a vulnerability being fixed; it is a new feature with minor input-trust considerations.
Changed components
plugins/bkpr/bookkeeper.cplugins/bkpr/bookkeeper.hdoc/lightningd-config.5.mdInspect captured patch +115 / −1
diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md
index 4832809d..c64cbf67 100644
--- a/doc/lightningd-config.5.md
+++ b/doc/lightningd-config.5.md
@@ -571,6 +571,10 @@ command, so they invoices can also be paid onchain.
This option controls how many routes askrene will calculate at once: this is only useful on nodes which make multiple payments at once, and setting the number higher than your number of cores/CPUS will not help. The default is 4.
+* **bkpr-currency**=*name* [plugin `bookkeeper`, *dynamic*]
+
+ The *name* is an ISO-4217 name (e.g. USD), which will be passed to *currencyrate* to fetch the exchange rate for that currency on each bookkeeper event.
+
### Networking options
Note that for simple setups, the implicit *autolisten* option does the
diff --git a/plugins/bkpr/bookkeeper.c b/plugins/bkpr/bookkeeper.c
index 87caf878..ce6848d1 100644
--- a/plugins/bkpr/bookkeeper.c
+++ b/plugins/bkpr/bookkeeper.c
@@ -13,6 +13,7 @@
#include <common/coin_mvt.h>
#include <common/json_param.h>
#include <common/json_stream.h>
+#include <common/memleak.h>
#include <common/node_id.h>
#include <db/exec.h>
#include <errno.h>
@@ -1080,6 +1081,84 @@ static struct command_result *lookup_invoice_desc(struct command *cmd,
return send_outreq(req);
}
+struct currency_time {
+ struct refresh_info *rinfo;
+ u64 timestamp;
+};
+
+static struct command_result *currency_done(struct command *cmd,
+ const char *method,
+ const char *buf,
+ const jsmntok_t *result,
+ struct currency_time *ctime)
+{
+ struct bkpr *bkpr = bkpr_of(cmd->plugin);
+ double rate;
+ const char *err;
+
+ err = json_scan(cmd, buf, result, "{rate:%}",
+ JSON_SCAN(json_to_double, &rate));
+ if (err) {
+ plugin_log(cmd->plugin, LOG_BROKEN,
+ "Invalid currencyrate return '%.*s': %s",
+ json_tok_full_len(result),
+ json_tok_full(buf, result), err);
+ } else {
+ double *p = tal_dup(bkpr->currency_rates, double, &rate);
+ /* Can fail if we raced and asked twice */
+ if (!uintmap_add(bkpr->currency_rates,
+ ctime->timestamp,
+ p)) {
+ tal_free(p);
+ }
+ }
+ return rinfo_one_done(cmd, ctime->rinfo);
+}
+
+static struct command_result *currency_error(struct command *cmd,
+ const char *method,
+ const char *buf,
+ const jsmntok_t *error,
+ struct currency_time *ctime)
+{
+ struct bkpr *bkpr = bkpr_of(cmd->plugin);
+
+ if (!bkpr->warned_currency_fail) {
+ plugin_log(cmd->plugin, LOG_BROKEN,
+ "error calling %s: %.*s",
+ method, json_tok_full_len(error),
+ json_tok_full(buf, error));
+ bkpr->warned_currency_fail = true;
+ }
+
+ return rinfo_one_done(cmd, ctime->rinfo);
+}
+
+static void lookup_currency(struct command *cmd,
+ struct bkpr *bkpr,
+ enum mvt_tag primary_tag,
+ u64 timestamp,
+ struct refresh_info *rinfo)
+{
+ struct out_req *req;
+ struct currency_time *ctime;
+
+ /* If we already have the timestamp, we're done */
+ if (uintmap_get(bkpr->currency_rates, timestamp) != NULL)
+ return;
+
+ ctime = tal(cmd, struct currency_time);
+ ctime->timestamp = timestamp;
+ ctime->rinfo = use_rinfo(rinfo);
+ req = jsonrpc_request_start(cmd,
+ "currencyrate",
+ currency_done,
+ currency_error,
+ ctime);
+ json_add_string(req->js, "currency", bkpr->currency);
+ send_outreq(req);
+}
+
static enum mvt_tag *json_to_tags(const tal_t *ctx, const char *buffer, const jsmntok_t *tok)
{
size_t i;
@@ -1273,6 +1352,9 @@ parse_and_log_chain_move(struct command *cmd,
break;
}
}
+
+ if (bkpr->currency)
+ lookup_currency(cmd, bkpr, tag, e->timestamp, rinfo);
}
static void
@@ -1353,8 +1435,10 @@ parse_and_log_channel_move(struct command *cmd,
maybe_record_rebalance(cmd, bkpr, e);
lookup_invoice_desc(cmd, e->credit, e->payment_id, rinfo);
- return;
}
+
+ if (bkpr->currency)
+ lookup_currency(cmd, bkpr, tag, e->timestamp, rinfo);
}
static bool json_to_tok(const char *buffer, const jsmntok_t *tok, const jsmntok_t **ret)
@@ -1532,6 +1616,12 @@ static bool json_hex_to_be64(const char *buffer, const jsmntok_t *tok,
val, sizeof(*val));
}
+static void memleak_scan_currencyrates(struct htable *memtable,
+ currencymap_t *currency_rates)
+{
+ memleak_scan_uintmap(memtable, currency_rates);
+}
+
static const char *init(struct command *init_cmd, const char *b, const jsmntok_t *t)
{
struct plugin *p = init_cmd->plugin;
@@ -1569,11 +1659,20 @@ int main(int argc, char *argv[])
/* No datadir is default */
bkpr = tal(NULL, struct bkpr);
+ bkpr->currency = NULL;
+ bkpr->warned_currency_fail = false;
+ bkpr->currency_rates = tal(bkpr, currencymap_t);
+ uintmap_init(bkpr->currency_rates);
+ memleak_add_helper(bkpr->currency_rates, memleak_scan_currencyrates);
plugin_main(argv, init, take(bkpr), PLUGIN_STATIC, true, NULL,
commands, ARRAY_SIZE(commands),
notifs, ARRAY_SIZE(notifs),
NULL, 0,
NULL, 0,
+ plugin_option("bkpr-currency",
+ "string",
+ "Look up and record this currency on each event",
+ charp_option, charp_jsonfmt, &bkpr->currency),
NULL);
return 0;
diff --git a/plugins/bkpr/bookkeeper.h b/plugins/bkpr/bookkeeper.h
index 5358f2cd..f69d1564 100644
--- a/plugins/bkpr/bookkeeper.h
+++ b/plugins/bkpr/bookkeeper.h
@@ -2,10 +2,14 @@
#define LIGHTNING_PLUGINS_BKPR_BOOKKEEPER_H
#include "config.h"
+#include <ccan/intmap/intmap.h>
#include <common/json_parse.h>
struct command;
+/* For allocation convenience. */
+typedef UINTMAP(double *) currencymap_t;
+
struct bkpr {
/* The datastore-backed lookup tables for our annotations */
struct accounts *accounts;
@@ -16,6 +20,13 @@ struct bkpr {
/* Where we're up to in listchainmoves, listchannelmoves */
u64 chainmoves_index, channelmoves_index;
+
+ /* Optional currency if we're doing currencyconvert lookups */
+ char *currency;
+ /* Map of UNIX time -> currency rate */
+ currencymap_t *currency_rates;
+ /* True if we've warned about currency failures */
+ bool warned_currency_fail;
};
/* Helper to ignore returns from datastore */
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.