From 9c147c4a2fb82361dfa8230c44d9fa8c7b0a8255 Mon Sep 17 00:00:00 2001 From: jay Date: Tue, 30 Dec 2025 09:50:04 -0500 Subject: [PATCH] Add Nostr Protocol Specification --- Nostr-Protocol-Specification.md | 591 ++++++++++++++++++++++++++++++++ 1 file changed, 591 insertions(+) create mode 100644 Nostr-Protocol-Specification.md diff --git a/Nostr-Protocol-Specification.md b/Nostr-Protocol-Specification.md new file mode 100644 index 0000000..ea1a35a --- /dev/null +++ b/Nostr-Protocol-Specification.md @@ -0,0 +1,591 @@ +## 1. Event Structure + +An event is the fundamental data unit. Every event contains exactly seven fields. + +``` +Event { + id: 64-character lowercase hexadecimal string + pubkey: 64-character lowercase hexadecimal string + created_at: integer (Unix timestamp in seconds) + kind: integer + tags: array of arrays of strings + content: string + sig: 128-character lowercase hexadecimal signature +} +``` + +**Example:** + +```json +{ + "id": "4376c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65", + "pubkey": "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", + "created_at": 1673347337, + "kind": 1, + "tags": [["p", "91cf9..4e5ca"], ["e", "ae3f2..7b29d"]], + "content": "Hello, Nostr!", + "sig": "908a15e46fb4d8675bab026fc230a0e3542bfade63da02d542fb78b2a8513fcd0092619a2c8c1221e581946e0191f2af505dfdf8657a414dbca329186f009262" +} +``` + +### Rule 1.1: Field Requirements + +All seven fields must be present. No field may be null or omitted. + +### Rule 1.2: Hexadecimal Encoding + +The `id`, `pubkey`, and `sig` fields use lowercase hexadecimal encoding without prefix. + +- `id`: 32 bytes = 64 hex characters +- `pubkey`: 32 bytes = 64 hex characters +- `sig`: 64 bytes = 128 hex characters + +## 2. Event ID Calculation + +The event ID is a SHA-256 hash of a serialized form. + +### Rule 2.1: Serialization Format + +Serialize as a JSON array with six elements in this exact order: + +``` +[0, pubkey, created_at, kind, tags, content] +``` + +The zero is a protocol version indicator. + +**Example serialization:** + +```json +[0,"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",1673347337,1,[["p","91cf9..4e5ca"]],"Hello"] +``` + +### Rule 2.2: Canonical JSON + +The serialization must use canonical JSON: + +- No whitespace between elements +- No whitespace after separators +- UTF-8 encoding +- Minimum representation (no trailing zeros on numbers) + +### Rule 2.3: Hash Computation + +``` +serialized_bytes = UTF8_encode(canonical_json) +hash_bytes = SHA256(serialized_bytes) +event.id = lowercase_hex_encode(hash_bytes) +``` + +**Example:** + +``` +Input: [0,"6e46...",1673347337,1,[],"Hello"] +SHA-256: 4376c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65 +``` + +## 3. Event Signature + +The signature proves the event was created by the holder of the private key corresponding to `pubkey`. + +### Rule 3.1: Signature Generation + +``` +signature_bytes = schnorr_sign(private_key, event_id_bytes) +event.sig = lowercase_hex_encode(signature_bytes) +``` + +Uses Schnorr signature scheme over the secp256k1 elliptic curve. + +### Rule 3.2: Signature Verification + +``` +is_valid = schnorr_verify( + public_key: event.pubkey_bytes, + message: event.id_bytes, + signature: event.sig_bytes +) +``` + +Returns true if signature is valid, false otherwise. + +### Rule 3.3: Key Derivation + +The public key is derived from the private key using secp256k1 scalar multiplication: + +``` +public_key_point = private_key * G +event.pubkey = lowercase_hex_encode(public_key_point.x_coordinate) +``` + +Where G is the secp256k1 generator point. + +## 4. Tag Structure + +Tags are arrays of strings. The first element identifies the tag type. + +### Rule 4.1: Minimum Tag Length + +Every tag must contain at least two elements: a tag name and a value. + +``` +valid_tag = ["e", "ae3f2..7b29d"] +invalid_tag = ["e"] // missing value +``` + +### Rule 4.2: Tag Array Format + +``` +Event.tags = [ + [tag_name, value, optional_1, optional_2, ...], + [tag_name, value, optional_1, optional_2, ...], + ... +] +``` + +**Example:** + +```json +"tags": [ + ["e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://relay.example.com"], + ["p", "91cf9..4e5ca", "", "alice"], + ["d", "my-article-slug"] +] +``` + +### Rule 4.3: Common Tag Types + +Standard single-letter tag names: + +- `e`: Event ID reference +- `p`: Public key reference +- `a`: Address reference (for replaceable events) +- `d`: Identifier (for parameterized replaceable events) +- `t`: Topic/hashtag + +Additional elements in the array provide context (relay hints, markers, etc.). + +## 5. WebSocket Message Protocol + +All communication uses JSON arrays over WebSocket connections. + +### Rule 5.1: Client to Relay Messages + +**EVENT** - Publish an event: + +```json +["EVENT", ] +``` + +**REQ** - Subscribe to events: + +```json +["REQ", , , , ...] +``` + +**CLOSE** - End subscription: + +```json +["CLOSE", ] +``` + +**AUTH** - Authenticate with relay: + +```json +["AUTH", ] +``` + +**Example:** + +```json +["REQ", "my-sub-1", {"kinds": [1], "limit": 10}] +``` + +### Rule 5.2: Relay to Client Messages + +**EVENT** - Deliver matching event: + +```json +["EVENT", , ] +``` + +**EOSE** - End of stored events: + +```json +["EOSE", ] +``` + +**OK** - Confirm event acceptance: + +```json +["OK", , , ] +``` + +**CLOSED** - Subscription ended: + +```json +["CLOSED", , ] +``` + +**NOTICE** - Human-readable message: + +```json +["NOTICE", ] +``` + +**AUTH** - Request authentication: + +```json +["AUTH", ] +``` + +**Example:** + +```json +["OK", "4376c65d...", true, "Event accepted"] +``` + +### Rule 5.3: Message Ordering + +Relays send stored events first, then EOSE, then real-time events for active subscriptions. + +``` +CLIENT: ["REQ", "sub1", {...filter...}] +RELAY: ["EVENT", "sub1", {...stored event 1...}] +RELAY: ["EVENT", "sub1", {...stored event 2...}] +RELAY: ["EOSE", "sub1"] +RELAY: ["EVENT", "sub1", {...new real-time event...}] +``` + +## 6. Filter Structure + +Filters define subscription criteria. All conditions within a filter are combined with AND logic. Multiple filters in one REQ are combined with OR logic. + +### Rule 6.1: Filter Fields + +``` +Filter { + ids: [string, ...] // event ID prefixes + authors: [string, ...] // pubkey prefixes + kinds: [integer, ...] // event kinds + since: integer // minimum timestamp + until: integer // maximum timestamp + limit: integer // maximum results + #: [string, ...] // tag filters +} +``` + +All fields are optional. An empty filter matches all events. + +**Example:** + +```json +{ + "ids": ["4376c65d"], + "authors": ["6e468422", "91cf9b32"], + "kinds": [1, 6, 7], + "since": 1673347337, + "until": 1673433737, + "limit": 100, + "#e": ["5c83da77", "ae3f2a91"], + "#p": ["91cf9b32"] +} +``` + +### Rule 6.2: Prefix Matching + +The `ids` and `authors` fields support prefix matching. A filter value matches if it equals the initial substring of the event field. + +``` +filter.ids = ["4376c6"] +matches: "4376c65d2f232afbe9b882a35baa4f6fe8667c4e..." +rejects: "5c83da77..." +``` + +Minimum recommended prefix length: 4 characters (8 hex digits). + +### Rule 6.3: Tag Filters + +Tag filters use the format `#` as the field name. + +``` +filter["#e"] = ["5c83da77...", "ae3f2a91..."] +``` + +Matches events containing tags where: + +- First element (tag name) equals the letter +- Second element (tag value) matches any value in the filter array + +**Example matching:** + +``` +Event tags: [["e", "5c83da77..."], ["p", "91cf9..."]] +Filter: {"#e": ["5c83da77..."]} +Result: MATCH (event has "e" tag with matching value) +``` + +### Rule 6.4: Time Range Filters + +- `since`: Event timestamp must be ≥ this value +- `until`: Event timestamp must be ≤ this value + +Both use Unix timestamps (seconds since 1970-01-01 00:00:00 UTC). + +### Rule 6.5: Limit + +The `limit` field caps the number of events returned. Relays should return the most recent events when applying limits. + +## 7. Event Validation + +### Rule 7.1: Structure Validation + +Check that: + +1. All seven required fields exist +2. Field types match specification +3. Hex fields have correct length +4. Tags is an array of arrays +5. Each tag has at least two elements +6. Timestamp is an integer + +### Rule 7.2: ID Validation + +Recompute the event ID and verify it matches the stored `id` field: + +``` +computed_id = sha256_hex(serialize([0, pubkey, created_at, kind, tags, content])) +is_valid = (computed_id == event.id) +``` + +### Rule 7.3: Signature Validation + +Verify the Schnorr signature: + +``` +is_valid = schnorr_verify( + pubkey_bytes, + id_bytes, + sig_bytes +) +``` + +An event must pass both ID validation and signature validation to be considered authentic. + +--- + +## Optional Features + +The following features are implemented by some clients and relays but are not required for basic protocol operation. + +## Optional: Event Kind Classification + +Some implementations categorize event kinds by behavior. + +### Optional Rule 8.1: Kind Ranges + +**Regular events** (default): Stored permanently with unique IDs. + +**Replaceable events** (10000-19999): Newest event per (kind, pubkey) replaces all previous. + +- Also: kinds 0, 3 are treated as replaceable + +**Ephemeral events** (20000-29999): Not stored by relays, only forwarded. + +**Parameterized replaceable events** (30000-39999): Newest event per (kind, pubkey, d-tag-value) replaces previous. + +### Optional Rule 8.2: Replaceable Event Behavior + +When a relay receives a replaceable event: + +1. Find existing events with same (kind, pubkey) +2. Compare timestamps +3. Keep only the newest event +4. Delete older events + +**Example:** + +``` +Existing: kind=0, pubkey="abc123", created_at=1000 +New: kind=0, pubkey="abc123", created_at=1500 +Action: Delete existing, store new +``` + +### Optional Rule 8.3: Parameterized Replaceable Events + +Uses a `d` tag to create unique identifiers: + +```json +{ + "kind": 30023, + "pubkey": "abc123...", + "tags": [["d", "my-article"]], + "content": "..." +} +``` + +Replacement key: `(kind=30023, pubkey="abc123...", d="my-article")` + +## Optional: Address Format + +Parameterized replaceable events can be referenced by address instead of ID. + +### Optional Rule 9.1: Address Construction + +``` +address = "::" +``` + +**Example:** + +``` +"30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:my-article" +``` + +### Optional Rule 9.2: Address Tag + +Reference addresses in tags: + +```json +["a", "30023:6e4684...:my-article", "wss://relay.example.com"] +``` + +## Optional: Proof of Work + +Events can include proof-of-work by mining a nonce. + +### Optional Rule 10.1: Difficulty Measurement + +Count leading zero bits in the event ID: + +``` +difficulty = count_leading_zero_bits(event.id) +``` + +**Example:** + +``` +event.id = "000000af4b3c2..." (24 leading zero bits = difficulty 24) +``` + +### Optional Rule 10.2: Nonce Tag + +Include a nonce tag: + +```json +["nonce", "12345678", "24"] +``` + +Where: + +- First value: nonce counter +- Second value: target difficulty +- Third value (optional): commitment + +### Optional Rule 10.3: Mining Process + +``` +target_difficulty = 24 +nonce = 0 +loop: + event.tags = [["nonce", string(nonce), string(target_difficulty)]] + event.id = compute_event_id(event) + if count_leading_zero_bits(event.id) >= target_difficulty: + break + nonce = nonce + 1 +``` + +## Optional: Full-Text Search + +Filters may include a search field. + +### Optional Rule 11.1: Search Filter + +```json +{ + "kinds": [1], + "search": "bitcoin protocol" +} +``` + +Matches events where the `content` field contains the search terms. Implementation-specific (case sensitivity, word boundaries, etc.). + +## Optional: Event Counting + +Request event counts without retrieving full events. + +### Optional Rule 12.1: COUNT Command + +Client sends: + +```json +["COUNT", , , ...] +``` + +Relay responds: + +```json +["COUNT", , {"count": 47}] +``` + +## Optional: Event Relationships + +Track parent-child relationships between events. + +### Optional Rule 13.1: Reply Markers + +The `e` tag can include a marker: + +```json +["e", "", "", "reply"] +``` + +Markers: `reply`, `root`, `mention` + +### Optional Rule 13.2: Parent Extraction + +``` +get_parent_ids(event): + return [tag[1] for tag in event.tags where tag[0] == "e"] + +get_parent_addresses(event): + return [tag[1] for tag in event.tags where tag[0] == "a"] +``` + +## Optional: Event Expiration + +Events can specify an expiration time. + +### Optional Rule 14.1: Expiration Tag + +```json +["expiration", "1673433737"] +``` + +Value is a Unix timestamp. Relays should delete the event after this time. + +### Optional Rule 14.2: Expiration Check + +``` +is_expired(event, current_time): + for tag in event.tags: + if tag[0] == "expiration": + return current_time > parse_int(tag[1]) + return false +``` + +## Optional: Event Sorting + +Canonical ordering for event lists. + +### Optional Rule 15.1: Sort Order + +Primary sort: `created_at` descending (newest first) +Tiebreaker: `id` ascending (lexicographic) + +``` +sort_events(events): + return sorted(events, key=lambda e: (-e.created_at, e.id)) +``` + +This gives a deterministic ordering when multiple events have the same timestamp. \ No newline at end of file