From 841a8bd03a9c4ca20870dadff667e103b64a9548 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 14 Aug 2025 10:57:55 +0930 Subject: [PATCH] lightningd: extract core of coin_movement notification, for use in list functions. Signed-off-by: Rusty Russell Changelog-Deprecated: JSON-RPC: `coin_movement` notification `utxo_txid`, `vout` and `txid` fields (use `utxo` and `spending_txid`). Changelog-Added: JSON-RPC: `coin_movement` notification `utxo` field. Changelog-Added: JSON-RPC: `coin_movement` notification `spending_txid` field. --- common/coin_mvt.c | 17 +-- common/coin_mvt.h | 2 +- doc/developers-guide/deprecated-features.md | 2 + lightningd/coin_mvts.c | 100 +++++++++++++++ lightningd/coin_mvts.h | 11 ++ lightningd/notification.c | 134 +++++--------------- plugins/bkpr/bookkeeper.c | 8 +- tests/utils.py | 21 +-- 8 files changed, 168 insertions(+), 127 deletions(-) diff --git a/common/coin_mvt.c b/common/coin_mvt.c index e9a059108..88cefa252 100644 --- a/common/coin_mvt.c +++ b/common/coin_mvt.c @@ -163,7 +163,7 @@ static struct chain_coin_mvt *new_chain_coin_mvt(const tal_t *ctx, const struct channel *channel, const char *account_name TAKES, u64 timestamp, - const struct bitcoin_txid *tx_txid, + const struct bitcoin_txid *spending_txid, const struct bitcoin_outpoint *outpoint, const struct sha256 *payment_hash TAKES, u32 blockheight, @@ -178,7 +178,7 @@ static struct chain_coin_mvt *new_chain_coin_mvt(const tal_t *ctx, assert(mvt_tags_valid(tags)); set_mvt_account_id(&mvt->account, channel, account_name); mvt->timestamp = timestamp; - mvt->tx_txid = tx_txid; + mvt->spending_txid = spending_txid; mvt->outpoint = *outpoint; mvt->originating_acct = NULL; @@ -485,9 +485,9 @@ void towire_chain_coin_mvt(u8 **pptr, const struct chain_coin_mvt *mvt) towire_bitcoin_outpoint(pptr, &mvt->outpoint); - if (mvt->tx_txid) { + if (mvt->spending_txid) { towire_bool(pptr, true); - towire_bitcoin_txid(pptr, cast_const(struct bitcoin_txid *, mvt->tx_txid)); + towire_bitcoin_txid(pptr, cast_const(struct bitcoin_txid *, mvt->spending_txid)); } else towire_bool(pptr, false); @@ -520,11 +520,12 @@ void fromwire_chain_coin_mvt(const u8 **cursor, size_t *max, struct chain_coin_m fromwire_bitcoin_outpoint(cursor, max, &mvt->outpoint); if (fromwire_bool(cursor, max)) { - mvt->tx_txid = tal(mvt, struct bitcoin_txid); - fromwire_bitcoin_txid(cursor, max, - cast_const(struct bitcoin_txid *, mvt->tx_txid)); + /* We need non-const temporary */ + struct bitcoin_txid *txid; + mvt->spending_txid = txid = tal(mvt, struct bitcoin_txid); + fromwire_bitcoin_txid(cursor, max, txid); } else - mvt->tx_txid = NULL; + mvt->spending_txid = NULL; if (fromwire_bool(cursor, max)) { struct sha256 *ph; diff --git a/common/coin_mvt.h b/common/coin_mvt.h index 3fffe61ab..1fa0e2add 100644 --- a/common/coin_mvt.h +++ b/common/coin_mvt.h @@ -90,7 +90,7 @@ struct chain_coin_mvt { u64 timestamp; struct bitcoin_outpoint outpoint; - const struct bitcoin_txid *tx_txid; + const struct bitcoin_txid *spending_txid; /* The id of the peer we have this channel with. * Only on our channel_open events */ diff --git a/doc/developers-guide/deprecated-features.md b/doc/developers-guide/deprecated-features.md index 1ac420c86..fe625220e 100644 --- a/doc/developers-guide/deprecated-features.md +++ b/doc/developers-guide/deprecated-features.md @@ -19,6 +19,8 @@ hidden: false | wait.details | Field | v25.05 | v26.06 | Use subsystem-specific object instead | | channel_state_changed.old_state.unknown | Notification Field | v25.05 | v26.03 | Value "unknown" is deprecated: field will be omitted instead | | coin_movement.tags | Notification Field | v25.09 | v26.09 | Use `primary_tag` (first tag) and `extra_tags` instead | +| coin_movement.utxo_txid | Notification Field | v25.09 | v26.09 | Use `utxo` instead of `utxo_txid` & `vout` | +| coin_movement.txid | Notification Field | v25.09 | v26.09 | Use `spending_txid` instead | Inevitably there are features which need to change: either to be generalized, or removed when they can no longer be supported. diff --git a/lightningd/coin_mvts.c b/lightningd/coin_mvts.c index f122fc9b5..056abdd83 100644 --- a/lightningd/coin_mvts.c +++ b/lightningd/coin_mvts.c @@ -136,3 +136,103 @@ void send_account_balance_snapshot(struct lightningd *ld) notify_balance_snapshot(ld, snap); tal_free(snap); } + +static void add_movement_tags(struct json_stream *stream, + bool include_tags_arr, + const struct mvt_tags tags, + bool extra_tags_field) +{ + const char **tagstrs = mvt_tag_strs(tmpctx, tags); + + if (include_tags_arr) { + json_array_start(stream, "tags"); + for (size_t i = 0; i < tal_count(tagstrs); i++) + json_add_string(stream, NULL, tagstrs[i]); + json_array_end(stream); + } + + json_add_string(stream, "primary_tag", tagstrs[0]); + if (extra_tags_field) { + json_array_start(stream, "extra_tags"); + for (size_t i = 1; i < tal_count(tagstrs); i++) + json_add_string(stream, NULL, tagstrs[i]); + json_array_end(stream); + } else { + assert(tal_count(tagstrs) == 1); + } +} + +static void json_add_mvt_account_id(struct json_stream *stream, + const char *fieldname, + const struct mvt_account_id *account_id) +{ + if (account_id->channel) + json_add_channel_id(stream, fieldname, &account_id->channel->cid); + else + json_add_string(stream, fieldname, account_id->alt_account); +} + +void json_add_chain_mvt_fields(struct json_stream *stream, + bool include_tags_arr, + bool include_old_utxo_fields, + bool include_old_txid_field, + const struct chain_coin_mvt *chain_mvt) +{ + if (chain_mvt->peer_id) + json_add_node_id(stream, "peer_id", chain_mvt->peer_id); + json_add_mvt_account_id(stream, "account_id", &chain_mvt->account); + + if (chain_mvt->originating_acct) + json_add_mvt_account_id(stream, "originating_account", chain_mvt->originating_acct); + + if (chain_mvt->spending_txid) { + if (include_old_txid_field) + json_add_txid(stream, "txid", + chain_mvt->spending_txid); + json_add_txid(stream, "spending_txid", chain_mvt->spending_txid); + } + + if (include_old_utxo_fields) { + json_add_string(stream, "utxo_txid", + fmt_bitcoin_txid(tmpctx, + &chain_mvt->outpoint.txid)); + json_add_u32(stream, "vout", chain_mvt->outpoint.n); + } + json_add_outpoint(stream, "utxo", &chain_mvt->outpoint); + + /* on-chain htlcs include a payment hash */ + if (chain_mvt->payment_hash) + json_add_sha256(stream, "payment_hash", chain_mvt->payment_hash); + json_add_amount_msat(stream, "credit_msat", chain_mvt->credit); + json_add_amount_msat(stream, "debit_msat", chain_mvt->debit); + + json_add_amount_sat_msat(stream, + "output_msat", chain_mvt->output_val); + if (chain_mvt->output_count > 0) + json_add_num(stream, "output_count", chain_mvt->output_count); + + add_movement_tags(stream, include_tags_arr, chain_mvt->tags, true); + json_add_u32(stream, "blockheight", chain_mvt->blockheight); + json_add_u64(stream, "timestamp", chain_mvt->timestamp); +} + +void json_add_channel_mvt_fields(struct json_stream *stream, + bool include_tags_arr, + const struct channel_coin_mvt *chan_mvt, + bool extra_tags_field) +{ + json_add_mvt_account_id(stream, "account_id", &chan_mvt->account); + /* push funding / leases don't have a payment_hash */ + if (chan_mvt->payment_hash) + json_add_sha256(stream, "payment_hash", chan_mvt->payment_hash); + if (chan_mvt->part_and_group) { + json_add_u64(stream, "part_id", chan_mvt->part_and_group->part_id); + json_add_u64(stream, "group_id", chan_mvt->part_and_group->group_id); + } + json_add_amount_msat(stream, "credit_msat", chan_mvt->credit); + json_add_amount_msat(stream, "debit_msat", chan_mvt->debit); + json_add_amount_msat(stream, "fees_msat", chan_mvt->fees); + + add_movement_tags(stream, include_tags_arr, chan_mvt->tags, extra_tags_field); + json_add_u64(stream, "timestamp", chan_mvt->timestamp); +} diff --git a/lightningd/coin_mvts.h b/lightningd/coin_mvts.h index 8d7613d82..749387d9b 100644 --- a/lightningd/coin_mvts.h +++ b/lightningd/coin_mvts.h @@ -34,4 +34,15 @@ struct channel_coin_mvt *new_channel_mvt_routed_hout(const tal_t *ctx, const struct channel *channel); void send_account_balance_snapshot(struct lightningd *ld); + +/* Shared by listcoinmoves and notifications code */ +void json_add_chain_mvt_fields(struct json_stream *stream, + bool include_tags_arr, + bool include_old_utxo_fields, + bool include_old_txid_field, + const struct chain_coin_mvt *chain_mvt); +void json_add_channel_mvt_fields(struct json_stream *stream, + bool include_tags_arr, + const struct channel_coin_mvt *chan_mvt, + bool extra_tags_field); #endif /* LIGHTNING_LIGHTNINGD_COIN_MVTS_H */ diff --git a/lightningd/notification.c b/lightningd/notification.c index 2c95ca4d9..4ff9c2f69 100644 --- a/lightningd/notification.c +++ b/lightningd/notification.c @@ -445,109 +445,14 @@ void notify_sendpay_failure(struct lightningd *ld, notify_send(ld, n); } -static void json_add_mvt_account_id(struct json_stream *stream, - const char *fieldname, - const struct mvt_account_id *account_id) -{ - if (account_id->channel) - json_add_channel_id(stream, fieldname, &account_id->channel->cid); - else - json_add_string(stream, fieldname, account_id->alt_account); -} - -static void add_movement_tags(struct json_stream *stream, - struct lightningd *ld, - const struct mvt_tags tags, - bool extra_tags_field) -{ - const char **tagstrs = mvt_tag_strs(tmpctx, tags); - - if (lightningd_deprecated_out_ok(ld, ld->deprecated_ok, - "coin_movement", "tags", - "v25.05", "v26.09")) { - json_array_start(stream, "tags"); - for (size_t i = 0; i < tal_count(tagstrs); i++) - json_add_string(stream, NULL, tagstrs[i]); - json_array_end(stream); - } - - json_add_string(stream, "primary_tag", tagstrs[0]); - if (extra_tags_field) { - json_array_start(stream, "extra_tags"); - for (size_t i = 1; i < tal_count(tagstrs); i++) - json_add_string(stream, NULL, tagstrs[i]); - json_array_end(stream); - } else { - assert(tal_count(tagstrs) == 1); - } -} - -static void chain_movement_notification_serialize(struct json_stream *stream, - struct lightningd *ld, - const struct chain_coin_mvt *chain_mvt) +static void json_add_standard_notify_mvt_fields(struct json_stream *stream, + struct lightningd *ld, + const char *type) { json_add_num(stream, "version", COIN_MVT_VERSION); - json_add_string(stream, "type", "chain_mvt"); - json_add_node_id(stream, "node_id", &ld->our_nodeid); - if (chain_mvt->peer_id) - json_add_node_id(stream, "peer_id", chain_mvt->peer_id); - json_add_mvt_account_id(stream, "account_id", &chain_mvt->account); - - if (chain_mvt->originating_acct) - json_add_mvt_account_id(stream, "originating_account", chain_mvt->originating_acct); - - /* some 'journal entries' don't have a txid */ - if (chain_mvt->tx_txid) - json_add_string(stream, "txid", - fmt_bitcoin_txid(tmpctx, - chain_mvt->tx_txid)); - json_add_string(stream, "utxo_txid", - fmt_bitcoin_txid(tmpctx, - &chain_mvt->outpoint.txid)); - json_add_u32(stream, "vout", chain_mvt->outpoint.n); - - /* on-chain htlcs include a payment hash */ - if (chain_mvt->payment_hash) - json_add_sha256(stream, "payment_hash", chain_mvt->payment_hash); - json_add_amount_msat(stream, "credit_msat", chain_mvt->credit); - json_add_amount_msat(stream, "debit_msat", chain_mvt->debit); - - json_add_amount_sat_msat(stream, - "output_msat", chain_mvt->output_val); - if (chain_mvt->output_count > 0) - json_add_num(stream, "output_count", chain_mvt->output_count); - - add_movement_tags(stream, ld, chain_mvt->tags, true); - - json_add_u32(stream, "blockheight", chain_mvt->blockheight); - json_add_u64(stream, "timestamp", chain_mvt->timestamp); - json_add_string(stream, "coin_type", chainparams->lightning_hrp); -} - -static void channel_movement_notification_serialize(struct json_stream *stream, - struct lightningd *ld, - const struct channel_coin_mvt *chan_mvt, - bool extra_tags_field) -{ - json_add_num(stream, "version", COIN_MVT_VERSION); - json_add_string(stream, "type", "channel_mvt"); - json_add_node_id(stream, "node_id", &ld->our_nodeid); - json_add_mvt_account_id(stream, "account_id", &chan_mvt->account); - /* push funding / leases don't have a payment_hash */ - if (chan_mvt->payment_hash) - json_add_sha256(stream, "payment_hash", chan_mvt->payment_hash); - if (chan_mvt->part_and_group) { - json_add_u64(stream, "part_id", chan_mvt->part_and_group->part_id); - json_add_u64(stream, "group_id", chan_mvt->part_and_group->group_id); - } - json_add_amount_msat(stream, "credit_msat", chan_mvt->credit); - json_add_amount_msat(stream, "debit_msat", chan_mvt->debit); - json_add_amount_msat(stream, "fees_msat", chan_mvt->fees); - - add_movement_tags(stream, ld, chan_mvt->tags, extra_tags_field); - - json_add_u64(stream, "timestamp", chan_mvt->timestamp); - json_add_string(stream, "coin_type", chainparams->lightning_hrp); + json_add_string(stream, "coin_type", chainparams->lightning_hrp); + json_add_node_id(stream, "node_id", &ld->our_nodeid); + json_add_string(stream, "type", type); } REGISTER_NOTIFICATION(coin_movement); @@ -555,21 +460,44 @@ REGISTER_NOTIFICATION(coin_movement); void notify_channel_mvt(struct lightningd *ld, const struct channel_coin_mvt *chan_mvt) { + bool include_tags_arr; struct jsonrpc_notification *n = notify_start(ld, "coin_movement"); if (!n) return; + include_tags_arr = lightningd_deprecated_out_ok(ld, ld->deprecated_ok, + "coin_movement", "tags", + "v25.09", "v26.09"); + + json_add_standard_notify_mvt_fields(n->stream, ld, "channel_mvt"); /* Adding (empty) extra_tags field unifies this with notify_chain_mvt */ - channel_movement_notification_serialize(n->stream, ld, chan_mvt, true); + json_add_channel_mvt_fields(n->stream, include_tags_arr, chan_mvt, true); notify_send(ld, n); } void notify_chain_mvt(struct lightningd *ld, const struct chain_coin_mvt *chain_mvt) { + bool include_tags_arr, include_old_utxo_fields, include_old_txid_field; struct jsonrpc_notification *n = notify_start(ld, "coin_movement"); if (!n) return; - chain_movement_notification_serialize(n->stream, ld, chain_mvt); + + include_tags_arr = lightningd_deprecated_out_ok(ld, ld->deprecated_ok, + "coin_movement", "tags", + "v25.09", "v26.09"); + include_old_utxo_fields = lightningd_deprecated_out_ok(ld, ld->deprecated_ok, + "coin_movement", "utxo_txid", + "v25.09", "v26.09"); + include_old_txid_field = lightningd_deprecated_out_ok(ld, ld->deprecated_ok, + "coin_movement", "txid", + "v25.09", "v26.09"); + + json_add_standard_notify_mvt_fields(n->stream, ld, "chain_mvt"); + json_add_chain_mvt_fields(n->stream, + include_tags_arr, + include_old_utxo_fields, + include_old_txid_field, + chain_mvt); notify_send(ld, n); } diff --git a/plugins/bkpr/bookkeeper.c b/plugins/bkpr/bookkeeper.c index d76e7d996..dc0b541b7 100644 --- a/plugins/bkpr/bookkeeper.c +++ b/plugins/bkpr/bookkeeper.c @@ -1481,13 +1481,11 @@ parse_and_log_chain_move(struct command *cmd, /* Fields we expect on *every* chain movement */ err = json_scan(tmpctx, buf, params, "{coin_movement:" - "{utxo_txid:%" - ",vout:%" + "{utxo:%" ",output_msat:%" ",blockheight:%" "}}", - JSON_SCAN(json_to_txid, &e->outpoint.txid), - JSON_SCAN(json_to_number, &e->outpoint.n), + JSON_SCAN(json_to_outpoint, &e->outpoint), JSON_SCAN(json_to_msat, &e->output_value), JSON_SCAN(json_to_number, &e->blockheight)); @@ -1501,7 +1499,7 @@ parse_and_log_chain_move(struct command *cmd, /* Now try to get out the optional parts */ err = json_scan(tmpctx, buf, params, "{coin_movement:" - "{txid:%" + "{spending_txid:%" "}}", JSON_SCAN(json_to_txid, spending_txid)); diff --git a/tests/utils.py b/tests/utils.py index 5d560954c..ad4ff8332 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -197,13 +197,14 @@ def account_balance(n, account_id): def extract_utxos(moves): utxos = {} for m in moves: - if 'utxo_txid' not in m: + if 'utxo' not in m: continue + m['utxo_txid'], m['vout'] = m['utxo'].split(':') txid = m['utxo_txid'] if txid not in utxos: - utxos[txid] = [] + utxos[m['utxo_txid']] = [] - if 'txid' not in m: + if 'spending_txid' not in m: utxos[txid].append([m, None]) else: evs = utxos[txid] @@ -222,7 +223,7 @@ def print_utxos(utxos): print(k) for u in us: if u[1]: - print('\t', u[0]['account_id'], u[0]['tags'], u[1]['tags'], u[1]['txid']) + print('\t', u[0]['account_id'], u[0]['tags'], u[1]['tags'], u[1]['spending_txid']) else: print('\t', u[0]['account_id'], u[0]['tags'], None, None) @@ -242,11 +243,11 @@ def utxos_for_channel(utxoset, channel_id): _add_relevant(txid, utxo) relevant_txids.append(txid) if utxo[1]: - relevant_txids.append(utxo[1]['txid']) + relevant_txids.append(utxo[1]['spending_txid']) elif txid in relevant_txids: _add_relevant(txid, utxo) if utxo[1]: - relevant_txids.append(utxo[1]['txid']) + relevant_txids.append(utxo[1]['spending_txid']) # if they're not well ordered, we'll leave some txids out for txid in relevant_txids: @@ -303,13 +304,13 @@ def matchup_events(u_set, evs, chans, tag_list): for x in ev[2]: if x[0] == get_tags(u[1]) and 'to_miner' not in get_tags(u[1]): # Save the 'spent to' txid in the tag-list - tag_list[x[1]] = u[1]['txid'] + tag_list[x[1]] = u[1]['spending_txid'] else: if ev[2] != get_tags(u[1]): raise ValueError(f"tags dont' match. exp {ev}, actual ({u[1]}) full utxo info: {u}") # Save the 'spent to' txid in the tag-list if 'to_miner' not in get_tags(u[1]): - tag_list[ev[3]] = u[1]['txid'] + tag_list[ev[3]] = u[1]['spending_txid'] found = True u_set.remove(u) @@ -329,11 +330,11 @@ def dedupe_moves(moves): deduped_moves = [] for move in moves: # Dupes only pertain to onchain moves? - if 'utxo_txid' not in move: + if 'utxo' not in move: deduped_moves.append(move) continue - outpoint = '{}:{};{}'.format(move['utxo_txid'], move['vout'], move['txid'] if 'txid' in move else 'xx') + outpoint = '{};{}'.format(move['utxo'], move['spending_txid'] if 'spending_txid' in move else 'xx') if outpoint not in move_set: deduped_moves.append(move) move_set[outpoint] = move