Changed: offers: offers that include offer_paths now omit the offer_issuer_id and sign with the last used blinded path alias' key by default#8238
Conversation
|
As requested by @vincenzopalazzo |
86ec127 to
5c53729
Compare
|
This invoice is invalid: let's break it down by type, length, value (hex)
This has no "b0" field (i.e. type: 176 ( To quote bolt12: And: So, who is producing this invalid invoice? |
5c53729 to
2d84be6
Compare
206ea1b to
ee7364c
Compare
|
Thank you Rusty, and sorry for the late reply. I just updated this PR with what I think fixes the issue. It now does the following: So as far as I know, this code is now properly working and addresses the issues I had identified. The invoice signature is recognized as valid by fetchinvoice, and it works whether or not an offer contains an issuer_id. Thank you! |
c1523df to
2da0d2e
Compare
|
This solves the issuer_id issue identified in #4689 |
2da0d2e to
fbcf9db
Compare
|
It seems this one slipped through the 25.12 release crack because the issue it was linked to had been closed. will request and prompt for review ahead of early 2026 release. @21M4TW can you please rebase? |
fbcf9db to
636bf38
Compare
Thanks. I just rebased it to the latest commit from master. |
|
thanks! now over to @endothermicdev for review please. |
I agree with you that it was not "broken" in the sense that it was not preventing a payment, but the offer from a node with only private channels contained the unannounced node ID of that node. I can tag it as "changed". I will look into adding an optional parameter to the |
b25a491 to
a602f56
Compare
a602f56 to
1676761
Compare
|
@endothermicdev I added an optional |
There was a problem hiding this comment.
I still need to review the core part of this PR because it requires me to look at the code that I haven't been looking at for a long time, but I left some comments during my light review.
BTW review the commit is hard because you are putting all together, unfortunately, and probably splitting this in the right way is hard too, because it's like a chicken and eggs problem, but I would love to see the gen file stripped out and add them in a separate commit.
What I would do if I were writing the commit from 0 is:
- Core functionality
- auto gen files
- tests
But if the CLN team is fine with the current organisation, I am fine too!
| node_factory.join_nodes([l3, l5], announce_channels=False) | ||
| wait_for(lambda: ['alias' in n for n in l4.rpc.listnodes()['nodes']] == [True, True, True, True]) | ||
|
|
||
| offer = l5.rpc.offer(amount='2msat', description='test_offer_with_private_channels_force_issuer_id_multyhop2', force_issuer_id=True)['bolt12'] |
There was a problem hiding this comment.
assert on the offer kind, like you expect that there is a blinded path and offer issuer id, otherwise this test will pass also if there is no offer issuer id because the code should use the blinded path introductory node
| "invoice already signed"); | ||
| hsm_sign_b12_invoice(cmd->ld, inv); | ||
|
|
||
| hsm_sign_b12_invoice(cmd->ld, inv, path_pubkey); |
There was a problem hiding this comment.
I need to look more inside the code, but is the path_pubkey already includd inside the invoice? the spec should enforce to copy it down IIRC
There was a problem hiding this comment.
I checked this more closely: path_pubkey is not copied into the invoice TLVs themselves. In this patch it is passed out-of-band to createinvoice and only used to select which key hsmd signs the invoice with, so the spec copy-down requirement does not apply to that value directly.
The concrete issue I do see here is HSM compatibility: this patch changes hsmd_sign_bolt12 (message 25) from the old u16 + bytes payload to ?pubkey, but lightningd still negotiates with HSM versions 5-7 and always calls towire_hsmd_sign_bolt12 with the new encoding. That means an older external signer can still pass init and then misparse the first BOLT12 invoice-signing request. I think this needs either a new wire message / version-gated fallback, or a minimum HSM bump to 7 so older signers fail fast at startup.
1676761 to
ccba97c
Compare
If I was putting the new force_issuer_id part in a separate commit it would separate the core functionalities quite well I think. Do you want me to do that? For the core functionalities, it is mostly divided in two, that is the more trivial part where I simply set the offer_issuer_id to NULL when offer_paths is present (and the issuer ID is not enforced through force_issuer_id), and the part where I pass down the last |
942924e to
6ca806f
Compare
offers: offers that include offer_paths now omit offer_issuer_id and sign with the blinded path alias' key by default. An optional force_issuer_id field is also added to the offer command so the offer_issuer_id is included even when not required to reach the issuer due to the existence of offer_paths.
6ca806f to
729f192
Compare
vincenzopalazzo
left a comment
There was a problem hiding this comment.
Code Review
Positive Aspects
- The core approach is spec-correct: omitting
offer_issuer_idwhenoffer_pathsis present saves space and enhances privacy, as per BOLT #12. - The
node_blinded_privkeyderivation correctly follows BOLT #4 (ECDH + HMAC blinding factor), which is the right way to derive the signing key for the blinded node. - The
force_issuer_idescape hatch addresses the valid use case endothermicdev raised (vendor identity verification), and the fallback re-addingoffer_issuer_idwhen no blinded path can be created is correct. - The test covers both the default (omit issuer_id) and forced-inclusion paths.
Issues & Suggestions
1. Error message typo propagated from old code (Cleanup)
hsmd/libhsmd.c — the new node_blinded_privkey has:
hsmd_status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Could tweak bolt12 key.");This is in the failure path (!= 1), so it should say "Could not tweak bolt12 key." This was a pre-existing issue in the old handle_sign_bolt12, but since the function is being rewritten, worth fixing.
2. Pointer declaration style (Cleanup)
CLN convention is struct foo *ptr, not struct foo* ptr. Two occurrences:
hsmd/libhsmd.c:struct pubkey* path_pubkeylightningd/invoice.c:const struct pubkey* path_pubkey
3. Indentation and whitespace in offers_offer.c (Cleanup)
if (!we_want_blinded_path(cmd->plugin, false) || *force_issuer_id ) {
offer->offer_issuer_id = tal_dup(offer, struct pubkey, &id);
}- Uses 2-space indent instead of tab (CLN uses tabs)
- Trailing space before
)in*force_issuer_id )
4. Comment typo in offers_offer.c (Cleanup)
"it can be forced is requested" should be "it can be forced if requested"
5. createinvoice schema not updated (Medium)
The path_pubkey optional parameter was added to createinvoice in lightningd/invoice.c, but there is no corresponding update to doc/schemas/createinvoice.json. This is a user-facing API change that should be documented.
6. Empty line inside if block (Cleanup)
plugins/offers_invreq_hook.c:
if (!ir->inv->offer_issuer_id && ir->invreq->offer_paths) {
ir->inv->invoice_node_id = ir->blinded_node_id;
} else {Trailing blank line before } else { is unusual for CLN style.
Summary
Concept ACK. The spec compliance and crypto derivation are correct. Needs a minor fix for the createinvoice schema documentation and some style cleanups before merging.
|
@21M4TW can you update per @vincenzopalazzo 's coments?
By the end of this week (with enough time for someone to review) is the timeline so we can add this to 26.06? Release candidate planned for Monday 11 May. If you can't we'll move it to 25.09. |
|
@madelinevibes this is also a kind of privacy like feature that people want to have, probably if there is anyone taking it over I could try to pair with claude and take it over if you want to include it inside the current release |
|
@vincenzopalazzo if you are @21M4TW are on the same page, then it'd be great to get in! We will need it by Thursday, Friday at the latest Australia time in order to give time to review for the Release candidate. |
|
It would be really great if this PR could be cleaned up a bit more. We want commits to be atomic (do one thing at a time) to preserve bisectability. The changelog is added in the commit description and the commit message starts with the relevant subdaemon, such as |
offer_issuer_idfrom created offers when there's anoffer_path, as explained in the Bolt12 specifications. This saves space and enhance privacy when it's not needed because we already have a blinded path.force_issuer_idoptional field to theoffercommand so theoffer_issuer_idis included even when not required due the presence of blinded paths.