Skip to content

Implement bLIP 56: BOLT 12 point-of-sale payment notifications#88

Open
vincenzopalazzo wants to merge 6 commits into
blip56-basefrom
blip56-pos-notifications
Open

Implement bLIP 56: BOLT 12 point-of-sale payment notifications#88
vincenzopalazzo wants to merge 6 commits into
blip56-basefrom
blip56-pos-notifications

Conversation

@vincenzopalazzo

Copy link
Copy Markdown
Owner

Full implementation of bLIP 56 on top of upstream main (c9260ee), replacing the stale macros/pos-bolt12-delegation branch.

What's implemented

  • payment_notifications feature bit 264/265 (provisional)
  • Offer records notification_path = 1000005601 and payment_token = 1000005603, odd experimental TLVs so unmodified wallets accept and mirror them
  • Onion messages payment_notification/notification_ack/notification_nack/payment_proof = 65560/65562/65564/65566 (provisional), new PosNotificationMessageHandler threaded through OnionMessenger
  • Payment token: ECIES to the template signing pubkey (fresh ephemeral key per order, ChaCha20Poly1305), sealing (amount, description, order_id) TLV-encoded
  • Merchant: nonce-only key derivation for templates (derive_keys_for_delegation), OffersContext::DelegatedInvoiceRequest verification, token checks per the feature-bit rules, order data embedded in payment_metadata (keys 128-130, the LDK-reserved range), notification retry queue persisted across restarts, abandoned after offer expiry + grace
  • PoS: stateless validation via PosNotificationContext::InboundOrder (authenticated by the path-level ReceiveAuthKey MAC), ack/nack with reason codes, Event::PaymentNotificationReceived / Event::PaymentProofReceived
  • ChannelManager APIs: create_pos_delegation_template, create_pos_order_offer, send_payment_proof

Tests

ln/pos_notification_tests.rs covers the full customer/merchant/PoS flow end to end (template, per-order offer, payment by an unmodified wallet, notification, ack, retry, duplicates), forged notifications, every nack reason, expired orders, bare-template and token-mismatch rejection at invoice-request time, and payment proof delivery. Full suite green on rustc 1.75.0 including doctests; each commit compiles standalone with tests.

Spec deltas to fold back into the bLIP

The PR base is blip56-base (= upstream main c9260ee) since the fork's main has diverged; the diff is exactly the six commits.

🤖 Generated with Claude Code

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

vincenzopalazzo and others added 6 commits June 10, 2026 18:59
bLIP 56 defines a protocol for point-of-sale devices to receive
payment notifications from merchants over onion messages. The merchant
advertises support by setting the payment_notifications feature bit on
the offer templates it hands to point-of-sale devices, and the bit
remains set on each per-order offer derived from a template.

The bit assignment (264/265) is provisional until the bLIP is merged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per bLIP 56, a point-of-sale device seals the order fields (amount,
description, and a per-order secret order_id) into a payment_token
encrypted to the offer's signing pubkey, which only the merchant can
recover the secret key for. The merchant verifies the sealed amount
and description against the mirrored offer at invoice-request time and
relays order_id in its payment notification, which is what
authenticates the notification to the device.

The token uses a fresh ephemeral key per order, so ciphertexts cannot
be linked to the merchant key across offers, with ChaCha20Poly1305
keyed by the ECDH shared secret providing authenticated encryption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the notification_path and payment_token offer records as odd
experimental TLVs (1000005601 and 1000005603, provisional) so that
wallets unaware of them still accept the offer and mirror the records
into the invoice_request, and an OfferModifier allowing a
point-of-sale device to construct a per-order offer from a merchant
template by filling in the amount, description, expiry, notification
paths, and an encrypted payment token.

Since the device modifies the offer's records, the merchant cannot
authenticate corresponding invoice requests via offer metadata, which
covers the entire TLV stream. Instead, a template's signing keys are
derived from a nonce alone and matched against the mirrored offer's
signing pubkey when verifying an invoice request for delegation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the payment_notification, notification_ack, notification_nack,
and payment_proof onion messages (TLV types 65560 through 65566,
provisional) along with a PosNotificationMessageHandler wired through
the OnionMessenger, and a PosNotificationContext for the blinded paths
carrying them.

A per-order offer's notification_path carries the full order state in
its message context, authenticated by the path itself, so the
point-of-sale device needs no persistent order state to validate
incoming notifications. The merchant's reply path context identifies
the pending notification an ack or nack refers to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On the merchant side, an invoice request arriving over a template's
DelegatedInvoiceRequest context is verified against the offer's
payment_token, rejecting it if the token or notification_path is
missing, fails to decrypt, or its sealed amount or description do not
match the mirrored offer. The recovered order_id, the notification
paths, and the order expiry are embedded in the payment context's
payment_metadata, using keys from the LDK-reserved range. When the
payment is claimed, a payment_notification is queued to the device
over the offer's notification_path and retried on timer ticks until
acknowledged or the order's expiry plus a grace period has passed,
persisting across restarts.

On the point-of-sale side, notifications are validated against the
notification path's message context per the bLIP 56 acceptance rules
and answered with an ack or a reason-coded nack. Authenticated
notifications surface as Event::PaymentNotificationReceived, and
customer payment proofs as Event::PaymentProofReceived, left
unverified for the consumer until BOLT 12 payer proofs are available.

ChannelManager grows create_pos_delegation_template,
create_pos_order_offer, and send_payment_proof to drive the three
roles of the protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the full customer/merchant/PoS flow: template creation,
per-order offer construction, payment by an unmodified wallet,
notification delivery, authentication, acknowledgement, retry, and
duplicate handling. Also covers rejection of forged notifications,
each nack reason code, expired orders, and invoice requests for bare
templates or offers whose cleartext fields do not match the sealed
payment token, as well as payment proof delivery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment on lines +880 to +893
/// Verifies that the request was for a point-of-sale offer template created using the given
/// key, by re-deriving the template's signing keys from the nonce included with the
/// [`BlindedMessagePath`] for which the request was sent through and matching them against
/// the offer's signing pubkey.
///
/// Unlike [`Self::verify_using_recipient_data`], the offer's TLV records are not covered by
/// the verification, since a point-of-sale device modifies them when constructing per-order
/// offers from the template. The nonce is authenticated by the blinded path itself.
///
/// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath
#[rustfmt::skip]
pub fn verify_for_delegation(
$self: $self_type, nonce: Nonce, key: &ExpandedKey,
) -> Result<InvoiceRequestVerifiedFromOffer, ()> {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this is skipping the formatting?

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