Add BOLT12 zaps - #2421
Conversation
|
CLINK offers already solved the LNURL/HTTP problem and has gotten uptake, so interesting framing/timing. The delay tolerance aspect sounds nice on the surface, until clients have to implement it. Waiting for settlement, because someone was convinced running a payment server on a phone was a good idea, shifts A LOT to clients. Beyond this clients will also now need additional remote wallet connection surface to background settle. That doesn't even make attribution less dubious, attribution is still the signed intent as lnp doesn’t bind Nostr↔payer. |
| - `description`: serialized zap intent event. | ||
| - `p`: recipient pubkey. | ||
| - `amount`: amount in millisatoshis. | ||
| - `offer`: canonical raw BOLT12 offer. |
There was a problem hiding this comment.
Is it worth including this? Any validating client will have to examine the profile to check the offer matches anyway, and a non-validating client (which hopefully isn't common?) doesn't care about the offer anyway.
There was a problem hiding this comment.
So, when people switch offers in their profiles, does it invalidate all past zaps? Or are there ways to know that two or more offers come from the same "account"?
There was a problem hiding this comment.
It would implicitly. We could, I suppose, include the signed profile info to prove the offer came from the recipient?
There was a problem hiding this comment.
Yeah we could embed the 10058 event instead so you can verify that its actually for the user without fetching it yourself and makes historical ones possible to validate
There was a problem hiding this comment.
Seems like it'd be relatively cheap to do it? Or would we be worried about the storage cost of it?
There was a problem hiding this comment.
I suggested below that it might be enough to embed a hash of the bolt12: #2421 (comment)
without fetching it yourself
I don't think that's too much to ask. And verifying historical zaps seems like a niche concern.
There was a problem hiding this comment.
And verifying historical zaps seems like a niche concern.
I dunno, I wouldn't generally expect that all of my zaps would dissapear just because I switched wallets and updated my offer.
There was a problem hiding this comment.
They don't disappear. Others just can't verify them. You could also make bolt12_offer not replaceable and have clients consider the highest timestamp one canonical. Presumably it won't get rotated that often anyway.
There was a problem hiding this comment.
We could use a variation of a p tag to prove the receiver P has authorized the signer in each zap.
Setup (recipient, once per service)
- The service generates its own keypair
K. - The recipient computes
m = tagged_hash("nostr-zap-signer", K), withKas 32 raw bytes (public key). - The recipient signs
mwith his regular identity keyR, producingsig. - The recipient hands the service the tag
["p", R, relay, sig].
Signing (service, every zap)
- The service builds the zap event.
- It adds the
ptag unchanged. - It signs the event with
K.
Verifying (anyone)
- Read
Kfrom the eventpubkey. - Read
R,relay, andsigfrom theptag. - Recompute
m = tagged_hash("nostr-zap-signer", K). - BIP340-verify
sigagainstR. Fail → reject. - Verify the event signature against
K. Fail → reject. - Pass → the zap is provably authorized by
R.
The proof lives in each event. Rotating services never invalidates old zaps.
No need to check the offer every time.
And the receiver can have multiple zap services running at the same time
There was a problem hiding this comment.
They don't disappear. Others just can't verify them.
Sure, but its weird that a new client won't see them.
Signing (service, every zap)
Eh, it'd be nice to not require yet another service? It seems fairly straightforward to just include the signed offer, or
You could also make bolt12_offer not replaceable and have clients consider the highest timestamp one canonical. Presumably it won't get rotated that often anyway.
seems fine to me too.
|
|
||
| ## Zap Intent | ||
|
|
||
| Before paying, the payer creates and signs a zap intent event. The zap intent binds the payer's Nostr key to the recipient, target, amount, and BOLT12 offer before the Lightning payment is attempted. |
There was a problem hiding this comment.
Hmm, seems like a lot of overhead to include intents here and we already require the payer_note match the zap info, why not just put whatever we need in there and drop the second event?
There was a problem hiding this comment.
This event isn't broadcast, its just embedded in the payer_note / zap event. This is similar to 9734 events in bolt 11 zaps, we want a signed message from the payer so we can commit the bolt12 invoice to them and vice versa. We use an event rather than a simple signed message because this is easier for clients because they already sign lots of events, it also works better for setups with remote keys where they can just whitelist this event type for signing.
|
Need to define where we get the offer for each user: kind: 0, another new kind: 10xxx for offers only, or maybe a PayTo event. |
|
Please not kind 0 |
764af34 to
a7ae9f9
Compare
can you explain this? I tried modeling this off of NIP 57 and it didn't list any of the things like this |
a7ae9f9 to
72dc8d5
Compare
|
Basically this: #2217 |
72dc8d5 to
6b00661
Compare
|
@vitorpamplona added |
|
Nice! How do we test payments to these offers today? Do we have a NWC channel also developed for bolt 12? Or do we need to call some lightning: uri with a bolt 12 payment? |
|
For nwc, we want people to transition to the bip321 based methods defined here: nostr-wallet-connect/nwc#2 |
|
Also, pick a NIP hex, please. How about B1? |
|
I couldn't test end-to-end at all, but added: vitorpamplona/amethyst#3685 |
Implements the quartz-side of the proposed "BOLT12 Zaps" NIP (nostr-protocol/nips#2421): public, self-verifying zap events that prove a BOLT12 payment without an LNURL server or recipient-operated receipt publisher. Events (nip-88 style templates/tags/builders): - Bolt12ZapEvent (kind 9736) and Bolt12ZapIntentEvent (kind 9737) - shared tags: amount (msats), offer, proof, P (payer), zap_id, description - registered both kinds in EventFactory BOLT12 decoding (new, no existing KMP library): - Bolt12Bech32: canonicalization (+ continuation / whitespace) + no-checksum, no-length-limit bech32 for lno1 offers and lnp1 payer proofs - Tlv: BigSize codec, TLV stream reader/writer, tu64 helpers - Bolt12Offer / Bolt12PayerProof parsers (proof TLV types per lightning/bolts#1346) - Bolt12Merkle: BOLT12 tagged-hash + signature merkle root + signature digest Validation: - Bolt12ZapValidator runs the NIP's steps (structure, embedded-intent match, payer-proof binding: invreq_payer_note == nostr:nipXX:<intent-id>, invoice_amount == amount) and returns a typed result with the payment-hash dedup key - Bolt12ProofVerifier checks preimage->payment_hash and the invoice/proof BIP-340 signatures for fully-disclosed proofs; compressed proofs are reported as unverified pending the (still-draft) lightning/bolts#1346 test vectors - Bolt12ZapBuilder assembles+signs the intent and the final zap Tests: 24 commonTest cases covering bech32/TLV/merkle round-trips, event tag structure + factory typing, and validator accept/reject paths (self-signed BOLT12 fixtures exercise the full merkle + schnorr path). Note: this is the receive/verify + assembly layer only. Origination is blocked on the upstream BOLT12 payer-proof spec merging and a wallet/NWC rail exposing lnp proofs; no Amethyst payment rail returns one today. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
6b00661 to
a43612b
Compare
|
Basically everybody on Nostr uses custodial wallets. Is BOLT12 supported by any of those? |
Some cashu mints support it, strike supports paying bolt12s |
|
What is the point of the "zap intent"? Can't you just publish the "zap" directly with the proof, tagging the recipient and the target event directly and be done with it? |
As stated above, to tie the bolt12 invoice to the zap and user. Prevents others from replaying your zap as their own |
I see, but this is too ugly. It makes sense on NIP-57 but here it is just unnecessary to have these fields embedded inside a JSON blob inside a tag. You could instead just define a "virtual event" that is produced with the intent of merely having its signature checked and then discarded, for example. Something like:
|
|
@fiatjaf I guess that works but feels a lot more complex and error prone. This flow is how everything else does it in nostr afaik |
|
Nothing else uses this weird maneuver of embedding events as strings inside other events except for NIP-57 and the reposts stuff, which is also ugly and a bad idea I always argued against. But it was just a suggestion to save space and make things more elegant. I have no horse in this race anyway. |
a43612b to
5015a52
Compare
Done |
|
Did you pick BI? BI is not a hex :) |
5015a52 to
035b3cf
Compare
Woops lol, changed to b1 |
|
ACK |
|
Nice! So if I understand correctly, as a user I just copy a bolt12 from my lightning node, paste it into a nostr client, and it just works(tm)? Render therefore unto Nostr the things which are Nostr's; and unto Lightning the things that are Lightning's.
This is great, for at least two reasons:
I used to do this with BTCPay, and it required a plugin and custom nginx rules. It was also very hard to debug when zaps failed. And this removes the dependency on DNS (other than nip-05 to find a user). |
Why not just include a reference? If for some reason a nostr client can't find the announcement, then it can't verify that zap, but that doesn't sound like a big deal. Maybe instead of a reference to a Why is the zap intent signed? If you don't sign it, then
I understand how that could happen. The NIP says:
The zap intent commits to the zapper (ephemeral) public key. |
Add BOLT12 offers and payer proofs as another zap option alongside the NIP-57 LNURL callback flow. Bind each Lightning payment proof to a signed Nostr zap intent so clients can verify settled payments and compute cumulative zap amounts without a recipient-operated receipt publisher.
035b3cf to
bf22089
Compare
It's not much different to include it and then get the full benefit of just needing the single zap event to e2e verify
Just including the bolt12 offer event solves all this more simply.
It makes it so you can't forge zaps for others. |
|
Fixed a few other references in the NIP that still said |
Tightens our implementation of the BIP-321-based NWC methods proposed in nostr-wallet-connect/nwc#2 — the successor to pay_invoice/make_invoice that clients are asked to transition to for bolt12 zaps in nostr-protocol/nips#2421: - reject cross-network payment instructions before paying (UNSUPPORTED_NETWORK) instead of failing after the debit - reject unknown req- BIP-321 params, including req-pop since we can't open proof-of-payment callbacks - reject a request amount that conflicts with the instruction amount - payer_note is delivered or rejected, never dropped: bolt11+note is rejected, internal settlements hand the note over as the payment memo - receive mints a per-request bolt12 offer carrying the requested description/amount instead of reusing the standing amountless offer - get_info reports the node's actual network instead of "mainnet" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Found a potential hole in this and curious on people's thoughts on the solution. Currently, you can replay a zap multiple times by just taking the same payment proof, zap intent, etc and creating a new zap event with just a different timestamp. We could fix this by requiring the zap event's |
|
Should clients not be de-dup'ing by the zap intent? |
That is a solution to this but in general is kind of an anti pattern in nostr from my understanding. Having to save lots of events and making the client have to dedup across a "global state" isn't cheap. It's also much nicer if it's just inherent to the spec |
|
I mean presumably if a client is displaying "zaped X total" or even just listing zaps it already scans through all such events so de-dup'ing is cheap? Unless they store a history of seen zap event ids and filter for new ones which would make zap intent filtering cost a bit more, but only like 2x-ish. |
My understanding is that is kinda an anti-pattern in nostr with having to dedup everything like this. Maybe @vitorpamplona or @jb55 can chime in? |
|
Yes, we dedup by request event already. It's annoying, but that's the cost of Nostr purposefully not being a blockchain. |
Add BOLT12 offers and payer proofs as another zap option alongside the NIP-57 LNURL callback flow. Bind each Lightning payment proof to a signed Nostr zap intent so clients can verify settled payments and compute cumulative zap amounts without a recipient-operated receipt publisher.