Skip to content

BLIP-0056: PoS-delegated BOLT 12 offers (alternative implementation)#87

Draft
vincenzopalazzo wants to merge 5 commits intomainfrom
claude/eager-northcutt-e62ca6
Draft

BLIP-0056: PoS-delegated BOLT 12 offers (alternative implementation)#87
vincenzopalazzo wants to merge 5 commits intomainfrom
claude/eager-northcutt-e62ca6

Conversation

@vincenzopalazzo
Copy link
Copy Markdown
Owner

Summary

Alternative implementation of BLIP-0056 (BOLT 12 PoS notifications), replacing #86.

Two deliberate design departures from #86:

  1. No symmetric crypto for the payment token. The encrypted payment_token (ChaCha20Poly1305 + per-PoS shared secret) is replaced with a signed-hash token: the offer carries sha256(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.
  2. All TLV types are odd, not even. Even-type experimental TLVs are rejected by every customer wallet that hasn't implemented BLIP-0056 (UnknownRequiredFeature); odd types are passed through. Verified against offer.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.

Commit Status Scope
[upstream PR 1] offers: add experimental BLIP-0056 PoS-delegation TLVs landed notification_paths (TLV 1_000_000_001) + payment_token (TLV 1_000_000_003) on Offer. New OfferModifier builder. offer_accessors! propagation.
[upstream PR 2] offers: add TLV-free signing key derivation for delegated offers landed derive_keys_for_delegation in signer.rs. Domain-separated IV_BYTES = b\"LDK Delegation ~\".
[upstream PR 3] offers: add signed payment token primitives for BLIP-0056 landed New offers/payment_token.rs. PaymentToken { order_hash } + PaymentNotificationDigest with sign / 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 trait landed New onion_message/pos_notification.rs. PaymentNotification / PaymentAck / PaymentNack + PosNotificationMessage + PosNotificationHandler trait. No OnionMessenger wiring yet.
[upstream PR 5] blinded_path: add DelegatedBolt12OfferContext for PoS payments landed PaymentContext::DelegatedBolt12Offer carrying (order_hash, notification_paths).
[upstream PR 6] offers: token verification + flow.rs wiring TODO verify_invoice_request reads the offer's payment_token, asks the merchant to validate against its order book, and embeds DelegatedBolt12OfferContext in the blinded payment paths.
[upstream PR 7] PaymentPurpose + OnionMessenger wiring + functional test TODO PaymentPurpose::DelegatedBolt12OfferPayment, OnionMessenger sibling-handler integration (5th generic, like DRH), IgnoringMessageHandler impl, end-to-end functional test in onion_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

  • `cargo +1.75.0 test -p lightning --lib offers::` — 152 tests pass (3 new for OfferModifier, 7 for payment_token)
  • `cargo +1.75.0 test -p lightning --lib onion_message::pos_notification::` — 6 tests pass (round-trip serialization for all 3 message types, TLV-type dispatch)
  • `cargo +1.75.0 test -p lightning --lib blinded_path::` — 5 tests pass, DelegatedBolt12OfferContext round-trips via impl_writeable_tlv_based!
  • `cargo +1.75.0 test -p lightning --lib` (no targeted filter) — 1199 tests pass, no regressions
  • `cargo +1.75.0 fmt --all -- --check` clean
  • Each commit compiles and passes tests in isolation per LDK review culture
  • End-to-end functional test (lands with PR 7)

Brainstorm reference

Design captured in `docs/brainstorms/2026-05-01-blip0056-pos-notifications-alternative.md` on this branch. Open questions still flagged there:

  • Notification path message context — extend OffersContext or new MessageContext variant?
  • BLIP draft text mismatch — the BLIP currently mentions an encrypted_payment_token; this implementation diverges. BLIP update is a separate effort.
  • Interaction with proposal/payer-proof-full-tree-signing (BOLT 12 PR 1295) — independent by default; verify before PR 6 lands.

🤖 Generated with Claude Code

vincenzopalazzo and others added 5 commits May 1, 2026 17:46
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant