Skip to content

Commit 16819f3

Browse files
committed
lightningd: make notifications from plugins just like native ones.
Rather than forcing them to wrap their parameters in a "payload" sub-object, copy in params directly. We include the "origin" field one level up, if they care. The next patch restores compatibility for the one place we currently use them, which is the pay plugin. Signed-off-by: Rusty Russell <[email protected]> Changelog-Deprecated: pyln-client: plugin custom notifications origins and payload (use parameters directly)
1 parent fc5edea commit 16819f3

File tree

9 files changed

+160
-38
lines changed

9 files changed

+160
-38
lines changed

doc/developers-guide/deprecated-features.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ hidden: false
2222
| coin_movement.utxo_txid | Notification Field | v25.09 | v26.09 | Use `utxo` instead of `utxo_txid` & `vout` |
2323
| coin_movement.txid | Notification Field | v25.09 | v26.09 | Use `spending_txid` instead |
2424
| channel_state_changed.null_scid | Notification Field | v25.09 | v26.09 | In channel_state_changed notification, `short_channel_id` will be missing instead of `null` |
25+
| notification.payload | Notification Field | v25.09 | v26.09 | Notifications from plugins used to have fields in `payload` sub-object, now they are not (just like normal notifications) |
2526

2627
Inevitably there are features which need to change: either to be generalized, or removed when they can no longer be supported.
2728

lightningd/jsonrpc.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1556,14 +1556,21 @@ static struct command_result *param_command(struct command *cmd,
15561556
tok->end - tok->start, buffer + tok->start);
15571557
}
15581558

