BLIP-0056: PoS-delegated BOLT 12 offers (alternative implementation)#87
Draft
vincenzopalazzo wants to merge 5 commits intomainfrom
Draft
BLIP-0056: PoS-delegated BOLT 12 offers (alternative implementation)#87vincenzopalazzo wants to merge 5 commits intomainfrom
vincenzopalazzo wants to merge 5 commits intomainfrom
Conversation
Add two experimental TLV records to BOLT 12 offers and an `OfferModifier`
builder so a point-of-sale device can extend a merchant's template offer
with order-specific data.
- notification_paths (TLV 1_000_000_001): blinded message paths used by
the merchant to deliver payment notifications to the PoS once the
payment is claimed.
- payment_token (TLV 1_000_000_003): opaque bytes the merchant uses to
correlate the incoming invoice_request with an order.
Both types are odd to remain backwards-compatible with customer wallets
that do not implement BLIP-0056: unknown odd experimental TLVs are
passed through, while unknown even ones are rejected as
`UnknownRequiredFeature`. The TLV numbers and the `payment_token` shape
are tentative pending BLIP-0056 finalization.
`OfferModifier` is a small move-style builder analogous to `OfferBuilder`,
with setters for the two new fields and a `build()` that re-serializes
the offer and recomputes its `OfferId`. It deliberately touches only the
experimental TLVs; canonical offer fields are preserved unchanged.
PR 1 is the first commit in a stacked series implementing BLIP-0056.
Each commit in this series is intended to land as a separate upstream
PR against lightningdevkit/rust-lightning. Subsequent commits add
TLV-free signing-key derivation, the signed payment-token primitive,
the PoS notification onion-message types, the delegated payment
context, the merchant-side verification, and end-to-end notification
emission.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ated offers Add a new `derive_keys_for_delegation` helper that derives a signing [`Keypair`] from only the [`Nonce`] and [`ExpandedKey`], with no dependency on offer TLV content. Standard offers in BOLT 12 derive their signing key by hashing the offer's TLV stream into the merchant's HMAC, which means modifying any TLV record after the merchant signs invalidates the merchant's ability to reconstruct the key. For BLIP-0056 PoS-delegated offers a point-of-sale device extends the merchant's template offer with order-specific TLVs (notification paths, payment token) after the merchant has produced the template, so the standard derivation is not usable. The new helper sidesteps this by binding only to the nonce and key material. Authentication that the nonce is genuine is provided by an out-of-band channel (the reply path the merchant publishes alongside the template); this primitive is just the key-derivation half. `IV_BYTES` (`b"LDK Delegation ~"`) is domain-separated from `derive_keys`'s `b"LDK Invoice ~~~~"` so a key derived for one purpose cannot be reused for the other. The function is `#[allow(dead_code)]` for now and will be wired into the merchant-side `verify_invoice_request` path by a later patch in this stack. Builds and tests pass in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…0056
New module `offers::payment_token` providing the cryptographic primitives
the merchant uses to authenticate a `payment_notification` message to
the PoS, replacing the ChaCha20Poly1305-encrypted token in earlier
drafts of BLIP-0056.
Two types:
- `PaymentToken { order_hash: [u8; 32] }` — a tagged-hash digest of
PoS-private order information. The 32-byte hash is what the PoS
embeds in the offer's experimental `payment_token` TLV
(added in upstream PR 1). Built either via
`PaymentToken::from_order_info(&[u8])` (BIP-340-style tagged hash
with tag `LDK/PoS/order_v1`) or `PaymentToken::from_order_hash` for
callers that already hold the hash.
- `PaymentNotificationDigest { order_hash, payment_hash, amount_msat }` —
the triple the merchant signs when emitting the
`payment_notification`. The signature is a Schnorr signature over
`tagged_hash("LDK/PoS/notification_v1", order_hash || payment_hash
|| amount_msat_be)`. Tying the signature to all three fields prevents
a captured signature from being replayed against a different payment.
`sign` and `verify` are thin wrappers around `secp256k1::sign_schnorr_no_aux_rand`
and `verify_schnorr`. No new dependencies; all primitives already in the
workspace.
The two tags (`order_v1` and `notification_v1`) are domain-separated so a
signature over one cannot be confused with a signature over the other,
even when the underlying byte sequence happens to match.
Wire-format types and serialization for the notification message live
in `crate::onion_message::pos_notification` and are added by a later
patch in this stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… handler trait
New module `onion_message::pos_notification` defining the wire-format types
and handler trait for BLIP-0056 PoS payment notifications.
Three onion-message bodies and an enum wrapper:
- `PaymentNotification` (TLV 1_000_000_100) — merchant → PoS, sent
after `PaymentClaimable` is processed for a PoS-delegated offer.
Carries `(order_hash, payment_hash, amount_msat, signature)` where
the signature is produced by `PaymentNotificationDigest::sign` on
the merchant's offer issuer keypair (added in upstream PR 3).
- `PaymentAck` (TLV 1_000_000_102) — PoS → merchant, after the PoS
has verified the signature and recognised the order.
- `PaymentNack` (TLV 1_000_000_104) — PoS → merchant, when the
notification cannot be matched (signature invalid, order unknown,
amount mismatch). Carries an optional untrusted human-readable
reason for diagnostics.
- `PosNotificationMessage` — enum wrapper with the
`OnionMessageContents` impl that carries the TLV-type dispatch.
`PosNotificationHandler` trait mirrors `DNSResolverMessageHandler` and
`AsyncPaymentsMessageHandler` so it slots into the existing OnionMessenger
sibling-handler pattern. Trait method shapes:
- `handle_payment_notification(message, responder) -> Option<(reply, instructions)>`
- `handle_payment_ack(message)`
- `handle_payment_nack(message)`
- `release_pending_messages() -> Vec<(message, instructions)>`
The blanket `Deref` impl matches the convention used by the other
handler traits.
The handler is not yet wired into `OnionMessenger`; that integration
(adding a fifth generic parameter, the dispatch hook, and the
`IgnoringMessageHandler` impl in `peer_handler.rs`) lands together with
the post-claim emission path in a later patch in this stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… payments
Add a new `PaymentContext::DelegatedBolt12Offer` variant carrying the
`order_hash` and `notification_paths` of a BLIP-0056 PoS-delegated
payment, plus the corresponding `DelegatedBolt12OfferContext` struct.
The context is persisted alongside the payment so that when
`PaymentClaimable` fires the merchant has, in one place, everything
needed to construct and dispatch the `PaymentNotification`:
- `order_hash`: matches the offer's `payment_token` TLV (added in
upstream PR 1) and lets the merchant correlate the claim back to a
PoS-side order.
- `notification_paths`: blinded message paths the merchant uses to
deliver the notification to the PoS, copied from the offer's
`notification_paths` TLV.
`PaymentContextRef::DelegatedBolt12Offer` mirrors the existing pattern
for the other variants. Encoded with discriminant `4` in
`impl_writeable_tlv_based_enum_legacy!` (preserving 1/2/3 for the
existing variants for forward-compat with serialized state).
`from_parts` in `events::mod` returns `Err` for the new variant with a
`debug_assert!`, mirroring the `AsyncBolt12Offer` precedent. The proper
`PaymentPurpose::DelegatedBolt12OfferPayment` variant lands in a later
patch in this stack along with the post-claim notification emission
path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Alternative implementation of BLIP-0056 (BOLT 12 PoS notifications), replacing #86.
Two deliberate design departures from #86:
payment_token(ChaCha20Poly1305 + per-PoS shared secret) is replaced with a signed-hash token: the offer carriessha256(order_info)as opaque bytes, and the merchant signs(order_hash, payment_hash, amount_msat)with its offer issuer key when emitting the notification. ~30% smaller code at the bottom of the stack and no symmetric-key management.UnknownRequiredFeature); odd types are passed through. Verified againstoffer.rs:2244.Stacked-PR plan
This branch carries one commit per intended upstream PR, prefixed
[upstream PR N]. Five of seven layers landed in this branch; the remaining two are scaffolded by these primitives and can be opened as follow-ups.[upstream PR 1] offers: add experimental BLIP-0056 PoS-delegation TLVsnotification_paths(TLV1_000_000_001) +payment_token(TLV1_000_000_003) onOffer. NewOfferModifierbuilder.offer_accessors!propagation.[upstream PR 2] offers: add TLV-free signing key derivation for delegated offersderive_keys_for_delegationinsigner.rs. Domain-separatedIV_BYTES = b\"LDK Delegation ~\".[upstream PR 3] offers: add signed payment token primitives for BLIP-0056offers/payment_token.rs.PaymentToken { order_hash }+PaymentNotificationDigestwithsign/verify. Two domain-separated tags (LDK/PoS/order_v1,LDK/PoS/notification_v1).[upstream PR 4] onion_message: add PoS notification message types and handler traitonion_message/pos_notification.rs.PaymentNotification/PaymentAck/PaymentNack+PosNotificationMessage+PosNotificationHandlertrait. NoOnionMessengerwiring yet.[upstream PR 5] blinded_path: add DelegatedBolt12OfferContext for PoS paymentsPaymentContext::DelegatedBolt12Offercarrying(order_hash, notification_paths).[upstream PR 6] offers: token verification + flow.rs wiringverify_invoice_requestreads the offer'spayment_token, asks the merchant to validate against its order book, and embedsDelegatedBolt12OfferContextin the blinded payment paths.[upstream PR 7] PaymentPurpose + OnionMessenger wiring + functional testPaymentPurpose::DelegatedBolt12OfferPayment,OnionMessengersibling-handler integration (5th generic, likeDRH),IgnoringMessageHandlerimpl, end-to-end functional test inonion_message/functional_tests.rs.The five landed PRs are the entire bottom of the dependency graph — every primitive and type the integration layers (PRs 6–7) need. PRs 6–7 are integration work (wiring, no new primitives) and can be opened as follow-up PRs against this branch or directly to mainline.
Token design (replaces BLIP §3 `encrypted_payment_token`)
```text
payment_token = tagged_hash("LDK/PoS/order_v1", order_info) // 32 bytes, embedded in the offer
notification_signature = schnorr(
issuer_priv,
tagged_hash("LDK/PoS/notification_v1", order_hash || payment_hash || amount_msat_be),
)
```
Notification-time signing (Option α from the brainstorm): the offer's TLV carries only the unsigned 32-byte hash. The merchant verifies the hash against its order book on `verify_invoice_request` and signs the (order_hash, payment_hash, amount_msat) triple in the notification. The triple binds the signature to the specific claim event, preventing a captured signature from being replayed against a different payment.
Test plan
OfferModifier, 7 forpayment_token)DelegatedBolt12OfferContextround-trips viaimpl_writeable_tlv_based!Brainstorm reference
Design captured in `docs/brainstorms/2026-05-01-blip0056-pos-notifications-alternative.md` on this branch. Open questions still flagged there:
OffersContextor newMessageContextvariant?encrypted_payment_token; this implementation diverges. BLIP update is a separate effort.proposal/payer-proof-full-tree-signing(BOLT 12 PR 1295) — independent by default; verify before PR 6 lands.🤖 Generated with Claude Code