lightningd: new internal-use commands to allow utxo spend / deposit injection.
What changed, and why it matters
This commit adds two new internal-only JSON commands, injectutxodeposit and injectutxospend, that let the bookkeeper plugin manually insert records of coins moving into or out of the node's accounting ledger. It also moves a helper for parsing outpoints into shared code. There is no direct evidence in the commit that these commands are exposed to untrusted users or that they introduce a vulnerability, but any command that can write arbitrary ledger entries deserves careful access control review.
Verify that injectutxodeposit and injectutxospend are restricted to the bookkeeper plugin or otherwise protected by appropriate RPC permissions, and confirm that arbitrary callers cannot invoke them. Audit wallet_save_chain_mvt() for handling of duplicate or conflicting outpoints, and review param_msat_as_sat() for rounding edge cases. Treat this as a routine defensive review item rather than an active vulnerability.
Security signals we found
New RPC commands that write accounting/ledger records
Commands marked internal-use but registered through AUTODATA(json_command, ...)
No authentication or permission checks visible in the diff
Amount parsing accepts millisatoshi and converts to satoshi
FIXME comment indicates this is a temporary bridge mechanism
Evidence from the diff
The patch introduces new_foreign_deposit() and new_foreign_withdrawal() coin-movement constructors and two new RPC commands in lightningd/coin_mvts.c: injectutxodeposit and injectutxospend. Both commands accept account, outpoint, amount, timestamp, blockheight, and (for deposit) an optional transfer_from, then persist a chain_coin_mvt via wallet_save_chain_mvt(). param_outpoint() is refactored from two local copies into common/json_param.c. The commit message explicitly labels these as internal-use interfaces for the bookkeeper plugin and notes a FIXME to handle notifications directly in future.
Changed components
lightningd/coin_mvts.ccommon/coin_mvt.ccommon/coin_mvt.hcommon/json_param.ccommon/json_param.hlightningd/closing_control.cplugins/bkpr/bookkeeper.cInspect captured patch +166 / −26
diff --git a/common/coin_mvt.c b/common/coin_mvt.c
index 5017f0bd..49268f73 100644
--- a/common/coin_mvt.c
+++ b/common/coin_mvt.c
@@ -529,6 +529,39 @@ struct channel_coin_mvt *new_coin_channel_push(const tal_t *ctx,
AMOUNT_MSAT(0));
}
+struct chain_coin_mvt *new_foreign_deposit(const tal_t *ctx,
+ const struct bitcoin_outpoint *outpoint,
+ u32 blockheight,
+ struct amount_sat amount,
+ const char *account,
+ u64 timestamp)
+{
+ struct chain_coin_mvt *e;
+
+ e = new_chain_coin_mvt_sat(ctx, NULL, account, NULL, outpoint, NULL,
+ blockheight, mk_mvt_tags(MVT_DEPOSIT), COIN_CREDIT,
+ amount);
+ e->timestamp = timestamp;
+ return e;
+}
+
+struct chain_coin_mvt *new_foreign_withdrawal(const tal_t *ctx,
+ const struct bitcoin_outpoint *outpoint,
+ const struct bitcoin_txid *spend_txid,
+ struct amount_sat amount,
+ u32 blockheight,
+ const char *account,
+ u64 timestamp)
+{
+ struct chain_coin_mvt *e;
+
+ e = new_chain_coin_mvt_sat(ctx, NULL, account, spend_txid, outpoint, NULL,
+ blockheight, mk_mvt_tags(MVT_WITHDRAWAL), COIN_DEBIT,
+ amount);
+ e->timestamp = timestamp;
+ return e;
+}
+
const char **mvt_tag_strs(const tal_t *ctx, struct mvt_tags tags)
{
const char **strs = tal_arr(ctx, const char *, 1);
diff --git a/common/coin_mvt.h b/common/coin_mvt.h
index ad72beb6..58bee3b8 100644
--- a/common/coin_mvt.h
+++ b/common/coin_mvt.h
@@ -258,6 +258,24 @@ struct channel_coin_mvt *new_coin_channel_push(const tal_t *ctx,
struct mvt_tags tags)
NON_NULL_ARGS(2);
+/* FIXME: Does not set originating_acct, caller must do that! */
+struct chain_coin_mvt *new_foreign_deposit(const tal_t *ctx,
+ const struct bitcoin_outpoint *outpoint,
+ u32 blockheight,
+ struct amount_sat amount,
+ const char *account,
+ u64 timestamp)
+ NON_NULL_ARGS(2, 5);
+
+struct chain_coin_mvt *new_foreign_withdrawal(const tal_t *ctx,
+ const struct bitcoin_outpoint *outpoint,
+ const struct bitcoin_txid *spend_txid,
+ struct amount_sat amount,
+ u32 blockheight,
+ const char *account,
+ u64 timestamp)
+ NON_NULL_ARGS(2, 3, 6);
+
/* There are three standard accounts:
* "wallet" for our internal wallet,
* "external" for other bitcoin sources,
diff --git a/common/json_param.c b/common/json_param.c
index 1a761112..454e6a55 100644
--- a/common/json_param.c
+++ b/common/json_param.c
@@ -883,6 +883,19 @@ struct command_result *param_txid(struct command *cmd,
"should be a txid");
}
+struct command_result *param_outpoint(struct command *cmd,
+ const char *name,
+ const char *buffer,
+ const jsmntok_t *tok,
+ struct bitcoin_outpoint **outp)
+{
+ *outp = tal(cmd, struct bitcoin_outpoint);
+ if (json_to_outpoint(buffer, tok, *outp))
+ return NULL;
+ return command_fail_badparam(cmd, name, buffer, tok,
+ "should be a txid:outnum");
+}
+
struct command_result *param_bitcoin_address(struct command *cmd,
const char *name,
const char *buffer,
diff --git a/common/json_param.h b/common/json_param.h
index cb06e080..5944550f 100644
--- a/common/json_param.h
+++ b/common/json_param.h
@@ -322,6 +322,12 @@ struct command_result *param_txid(struct command *cmd,
const jsmntok_t *tok,
struct bitcoin_txid **txid);
+struct command_result *param_outpoint(struct command *cmd,
+ const char *name,
+ const char *buffer,
+ const jsmntok_t *tok,
+ struct bitcoin_outpoint **outp);
+
enum address_parse_result {
/* Not recognized as an onchain address */
ADDRESS_PARSE_UNRECOGNIZED,
diff --git a/lightningd/closing_control.c b/lightningd/closing_control.c
index bd91a79a..36fe90f6 100644
--- a/lightningd/closing_control.c
+++ b/lightningd/closing_control.c
@@ -529,19 +529,6 @@ void peer_start_closingd(struct channel *channel, struct peer_fd *peer_fd)
subd_send_msg(channel->owner, take(initmsg));
}
-static struct command_result *param_outpoint(struct command *cmd,
- const char *name,
- const char *buffer,
- const jsmntok_t *tok,
- struct bitcoin_outpoint **outp)
-{
- *outp = tal(cmd, struct bitcoin_outpoint);
- if (json_to_outpoint(buffer, tok, *outp))
- return NULL;
- return command_fail_badparam(cmd, name, buffer, tok,
- "should be a txid:outnum");
-}
-
static struct command_result *param_feerate_range(struct command *cmd,
const char *name,
const char *buffer,
diff --git a/lightningd/coin_mvts.c b/lightningd/coin_mvts.c
index 97d54b1b..3ceac59e 100644
--- a/lightningd/coin_mvts.c
+++ b/lightningd/coin_mvts.c
@@ -396,3 +396,99 @@ static const struct json_command listchannelmoves_command = {
json_listchannelmoves,
};
AUTODATA(json_command, &listchannelmoves_command);
+
+static struct command_result *param_msat_as_sat(struct command *cmd,
+ const char *name,
+ const char *buffer,
+ const jsmntok_t *tok,
+ struct amount_sat **sat)
+{
+ struct amount_msat msat;
+
+ *sat = tal(cmd, struct amount_sat);
+ if (parse_amount_msat(&msat, buffer + tok->start, tok->end - tok->start)
+ && amount_msat_to_sat(*sat, msat))
+ return NULL;
+
+ return command_fail_badparam(cmd, name, buffer, tok,
+ "should be a millisatoshi amount");
+}
+
+/* Internal interfaces for bookkeeper.c.
+ * FIXME: handle utxo_deposit / utxo_spend notifications directly! */
+static struct command_result *json_injectutxodeposit(struct command *cmd,
+ const char *buffer,
+ const jsmntok_t *obj UNNEEDED,
+ const jsmntok_t *params)
+{
+ struct chain_coin_mvt *ev;
+ const char *account, *origin_acct;
+ struct bitcoin_outpoint *outpoint;
+ struct amount_sat *amount;
+ u64 *timestamp;
+ u32 *blockheight;
+
+ if (!param(cmd, buffer, params,
+ p_req("account", param_string, &account),
+ p_req("outpoint", param_outpoint, &outpoint),
+ p_req("amount_msat", param_msat_as_sat, &amount),
+ p_req("timestamp", param_u64, ×tamp),
+ p_req("blockheight", param_u32, &blockheight),
+ p_opt("transfer_from", param_string, &origin_acct),
+ NULL))
+ return command_param_failed();
+
+ ev = new_foreign_deposit(cmd, outpoint, *blockheight, *amount,
+ account, *timestamp);
+ if (origin_acct) {
+ /* Need temporary because originating_acct is const */
+ struct mvt_account_id *acct;
+ ev->originating_acct = acct = tal(ev, struct mvt_account_id);
+ acct->channel = NULL;
+ acct->alt_account = tal_strdup(acct, origin_acct);
+ }
+ wallet_save_chain_mvt(cmd->ld, ev);
+
+ return command_success(cmd, json_stream_success(cmd));
+}
+static const struct json_command injectutxodeposit_command = {
+ "injectutxodeposit",
+ json_injectutxodeposit,
+};
+AUTODATA(json_command, &injectutxodeposit_command);
+
+static struct command_result *json_injectutxospend(struct command *cmd,
+ const char *buffer,
+ const jsmntok_t *obj UNNEEDED,
+ const jsmntok_t *params)
+{
+ struct chain_coin_mvt *ev;
+ const char *account;
+ struct bitcoin_txid *spending_txid;
+ struct bitcoin_outpoint *outpoint;
+ struct amount_sat *amount;
+ u64 *timestamp;
+ u32 *blockheight;
+
+ if (!param(cmd, buffer, params,
+ p_req("account", param_string, &account),
+ p_req("outpoint", param_outpoint, &outpoint),
+ p_req("spending_txid", param_txid, &spending_txid),
+ p_req("amount_msat", param_msat_as_sat, &amount),
+ p_req("timestamp", param_u64, ×tamp),
+ p_req("blockheight", param_u32, &blockheight),
+ NULL))
+ return command_param_failed();
+
+ ev = new_foreign_withdrawal(cmd, outpoint, spending_txid,
+ *amount, *blockheight, account, *timestamp);
+ wallet_save_chain_mvt(cmd->ld, ev);
+
+ return command_success(cmd, json_stream_success(cmd));
+}
+static const struct json_command injectutxospend_command = {
+ "injectutxospend",
+ json_injectutxospend,
+};
+AUTODATA(json_command, &injectutxospend_command);
+
diff --git a/plugins/bkpr/bookkeeper.c b/plugins/bkpr/bookkeeper.c
index 2e67ede8..45258abb 100644
--- a/plugins/bkpr/bookkeeper.c
+++ b/plugins/bkpr/bookkeeper.c
@@ -491,19 +491,6 @@ static struct command_result *json_list_account_events(struct command *cmd,
return command_finished(cmd, res);
}
-static struct command_result *param_outpoint(struct command *cmd,
- const char *name,
- const char *buffer,
- const jsmntok_t *tok,
- struct bitcoin_outpoint **outp)
-{
- *outp = tal(cmd, struct bitcoin_outpoint);
- if (json_to_outpoint(buffer, tok, *outp))
- return NULL;
- return command_fail_badparam(cmd, name, buffer, tok,
- "should be a txid:outnum");
-}
-
static struct command_result *json_edit_desc_utxo(struct command *cmd,
const char *buf,
const jsmntok_t *params)
Why this scored 21/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.