diff --git a/doc/developers-guide/deprecated-features.md b/doc/developers-guide/deprecated-features.md index 2ed1fb06e..1ac420c86 100644 --- a/doc/developers-guide/deprecated-features.md +++ b/doc/developers-guide/deprecated-features.md @@ -18,6 +18,7 @@ hidden: false | listpeerchannels.max_total_htlc_in_msat | Field | v25.02 | v26.03 | Use our_max_total_htlc_out_msat | | 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 | Inevitably there are features which need to change: either to be generalized, or removed when they can no longer be supported. diff --git a/doc/developers-guide/plugin-development/event-notifications.md b/doc/developers-guide/plugin-development/event-notifications.md index 37d69a771..c16c1e834 100644 --- a/doc/developers-guide/plugin-development/event-notifications.md +++ b/doc/developers-guide/plugin-development/event-notifications.md @@ -339,7 +339,8 @@ A notification for topic `coin_movement` is sent to record the movement of coins "output_msat": 2000000000, // ('chain_mvt' only) "output_count": 2, // ('chain_mvt' only, typically only channel closes) "fees_msat": 382, // ('channel_mvt' only) - "tags": ["deposit"], + "primary_tag": "deposit", + "extra_tags": [], "blockheight":102, // 'chain_mvt' only "timestamp":1585948198, "coin_type":"bc" @@ -376,7 +377,7 @@ _Only_ tagged on external events (deposits/withdrawals to an external party). `fees` is an HTLC annotation for the amount of fees either paid or earned. For "invoice" tagged events, the fees are the total fees paid to send that payment. The end amount can be found by subtracting the total fees from the `debited` amount. For "routed" tagged events, both the debit/credit contain fees. Technically routed debits are the 'fee generating' event, however we include them on routed credits as well. -`tag` is a movement descriptor. Current tags are as follows: +`primary_tag` is a movement descriptor. Current primary tags are as follows: - `deposit`: funds deposited - `withdrawal`: funds withdrawn @@ -391,15 +392,21 @@ _Only_ tagged on external events (deposits/withdrawals to an external party). - `htlc_fulfill`: on-chian htlc fulfill output - `htlc_tx`: on-chain htlc tx has happened - `to_wallet`: output being spent into our wallet -- `ignored`: output is being ignored - `anchor`: an anchor output - `to_them`: output intended to peer's wallet - `penalized`: output we've 'lost' due to a penalty (failed cheat attempt) - `stolen`: output we've 'lost' due to peer's cheat - `to_miner`: output we've burned to miner (OP_RETURN) -- `opener`: tags channel_open, we are the channel opener - `lease_fee`: amount paid as lease fee -- `leased`: tags channel_open, channel contains leased funds +- `channel_proposed`: a zero-conf channel + +`extra_tags` is zero or more additional tags. Current extra tags are as follows: + +- `ignored`: output is being ignored +- `opener`: tags `channel_open` or `channel_proposed`, we are the channel opener +- `stealable`: funds can be taken by the other party +- `leased`: tags `channel_open` or `channel_proposed`, channel contains leased funds +- `splice`: a channel close due to splice operation. `blockheight` is the block the txid is included in. `channel_mvt`s will be null, so will the blockheight for withdrawals to external parties (we issue these events when we send the tx containing them, before they're included in the chain). diff --git a/lightningd/notification.c b/lightningd/notification.c index d45123f82..e67228914 100644 --- a/lightningd/notification.c +++ b/lightningd/notification.c @@ -455,12 +455,37 @@ static void json_add_mvt_account_id(struct json_stream *stream, 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) { - const char **tags; - 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); @@ -497,11 +522,7 @@ static void chain_movement_notification_serialize(struct json_stream *stream, if (chain_mvt->output_count > 0) json_add_num(stream, "output_count", chain_mvt->output_count); - json_array_start(stream, "tags"); - tags = mvt_tag_strs(tmpctx, chain_mvt->tags); - for (size_t i = 0; i < tal_count(tags); i++) - json_add_string(stream, NULL, tags[i]); - json_array_end(stream); + add_movement_tags(stream, ld, chain_mvt->tags, true); json_add_u32(stream, "blockheight", chain_mvt->blockheight); json_add_u32(stream, "timestamp", time_now().ts.tv_sec); @@ -510,10 +531,9 @@ static void chain_movement_notification_serialize(struct json_stream *stream, static void channel_movement_notification_serialize(struct json_stream *stream, struct lightningd *ld, - const struct channel_coin_mvt *chan_mvt) + const struct channel_coin_mvt *chan_mvt, + bool extra_tags_field) { - const char **tags; - 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); @@ -529,11 +549,7 @@ static void channel_movement_notification_serialize(struct json_stream *stream, json_add_amount_msat(stream, "debit_msat", chan_mvt->debit); json_add_amount_msat(stream, "fees_msat", chan_mvt->fees); - json_array_start(stream, "tags"); - tags = mvt_tag_strs(tmpctx, chan_mvt->tags); - for (size_t i = 0; i < tal_count(tags); i++) - json_add_string(stream, NULL, tags[i]); - json_array_end(stream); + add_movement_tags(stream, ld, chan_mvt->tags, extra_tags_field); json_add_u32(stream, "timestamp", time_now().ts.tv_sec); json_add_string(stream, "coin_type", chainparams->lightning_hrp); @@ -547,7 +563,8 @@ void notify_channel_mvt(struct lightningd *ld, struct jsonrpc_notification *n = notify_start(ld, "coin_movement"); if (!n) return; - channel_movement_notification_serialize(n->stream, ld, chan_mvt); + /* Adding (empty) extra_tags field unifies this with notify_chain_mvt */ + channel_movement_notification_serialize(n->stream, ld, chan_mvt, true); notify_send(ld, n); } diff --git a/plugins/bkpr/bookkeeper.c b/plugins/bkpr/bookkeeper.c index fb331c190..d76e7d996 100644 --- a/plugins/bkpr/bookkeeper.c +++ b/plugins/bkpr/bookkeeper.c @@ -1765,16 +1765,20 @@ static char *parse_tags(const tal_t *ctx, enum mvt_tag **tags) { size_t i; - const jsmntok_t *tag_tok, - *tags_tok = json_get_member(buf, tok, "tags"); + const jsmntok_t *extras_tok, + *tag_tok = json_get_member(buf, tok, "primary_tag"); - if (tags_tok == NULL || tags_tok->type != JSMN_ARRAY) - return "Invalid/missing 'tags' field"; + if (tag_tok == NULL) + return "missing 'primary_tag' field"; + *tags = tal_arr(ctx, enum mvt_tag, 1); + if (!json_to_coin_mvt_tag(buf, tag_tok, &(*tags)[0])) + return "Unable to parse 'primary_tag'"; - *tags = tal_arr(ctx, enum mvt_tag, tags_tok->size); - json_for_each_arr(i, tag_tok, tags_tok) { - if (!json_to_coin_mvt_tag(buf, tag_tok, &(*tags)[i])) - return "Unable to parse 'tags'"; + extras_tok = json_get_member(buf, tok, "extra_tags"); + tal_resize(tags, 1 + extras_tok->size); + json_for_each_arr(i, tag_tok, extras_tok) { + if (!json_to_coin_mvt_tag(buf, tag_tok, &(*tags)[i + 1])) + return "Unable to parse 'extra_tags'"; } return NULL; diff --git a/tests/utils.py b/tests/utils.py index 6c701021e..5d560954c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -140,11 +140,12 @@ def check_coin_moves(n, account_id, expected_moves, chainparams): # Stash moves for errors, if needed _acct_moves = acct_moves for mv in acct_moves: + mv['tags'] = [mv['primary_tag']] + mv['extra_tags'] print("{{'type': '{}', 'credit_msat': {}, 'debit_msat': {}, 'tags': '{}' , ['fees_msat'?: '{}']}}," .format(mv['type'], Millisatoshi(mv['credit_msat']).millisatoshis, Millisatoshi(mv['debit_msat']).millisatoshis, - sorted(mv['tags']), + mv['tags'], mv['fees_msat'] if 'fees_msat' in mv else '')) if mv['version'] != 2: raise ValueError(f'version not 2 {mv}') @@ -261,6 +262,9 @@ def matchup_events(u_set, evs, chans, tag_list): if len(u_set) == 0: raise ValueError(f"utxo-set is empty. exp {evs}, actual {u_set}") + def get_tags(utxo): + return [utxo['primary_tag']] + utxo['extra_tags'] + txid = u_set[0][0]['utxo_txid'] # Stash the set for logging at end, if error _u_set = u_set @@ -277,7 +281,7 @@ def matchup_events(u_set, evs, chans, tag_list): else: acct = ev[0] - if u[0]['account_id'] != acct or sorted(u[0]['tags']) != sorted(ev[1]): + if u[0]['account_id'] != acct or get_tags(u[0]) != ev[1]: continue if ev[2] is None: @@ -289,7 +293,7 @@ def matchup_events(u_set, evs, chans, tag_list): # ugly hack to annotate two possible futures for a utxo if type(ev[2]) is tuple: - tag = u[1]['tags'] if u[1] else u[1] + tag = get_tags(u[1]) if u[1] else u[1] if tag not in [x[0] for x in ev[2]]: raise ValueError(f"Unable to find {tag} in event set {ev}") if not u[1]: @@ -297,14 +301,14 @@ def matchup_events(u_set, evs, chans, tag_list): u_set.remove(u) break for x in ev[2]: - if x[0] == u[1]['tags'] and 'to_miner' not in u[1]['tags']: + 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'] else: - if sorted(ev[2]) != sorted(u[1]['tags']): + 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 u[1]['tags']: + if 'to_miner' not in get_tags(u[1]): tag_list[ev[3]] = u[1]['txid'] found = True