lightningd: have onchaind inform us when to make a channel penalty_adj.
What changed, and why it matters
This commit moves bookkeeping of penalty-related channel balance adjustments from the bookkeeper plugin into the main lightningd process. It adds new internal accounting tags and a message so the on-chain subsystem can tell lightningd when to record these adjustments. There is no direct security vulnerability visible in the diff; it is a refactor of how internal accounting events are generated.
No immediate security action required. Treat as a normal refactor/functional change. Reviewers may want to confirm that the paired credit/debit events correctly preserve the channel accounting invariant and that no other bookkeeper logic relied on the removed penalty_adj generation.
Security signals we found
New wire message type added between onchaind and lightningd
Bookkeeper plugin stops generating penalty adjustment journal entries and now ignores the new tags
No input validation beyond fromwire parsing in the new handler
Paired credit/debit events are emitted for the same amount, preserving accounting balance
Evidence from the diff
The patch introduces two new coin movement tags (MVT_PENALTY_ADJ and MVT_JOURNAL), a new onchaind→lightningd wire message (onchaind_notify_penalty_adj), and handler logic in lightningd/onchain_control.c that emits paired credit/debit channel_coin_mvt events. The bookkeeper plugin no longer synthesizes these penalty adjustment events itself; instead it ignores the new tags. The change is architectural: responsibility for generating penalty_adj channel events shifts from plugins/bkpr/recorder.c to lightningd/coin_mvts.c.
Changed components
lightningd/onchain_control.clightningd/coin_mvts.conchaind/onchaind.conchaind/onchaind_wire.csvplugins/bkpr/recorder.ccommon/coin_mvt.ccommon/coin_mvt.hInspect captured patch +66 / −57
diff --git a/common/coin_mvt.c b/common/coin_mvt.c
index 013ee03c..cadd7b45 100644
--- a/common/coin_mvt.c
+++ b/common/coin_mvt.c
@@ -33,6 +33,8 @@ static const char *mvt_tags[] = {
"stealable",
"channel_proposed",
"splice",
+ "penalty_adj",
+ "journal",
};
#define PRIMARY_TAG_BITS ((1ULL << MVT_DEPOSIT) | \
@@ -54,6 +56,8 @@ static const char *mvt_tags[] = {
(1ULL << MVT_STOLEN) | \
(1ULL << MVT_TO_MINER) | \
(1ULL << MVT_LEASE_FEE) | \
+ (1ULL << MVT_PENALTY_ADJ) | \
+ (1ULL << MVT_JOURNAL) | \
(1ULL << MVT_CHANNEL_PROPOSED))
const char *mvt_tag_str(enum mvt_tag tag)
diff --git a/common/coin_mvt.h b/common/coin_mvt.h
index ca20f43d..682387a2 100644
--- a/common/coin_mvt.h
+++ b/common/coin_mvt.h
@@ -36,7 +36,9 @@ enum mvt_tag {
MVT_STEALABLE = 21,
MVT_CHANNEL_PROPOSED = 22,
MVT_SPLICE = 23,
-#define NUM_MVT_TAGS (MVT_SPLICE + 1)
+ MVT_PENALTY_ADJ = 24,
+ MVT_JOURNAL = 25,
+#define NUM_MVT_TAGS (MVT_JOURNAL + 1)
};
struct mvt_tags {
diff --git a/common/test/run-coin_mvt.c b/common/test/run-coin_mvt.c
index 357b48d1..6746ad75 100644
--- a/common/test/run-coin_mvt.c
+++ b/common/test/run-coin_mvt.c
@@ -177,6 +177,10 @@ static bool mvt_tag_is_primary(enum mvt_tag tag)
return true;
case MVT_SPLICE:
return false;
+ case MVT_PENALTY_ADJ:
+ return true;
+ case MVT_JOURNAL:
+ return true;
}
abort();
}
diff --git a/lightningd/coin_mvts.c b/lightningd/coin_mvts.c
index 2cb539a9..415a8b93 100644
--- a/lightningd/coin_mvts.c
+++ b/lightningd/coin_mvts.c
@@ -61,6 +61,18 @@ struct channel_coin_mvt *new_channel_mvt_routed_hout(const tal_t *ctx,
hout->fees);
}
+struct channel_coin_mvt *new_channel_mvt_penalty_adj(const tal_t *ctx,
+ const struct channel *channel,
+ struct amount_msat amount,
+ enum coin_mvt_dir direction)
+{
+ return new_channel_coin_mvt(ctx, channel, time_now().ts.tv_sec,
+ NULL, NULL, NULL,
+ direction, amount,
+ mk_mvt_tags(MVT_PENALTY_ADJ),
+ AMOUNT_MSAT(0));
+}
+
static bool report_chan_balance(const struct channel *chan)
{
switch (chan->state) {
diff --git a/lightningd/coin_mvts.h b/lightningd/coin_mvts.h
index 749387d9..3201c203 100644
--- a/lightningd/coin_mvts.h
+++ b/lightningd/coin_mvts.h
@@ -32,6 +32,10 @@ struct channel_coin_mvt *new_channel_mvt_invoice_hout(const tal_t *ctx,
struct channel_coin_mvt *new_channel_mvt_routed_hout(const tal_t *ctx,
const struct htlc_out *hout,
const struct channel *channel);
+struct channel_coin_mvt *new_channel_mvt_penalty_adj(const tal_t *ctx,
+ const struct channel *channel,
+ struct amount_msat amount,
+ enum coin_mvt_dir direction);
void send_account_balance_snapshot(struct lightningd *ld);
diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c
index e536e532..a82665c2 100644
--- a/lightningd/onchain_control.c
+++ b/lightningd/onchain_control.c
@@ -358,6 +358,22 @@ static void handle_onchain_log_coin_move(struct channel *channel, const u8 *msg)
tal_free(mvt);
}
+static void handle_onchain_log_penalty_adj(struct channel *channel, const u8 *msg)
+{
+ struct amount_msat msat;
+ struct channel_coin_mvt *mvt;
+
+ if (!fromwire_onchaind_notify_penalty_adj(msg, &msat)) {
+ channel_internal_error(channel, "Invalid onchain notify_penalty_adj");
+ return;
+ }
+
+ mvt = new_channel_mvt_penalty_adj(tmpctx, channel, msat, COIN_CREDIT);
+ notify_channel_mvt(channel->peer->ld, mvt);
+ mvt = new_channel_mvt_penalty_adj(tmpctx, channel, msat, COIN_DEBIT);
+ notify_channel_mvt(channel->peer->ld, mvt);
+}
+
static void replay_watch_tx(struct channel *channel,
u32 blockheight,
const struct bitcoin_tx *tx TAKES)
@@ -1651,6 +1667,10 @@ static unsigned int onchain_msg(struct subd *sd, const u8 *msg, const int *fds U
handle_onchain_log_coin_move(sd->channel, msg);
break;
+ case WIRE_ONCHAIND_NOTIFY_PENALTY_ADJ:
+ handle_onchain_log_penalty_adj(sd->channel, msg);
+ break;
+
case WIRE_ONCHAIND_SPEND_TO_US:
handle_onchaind_spend_to_us(sd->channel, msg);
break;
diff --git a/onchaind/onchaind.c b/onchaind/onchaind.c
index bafa101d..0fbc8666 100644
--- a/onchaind/onchaind.c
+++ b/onchaind/onchaind.c
@@ -378,6 +378,15 @@ static void record_coin_movements(struct tracked_output *out,
else
record_channel_withdrawal(txid, out, blockheight, mk_mvt_tags(MVT_TO_WALLET));
}
+
+ /* Tell lightningd to create penalty_adj on channel balance */
+ if (out->resolved->tx_type == OUR_PENALTY_TX) {
+ struct amount_msat msat;
+ if (!amount_sat_to_msat(&msat, out->sat))
+ abort();
+ wire_sync_write(REQ_FD,
+ take(towire_onchaind_notify_penalty_adj(NULL, msat)));
+ }
}
/* We vary feerate until signature they offered matches. */
@@ -1660,6 +1669,7 @@ static void wait_for_resolved(struct tracked_output **outs)
case WIRE_ONCHAIND_ANNOTATE_TXOUT:
case WIRE_ONCHAIND_ANNOTATE_TXIN:
case WIRE_ONCHAIND_NOTIFY_COIN_MVT:
+ case WIRE_ONCHAIND_NOTIFY_PENALTY_ADJ:
case WIRE_ONCHAIND_SPEND_TO_US:
case WIRE_ONCHAIND_SPEND_PENALTY:
case WIRE_ONCHAIND_SPEND_HTLC_SUCCESS:
diff --git a/onchaind/onchaind_wire.csv b/onchaind/onchaind_wire.csv
index 9937197d..f8f5e8ea 100644
--- a/onchaind/onchaind_wire.csv
+++ b/onchaind/onchaind_wire.csv
@@ -125,6 +125,9 @@ msgdata,onchaind_annotate_txin,type,enum wallet_tx_type,
msgtype,onchaind_notify_coin_mvt,5037
msgdata,onchaind_notify_coin_mvt,mvt,chain_coin_mvt,
+msgtype,onchaind_notify_penalty_adj,5038
+msgdata,onchaind_notify_penalty_adj,amount,amount_msat,
+
# We tell lightningd to create, sign and broadcast this tx:
msgtype,onchaind_spend_to_us,5040
msgdata,onchaind_spend_to_us,outpoint,bitcoin_outpoint,
diff --git a/onchaind/test/run-grind_feerate.c b/onchaind/test/run-grind_feerate.c
index 6ff7c6c3..e413f100 100644
--- a/onchaind/test/run-grind_feerate.c
+++ b/onchaind/test/run-grind_feerate.c
@@ -292,6 +292,9 @@ u8 *towire_onchaind_missing_htlc_output(const tal_t *ctx UNNEEDED, const struct
/* Generated stub for towire_onchaind_notify_coin_mvt */
u8 *towire_onchaind_notify_coin_mvt(const tal_t *ctx UNNEEDED, const struct chain_coin_mvt *mvt UNNEEDED)
{ fprintf(stderr, "towire_onchaind_notify_coin_mvt called!\n"); abort(); }
+/* Generated stub for towire_onchaind_notify_penalty_adj */
+u8 *towire_onchaind_notify_penalty_adj(const tal_t *ctx UNNEEDED, struct amount_msat amount UNNEEDED)
+{ fprintf(stderr, "towire_onchaind_notify_penalty_adj called!\n"); abort(); }
/* Generated stub for towire_onchaind_spend_fulfill */
u8 *towire_onchaind_spend_fulfill(const tal_t *ctx UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED, struct amount_sat outpoint_amount UNNEEDED, u64 htlc_id UNNEEDED, const struct pubkey *remote_per_commitment_point UNNEEDED, const struct preimage *preimage UNNEEDED, const u8 *wscript UNNEEDED)
{ fprintf(stderr, "towire_onchaind_spend_fulfill called!\n"); abort(); }
diff --git a/plugins/bkpr/recorder.c b/plugins/bkpr/recorder.c
index 6924527d..55a7b2d3 100644
--- a/plugins/bkpr/recorder.c
+++ b/plugins/bkpr/recorder.c
@@ -1407,6 +1407,8 @@ void maybe_update_account(struct db *db,
case MVT_LEASE_FEE:
case MVT_STEALABLE:
case MVT_SPLICE:
+ case MVT_PENALTY_ADJ:
+ case MVT_JOURNAL:
/* Ignored */
break;
}
@@ -1686,36 +1688,7 @@ char *update_channel_onchain_fees(const tal_t *ctx,
ev->tag);
}
- /* Was this an 'old state' tx, where we ended up
- * with more sats than we had on record? */
- if (amount_msat_greater(onchain_amt, close_ev->debit)) {
- struct channel_event *ev;
- struct amount_msat diff;
-
- if (!amount_msat_sub(&diff, onchain_amt,
- close_ev->debit))
- return tal_fmt(ctx, "Unable to sub"
- "close debit from onchain_amt");
- /* Add in/out journal entries for it */
- ev = new_channel_event(ctx,
- tal_fmt(tmpctx, "%s",
- account_entry_tag_str(PENALTY_ADJ)),
- diff,
- AMOUNT_MSAT(0),
- AMOUNT_MSAT(0),
- NULL, 0,
- close_ev->timestamp);
- log_channel_event(db, acct, ev);
- ev = new_channel_event(ctx,
- tal_fmt(tmpctx, "%s",
- account_entry_tag_str(PENALTY_ADJ)),
- AMOUNT_MSAT(0),
- diff,
- AMOUNT_MSAT(0),
- NULL, 0,
- close_ev->timestamp);
- log_channel_event(db, acct, ev);
- } else {
+ if (amount_msat_less_eq(onchain_amt, close_ev->debit)) {
struct amount_msat fees;
if (!amount_msat_sub(&fees, close_ev->debit,
onchain_amt))
diff --git a/plugins/bkpr/test/run-bkpr_db.c b/plugins/bkpr/test/run-bkpr_db.c
index f512b5b5..46c3b4c9 100644
--- a/plugins/bkpr/test/run-bkpr_db.c
+++ b/plugins/bkpr/test/run-bkpr_db.c
@@ -21,9 +21,6 @@
#include <wire/wire.h>
/* AUTOGENERATED MOCKS START */
-/* Generated stub for account_entry_tag_str */
-const char *account_entry_tag_str(enum account_entry_tag tag UNNEEDED)
-{ fprintf(stderr, "account_entry_tag_str called!\n"); abort(); }
/* Generated stub for command_fail_badparam */
struct command_result *command_fail_badparam(struct command *cmd UNNEEDED,
const char *paramname UNNEEDED,
@@ -202,16 +199,6 @@ enum htlc_state last_fee_state(enum side opener UNNEEDED)
/* Generated stub for log_level_name */
const char *log_level_name(enum log_level level UNNEEDED)
{ fprintf(stderr, "log_level_name called!\n"); abort(); }
-/* Generated stub for new_channel_event */
-struct channel_event *new_channel_event(const tal_t *ctx UNNEEDED,
- const char *tag UNNEEDED,
- struct amount_msat credit UNNEEDED,
- struct amount_msat debit UNNEEDED,
- struct amount_msat fees UNNEEDED,
- struct sha256 *payment_id STEALS UNNEEDED,
- u32 part_id UNNEEDED,
- u64 timestamp UNNEEDED)
-{ fprintf(stderr, "new_channel_event called!\n"); abort(); }
/* Generated stub for param_check */
bool param_check(struct command *cmd UNNEEDED,
const char *buffer UNNEEDED,
diff --git a/plugins/bkpr/test/run-recorder.c b/plugins/bkpr/test/run-recorder.c
index b2e352e9..b1d5c9b9 100644
--- a/plugins/bkpr/test/run-recorder.c
+++ b/plugins/bkpr/test/run-recorder.c
@@ -27,9 +27,6 @@
/* AUTOGENERATED MOCKS START */
-/* Generated stub for account_entry_tag_str */
-const char *account_entry_tag_str(enum account_entry_tag tag UNNEEDED)
-{ fprintf(stderr, "account_entry_tag_str called!\n"); abort(); }
/* Generated stub for command_fail_badparam */
struct command_result *command_fail_badparam(struct command *cmd UNNEEDED,
const char *paramname UNNEEDED,
@@ -208,16 +205,6 @@ enum htlc_state last_fee_state(enum side opener UNNEEDED)
/* Generated stub for log_level_name */
const char *log_level_name(enum log_level level UNNEEDED)
{ fprintf(stderr, "log_level_name called!\n"); abort(); }
-/* Generated stub for new_channel_event */
-struct channel_event *new_channel_event(const tal_t *ctx UNNEEDED,
- const char *tag UNNEEDED,
- struct amount_msat credit UNNEEDED,
- struct amount_msat debit UNNEEDED,
- struct amount_msat fees UNNEEDED,
- struct sha256 *payment_id STEALS UNNEEDED,
- u32 part_id UNNEEDED,
- u64 timestamp UNNEEDED)
-{ fprintf(stderr, "new_channel_event called!\n"); abort(); }
/* Generated stub for param_check */
bool param_check(struct command *cmd UNNEEDED,
const char *buffer UNNEEDED,
Why this scored 17/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.