Skip to content

Nostr zaps: signature verification + kind-9735 receipt publishing [NIP-57] #31

Description

@Chemaclass

Context

Follow-up to the Nostr server-side issue (advertise allowsNostr/nostrPubkey, accept + structurally validate the nostr zap request, use it as the invoice description). This issue adds the parts that need cryptography, a relay/WebSocket client, and a payment-settled hook to make the server fully NIP-57 compliant: verify the incoming zap request's signature, and sign + publish the kind-9735 zap receipt after payment.

This is the largest new surface — the library currently has no payment-notification path (requestInvoice returns pr and stops) and no outbound WebSocket.

Goal

Full NIP-57 zap compliance: verified zap requests, and signed kind-9735 receipts published to the request's relays once the invoice settles.

Scope / Tasks

  • Add a Nostr crypto dependency. Evaluate swentel/nostr-php (Schnorr signing + relay WebSocket — covers both signing and publishing) and/or an extension-backed verifier (uma/secp256k1-nostr, innis/nostr-core) for fast signature verification. Pick and justify.
  • Verify the zap request signature (NIP-57 Appendix D rule 1): recompute the event id = sha256 of the NIP-01 serialized array and BIP-340 Schnorr-verify against the event pubkey. Reject invalid signatures.
  • Server Nostr key management: config for the server's Nostr private key (matching the advertised nostrPubkey). Handle securely (env/secret, never logged/committed).
  • Payment-settled hook: add a way to learn when an invoice is paid — an LNbits webhook route (the invoice request supports webhook) and/or polling. Persist the stored zap request keyed to the invoice/payment hash so it can be matched on settlement.
  • Build + sign the kind-9735 receipt (NIP-57 Appendix E):
    • kind:9735, content:"", created_at: invoice paid_at, pubkey: server's nostrPubkey.
    • tags: p (recipient, from request), optional P (sender = request pubkey), optional e/a/k copied from the request, bolt11 (the paid invoice), description (the exact zap-request JSON that was hashed), optional preimage.
    • id = sha256(serialization), sig = BIP-340 Schnorr by the server key.
  • Publish the receipt over WebSocket to every relay in the zap request's relays tag (["EVENT", <9735>]), handling OK/NOTICE.
  • Tests: signature verify accept/reject with fixtures; receipt event construction (tags/content/created_at) against the NIP-57 example; a mocked relay-publish path.

Implementation notes

  • SHA256(description) on the receipt MUST match the invoice's description_hash — reuse the exact stored zap-request bytes from the server-side issue.
  • This likely warrants a new module (e.g. src/Zap/…) plus an infrastructure route for the LNbits webhook.
  • Relay publishing and signing are the genuinely new capabilities; keep them behind interfaces so they're testable without live relays/nodes.

Acceptance criteria

  • Forged zap requests are rejected (bad Schnorr sig).
  • On invoice settlement, a correctly-signed 9735 receipt is generated and pushed to the request's relays (verified against a mock relay).
  • Receipt description matches the invoice description_hash.
  • composer test-all green; server private key never logged/committed.

Dependencies

  • Depends on the Nostr server-side issue (advertisement + request handling + zap-as-description).

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions