offers: loosen payment_constraints on invoices' blinded paths.
What changed, and why it matters
This change fixes a bug where Core Lightning nodes that only had private channels could not receive payments through 'offers' (a type of invoice). The node was setting an overly tight deadline (called a CLTV payment constraint) on the hidden payment route. If any intermediate node added a small amount of padding to hide the route, the payment would fail with an 'invalid payload' error. The patch loosens the allowed deadline from about 6 blocks to 1008 blocks, matching real-world network delays and making such payments succeed.
Treat as a normal reliability/bug-fix patch. No urgent security action required. Users relying on BOLT12 offers with private-only channels should upgrade to restore payment compatibility with wallets that pad CLTV deltas.
Security signals we found
Change is a bug fix for payment failures, not a vulnerability fix
Loosens a protocol constraint that was too strict in practice
No input validation removed; only a numeric slack value increased
No memory safety, authentication, or authorization changes
No cryptographic operations modified
Evidence from the diff
In plugins/offers_invreq_hook.c, when constructing a BOLT12 invoice with blinded paths for a node reachable only via private channels, the code sets encrypted_data_tlv.payment_constraints.max_cltv_expiry. Previously it added only 6 blocks of slack to the computed base + best->cltv + cltv_final. Because senders commonly pad CLTV deltas to obscure path length, this tight constraint caused HTLC failures (TLV 10 pos 103: cltv_expiry > payment_constraint). The patch removes the 6-block slack and instead adds 1008 blocks (half the protocol maximum), aligning with observed network CLTV delays and preventing spurious payment failures.
Changed components
plugins/offers_invreq_hook.cBOLT12 offers invoice generationBlinded path payment_constraints constructionInspect captured patch +18 / −5
diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c
index 5cefec99..c8fe8d6e 100644
--- a/plugins/offers_invreq_hook.c
+++ b/plugins/offers_invreq_hook.c
@@ -292,15 +292,28 @@ static struct command_result *found_best_peer(struct command *cmd,
* after `invoice_created_at`:
* - MUST set `invoice_relative_expiry`
*/
- /* Give them 6 blocks, plus one per 10 minutes until expiry. */
if (ir->inv->invoice_relative_expiry)
- base = blockheight + 6 + *ir->inv->invoice_relative_expiry / 600;
+ base = blockheight + *ir->inv->invoice_relative_expiry / 600;
else
- base = blockheight + 6 + 7200 / 600;
-
+ base = blockheight + 7200 / 600;
+
+ /* BOLT #4:
+ * - MUST set `encrypted_data_tlv.payment_constraints`
+ * for each non-final node and MAY set it for the
+ * final node:
+ * - `max_cltv_expiry` to the largest block height at which
+ * the route is allowed to be used, starting from the final
+ * node's chosen `max_cltv_expiry` height at which the route
+ * should expire, adding the final node's
+ * `min_final_cltv_expiry_delta` and then adding
+ * `encrypted_data_tlv.payment_relay.cltv_expiry_delta` at
+ * each hop.
+ */
+ /* BUT: we also recommend padding CLTV when paying, to obscure paths: if this is too tight
+ * payments fail in practice! We add 1008 (half the max possible) */
etlvs[0]->payment_constraints = tal(etlvs[0],
struct tlv_encrypted_data_tlv_payment_constraints);
- etlvs[0]->payment_constraints->max_cltv_expiry = base + best->cltv + cltv_final;
+ etlvs[0]->payment_constraints->max_cltv_expiry = 1008 + base + best->cltv + cltv_final;
etlvs[0]->payment_constraints->htlc_minimum_msat = best->htlc_min.millisatoshis; /* Raw: tlv */
/* So we recognize this payment */
Why this scored 33/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.