Skip to content

feat(zap): validate zap receipts before counting#12

Merged
DocNR merged 1 commit into
mainfrom
feat/validate-zap-receipts
Jun 14, 2026
Merged

feat(zap): validate zap receipts before counting#12
DocNR merged 1 commit into
mainfrom
feat/validate-zap-receipts

Conversation

@DocNR

@DocNR DocNR commented Jun 14, 2026

Copy link
Copy Markdown
Owner

What

Ports zap-receipt validation from upstream Jumble (b2f4714c), adapted to jank's services. Forged kind-9735 receipts no longer inflate stats, notifications, or the supporters list.

New lightning.validateZapReceipt(receipt) runs two NIP-57 checks:

  1. Issuer (Appendix F): the receipt's pubkey must equal the nostrPubkey advertised by the recipient's LNURL-pay endpoint. Lenient when the endpoint or its nostrPubkey can't be resolved — we can't prove forgery, so we don't drop the zap (no over-filtering).
  2. Preimage (Appendix E): when a preimage tag is present, sha256(preimage) must equal the bolt11 payment hash. The tag is optional, so absence isn't a failure.

The per-recipient nostrPubkey lookup is cached via DataLoader.

Wiring

  • stuff-stats.serviceaddZapByEvent is now async: it validates, then addZap notifies subscribers itself (the async check can't feed the batch's end-of-loop notify).
  • ZapNotification — gates render on an async validation result.
  • fetchRecentSupporters — drops the hardcoded authors: [alby] filter in favor of real per-receipt validation (matches upstream).

jank adaptations vs upstream

  • Uses jank's profileFetcher.fetchProfile / eventCache.fetchEvents / relayListService.fetchRelayList (not upstream's client.fetch*).
  • Adds a unit test (lightning.validate-zap-receipt.spec.ts) — upstream shipped this with no test.

Known limitation (documented in code)

NIP-57 Appendix F also asks that the bolt11 invoice amount equal the zap request's amount tag. That cross-check is not performed (matches upstream); the issuer check is the primary forgery defense. Flagging as a possible hardening follow-up.

Gates

  • Gate 1 (NIP-57): ✅ verified against the spec itself — Appendix F (issuer = LNURL nostrPubkey) + Appendix E (optional preimage→payment-hash).

  • Gate 2 (signer/identity): N/A — read/validate only; nothing is signed.

  • Gate 3 (realistic-relay): ✅ premise proven on live data. Fetched real kind-9735 receipts via nak and resolved each recipient's LNURL endpoint:

    recipient provider issuer == endpoint.nostrPubkey
    onyxvixen walletofsatoshi.com ✅ match
    ibnimam rizful.com ✅ match
    zap21 wallet.yakihonne.com ✅ match

    Real providers sign with their advertised nostrPubkey, so legitimate zaps pass validation; the lenient-on-unresolvable fallback protects the rest.

In-app smoke (please confirm before merge)

  1. Your Notifications still show real zaps you've received (not over-filtered).
  2. A note's zap count/stats still reflect real zaps.
  3. (optional) A hand-forged 9735 (wrong issuer) to a WoS/Primal recipient is not counted.

Verification

🤖 Generated with Claude Code

Verify each kind-9735 zap receipt before it inflates stats, notifications, or
the supporters list. Two NIP-57 checks (Appendix F / E):
  1. Issuer: the receipt must be signed by the nostrPubkey advertised by the
     recipient's LNURL pay endpoint. Lenient when the endpoint or its
     nostrPubkey can't be resolved (can't prove forgery).
  2. Preimage: when present, sha256(preimage) must equal the bolt11 payment hash.

lightning.validateZapReceipt() caches the per-recipient nostrPubkey lookup
(DataLoader). Wired into stuff-stats (addZapByEvent now async-validates, then
notifies itself) and ZapNotification (gates render on validation). The
supporters query drops its alby-author hardcode in favor of real validation.

Ported from upstream Jumble b2f4714c, adapted to jank's profileFetcher /
eventCache / relayListService. Adds a unit test (no upstream equivalent).
Live-verified: Wallet of Satoshi, Rizful, and yakihonne all sign with their
advertised nostrPubkey (issuer == endpoint nostrPubkey), so legit zaps pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying jank with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9dd401a
Status: ✅  Deploy successful!
Preview URL: https://a25e3fd6.jank-4ii.pages.dev
Branch Preview URL: https://feat-validate-zap-receipts.jank-4ii.pages.dev

View logs

@DocNR
DocNR merged commit a516f18 into main Jun 14, 2026
2 checks passed
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