offers: honor `payment-fronting-nodes` when creating invoices.
What changed, and why it matters
This change fixes a bug in Core Lightning's BOLT12 'offers' feature where a node operator's explicit request to use specific 'payment-fronting nodes' was ignored when creating invoices. Previously, the node could silently issue a bare invoice without the requested privacy/relay path. Now it either honors the configured fronting nodes or fails the invoice request if it cannot. This is a correctness and privacy-hardening fix rather than a critical remote-exploitable vulnerability.
Treat as a low-to-moderate privacy bug fix. Review whether prior releases silently ignored payment-fronting-nodes and consider documenting the corrected behavior in release notes. No emergency response is indicated.
Security signals we found
Configuration intent not enforced, leading to weaker-than-expected invoice privacy paths
Adds explicit failure path when required fronting nodes cannot be satisfied
Test assertions added to verify invoice path first_node_id matches configured fronting nodes
No memory safety, cryptographic, or remote-code-execution signals present
Evidence from the diff
The patch modifies plugins/offers_invreq_hook.c so that add_blindedpaths passes od->fronting_nodes into find_best_peer, and found_best_peer rejects bare invoices when fronting_nodes was configured but no suitable peer is found. The test is updated to assert that generated invoice paths start at the configured fronting node(s) and that payments succeed. This closes a gap between configuration intent and invoice generation behavior.
Changed components
plugins/offers_invreq_hook.cBOLT12 offers invoice creationpayment-fronting-nodes configuration optiontests/test_invoices.pyInspect captured patch +18 / −3
diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c
index 0a330597..7926cb9c 100644
--- a/plugins/offers_invreq_hook.c
+++ b/plugins/offers_invreq_hook.c
@@ -274,6 +274,12 @@ static struct command_result *found_best_peer(struct command *cmd,
* for each `blinded_path` in `paths`, in order.
*/
if (!best) {
+ /* Don't allow bare invoices if they explicitly told us to front */
+ if (od->fronting_nodes) {
+ return fail_invreq(cmd, ir,
+ "Could not find path from payment-fronting-node");
+ }
+
/* Note: since we don't make one, createinvoice adds a dummy. */
plugin_log(cmd->plugin, LOG_UNUSUAL,
"No incoming channel for %s, so no blinded path",
@@ -384,6 +390,8 @@ static struct command_result *found_best_peer(struct command *cmd,
static struct command_result *add_blindedpaths(struct command *cmd,
struct invreq *ir)
{
+ const struct offers_data *od = get_offers_data(cmd->plugin);
+
if (!we_want_blinded_path(cmd->plugin, true))
return create_invoicereq(cmd, ir);
@@ -393,7 +401,7 @@ static struct command_result *add_blindedpaths(struct command *cmd,
* us onion messaging. */
return find_best_peer(cmd,
(1ULL << OPT_ROUTE_BLINDING) | (1ULL << OPT_ONION_MESSAGES),
- NULL, found_best_peer, ir);
+ od->fronting_nodes, found_best_peer, ir);
}
static struct command_result *cancel_invoice(struct command *cmd,
diff --git a/tests/test_invoices.py b/tests/test_invoices.py
index 1bb47d46..65fadd6c 100644
--- a/tests/test_invoices.py
+++ b/tests/test_invoices.py
@@ -976,8 +976,15 @@ def test_payment_fronting(node_factory):
l4offer = l4.rpc.offer(1000, 'l4offer', 'l4offer')['bolt12']
assert [r['first_node_id'] for r in l4.rpc.decode(l4offer)['offer_paths']] == [l1.info['id'], l2.info['id']]
- l1.rpc.fetchinvoice(l3offer)['invoice']
- l1.rpc.fetchinvoice(l4offer)['invoice']
+ l3invb12 = l1.rpc.fetchinvoice(l3offer)['invoice']
+ l4invb12 = l1.rpc.fetchinvoice(l4offer)['invoice']
+
+ assert only_one(l3.rpc.decode(l3invb12)['invoice_paths'])['first_node_id'] == l1.info['id']
+ # Given multiple, it will pick one.
+ assert only_one(l3.rpc.decode(l4invb12)['invoice_paths'])['first_node_id'] in (l1.info['id'], l2.info['id'])
+
+ l1.rpc.xpay(l3invb12)
+ l1.rpc.xpay(l4invb12)
def test_invoice_maxdesc(node_factory, chainparams):
Why this scored 32/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.