1559-
struct jsonrpc_notification *jsonrpc_notification_start(const tal_t *ctx, const char *method)
1559+
struct jsonrpc_notification *jsonrpc_notification_start_noparams(const tal_t *ctx, const char *method)
15601560
{
15611561
struct jsonrpc_notification *n = tal(ctx, struct jsonrpc_notification);
15621562
n->method = tal_strdup(n, method);
15631563
n->stream = new_json_stream(n, NULL, NULL);
15641564
json_object_start(n->stream, NULL);
15651565
json_add_string(n->stream, "jsonrpc", "2.0");
15661566
json_add_string(n->stream, "method", method);
1567+
1568+
return n;
1569+
}
1570+
1571+
struct jsonrpc_notification *jsonrpc_notification_start(const tal_t *ctx, const char *method)
1572+
{
1573+
struct jsonrpc_notification *n = jsonrpc_notification_start_noparams(ctx, method);
15671574
json_object_start(n->stream, "params");
15681575

15691576
return n;
@@ -1572,6 +1579,11 @@ struct jsonrpc_notification *jsonrpc_notification_start(const tal_t *ctx, const
15721579
void jsonrpc_notification_end(struct jsonrpc_notification *n)
15731580
{
15741581
json_object_end(n->stream); /* closes '.params' */
1582+
jsonrpc_notification_end_noparams(n);
1583+
}
1584+
1585+
void jsonrpc_notification_end_noparams(struct jsonrpc_notification *n)
1586+
{
15751587
json_object_end(n->stream); /* closes '.' */
15761588

15771589
/* We guarantee to have \n\n at end of each response. */

lightningd/jsonrpc.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,23 @@ bool jsonrpc_command_add(struct jsonrpc *rpc, struct json_command *command,
226226
*/
227227
struct jsonrpc_notification *jsonrpc_notification_start(const tal_t *ctx, const char *topic);
228228

229+
/**
230+
* Begin a JSON-RPC notification with the specified topic.
231+
*
232+
* Does *not* start a "params" object.
233+
*/
234+
struct jsonrpc_notification *jsonrpc_notification_start_noparams(const tal_t *ctx, const char *topic);
235+
229236
/**
230237
* Counterpart to jsonrpc_notification_start.
231238
*/
232239
void jsonrpc_notification_end(struct jsonrpc_notification *n);
233240

241+
/**
242+
* Counterpart to jsonrpc_notification_start_noparams.
243+
*/
244+
void jsonrpc_notification_end_noparams(struct jsonrpc_notification *n);
245+
234246
/**
235247
* start a JSONRPC request; id_prefix is non-NULL if this was triggered by
236248
* another JSONRPC request.

lightningd/plugin.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -624,10 +624,10 @@ static const char *plugin_notification_handle(struct plugin *plugin,
624624
"forwarding to subscribers.",
625625
methname);
626626
} else if (notifications_have_topic(plugin->plugins, methname)) {
627-
n = jsonrpc_notification_start(NULL, methname);
627+
n = jsonrpc_notification_start_noparams(NULL, methname);
628628
json_add_string(n->stream, "origin", plugin->shortname);
629-
json_add_tok(n->stream, "payload", paramstok, plugin->buffer);
630-
jsonrpc_notification_end(n);
629+
json_add_tok(n->stream, "params", paramstok, plugin->buffer);
630+
jsonrpc_notification_end_noparams(n);
631631

632632
plugins_notify(plugin->plugins, take(n));
633633
}

plugins/bkpr/bookkeeper.c

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,7 +1428,7 @@ parse_and_log_chain_move(struct command *cmd,
14281428

14291429
if (err)
14301430
plugin_err(cmd->plugin,
1431-
"`coin_movement` payload did"
1431+
"`coin_movement` parameters did"
14321432
" not scan %s: %.*s",
14331433
err, json_tok_full_len(params),
14341434
json_tok_full(buf, params));
@@ -1725,14 +1725,14 @@ static struct command_result *json_utxo_deposit(struct command *cmd, const char
17251725
const char *err;
17261726

17271727
err = json_scan(tmpctx, buf, params,
1728-
"{payload:{utxo_deposit:{"
1728+
"{utxo_deposit:{"
17291729
"account:%"
17301730
",transfer_from:%"
17311731
",outpoint:%"
17321732
",amount_msat:%"
17331733
",timestamp:%"
17341734
",blockheight:%"
1735-
"}}}",
1735+
"}}",
17361736
JSON_SCAN_TAL(tmpctx, json_strdup, &ev->acct_name),
17371737
JSON_SCAN_TAL(tmpctx, json_strdup, &ev->origin_acct),
17381738
JSON_SCAN(json_to_outpoint, &ev->outpoint),
@@ -1742,7 +1742,7 @@ static struct command_result *json_utxo_deposit(struct command *cmd, const char
17421742

17431743
if (err)
17441744
plugin_err(cmd->plugin,
1745-
"`%s` payload did not scan %s: %.*s",
1745+
"`%s` parameters did not scan %s: %.*s",
17461746
move_tag, err, json_tok_full_len(params),
17471747
json_tok_full(buf, params));
17481748

@@ -1800,14 +1800,14 @@ static struct command_result *json_utxo_spend(struct command *cmd, const char *b
18001800

18011801
ev->spending_txid = tal(ev, struct bitcoin_txid);
18021802
err = json_scan(tmpctx, buf, params,
1803-
"{payload:{utxo_spend:{"
1803+
"{utxo_spend:{"
18041804
"account:%"
18051805
",outpoint:%"
18061806
",spending_txid:%"
18071807
",amount_msat:%"
18081808
",timestamp:%"
18091809
",blockheight:%"
1810-
"}}}",
1810+
"}}",
18111811
JSON_SCAN_TAL(tmpctx, json_strdup, &acct_name),
18121812
JSON_SCAN(json_to_outpoint, &ev->outpoint),
18131813
JSON_SCAN(json_to_txid, ev->spending_txid),
@@ -1817,7 +1817,7 @@ static struct command_result *json_utxo_spend(struct command *cmd, const char *b
18171817

18181818
if (err)
18191819
plugin_err(cmd->plugin,
1820-
"`%s` payload did not scan %s: %.*s",
1820+
"`%s` parameters did not scan %s: %.*s",
18211821
move_tag, err, json_tok_full_len(params),
18221822
json_tok_full(buf, params));
18231823

@@ -1911,7 +1911,7 @@ static struct command_result *json_coin_moved(struct command *cmd,
19111911

19121912
if (err)
19131913
plugin_err(cmd->plugin,
1914-
"`coin_movement` payload did not scan %s: %.*s",
1914+
"`coin_movement` parameters did not scan %s: %.*s",
19151915
err, json_tok_full_len(params),
19161916
json_tok_full(buf, params));
19171917

@@ -1920,7 +1920,7 @@ static struct command_result *json_coin_moved(struct command *cmd,
19201920
&tags);
19211921
if (err)
19221922
plugin_err(cmd->plugin,
1923-
"`coin_movement` payload did not scan %s: %.*s",
1923+
"`coin_movement` parameters did not scan %s: %.*s",
19241924
err, json_tok_full_len(params),
19251925
json_tok_full(buf, params));
19261926

plugins/channel_hint.c

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,17 @@ struct channel_hint *channel_hint_from_json(const tal_t *ctx,
184184
const jsmntok_t *toks)
185185
{
186186
const char *ret;
187-
const jsmntok_t *payload = json_get_member(buffer, toks, "payload"),
188-
*jhint =
189-
json_get_member(buffer, payload, "channel_hint");
187+
const jsmntok_t *payload , *jhint;
190188
struct channel_hint *hint = tal(ctx, struct channel_hint);
191189

190+
/* Deprecated API uses "payload" */
191+
payload = json_get_member(buffer, toks, "payload");
192+
/* Modern API includes fields directly */
193+
if (!payload) {
194+
jhint = json_get_member(buffer, toks, "channel_hint");
195+
} else {
196+
jhint = json_get_member(buffer, payload, "channel_hint");
197+
}
192198
ret = json_scan(ctx, buffer, jhint,
193199
"{timestamp:%,scid:%,estimated_capacity_msat:%,total_capacity_msat:%,enabled:%}",
194200
JSON_SCAN(json_to_u32, &hint->timestamp),

tests/plugins/custom_notifications.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77

88
@plugin.subscribe("custom")
9-
def on_custom_notification(origin, payload, **kwargs):
10-
plugin.log("Got a custom notification {} from plugin {}".format(payload, origin))
9+
def on_custom_notification(origin, message, **kwargs):
10+
plugin.log("Got a custom notification {} from plugin {}".format(message, origin))
1111

1212

1313
@plugin.method("emit")
@@ -25,23 +25,23 @@ def faulty_emit(plugin):
2525

2626

2727
@plugin.subscribe("pay_success")
28-
def on_pay_success(origin, payload, **kwargs):
28+
def on_pay_success(origin, payment_hash, **kwargs):
2929
plugin.log(
3030
"Got a pay_success notification from plugin {} for payment_hash {}".format(
3131
origin,
32-
payload['payment_hash']
32+
payment_hash
3333
)
3434
)
3535

3636

3737
@plugin.subscribe("pay_part_start")
38-
def on_pay_part_start(origin, payload, **kwargs):
39-
plugin.log("Got pay_part_start: {}".format(payload))
38+
def on_pay_part_start(origin, **kwargs):
39+
plugin.log("Got pay_part_start: {}".format(kwargs))
4040

4141

4242
@plugin.subscribe("pay_part_end")
43-
def on_pay_part_end(origin, payload, **kwargs):
44-
plugin.log("Got pay_part_end: {}".format(payload))
43+
def on_pay_part_end(origin, **kwargs):
44+
plugin.log("Got pay_part_end: {}".format(kwargs))
4545

4646

4747
@plugin.subscribe("ididntannouncethis")

tests/test_plugin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2641,7 +2641,7 @@ def test_custom_notification_topics(node_factory):
26412641
)
26422642
l1, l2 = node_factory.line_graph(2, opts=[{'plugin': plugin}, {}])
26432643
l1.rpc.emit()
2644-
l1.daemon.wait_for_log("Got a custom notification {'message': 'Hello world'} from plugin custom_notifications.py")
2644+
l1.daemon.wait_for_log("Got a custom notification Hello world from plugin custom_notifications.py")
26452645

26462646
inv = l2.rpc.invoice(42, "lbl", "desc")['bolt11']
26472647
l1.rpc.pay(inv)
@@ -4374,6 +4374,7 @@ def test_peer_storage(node_factory, bitcoind):
43744374
assert not l2.daemon.is_in_log(r'PeerStorageFailed')
43754375

43764376

4377+
@pytest.mark.xfail(strict=True)
43774378
def test_pay_plugin_notifications(node_factory, bitcoind, chainparams):
43784379
plugin = os.path.join(os.getcwd(), 'tests/plugins/all_notifications.py')
43794380
opts = {"plugin": plugin}

tests/test_xpay.py

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
sync_blockheight,
88
)
99

10+
import ast
1011
import os
1112
import pytest
1213
import re
@@ -835,6 +836,19 @@ def test_xpay_twohop_bug(node_factory, bitcoind):
835836

836837

837838
def test_attempt_notifications(node_factory):
839+
def zero_fields(obj, fieldnames):
840+
if isinstance(obj, dict):
841+
for k, v in obj.items():
842+
if k in fieldnames:
843+
obj[k] = 0
844+
else:
845+
zero_fields(v, fieldnames)
846+
elif isinstance(obj, list):
847+
for item in obj:
848+
zero_fields(item, fieldnames)
849+
# other types are ignored
850+
return obj
851+
838852
plugin_path = os.path.join(os.getcwd(), 'tests/plugins/custom_notifications.py')
839853
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True,
840854
opts=[{"plugin": plugin_path}, {}, {}])
@@ -847,13 +861,34 @@ def test_attempt_notifications(node_factory):
847861
l1.rpc.xpay(inv1['bolt11'])
848862

849863
line = l1.daemon.wait_for_log("plugin-custom_notifications.py: Got pay_part_start: ")
850-
regex = r".*Got pay_part_start: \{'payment_hash': '" + inv1['payment_hash'] + r"', 'groupid': [0-9]*, 'partid': 1, 'total_payment_msat': 5000000, 'attempt_msat': 5000000, 'hops': \[\{'next_node': '" + l2.info['id'] + r"', 'short_channel_id': '" + scid12 + r"', 'direction': " + str(scid12_dir) + r", 'channel_in_msat': 5000051, 'channel_out_msat': 5000051\}, \{'next_node': '" + l3.info['id'] + r"', 'short_channel_id': '" + scid23 + r"', 'direction': " + str(scid23_dir) + r", 'channel_in_msat': 5000051, 'channel_out_msat': 5000000\}\]\}"
851-
assert re.match(regex, line)
864+
dict_str = line.split("Got pay_part_start: ", 1)[1]
865+
data = zero_fields(ast.literal_eval(dict_str), ['groupid'])
866+
expected = {'payment_hash': inv1['payment_hash'],
867+
'groupid': 0,
868+
'partid': 1,
869+
'total_payment_msat': 5000000,
870+
'attempt_msat': 5000000,
871+
'hops': [{'next_node': l2.info['id'],
872+
'short_channel_id': scid12,
873+
'direction': scid12_dir,
874+
'channel_in_msat': 5000051,
875+
'channel_out_msat': 5000051},
876+
{'next_node': l3.info['id'],
877+
'short_channel_id': scid23,
878+
'direction': scid23_dir,
879+
'channel_in_msat': 5000051,
880+
'channel_out_msat': 5000000}]}
881+
assert data == expected
852882

853-
# Note, duration always has 9 decimals, EXCEPT that the python code interprets it, so if the last digit is a 0 it will only print 8.
854883
line = l1.daemon.wait_for_log("plugin-custom_notifications.py: Got pay_part_end: ")
855-
regex = r".*Got pay_part_end: \{'status': 'success', 'duration': [0-9]*\.[0-9]*, 'payment_hash': '" + inv1['payment_hash'] + r"', 'groupid': [0-9]*, 'partid': 1\}"
856-
assert re.match(regex, line)
884+
dict_str = line.split("Got pay_part_end: ", 1)[1]
885+
data = zero_fields(ast.literal_eval(dict_str), ('duration', 'groupid'))
886+
expected = {'payment_hash': inv1['payment_hash'],
887+
'status': 'success',
888+
'duration': 0,
889+
'groupid': 0,
890+
'partid': 1}
891+
assert data == expected
857892

858893
inv2 = l3.rpc.invoice(10000000, 'test_attempt_notifications2', 'test_attempt_notifications2')
859894
l3.rpc.delinvoice('test_attempt_notifications2', "unpaid")
@@ -863,22 +898,77 @@ def test_attempt_notifications(node_factory):
863898
l1.rpc.xpay(inv2['bolt11'])
864899

865900
line = l1.daemon.wait_for_log("plugin-custom_notifications.py: Got pay_part_start: ")
866-
regex = r".*Got pay_part_start: \{'payment_hash': '" + inv2['payment_hash'] + r"', 'groupid': [0-9]*, 'partid': 1, 'total_payment_msat': 10000000, 'attempt_msat': 10000000, 'hops': \[\{'next_node': '" + l2.info['id'] + r"', 'short_channel_id': '" + scid12 + r"', 'direction': " + str(scid12_dir) + r", 'channel_in_msat': 10000101, 'channel_out_msat': 10000101\}, \{'next_node': '" + l3.info['id'] + r"', 'short_channel_id': '" + scid23 + r"', 'direction': " + str(scid23_dir) + r", 'channel_in_msat': 10000101, 'channel_out_msat': 10000000\}\]\}"
867-
assert re.match(regex, line)
901+
dict_str = line.split("Got pay_part_start: ", 1)[1]
902+
data = zero_fields(ast.literal_eval(dict_str), ['groupid'])
903+
expected = {'payment_hash': inv2['payment_hash'],
904+
'groupid': 0,
905+
'partid': 1,
906+
'total_payment_msat': 10000000,
907+
'attempt_msat': 10000000,
908+
'hops': [{'next_node': l2.info['id'],
909+
'short_channel_id': scid12,
910+
'direction': scid12_dir,
911+
'channel_in_msat': 10000101,
912+
'channel_out_msat': 10000101},
913+
{'next_node': l3.info['id'],
914+
'short_channel_id': scid23,
915+
'direction': scid23_dir,
916+
'channel_in_msat': 10000101,
917+
'channel_out_msat': 10000000}]}
918+
assert data == expected
868919

869920
line = l1.daemon.wait_for_log("plugin-custom_notifications.py: Got pay_part_end: ")
870-
regex = r".*Got pay_part_end: \{'status': 'failure', 'payment_hash': '" + inv2['payment_hash'] + r"', 'groupid': [0-9]*, 'partid': 1, 'failed_msg': '400f00000000009896800000006c', 'duration': [0-9]*\.[0-9]*, 'failed_node_id': '" + l3.info['id'] + r"', 'error_code': 16399, 'error_message': 'incorrect_or_unknown_payment_details'\}"
871-
assert re.match(regex, line)
921+
dict_str = line.split("Got pay_part_end: ", 1)[1]
922+
data = zero_fields(ast.literal_eval(dict_str), ('duration', 'groupid'))
923+
expected = {'payment_hash': inv2['payment_hash'],
924+
'status': 'failure',
925+
'duration': 0,
926+
'groupid': 0,
927+
'partid': 1,
928+
'failed_msg': '400f00000000009896800000006c',
929+
'failed_node_id': l3.info['id'],
930+
'error_code': 16399,
931+
'error_message': 'incorrect_or_unknown_payment_details'}
932+
assert data == expected
872933

873934
# Intermediary node failure
874935
l3.stop()
875936
with pytest.raises(RpcError, match=r"Failed after 1 attempts"):
876937
l1.rpc.xpay(inv2['bolt11'])
877938

878939
line = l1.daemon.wait_for_log("plugin-custom_notifications.py: Got pay_part_start: ")
879-
regex = r".*Got pay_part_start: \{'payment_hash': '" + inv2['payment_hash'] + r"', 'groupid': [0-9]*, 'partid': 1, 'total_payment_msat': 10000000, 'attempt_msat': 10000000, 'hops': \[\{'next_node': '" + l2.info['id'] + r"', 'short_channel_id': '" + scid12 + r"', 'direction': " + str(scid12_dir) + r", 'channel_in_msat': 10000101, 'channel_out_msat': 10000101\}, \{'next_node': '" + l3.info['id'] + r"', 'short_channel_id': '" + scid23 + r"', 'direction': " + str(scid23_dir) + r", 'channel_in_msat': 10000101, 'channel_out_msat': 10000000\}\]\}"
880-
assert re.match(regex, line)
940+
dict_str = line.split("Got pay_part_start: ", 1)[1]
941+
data = zero_fields(ast.literal_eval(dict_str), ['groupid'])
942+
expected = {'payment_hash': inv2['payment_hash'],
943+
'groupid': 0,
944+
'partid': 1,
945+
'total_payment_msat': 10000000,
946+
'attempt_msat': 10000000,
947+
'hops': [{'next_node': l2.info['id'],
948+
'short_channel_id': scid12,
949+
'direction': scid12_dir,
950+
'channel_in_msat': 10000101,
951+
'channel_out_msat': 10000101},
952+
{'next_node': l3.info['id'],
953+
'short_channel_id': scid23,
954+
'direction': scid23_dir,
955+
'channel_in_msat': 10000101,
956+
'channel_out_msat': 10000000}]}
957+
assert data == expected
881958

882959
line = l1.daemon.wait_for_log("plugin-custom_notifications.py: Got pay_part_end: ")
883-
regex = r".*Got pay_part_end: \{'status': 'failure', 'payment_hash': '" + inv2['payment_hash'] + r"', 'groupid': [0-9]*, 'partid': 1, 'failed_msg': '1007[a-f0-9]*', 'duration': [0-9]*\.[0-9]*, 'failed_node_id': '" + l2.info['id'] + r"', 'failed_short_channel_id': '" + scid23 + r"', 'failed_direction': " + str(scid23_dir) + r", 'error_code': 4103, 'error_message': 'temporary_channel_failure'\}"
884-
assert re.match(regex, line)
960+
dict_str = line.split("Got pay_part_end: ", 1)[1]
961+
data = zero_fields(ast.literal_eval(dict_str), ('duration', 'groupid', 'failed_msg'))
962+
expected = {'payment_hash': inv2['payment_hash'],
963+
'status': 'failure',
964+
'duration': 0,
965+
'groupid': 0,
966+
'partid': 1,
967+
# This includes the channel update: just zero it out
968+
'failed_msg': 0,
969+
'failed_direction': 0,
970+
'failed_node_id': l2.info['id'],
971+
'failed_short_channel_id': scid23,
972+
'error_code': 4103,
973+
'error_message': 'temporary_channel_failure'}
974+
assert data == expected

0 commit comments

Comments
 (0)