lightningd: add preimage to forward_event notification
What changed, and why it matters
This change adds the payment preimage to a plugin notification that fires when a forwarded Lightning payment succeeds. The preimage is already known to the node at that moment, so nothing secret is being exposed that wasn't already available internally. It is a feature enhancement, not a security fix or vulnerability.
No security action required. Reviewers may want to confirm that plugins subscribing to `forward_event` are expected to receive preimages and that this does not conflict with any privacy model documented for plugin authors.
Security signals we found
No memory-safety defects visible in diff
No change to authentication, authorization, or cryptographic checks
Preimage is already possessed by the node before notification is sent
No vendor security framing in commit message or changelog
Evidence from the diff
The commit extends the forward_event notification by passing in->preimage into json_add_forwarding_fields() when the HTLC status is settled. The preimage is only emitted for settled forwards and is omitted from the listforwards RPC. This is an intentional API change to give plugins more context; it does not alter access controls or introduce a memory-safety bug.
Changed components
lightningd/forwards.clightningd/forwards.hlightningd/notification.cdoc/developers-guide/plugin-development/event-notifications.mdtests/test_plugin.pyInspect captured patch +18 / −7
diff --git a/doc/developers-guide/plugin-development/event-notifications.md b/doc/developers-guide/plugin-development/event-notifications.md
index 4ee034a8..ea906c25 100644
--- a/doc/developers-guide/plugin-development/event-notifications.md
+++ b/doc/developers-guide/plugin-development/event-notifications.md
@@ -223,7 +223,8 @@ A notification for topic `forward_event` is sent every time the status of a forw
"fee_msat": 1001,
"status": "settled",
"received_time": 1560696342.368,
- "resolved_time": 1560696342.556
+ "resolved_time": 1560696342.556,
+ "preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
```
@@ -262,6 +263,7 @@ or
fields;
- `received_time` means when we received the htlc of this payment from the previous peer. It will be contained into all status case;
- `resolved_time` means when the htlc of this payment between us and the next peer was resolved. The resolved result may success or fail, so only `settled` and `failed` case contain `resolved_time`;
+- `preimage` is the 64-hex-char payment preimage revealed when the HTLC was fulfilled. Only present when `status` is `settled`;
- The `failcode` and `failreason` are defined in [BOLT 4](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#failure-messages).
### `sendpay_success`
diff --git a/lightningd/forwards.c b/lightningd/forwards.c
index 6c86d506..4ca7c0ee 100644
--- a/lightningd/forwards.c
+++ b/lightningd/forwards.c
@@ -1,7 +1,9 @@
#include "config.h"
+#include <bitcoin/preimage.h>
#include <ccan/mem/mem.h>
#include <ccan/tal/str/str.h>
#include <common/json_command.h>
+#include <common/json_stream.h>
#include <inttypes.h>
#include <lightningd/forwards.h>
#include <lightningd/jsonrpc.h>
@@ -86,7 +88,8 @@ bool string_to_forward_status(const char *status_str,
* between 'listforwards' API and 'forward_event' notification. */
void json_add_forwarding_fields(struct json_stream *response,
const struct forwarding *cur,
- const struct sha256 *payment_hash)
+ const struct sha256 *payment_hash,
+ const struct preimage *preimage)
{
/* We don't bother grabbing id from db on update. */
if (cur->created_index)
@@ -136,6 +139,8 @@ void json_add_forwarding_fields(struct json_stream *response,
json_add_timeabs(response, "received_time", cur->received_time);
if (cur->resolved_time)
json_add_timeabs(response, "resolved_time", *cur->resolved_time);
+ if (preimage)
+ json_add_preimage(response, "preimage", preimage);
}
static void listforwardings_add_forwardings(struct json_stream *response,
@@ -155,7 +160,7 @@ static void listforwardings_add_forwardings(struct json_stream *response,
while (stmt) {
const struct forwarding *cur = forwarding_details(tmpctx, wallet, stmt);
json_object_start(response, NULL);
- json_add_forwarding_fields(response, cur, NULL);
+ json_add_forwarding_fields(response, cur, NULL, NULL);
json_object_end(response);
tal_free(cur);
stmt = forwarding_next(wallet, stmt);
diff --git a/lightningd/forwards.h b/lightningd/forwards.h
index 0436035b..0156e63d 100644
--- a/lightningd/forwards.h
+++ b/lightningd/forwards.h
@@ -56,7 +56,8 @@ struct forwarding {
* `listforwardings_add_forwardings()`. */
void json_add_forwarding_fields(struct json_stream *response,
const struct forwarding *cur,
- const struct sha256 *payment_hash);
+ const struct sha256 *payment_hash,
+ const struct preimage *preimage);
static inline const char* forward_status_name(enum forward_status status)
{
diff --git a/lightningd/notification.c b/lightningd/notification.c
index 755a606b..9a5eac50 100644
--- a/lightningd/notification.c
+++ b/lightningd/notification.c
@@ -1,4 +1,5 @@
#include "config.h"
+#include <bitcoin/preimage.h>
#include <ccan/cast/cast.h>
#include <lightningd/channel.h>
#include <lightningd/coin_mvts.h>
@@ -389,8 +390,7 @@ static void forward_event_notification_serialize(struct json_stream *stream,
cur->htlc_id_in = in->key.id;
cur->created_index = created_index;
cur->updated_index = updated_index;
-
- json_add_forwarding_fields(stream, cur, &in->payment_hash);
+ json_add_forwarding_fields(stream, cur, &in->payment_hash, in->preimage);
}
REGISTER_NOTIFICATION(forward_event);
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 594e4ab3..75e8b51f 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -1490,9 +1490,12 @@ def test_forward_event_notification(node_factory, bitcoind, executor):
plugin_stats = l2.rpc.call('listforwards_plugin')['forwards']
assert len(plugin_stats) == 6
- # We don't have payment_hash in listforwards any more.
+ # We don't have payment_hash in listforwards any more. We also don't have
+ # preimage in listforwards
for p in plugin_stats:
del p['payment_hash']
+ if p.get('preimage') is not None:
+ del p['preimage']
# use stats to build what we expect went to plugin.
expect = stats[0].copy()
Why this scored 19/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.