offers: BLIP 42 contacts with BIP 353 receive side and cross-compat vectors#89
Open
vincenzopalazzo wants to merge 10 commits into
Open
offers: BLIP 42 contacts with BIP 353 receive side and cross-compat vectors#89vincenzopalazzo wants to merge 10 commits into
vincenzopalazzo wants to merge 10 commits into
Conversation
Implements bLIP 42 contact secret derivation for mutual authentication in Lightning Network payments. - Add ContactSecret struct for TLV serialization with Readable/Writeable - Add ContactSecrets for managing primary and additional remote secrets - Add compute_contact_secret() for deterministic secret derivation - Support offers with issuer_signing_pubkey and blinded paths Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
…et, invreq_payer_offer Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Implements BLIP-42 contact management for the sender side: - Add contact_secret and payer_offer fields to InvoiceRequestContents - Add builder methods: contact_secrets(), payer_offer() - Add accessor methods: contact_secret(), payer_offer() - Add OptionalOfferPaymentParams fields for contact_secrects and payer_offer - Update ChannelManager::pay_for_offer to pass contact information - Add create_compact_offer_builder to OffersMessageFlow for small payer offers - Update tests to include new InvoiceRequestFields Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Replace `Option<[u8; 32]>` with `Option<ContactSecret>` across the invoice_request internal storage, the `InvoiceRequest::contact_secret()` accessor, and the `InvoiceRequestFields` round-trip type. The wire format is unchanged (`ContactSecret` writes/reads the same 32 raw bytes via its existing `Writeable`/`Readable` impls), so this is a pure type-system refactor. Add `INVREQ_CONTACT_SECRET_TYPE` and `INVREQ_PAYER_OFFER_TYPE` `pub(super)` constants in `offers::contacts`, mirroring the pattern of `INVOICE_REQUEST_PAYER_ID_TYPE` and the other TLV-type constants in `offers/`. Replace the numeric literals in `ExperimentalInvoiceRequestTlvStream` with these constants so the BLIP 42 TLV numbers are defined in exactly one place. Also document BLIP 42's anti-redirection rule on the public `InvoiceRequestFields::contact_secret` field, since enforcement is the application's responsibility — LDK does not own a contacts store. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`InvoiceRequestFields` is round-tripped through the blinded payment path's `path_id` (via `Bolt12OfferContext`), so older LDK nodes can end up parsing a serialization produced by a newer LDK. Per BOLT 1 "odd, it's OK; even, you die", optional/forward-compatible fields must use odd TLV types so unknown-to-an-older-reader fields are skipped rather than rejected with `DecodeError::UnknownRequiredField`. The original PoC used even types 6 and 8 for `contact_secret` and `payer_offer`, which would have broken forward compat between LDK versions once shipped. Move them to odd 7 and 9. Reserve 11 and 13 for `invreq_payer_bip_353_name` and `invreq_payer_bip_353_signature` in a follow-up commit. The wire format change is safe in-flight: PR lightningdevkit#4210 has not been released, so no deployed consumer is reading types 6 or 8 today. Flagged by `ldk-claude-review-bot` on the previous push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per bLIP 42, the contact secret is computed via ECDH between the private key behind our offer's offer_node_id and the counterparty offer's offer_node_id. The previous API took "our node identity" key, which never matches the derived signing pubkey LDK puts in its offers, so the counterparty could not reproduce the secret. Re-derive the offer signing keys from the offer metadata instead and expose the derivation as OffersMessageFlow::compute_contact_secret and ChannelManager::compute_contact_secret. The freestanding helper now takes a caller-provided secp context, propagates ECDH errors instead of panicking, and documents which key it expects. Also fold from_remote_secret into ContactSecrets, make secret matching constant-time, document the contact lifecycle end-to-end, and deduplicate the test vectors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Store the payer offer as a parsed Offer rather than raw bytes so that malformed offers are rejected when the invoice request is parsed instead of being silently dropped by accessors, and so accessors no longer re-parse and clone on every call. Reading persisted InvoiceRequestFields now propagates a decode error for corrupted payer offer bytes for the same reason. Also enforce the bLIP 42 size recommendation by failing to build an invoice request whose payer offer exceeds PAYER_OFFER_MAX_BYTES, right-size the experimental TLV allocation, and cover the new behavior with builder, parsing, and round-trip tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flow-built offers with blinded paths don't carry derivable metadata: the keys behind their signing pubkey are derived from a nonce that only lives in the paths' OffersContext, so deriving contact secrets via Offer::verify_using_metadata could never succeed. Pathless offers fare no better as they advertise the node id, whose key isn't available for the raw-point ECDH BLIP 42 requires. Instead, require a blinded path in compact contact offers and hand the caller the nonce used to build them, mirroring how the async receive offer cache persists offers. compute_contact_secret then re-derives the signing keys from that nonce via Offer::verify_using_recipient_data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parse the invreq_payer_bip_353_name (2000001733) and invreq_payer_bip_353_signature (2000001735) records, rejecting invoice requests whose name comes without a signature or whose BIP 340 signature doesn't cover the invoice request's merkle root (sans the top-level signature and the record itself) under the lightning/invoice_request/invreq_payer_bip_353_signature tag. Sending names is left for follow-up work. The verified name and committed offer signing key are surfaced through InvoiceRequestFields so recipients can later check the resolved offer via PayerBip353Name::matches_offer. Also add InvoiceRequestFields::payer_contact, which applies the spec's receive rules against the caller's contacts: when the received secret matches a known contact the payer's offer and BIP 353 name are withheld by construction, preventing a leaked secret from redirecting future payments to an impersonator's offer. Wire-format coverage comes from lightning-kmp (the BLIP 42 reference implementation): LDK parses its encoded invoice requests, verifies the payer name signature, and re-serializes byte-for-byte. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pay an offer with contact fields attached and assert they survive the trip through the invoice request, the invoice's blinded path data, and the recipient's PaymentClaimable context. The reverse payment reuses the same deterministically derived secret, letting both sides attribute each other's payments to the same contact pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR makes the BLIP 42 contacts implementation feature complete on the receive side, on top of the rebased work from the upstream PR lightningdevkit#4210.
What's new on top of the previous branch:
compute_contact_secretnow re-derives the offer signing keys from the offerNonce, whichcreate_compact_offer_builderreturns alongside the builder. The previous metadata-based derivation could never work for flow-built offers, the nonce only lives in the blinded path contexts. The new end-to-end test caught this!lightning||invoice_request||invreq_payer_bip_353_signature) and reject the invoice request when the name comes without a valid signature. Sending names is left for a follow-up.InvoiceRequestFields::payer_contactapplies the spec rule for known contacts: when the received secret matches an existing contact the payer offer and BIP 353 name are withheld by construction, so a leaked secret cannot redirect future payments to an impersonator offer.Still out of scope, maybe in another PR: the sender side for BIP 353 names, and reusing the async-receive static offer as long-lived payer offer.
Spec: https://github.com/lightning/blips/blob/master/blip-0042.md
Note: the base branch
blip42-basepoints to the upstream main commit this work is rebased on, so the diff shows only the 10 contacts commits.This work was done with AI assistance, disclosed also in the commit messages via Co-Authored-By.
Signed-off-by: Vincenzo Palazzo vincenzopalazzodev@gmail.com
🤖 Generated with Claude Code