wallet: record all coin movements into chain_moves or channel_moves tables.
What changed, and why it matters
This change makes Core Lightning save every on-chain and channel coin movement to its internal database before sending out a notification. Previously, some coin movements were only notified and not persisted. It is an accounting/audit-logging improvement rather than a fix for an exploitable security flaw.
No immediate security action required. Treat as a normal reliability/auditability improvement. Reviewers may want to confirm the duplicate-notification path is intentional and documented, and verify database migration/schema support for chain_moves/channel_moves already exists.
Security signals we found
Persistent storage added for all coin movement events
Notification emission moved inside wallet persistence helper
Memory ownership change from explicit tal_free to take() in one call site
Duplicate-suppression path still notifies to preserve downstream behavior
Evidence from the diff
The commit renames notify_chain_mvt/notify_channel_mvt call sites to wallet_save_chain_mvt/wallet_save_channel_mvt, which insert records into chain_moves/channel_moves tables and then emit the original notification. It also fixes a memory-management inconsistency in onchain_control.c by using take() so the new helper can free the object. A duplicate-detection path still emits the notification for bookkeeper compatibility. No cryptographic, network, or permission checks are altered.
Changed components
wallet/wallet.cwallet/wallet.hlightningd/chaintopology.clightningd/channel_control.clightningd/onchain_control.clightningd/peer_htlcs.cwallet/walletrpc.cwallet/test/run-db.cInspect captured patch +23 / −16
diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c
index dd22ccad..59f004f7 100644
--- a/lightningd/chaintopology.c
+++ b/lightningd/chaintopology.c
@@ -26,6 +26,7 @@
#include <lightningd/notification.h>
#include <math.h>
#include <wallet/txfilter.h>
+#include <wallet/wallet.h>
/* Mutual recursion via timer. */
static void try_extend_tip(struct chain_topology *topo);
@@ -880,7 +881,7 @@ static void record_wallet_spend(struct lightningd *ld,
return;
}
- notify_chain_mvt(ld, new_coin_wallet_withdraw(tmpctx, txid, outpoint,
+ wallet_save_chain_mvt(ld, new_coin_wallet_withdraw(tmpctx, txid, outpoint,
tx_blockheight,
utxo->amount, mk_mvt_tags(MVT_WITHDRAWAL)));
}
diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c
index 93fb13ec..a72f9598 100644
--- a/lightningd/channel_control.c
+++ b/lightningd/channel_control.c
@@ -976,7 +976,7 @@ static void channel_record_splice(struct channel *channel,
orig_funding_sats,
output_count,
/* is_splice = */true);
- notify_chain_mvt(channel->peer->ld, mvt);
+ wallet_save_chain_mvt(channel->peer->ld, mvt);
}
void channel_record_open(struct channel *channel, u32 blockheight, bool record_push)
@@ -1022,11 +1022,11 @@ void channel_record_open(struct channel *channel, u32 blockheight, bool record_p
channel->opener == LOCAL,
is_leased);
- notify_chain_mvt(channel->peer->ld, mvt);
+ wallet_save_chain_mvt(channel->peer->ld, mvt);
/* If we pushed sats, *now* record them */
if (is_pushed && record_push)
- notify_channel_mvt(channel->peer->ld,
+ wallet_save_channel_mvt(channel->peer->ld,
new_coin_channel_push(tmpctx, channel,
channel->opener == REMOTE ? COIN_CREDIT : COIN_DEBIT,
channel->push,
diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c
index a82665c2..b82523ab 100644
--- a/lightningd/onchain_control.c
+++ b/lightningd/onchain_control.c
@@ -17,13 +17,13 @@
#include <lightningd/channel_control.h>
#include <lightningd/coin_mvts.h>
#include <lightningd/hsm_control.h>
-#include <lightningd/notification.h>
#include <lightningd/onchain_control.h>
#include <lightningd/peer_control.h>
#include <lightningd/peer_htlcs.h>
#include <lightningd/subd.h>
#include <onchaind/onchaind_wiregen.h>
#include <wallet/txfilter.h>
+#include <wallet/wallet.h>
#include <wally_bip32.h>
#include <wally_psbt.h>
#include <wire/wire_sync.h>
@@ -354,8 +354,7 @@ static void handle_onchain_log_coin_move(struct channel *channel, const u8 *msg)
mvt->originating_acct = new_mvt_account_id(mvt, channel, NULL);
}
- notify_chain_mvt(channel->peer->ld, mvt);
- tal_free(mvt);
+ wallet_save_chain_mvt(channel->peer->ld, take(mvt));
}
static void handle_onchain_log_penalty_adj(struct channel *channel, const u8 *msg)
@@ -369,9 +368,9 @@ static void handle_onchain_log_penalty_adj(struct channel *channel, const u8 *ms
}
mvt = new_channel_mvt_penalty_adj(tmpctx, channel, msat, COIN_CREDIT);
- notify_channel_mvt(channel->peer->ld, mvt);
+ wallet_save_channel_mvt(channel->peer->ld, mvt);
mvt = new_channel_mvt_penalty_adj(tmpctx, channel, msat, COIN_DEBIT);
- notify_channel_mvt(channel->peer->ld, mvt);
+ wallet_save_channel_mvt(channel->peer->ld, mvt);
}
static void replay_watch_tx(struct channel *channel,
@@ -597,7 +596,7 @@ static void onchain_add_utxo(struct channel *channel, const u8 *msg)
amount, mk_mvt_tags(MVT_DEPOSIT));
mvt->originating_acct = new_mvt_account_id(mvt, channel, NULL);
- notify_chain_mvt(channel->peer->ld, mvt);
+ wallet_save_chain_mvt(channel->peer->ld, mvt);
}
static void onchain_annotate_txout(struct channel *channel, const u8 *msg)
diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c
index fef1be92..23519012 100644
--- a/lightningd/peer_htlcs.c
+++ b/lightningd/peer_htlcs.c
@@ -1967,7 +1967,7 @@ static void remove_htlc_in(struct channel *channel, struct htlc_in *hin)
"Unable to calculate fees collected."
" Not logging an inbound HTLC");
else
- notify_channel_mvt(channel->peer->ld, mvt);
+ wallet_save_channel_mvt(channel->peer->ld, mvt);
}
tal_free(hin);
@@ -2018,7 +2018,7 @@ static void remove_htlc_out(struct channel *channel, struct htlc_out *hout)
"Unable to calculate fees."
" Not logging an outbound HTLC");
else
- notify_channel_mvt(channel->peer->ld, mvt);
+ wallet_save_channel_mvt(channel->peer->ld, mvt);
}
tal_free(hout);
diff --git a/wallet/test/run-db.c b/wallet/test/run-db.c
index 43ea5340..bb054990 100644
--- a/wallet/test/run-db.c
+++ b/wallet/test/run-db.c
@@ -289,6 +289,10 @@ struct peer *new_peer(struct lightningd *ld UNNEEDED, u64 dbid UNNEEDED,
void notify_chain_mvt(struct lightningd *ld UNNEEDED,
const struct chain_coin_mvt *chain_mvt UNNEEDED)
{ fprintf(stderr, "notify_chain_mvt called!\n"); abort(); }
+/* Generated stub for notify_channel_mvt */
+void notify_channel_mvt(struct lightningd *ld UNNEEDED,
+ const struct channel_coin_mvt *chan_mvt UNNEEDED)
+{ fprintf(stderr, "notify_channel_mvt called!\n"); abort(); }
/* Generated stub for notify_forward_event */
void notify_forward_event(struct lightningd *ld UNNEEDED,
const struct htlc_in *in UNNEEDED,
diff --git a/wallet/wallet.c b/wallet/wallet.c
index f359c8a9..96aed660 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -3195,7 +3195,7 @@ type_ok:
*blockheight,
utxo->amount,
mk_mvt_tags(MVT_DEPOSIT));
- notify_chain_mvt(w->ld, mvt);
+ wallet_save_chain_mvt(w->ld, mvt);
}
if (!wallet_add_utxo(w, utxo, utxo->utxotype == UTXO_P2SH_P2WPKH ? WALLET_OUTPUT_P2SH_WPKH : WALLET_OUTPUT_OUR_CHANGE)) {
@@ -6930,6 +6930,7 @@ void wallet_save_channel_mvt(struct lightningd *ld,
db_bind_amount_msat(stmt, chan_mvt->fees);
db_exec_prepared_v2(take(stmt));
+ notify_channel_mvt(ld, chan_mvt);
if (taken(chan_mvt))
tal_free(chan_mvt);
}
@@ -6994,6 +6995,8 @@ void wallet_save_chain_mvt(struct lightningd *ld,
/* It's a duplicate. Don't re-add. */
tal_free(stmt);
+ /* FIXME: This is currently required for bookkeeper tests, if bookkeeper is offline */
+ notify_chain_mvt(ld, chain_mvt);
goto out;
}
tal_free(stmt);
@@ -7045,6 +7048,7 @@ void wallet_save_chain_mvt(struct lightningd *ld,
db_bind_null(stmt);
db_exec_prepared_v2(take(stmt));
+ notify_chain_mvt(ld, chain_mvt);
out:
if (taken(chain_mvt))
tal_free(chain_mvt);
diff --git a/wallet/wallet.h b/wallet/wallet.h
index fde78b52..0ee7914b 100644
--- a/wallet/wallet.h
+++ b/wallet/wallet.h
@@ -1873,7 +1873,7 @@ struct issued_address_type *wallet_list_addresses(const tal_t *ctx, struct walle
*/
void wallet_begin_old_close_rescan(struct lightningd *ld);
-/* Coin movement storage */
+/* Coin movement storage: also calls notifications */
void wallet_save_channel_mvt(struct lightningd *ld,
const struct channel_coin_mvt *chan_mvt);
diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c
index 282ecea2..677db8ba 100644
--- a/wallet/walletrpc.c
+++ b/wallet/walletrpc.c
@@ -950,8 +950,7 @@ static void maybe_notify_new_external_send(struct lightningd *ld,
mvt->originating_acct = new_mvt_account_id(mvt, NULL, ACCOUNT_NAME_WALLET);
- notify_chain_mvt(ld, mvt);
- tal_free(mvt);
+ wallet_save_chain_mvt(ld, take(mvt));
}
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.