Skip to content

Commit 2c96968

Browse files
committed
xpay: add xpay_attempt_start and xpay_attempt_end notifications.
Requested-by: Michael at Boltz Signed-off-by: Rusty Russell <[email protected]> Changelog-Added: Plugins: `xpay` now publishes `xpay_attempt_start` and `xpay_attempt_end` notifications on every payment send attempt.
1 parent faf08f8 commit 2c96968

File tree

2 files changed

+164
-1
lines changed

2 files changed

+164
-1
lines changed

doc/developers-guide/plugin-development/event-notifications.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,85 @@ Where:
567567
- `plugin_name`: The short name of the plugin.
568568
- `plugin_path`: The full file path to the plugin executable.
569569
- `methods`: An array of RPC method names that the plugin registered.
570+
571+
572+
### `xpay_attempt_start` (v25.09 onward)
573+
574+
Emitted by `xpay` when part of a payment begins. `payment_hash` and
575+
`groupid` uniquely identify this xpay invocation, and `partid` then identifies
576+
this particular attempt to pay part of that.
577+
578+
`total_payment_msat` is the total amount (usually the invoice amount),
579+
which will be the same across all parts, adn `attempt_msat` is the
580+
amount being delivered to the destination by this part.
581+
582+
Each element in `hops` shows the amount going into the node (i.e. with
583+
fees, `channel_in_msat`) and the amount we're telling it to send
584+
to the other end (`channel_out_msat`). The `channel_out_msat` will
585+
be equal to the next `channel_in_msat. The final
586+
`channel_out_msat` will be equal to the `attempt_msat`.
587+
588+
The example shows a payment from this node via 1x2x3 (direction 1) to 035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d, then via 2x3x4 (direction 0) to 022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59.
589+
590+
```json
591+
{
592+
"xpay_attempt_start": {
593+
"payment_hash": "f5a6a059a25d1e329d9b094aeeec8c2191ca037d3f5b0662e21ae850debe8ea2",
594+
"groupid": 1,
595+
"partid": 1,
596+
"total_payment_msat": 200000,
597+
"attempt_msat": 100000,
598+
"hops": [
599+
{
600+
"next_node": "035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d",
601+
"short_channel_id": "1x2x3",
602+
"direction": 1,
603+
"channel_in_msat": 100030,
604+
"channel_out_msat": 100030
605+
},
606+
{
607+
"next_node": "022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59",
608+
"short_channel_id": "2x3x4",
609+
"direction": 0,
610+
"channel_in_msat": 100030,
611+
"channel_out_msat": 100000
612+
}
613+
]
614+
}
615+
}
616+
```
617+
618+
### `xpay_attempt_end` (v25.09 onward)
619+
620+
Emitted by `xpay` when part of a payment ends. `payment_hash`, `groupid` and `partid`
621+
will match a previous `xpay_attempt_start`.
622+
623+
`status` will be "success" or "failure". `duration` will be a number of seconds, with 9 decimal places. This is the time between `xpay` telling lightningd to send the onion, to when `xpay` processes the response.
624+
625+
If `status` is "failure", there will always be an `error_message`: the other fields below
626+
will be missing in the unusual case where the error onion is corrupted.
627+
628+
`failed_node_id`: If it's a non-local error, the source of the error.
629+
`failed_short_channel_id`: if it's not the final node, the channel it's complaining about.
630+
`failed_direction`: if it's not the final node, the channel direction.
631+
`failed_msg`: the decrypted onion message, in hex, if it was valid.
632+
`error_code`: the error code returned (present unless onion was corrupted).
633+
`error_message`: always present: if `failed_node_id` is present it's just the name of the `error_code`, but otherwise it can be a more informative error from our own node.
634+
635+
```json
636+
{
637+
"xpay_attempt_end": {
638+
"payment_hash": "f5a6a059a25d1e329d9b094aeeec8c2191ca037d3f5b0662e21ae850debe8ea2",
639+
"groupid": 12345677890,
640+
"partid": 1,
641+
"duration": 1.123456789,
642+
"status": "failure",
643+
"failed_node_id": "035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d",
644+
"failed_msg": "1007008a01024eb43f5212a864e19c426ec0278fb1c506eb043a1cdfde88bd1747080f711dbb472ecce9b1c44f2df7dbbc501a78451fe3ac93b6b9a2aac1bddc9dbb86e81b1b06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0000670000010000684e64ea010000060000000000000000000000010000000a000000003b023380",
645+
"failed_short_channel_id": "1x2x3",
646+
"failed_direction": 1,
647+
"error_code": 4103,
648+
"error_message": "temporary_channel_failure"
649+
}
650+
}
651+
```

plugins/xpay/xpay.c

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ struct attempt {
149149

150150
struct payment *payment;
151151
struct amount_msat delivers;
152+
struct timemono start_time;
152153

153154
/* Path we tried, so we can unreserve, and tell askrene the results */
154155
const struct hop *hops;
@@ -536,6 +537,73 @@ static struct amount_msat total_delivered(const struct payment *payment)
536537
return sum;
537538
}
538539

540+
/* We can notify others of what the details are, so they can do their own
541+
* layer heuristics. */
542+
static void json_add_attempt_fields(struct json_stream *js,
543+
const struct attempt *attempt)
544+
{
545+
/* These three uniquely identify this attempt */
546+
json_add_sha256(js, "payment_hash", &attempt->payment->payment_hash);
547+
json_add_u64(js, "groupid", attempt->payment->group_id);
548+
json_add_u64(js, "partid", attempt->partid);
549+
}
550+
551+
static void outgoing_notify_start(const struct attempt *attempt)
552+
{
553+
struct json_stream *js = plugin_notification_start(NULL, "xpay_attempt_start");
554+
json_add_attempt_fields(js, attempt);
555+
json_add_amount_msat(js, "total_payment_msat", attempt->payment->amount);
556+
json_add_amount_msat(js, "attempt_msat", attempt->delivers);
557+
json_array_start(js, "hops");
558+
for (size_t i = 0; i < tal_count(attempt->hops); i++) {
559+
const struct hop *hop = &attempt->hops[i];
560+
json_object_start(js, NULL);
561+
json_add_pubkey(js, "next_node", &hop->next_node);
562+
json_add_short_channel_id(js, "short_channel_id", hop->scidd.scid);
563+
json_add_u32(js, "direction", hop->scidd.dir);
564+
json_add_amount_msat(js, "channel_in_msat", hop->amount_in);
565+
json_add_amount_msat(js, "channel_out_msat", hop->amount_out);
566+
json_object_end(js);
567+
}
568+
json_array_end(js);
569+
plugin_notification_end(attempt->payment->plugin, js);
570+
}
571+
572+
static void outgoing_notify_success(const struct attempt *attempt)
573+
{
574+
struct json_stream *js = plugin_notification_start(NULL, "xpay_attempt_end");
575+
json_add_string(js, "status", "success");
576+
json_add_timerel(js, "duration", timemono_between(time_mono(), attempt->start_time));
577+
json_add_attempt_fields(js, attempt);
578+
plugin_notification_end(attempt->payment->plugin, js);
579+
}
580+
581+
static void outgoing_notify_failure(const struct attempt *attempt,
582+
int failindex, int errcode,
583+
const u8 *replymsg,
584+
const char *errstr)
585+
{
586+
struct json_stream *js = plugin_notification_start(NULL, "xpay_attempt_end");
587+
json_add_string(js, "status", "failure");
588+
json_add_attempt_fields(js, attempt);
589+
if (replymsg)
590+
json_add_hex_talarr(js, "failed_msg", replymsg);
591+
json_add_timerel(js, "duration", timemono_between(time_mono(), attempt->start_time));
592+
if (failindex != -1) {
593+
if (failindex != 0)
594+
json_add_pubkey(js, "failed_node_id", &attempt->hops[failindex-1].next_node);
595+
if (failindex != tal_count(attempt->hops)) {
596+
const struct hop *hop = &attempt->hops[failindex];
597+
json_add_short_channel_id(js, "failed_short_channel_id", hop->scidd.scid);
598+
json_add_u32(js, "failed_direction", hop->scidd.dir);
599+
}
600+
}
601+
if (errcode != -1)
602+
json_add_u32(js, "error_code", errcode);
603+
json_add_string(js, "error_message", errstr);
604+
plugin_notification_end(attempt->payment->plugin, js);
605+
}
606+
539607
static void update_knowledge_from_error(struct command *aux_cmd,
540608
const char *buf,
541609
const jsmntok_t *error,
@@ -590,6 +658,7 @@ static void update_knowledge_from_error(struct command *aux_cmd,
590658

591659
/* Garbled? Blame random hop. */
592660
if (!replymsg) {
661+
outgoing_notify_failure(attempt, -1, -1, replymsg, "Garbled error message");
593662
index = pseudorand(tal_count(attempt->hops));
594663
description = "Garbled error message";
595664
add_result_summary(attempt, LOG_UNUSUAL,
@@ -627,6 +696,7 @@ static void update_knowledge_from_error(struct command *aux_cmd,
627696
} else
628697
errmsg = failcode_name;
629698

699+
outgoing_notify_failure(attempt, index, failcode, replymsg, errmsg);
630700
description = tal_fmt(tmpctx,
631701
"Error %s for path %s, from %s",
632702
errmsg,
@@ -881,6 +951,8 @@ static struct command_result *injectpaymentonion_succeeded(struct command *aux_c
881951
plugin_err(aux_cmd->plugin, "Invalid injectpaymentonion result '%.*s'",
882952
json_tok_full_len(result), json_tok_full(buf, result));
883953

954+
outgoing_notify_success(attempt);
955+
884956
/* Move from current_attempts to past_attempts */
885957
list_del_from(&payment->current_attempts, &attempt->list);
886958
list_add(&payment->past_attempts, &attempt->list);
@@ -1008,6 +1080,9 @@ static struct command_result *do_inject(struct command *aux_cmd,
10081080
return command_still_pending(aux_cmd);
10091081
}
10101082

1083+
outgoing_notify_start(attempt);
1084+
attempt->start_time = time_mono();
1085+
10111086
req = jsonrpc_request_start(aux_cmd,
10121087
"injectpaymentonion",
10131088
injectpaymentonion_succeeded,
@@ -2118,6 +2193,12 @@ static const struct plugin_hook hooks[] = {
21182193
},
21192194
};
21202195

2196+
/* Notifications for each payment part we attempt */
2197+
static const char *outgoing_notifications[] = {
2198+
"xpay_attempt_start",
2199+
"xpay_attempt_end",
2200+
};
2201+
21212202
int main(int argc, char *argv[])
21222203
{
21232204
struct xpay *xpay;
@@ -2131,7 +2212,7 @@ int main(int argc, char *argv[])
21312212
commands, ARRAY_SIZE(commands),
21322213
notifications, ARRAY_SIZE(notifications),
21332214
hooks, ARRAY_SIZE(hooks),
2134-
NULL, 0,
2215+
outgoing_notifications, ARRAY_SIZE(outgoing_notifications),
21352216
plugin_option_dynamic("xpay-handle-pay", "bool",
21362217
"Make xpay take over pay commands it can handle.",
21372218
bool_option, bool_jsonfmt, &xpay->take_over_pay),

0 commit comments

Comments
 (0)