common: coin movements for payments need group id as well as part id.
What changed, and why it matters
This change fixes accounting data that Core Lightning sends to plugins. For multi-part payments, the existing 'part_id' alone was not unique; the same part number could appear in different payment groups. The commit adds a 'group_id' field so plugins can reliably match each coin movement to the correct payment. It is a correctness/audit fix rather than a direct funds-loss bug, but inaccurate accounting could mislead downstream tools.
Treat as a low-severity correctness fix. Operators and plugin authors relying on coin_movement events should update parsers to consume group_id alongside part_id. No urgent patching is required for direct fund safety, but accounting plugins should be reviewed for duplicate-key assumptions.
Security signals we found
Data-integrity fix for payment event correlation
Multi-part payment identifier ambiguity resolved
Plugin notification schema change adding group_id
No cryptographic, memory-safety, or authorization changes observed
Evidence from the diff
The commit updates the channel coin-movement structure to carry a (part_id, group_id) pair instead of part_id alone, because part_id is only unique within a group. The payment_hash/part_id/group_id tuple is the true unique identifier. The JSON notification now emits ‘group_id’ whenever ‘part_id’ is present. Call sites for invoice houts pass hout->groupid; other call sites pass NULL for both part_id and group_id, preserving prior behavior.
Changed components
common/coin_mvt.ccommon/coin_mvt.hlightningd/coin_mvts.clightningd/notification.ccoin_movement plugin notificationInspect captured patch +37 / −14
diff --git a/common/coin_mvt.c b/common/coin_mvt.c
index d0d6d500..d1f97322 100644
--- a/common/coin_mvt.c
+++ b/common/coin_mvt.c
@@ -51,7 +51,8 @@ enum mvt_tag *new_tag_arr(const tal_t *ctx, enum mvt_tag tag)
struct channel_coin_mvt *new_channel_coin_mvt(const tal_t *ctx,
const struct channel_id *cid,
const struct sha256 *payment_hash TAKES,
- const u64 *part_id TAKES,
+ const u64 *part_id,
+ const u64 *group_id,
struct amount_msat amount,
const enum mvt_tag *tags TAKES,
bool is_credit,
@@ -61,7 +62,15 @@ struct channel_coin_mvt *new_channel_coin_mvt(const tal_t *ctx,
mvt->chan_id = *cid;
mvt->payment_hash = tal_dup_or_null(mvt, struct sha256, payment_hash);
- mvt->part_id = tal_dup_or_null(mvt, u64, part_id);
+ if (!part_id) {
+ assert(!group_id);
+ mvt->part_and_group = NULL;
+ } else {
+ mvt->part_and_group = tal(mvt, struct channel_coin_mvt_id);
+ mvt->part_and_group->part_id = *part_id;
+ mvt->part_and_group->group_id = *group_id;
+ }
+
mvt->tags = tal_dup_talarr(mvt, enum mvt_tag, tags);
if (is_credit) {
@@ -380,7 +389,7 @@ struct channel_coin_mvt *new_coin_channel_push(const tal_t *ctx,
bool is_credit)
{
return new_channel_coin_mvt(ctx, cid, NULL,
- NULL, amount,
+ NULL, NULL, amount,
take(new_tag_arr(NULL, tag)), is_credit,
AMOUNT_MSAT(0));
}
diff --git a/common/coin_mvt.h b/common/coin_mvt.h
index 664b3ac9..474a9678 100644
--- a/common/coin_mvt.h
+++ b/common/coin_mvt.h
@@ -37,6 +37,13 @@ enum mvt_tag {
SPLICE = 23,
};
+struct channel_coin_mvt_id {
+ /* multi-part payments may share a payment hash,
+ * so we should also record part-id and group-id for them */
+ u64 part_id;
+ u64 group_id;
+};
+
struct channel_coin_mvt {
/* account_id */
struct channel_id chan_id;
@@ -44,9 +51,9 @@ struct channel_coin_mvt {
/* identifier */
struct sha256 *payment_hash;
- /* mutli-part payments may share a payment hash,
- * so we should also record a 'part-id' for them */
- u64 *part_id;
+ /* multi-part payments may share a payment hash,
+ * so we should also record part-id and group-id for them */
+ struct channel_coin_mvt_id *part_and_group;
/* label / tag array */
enum mvt_tag *tags;
@@ -96,10 +103,12 @@ struct chain_coin_mvt {
enum mvt_tag *new_tag_arr(const tal_t *ctx, enum mvt_tag tag);
+/* Either part_id and group_id both NULL, or neither are */
struct channel_coin_mvt *new_channel_coin_mvt(const tal_t *ctx,
const struct channel_id *cid,
const struct sha256 *payment_hash TAKES,
- const u64 *part_id TAKES,
+ const u64 *part_id,
+ const u64 *group_id,
struct amount_msat amount,
const enum mvt_tag *tags TAKES,
bool is_credit,
diff --git a/doc/developers-guide/plugin-development/event-notifications.md b/doc/developers-guide/plugin-development/event-notifications.md
index bc649c98..37d69a77 100644
--- a/doc/developers-guide/plugin-development/event-notifications.md
+++ b/doc/developers-guide/plugin-development/event-notifications.md
@@ -333,6 +333,7 @@ A notification for topic `coin_movement` is sent to record the movement of coins
"vout":1, // (`chain_mvt` only)
"payment_hash": "xxx", // (either type, optional on both)
"part_id": 0, // (`channel_mvt` only, optional)
+ "group_id": 0, // (`channel_mvt` only, optional)
"credit_msat":2000000000,
"debit_msat":0,
"output_msat": 2000000000, // ('chain_mvt' only)
@@ -364,7 +365,7 @@ _Only_ tagged on external events (deposits/withdrawals to an external party).
`payment_hash` is the hash of the preimage used to move this payment. Only present for HTLC mediated moves (both `chain_mvt` and `channel_mvt`) A `chain_mvt` will have a `payment_hash` iff it's recording an htlc that was fulfilled onchain.
-`part_id` is an identifier for parts of a multi-part payment. useful for aggregating payments for an invoice or to indicate why a payment hash appears multiple times. `channel_mvt` only
+`part_id` and `group_id` are identifiers for parts of a multi-part payment. useful for aggregating payments for an invoice or to indicate why a payment hash appears multiple times. `channel_mvt` only
`credit` and `debit` are millisatoshi denominated amounts of the fund movement. A
'credit' is funds deposited into an account; a `debit` is funds withdrawn.
diff --git a/lightningd/coin_mvts.c b/lightningd/coin_mvts.c
index 5ac2ef98..80cd0b22 100644
--- a/lightningd/coin_mvts.c
+++ b/lightningd/coin_mvts.c
@@ -11,7 +11,7 @@ struct channel_coin_mvt *new_channel_mvt_invoice_hin(const tal_t *ctx,
const struct channel *channel)
{
return new_channel_coin_mvt(ctx, &channel->cid,
- &hin->payment_hash, NULL,
+ &hin->payment_hash, NULL, NULL,
hin->msat, new_tag_arr(ctx, INVOICE),
true, AMOUNT_MSAT(0));
}
@@ -30,7 +30,7 @@ struct channel_coin_mvt *new_channel_mvt_routed_hin(const tal_t *ctx,
return NULL;
return new_channel_coin_mvt(ctx, &channel->cid,
- &hin->payment_hash, NULL,
+ &hin->payment_hash, NULL, NULL,
hin->msat, new_tag_arr(ctx, ROUTED),
true, fees_collected);
}
@@ -40,7 +40,9 @@ struct channel_coin_mvt *new_channel_mvt_invoice_hout(const tal_t *ctx,
const struct channel *channel)
{
return new_channel_coin_mvt(ctx, &channel->cid,
- &hout->payment_hash, &hout->partid,
+ &hout->payment_hash,
+ &hout->partid,
+ &hout->groupid,
hout->msat, new_tag_arr(ctx, INVOICE),
false, hout->fees);
}
@@ -50,7 +52,7 @@ struct channel_coin_mvt *new_channel_mvt_routed_hout(const tal_t *ctx,
const struct channel *channel)
{
return new_channel_coin_mvt(ctx, &channel->cid,
- &hout->payment_hash, NULL,
+ &hout->payment_hash, NULL, NULL,
hout->msat, new_tag_arr(ctx, ROUTED),
false,
hout->fees);
diff --git a/lightningd/notification.c b/lightningd/notification.c
index cb60dabd..6368ec27 100644
--- a/lightningd/notification.c
+++ b/lightningd/notification.c
@@ -505,8 +505,10 @@ static void channel_movement_notification_serialize(struct json_stream *stream,
/* 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_id)
- json_add_u64(stream, "part_id", *chan_mvt->part_id);
+ 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);
Why this scored 24/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.