diff --git a/x.md b/x.md new file mode 100644 index 0000000000..3bb449ce62 --- /dev/null +++ b/x.md @@ -0,0 +1,283 @@ +NIP-XX +====== + +Encrypted Betting Pools +----------------------- + +`draft` `optional` + +This NIP defines a protocol for running private betting pools over nostr and lightning. An admin publishes an encrypted _pool_ describing a question and a set of outcome options. Participants place bets by zapping the admin ([NIP-57](57.md)) with a zap request carrying the encrypted bet in dedicated tags, and the admin later settles the pool by declaring a winner (or cancelling) and paying winners over lightning. + +All pool data on relays is encrypted with a symmetric key that never touches a relay: it travels only inside the pool's share link (via query parameters or hash fragment). Relays and non-participants see opaque ciphertext; anyone holding the share link can read the pool and its bets; only the admin can read bettors' payout addresses. + +It defines three new event kinds: + +| kind | description | +| ------ | ----------------- | +| `8880` | Betting Pool | +| `8881` | Pool Admin Action | +| `8882` | Pool Comment | + +All three are _regular_ events. Bets reuse kind `9734`/`9735` from [NIP-57](57.md) with additional requirements described below. + +## Roles + +- **Admin**: creates the pool, receives all bets as zaps to their own lightning wallet, declares the outcome, and pays winners. The admin is fully trusted with custody of the pot. +- **Bettor**: anyone holding the share link. Bettors sign zap requests with their own key, which MAY be an ephemeral throwaway key. +- **LNURL provider**: the admin's lightning wallet provider. Its `nostrPubkey` (per NIP-57 Appendix C) is the only key whose zap receipts count as bets. + +## The pool key + +Every pool has a single symmetric key: + +- 32 bytes, generated from a cryptographically secure random source when the pool is created. +- Never published to relays or sent to any server. It is distributed exclusively through the share link (see below). +- Never rotated; it is valid for the pool's lifetime. + +Possession of the pool key grants read access to the pool definition, admin actions, comments, and each bet's chosen option. It does NOT grant access to bettors' payout addresses, which have a second encryption layer (see "Reward addresses"). + +### Symmetric encryption + +All pool-key encryption uses **AES-256-GCM**: + +1. Generate a random 12-byte IV. +2. Encrypt the plaintext with AES-256-GCM under the pool key, optionally with associated data (AAD, used only for the bet tags - see below). The 16-byte authentication tag is appended to the ciphertext, as is conventional for GCM. +3. The payload is `IV || ciphertext || tag`. +4. When the payload is carried in an event's `content` or in a tag value, it is encoded with standard base64 (with padding, not base64url). + +Decryption is the inverse; implementations MUST reject payloads shorter than 28 bytes (12-byte IV + 16-byte tag) and MUST treat authentication failure as "not part of this pool" rather than as a hard error, since anyone can publish garbage events with matching tags. + +## Share links + +A pool is joined via a share URL whose **hash fragment** carries both the pool pointer and the pool key. The fragment is never sent to web servers by browsers, so a statically-hosted client never sees the key server-side. + +``` +#/p// +``` + +- `` is a [NIP-19](19.md) `nevent` encoding of the pool event: its `id`, the admin's pubkey as `author`, kind `8880`, and up to 3 relay hints. +- `` is the 32-byte pool key encoded as unpadded base64url. + +Clients MUST reject a `` that does not decode to exactly 32 bytes, and SHOULD reject non-canonical base64url encodings (i.e. re-encoding the decoded bytes must reproduce the input), so that each pool has exactly one valid link and corrupted links fail visibly rather than silently decrypting nothing. + +There is no discovery mechanism: pools are unlisted and reachable only through their share link. + +## Event definitions + +Every event authored under this NIP carries a version tag: + +```json +["nipx-version", "1"] +``` + +> The `nipx-` prefix in tag names and AAD strings throughout this document is a placeholder; it becomes `nip-` once this NIP is allocated a number. + +The tag versions the encrypted payload schemas, letting future clients distinguish old events before decrypting them. Clients MUST ignore events carrying an unknown version. Events SHOULD also carry an `alt` tag ([NIP-31](31.md)) with a short human-readable summary, since their content is unreadable ciphertext to generic clients. + +### Betting Pool (kind 8880) + +A regular, immutable event signed by the admin. The pool is identified by this event's `id` and administered by its `pubkey` forever; there is no mechanism to add information to a pool after publication. + +```yaml +{ + "kind": 8880, + "pubkey": "", + "tags": [ + ["alt", ""], + ["nipx-version", "1"] + ], + "content": "" +} +``` + +Decrypted `content` is a JSON object: + +```jsonc +{ + "v": 1, // payload version, MUST be 1 + "title": "", // required, non-empty + "description": "", // optional + "imageUrl": "", // optional + "backgroundUrl": "", // optional + "options": [ // required, at least 2 + { "id": "a", "title": "", "description": "" }, + { "id": "b", "title": "" } + ], + "adminFeePct": 2, // 0–100; fee retained by the admin + "maxBets": 20, // integer >= 1 + "maxBetSats": 100000, // integer >= 1; per-bet cap + "deadline": 1735689600 // optional; unix seconds after which bets are refused +} +``` + +Option `id`s MUST be non-empty and unique within the pool. They SHOULD all have the same UTF-8 byte length (e.g. `"a"`, `"b"`, `"c"`), because the encrypted option id travels in a bet tag whose ciphertext length equals the plaintext length - equal-length ids keep the ciphertext from narrowing down which option was picked. Clients MUST reject pools that fail these validations rather than displaying partial data. + +Before publishing a pool, the admin's lightning address MUST be verified to support NIP-57 (`allowsNostr: true` and a `nostrPubkey` in its LNURL-pay params). No `commentAllowed` capacity is required: the encrypted bet travels in zap request tags, not in the comment-limited `content`. + +### Pool Admin Action (kind 8881) + +A regular event signed by the admin that mutates pool state. Pool state is derived by folding all admin actions rather than by replaceable events, so the full settlement history remains auditable by participants. + +```yaml +{ + "kind": 8881, + "pubkey": "", + "tags": [ + ["e", ""], + ["alt", "<...>"], + ["nipx-version", "1"] + ], + "content": "" +} +``` + +Decrypted `content` is one of: + +```jsonc +{ "action": "close" } // stop accepting bets +{ "action": "winner", "optionId": "