xpay: don't use MPP for bolt12 unless the invoice explicitly supports it.
What changed, and why it matters
This change removes a deprecated option that let Core Lightning's xpay plugin split BOLT12 invoice payments into multiple parts even when the invoice did not explicitly allow it. Going forward, xpay will respect the invoice's feature flags and avoid multi-part payments unless permitted. The commit notes this behavior actually changed by default in the previous release without anyone noticing, and this patch cleans up the leftover override and deprecation.
No immediate action required. Operators who relied on `xpay.ignore_bolt12_mpp` should ensure their BOLT12 invoices advertise MPP support if they want multi-part payments. Reviewers may want to confirm that the previous release's default change was intentional and that no other deprecated overrides remain.
Security signals we found
Removes deprecated API that bypassed invoice feature constraints
Enforces BOLT12 invoice feature bits for MPP eligibility
Could reduce payment failures or protocol non-compliance caused by forced MPP against non-supporting invoices
Evidence from the diff
The patch removes the xpay.ignore_bolt12_mpp deprecated option and its handling in plugins/xpay/xpay.c. Previously, xpay would disable MPP only if the BOLT12 invoice did not offer OPT_BASIC_MPP, but a deprecated config flag could force MPP anyway. Now the override is gone, so payment->disable_mpp is set purely based on whether the invoice features advertise MPP support, aligning with BOLT12 spec language. The test test_xpay_bolt12_no_mpp is simplified to only test the non-deprecated path.
Changed components
plugins/xpay/xpay.ctests/test_xpay.pydoc/developers-guide/deprecated-features.mdInspect captured patch +11 / −16
diff --git a/doc/developers-guide/deprecated-features.md b/doc/developers-guide/deprecated-features.md
index be44a583..acbb3184 100644
--- a/doc/developers-guide/deprecated-features.md
+++ b/doc/developers-guide/deprecated-features.md
@@ -9,7 +9,6 @@ privacy:
| Name | Type | First Deprecated | Last Supported | Description |
|----------------------------------------------------|--------------------|------------------|----------------|---------------------------------------------------------------------------------------------------------------------------|
-| xpay.ignore_bolt12_mpp | Field | v25.05 | v25.12 | Try MPP even if the BOLT12 invoice doesn't explicitly allow it (CLN didn't until 25.02) |
| listpeerchannels.max_total_htlc_in_msat | Field | v25.02 | v26.04 | 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.04 | Value "unknown" is deprecated: field will be omitted instead |
diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c
index f3a518c1..05d001d8 100644
--- a/plugins/xpay/xpay.c
+++ b/plugins/xpay/xpay.c
@@ -2033,10 +2033,15 @@ static struct command_result *xpay_core(struct command *cmd,
* paths, we just know the cltv we use to enter the
* final hop. */
payment->final_cltv = 0;
- /* We will start honoring this flag in future */
+ /* BOLT #12:
+ * - if `invoice_features` contains the MPP/compulsory bit:
+ * - MUST pay the invoice via multiple separate blinded paths.
+ * - otherwise, if `invoice_features` contains the MPP/optional bit:
+ * - MAY pay the invoice via multiple separate payments.
+ * - otherwise:
+ * - MUST NOT use multiple parts to pay the invoice.
+ */
payment->disable_mpp = !feature_offered(b12inv->invoice_features, OPT_BASIC_MPP);
- if (payment->disable_mpp && command_deprecated_in_ok(cmd, "ignore_bolt12_mpp", "v25.05", "v25.12"))
- payment->disable_mpp = false;
} else {
struct bolt11 *b11
= bolt11_decode(tmpctx, payment->invstring,
diff --git a/tests/test_xpay.py b/tests/test_xpay.py
index 3ba80066..db4fde9f 100644
--- a/tests/test_xpay.py
+++ b/tests/test_xpay.py
@@ -637,15 +637,10 @@ def test_xpay_no_mpp(node_factory, chainparams):
assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1
-@pytest.mark.parametrize("deprecations", [False, True])
-def test_xpay_bolt12_no_mpp(node_factory, chainparams, deprecations):
+def test_xpay_bolt12_no_mpp(node_factory, chainparams):
"""If we force it, we use MPP even if BOLT12 invoice doesn't say we should"""
# l4 needs dev-allow-localhost so it considers itself to have an advertized address, and doesn't create a blinded path from l2/l4.
opts = [{}, {}, {'dev-force-features': -17, 'dev-allow-localhost': None}, {}]
- if deprecations is True:
- for o in opts:
- o['i-promise-to-fix-broken-api-user'] = 'xpay.ignore_bolt12_mpp'
- o['broken_log'] = 'DEPRECATED API USED: xpay.ignore_bolt12_mpp'
l1, l2, l3, l4 = node_factory.get_nodes(4, opts=opts)
node_factory.join_nodes([l1, l2, l3], wait_for_announce=True)
@@ -666,12 +661,8 @@ def test_xpay_bolt12_no_mpp(node_factory, chainparams, deprecations):
ret = l1.rpc.xpay(invl3['invoice'])
assert ret['failed_parts'] == 0
- if deprecations:
- assert ret['successful_parts'] == 2
- assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 2
- else:
- assert ret['successful_parts'] == 1
- assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1
+ assert ret['successful_parts'] == 1
+ assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1
assert ret['amount_msat'] == AMOUNT
Why this scored 36/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.