Fixed mismatched kinds in documentation Co-authored-by: Asai Toshiya <to.asai.60@gmail.com>
7.2 KiB
NIP-108
Lightning Gated Notes
draft
optional
author:coachchuckff
author:excalibur_guild
This NIP defines three events for gating Notes behind lightning paywalls:
- Lightning-Gated Note (
kind:55
): This note allows you wrap any type of note behind a lightning gated paywall by encrypting the payload with a purchasable decrypt key. - Key Note (
kind:56
): This note encrypts the key for a given note, per user, using NIP-04 between the corresponding gate creator's public key and the purchasers. It is linked to the gated note with theg
tag. - Announcement Note (
kind:54
): This note acts as the announcement of the gated note, giving a short preview of the content. It is linked to the gated note with theg
tag.
A complete open-source implantation of NIP-108 is available.
An example client can be found at Nostr Playground.
Protocol flow
Creating Gated Notes
- Poster creates a note of any kind
- Poster
JSON.stringify
s the whole note. - Poster encrypts the note string with a new nsec
secret
, andiv
usingaes-256-cbc
. - Poster creates the gated note
kind:55
with the encrypted note json string in thecontent
field, while puttingiv
,cost
(mSats), andendpoint
as tags. Theendpoint
tag is the server endpoint you use to hold yoursecret
's and issue lightning invoices from yourlud16
. - Poster then creates an announcement note
kind:54
with theg
tag (gated note's id) to preview the gated content.
Consuming Gated Notes
- Client finds gated content they want to purchase by browsing
kind:54
announcement notes. - Client loads the associated gated note of
kind:55
found in theg
tag - Client then GETs the
[endpoint]/[id]
- Gate server will respond with a 402 PR requesting a payment for the
cost
tag's amount in mSats - Client pays the amount
- Client uses the
successAction
url returned in the PR to fetch thesecret
which will unlock the gated content. - Client uses the
secret
and the gated note'siv
tag to decrypt the content usingaes-256-cbc
- Client then creates a key note
kind:56
with the content being thenip-04
encrypted secret with their publicKey and the gate note's creator publicKey. - Upon revisiting the gated note, the client can then decrypt the content using their key note.
Server Functions
NIP-108 requires an outside server to store secret
s and issue lighting invoices to those wishing to purchase the digital content.
The server should have two endpoints:
[endpoint]/create
- POST to create new notes
[endpoint]/[id]
- GET to fetch the PR to purchase the gated note's secret
, where id
is the gated note's id
.
Create a Gated Note
The server first needs to be able to store a gated note's secret
. Minimally, the server needs to store four items: the gated note's id
, the owner's lud16
, the decrypt secret
, and the cost
in msats.
It is advised to also check, server-side, that the gated note can be unlocked. To accomplish this the following should be done:
- Accept a POST to the server's
create
endpoint, with the following:gateEvent
,lud16
,secret
, andcost
; - The server should then decrypt the
gateEvent
using thesecret
and theiv
tag provided in the event. Since a gated event is just an encrypted JSON stringified event, you should be able to check any of the decrypted note's field to know it's been decrypted successfully. - One should also check that the
endpoint
matches the server's domain - Store in the server's database the gated note's
id
, owner'slud16
, decryptsecret
andcost
.
APP.post("/create")
export interface CreateNotePostBody {
gateEvent: VerifiedEvent<number>;
lud16: string;
secret: string;
cost: number;
}
Handling Purchases
Once the server has stored a gated note's secret
, it can then be purchased via lightning.
- A user will GET
[endpoint]/[id]
and the server will...- If
id
exists, return a 402 with a PR fetched from the storedlud16
for the amount of the storedcost
- If
id
does not exist, return a 404.
- If
- The PR will contain a
successAction
url which should be formatted as such:[endpoint]/[id]/[payment_hash]
. It is up to the user to poll thissuccessAction
. - When the
[endpoint]/[id]/[payment_hash]
endpoint is hit, the server should check the payment status...- If paid, return a JSON string
{secret: [secret]}
- If not paid, return a 402 with the same PR
- If paid, return a JSON string
APP.get("/:noteId")
APP.get("/:noteId/:paymentHash")
Event Reference and Examples
Gated Note ( Kind:55 )
kind:55
.content
should be a JSON stringified event of any kind.
.tag
MUST include the following:
iv
, the initialization vector used to encrypt thecontent
cost
, the cost to unlock in msatsendpoint
, the domain of the server used to store your decryptsecret
. The user can then call GET on[endpoint]/[id]
to fetch the unlock PR.
Announcement Note ( Kind:54 )
kind:54
.content
some preview or announcement of the content you have locked away.
.tag
MUST include the following:
g
, the id of the gated event.
Key Note ( Kind:56 )
kind:56
.content
the secret
encrypted via NIP-04's encrypt function between the gated note's creator's pubkey and your pubkey.
.tag
MUST include the following:
g
, the id of the gated event.
Encryption/Decryption
To encrypt/decrypt kind:56
key notes, we use NIP-04's encrypt/decrypt functions between the gated note's creator's pubkey and your pubkey.
To encrypt/decrypt the gated note, we use aes-256-cbc
. Below is a simple implementation in ts:
import * as cryptoBrowser from 'crypto-browserify';
const algorithm: string = 'aes-256-cbc';
export interface EncryptedOutput {
iv: string;
content: string;
}
export function hashToKey(inputString: string): Buffer {
return cryptoBrowser.createHash('sha256').update(inputString).digest();
}
export function encrypt(text: string, key: Buffer): EncryptedOutput {
const iv: Buffer = cryptoBrowser.randomBytes(16);
const cipher = cryptoBrowser.createCipheriv(algorithm, key, iv);
const encrypted: Buffer = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
return {
iv: iv.toString('hex'),
content: encrypted.toString('hex')
};
}
export function decrypt(iv: string, content: string, key: Buffer): string {
const decipher = cryptoBrowser.createDecipheriv(algorithm, key, Buffer.from(iv, 'hex'));
const decrypted: Buffer = Buffer.concat([decipher.update(Buffer.from(content, 'hex')), decipher.final()]);
return decrypted.toString('utf8');
}
Problems
- Servers need to be trusted
- Nothing is stopping people from freely giving their decrypt key