{"id":"a6fe6bc11ed619ed818a4f98ccccc146fe63977c73793e3576ef9fd48421de90","pubkey":"7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200","created_at":1777737212,"kind":30817,"tags":[["d","core-protocol-specification"],["title","Core Protocol Specification"],["client","nostrhub.io"]],"content":"# Nostr Core Protocol Specification\n\n## 1. Event Structure\n\nAn event is the fundamental data unit. Every event contains exactly seven fields.\n\n```\nEvent {\n  id: 64-character lowercase hexadecimal string\n  pubkey: 64-character lowercase hexadecimal string  \n  created_at: integer (Unix timestamp in seconds)\n  kind: integer\n  tags: array of arrays of strings\n  content: string\n  sig: 128-character lowercase hexadecimal signature\n}\n```\n\n**Example:**\n\n```json\n{\n  \"id\": \"4376c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65\",\n  \"pubkey\": \"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93\",\n  \"created_at\": 1673347337,\n  \"kind\": 1,\n  \"tags\": [[\"p\", \"91cf9..4e5ca\"], [\"e\", \"ae3f2..7b29d\"]],\n  \"content\": \"Hello, Nostr!\",\n  \"sig\": \"908a15e46fb4d8675bab026fc230a0e3542bfade63da02d542fb78b2a8513fcd0092619a2c8c1221e581946e0191f2af505dfdf8657a414dbca329186f009262\"\n}\n```\n\n### Rule 1.1: Field Requirements\n\nAll seven fields must be present. No field may be null or omitted.\n\n### Rule 1.2: Hexadecimal Encoding\n\nThe `id`, `pubkey`, and `sig` fields use lowercase hexadecimal encoding without prefix.\n\n- `id`: 32 bytes = 64 hex characters\n- `pubkey`: 32 bytes = 64 hex characters\n- `sig`: 64 bytes = 128 hex characters\n\n## 2. Event ID Calculation\n\nThe event ID is a SHA-256 hash of a serialized form.\n\n### Rule 2.1: Serialization Format\n\nSerialize as a JSON array with six elements in this exact order:\n\n```\n[0, pubkey, created_at, kind, tags, content]\n```\n\nThe zero is a protocol version indicator.\n\n**Example serialization:**\n\n```json\n[0,\"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93\",1673347337,1,[[\"p\",\"91cf9..4e5ca\"]],\"Hello\"]\n```\n\n### Rule 2.2: Canonical JSON\n\nThe serialization must use canonical JSON:\n\n- No whitespace between elements\n- No whitespace after separators\n- UTF-8 encoding\n- Minimum representation (no trailing zeros on numbers)\n\n### Rule 2.3: Hash Computation\n\n```\nserialized_bytes = UTF8_encode(canonical_json)\nhash_bytes = SHA256(serialized_bytes)\nevent.id = lowercase_hex_encode(hash_bytes)\n```\n\n**Example:**\n\n```\nInput: [0,\"6e46...\",1673347337,1,[],\"Hello\"]\nSHA-256: 4376c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65\n```\n\n## 3. Event Signature\n\nThe signature proves the event was created by the holder of the private key corresponding to `pubkey`.\n\n### Rule 3.1: Signature Generation\n\n```\nsignature_bytes = schnorr_sign(private_key, event_id_bytes)\nevent.sig = lowercase_hex_encode(signature_bytes)\n```\n\nUses Schnorr signature scheme over the secp256k1 elliptic curve.\n\n### Rule 3.2: Signature Verification\n\n```\nis_valid = schnorr_verify(\n  public_key: event.pubkey_bytes,\n  message: event.id_bytes,\n  signature: event.sig_bytes\n)\n```\n\nReturns true if signature is valid, false otherwise.\n\n### Rule 3.3: Key Derivation\n\nThe public key is derived from the private key using secp256k1 scalar multiplication:\n\n```\npublic_key_point = private_key * G\nevent.pubkey = lowercase_hex_encode(public_key_point.x_coordinate)\n```\n\nWhere G is the secp256k1 generator point.\n\n## 4. Tag Structure\n\nTags are arrays of strings. The first element identifies the tag type.\n\n### Rule 4.1: Minimum Tag Length\n\nEvery tag must contain at least two elements: a tag name and a value.\n\n```\nvalid_tag = [\"e\", \"ae3f2..7b29d\"]\ninvalid_tag = [\"e\"]  // missing value\n```\n\n### Rule 4.2: Tag Array Format\n\n```\nEvent.tags = [\n  [tag_name, value, optional_1, optional_2, ...],\n  [tag_name, value, optional_1, optional_2, ...],\n  ...\n]\n```\n\n**Example:**\n\n```json\n\"tags\": [\n  [\"e\", \"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36\", \"wss://relay.example.com\"],\n  [\"p\", \"91cf9..4e5ca\", \"\", \"alice\"],\n  [\"d\", \"my-article-slug\"]\n]\n```\n\n### Rule 4.3: Common Tag Types\n\nStandard single-letter tag names:\n\n- `e`: Event ID reference\n- `p`: Public key reference\n- `a`: Address reference (for replaceable events)\n- `d`: Identifier (for parameterized replaceable events)\n- `t`: Topic/hashtag\n\nAdditional elements in the array provide context (relay hints, markers, etc.).\n\n## 5. WebSocket Message Protocol\n\nAll communication uses JSON arrays over WebSocket connections.\n\n### Rule 5.1: Client to Relay Messages\n\n**EVENT** - Publish an event:\n\n```json\n[\"EVENT\", <event-object>]\n```\n\n**REQ** - Subscribe to events:\n\n```json\n[\"REQ\", <subscription-id>, <filter-object>, <filter-object>, ...]\n```\n\n**CLOSE** - End subscription:\n\n```json\n[\"CLOSE\", <subscription-id>]\n```\n\n**AUTH** - Authenticate with relay:\n\n```json\n[\"AUTH\", <signed-event>]\n```\n\n**Example:**\n\n```json\n[\"REQ\", \"my-sub-1\", {\"kinds\": [1], \"limit\": 10}]\n```\n\n### Rule 5.2: Relay to Client Messages\n\n**EVENT** - Deliver matching event:\n\n```json\n[\"EVENT\", <subscription-id>, <event-object>]\n```\n\n**EOSE** - End of stored events:\n\n```json\n[\"EOSE\", <subscription-id>]\n```\n\n**OK** - Confirm event acceptance:\n\n```json\n[\"OK\", <event-id>, <true|false>, <message>]\n```\n\n**CLOSED** - Subscription ended:\n\n```json\n[\"CLOSED\", <subscription-id>, <message>]\n```\n\n**NOTICE** - Human-readable message:\n\n```json\n[\"NOTICE\", <message>]\n```\n\n**AUTH** - Request authentication:\n\n```json\n[\"AUTH\", <challenge-string>]\n```\n\n**Example:**\n\n```json\n[\"OK\", \"4376c65d...\", true, \"Event accepted\"]\n```\n\n### Rule 5.3: Message Ordering\n\nRelays send stored events first, then EOSE, then real-time events for active subscriptions.\n\n```\nCLIENT: [\"REQ\", \"sub1\", {...filter...}]\nRELAY:  [\"EVENT\", \"sub1\", {...stored event 1...}]\nRELAY:  [\"EVENT\", \"sub1\", {...stored event 2...}]\nRELAY:  [\"EOSE\", \"sub1\"]\nRELAY:  [\"EVENT\", \"sub1\", {...new real-time event...}]\n```\n\n## 6. Filter Structure\n\nFilters define subscription criteria. All conditions within a filter are combined with AND logic. Multiple filters in one REQ are combined with OR logic.\n\n### Rule 6.1: Filter Fields\n\n```\nFilter {\n  ids: [string, ...]           // event ID prefixes\n  authors: [string, ...]       // pubkey prefixes\n  kinds: [integer, ...]        // event kinds\n  since: integer               // minimum timestamp\n  until: integer               // maximum timestamp\n  limit: integer               // maximum results\n  #<letter>: [string, ...]     // tag filters\n}\n```\n\nAll fields are optional. An empty filter matches all events.\n\n**Example:**\n\n```json\n{\n  \"ids\": [\"4376c65d\"],\n  \"authors\": [\"6e468422\", \"91cf9b32\"],\n  \"kinds\": [1, 6, 7],\n  \"since\": 1673347337,\n  \"until\": 1673433737,\n  \"limit\": 100,\n  \"#e\": [\"5c83da77\", \"ae3f2a91\"],\n  \"#p\": [\"91cf9b32\"]\n}\n```\n\n### Rule 6.2: Prefix Matching\n\nThe `ids` and `authors` fields support prefix matching. A filter value matches if it equals the initial substring of the event field.\n\n```\nfilter.ids = [\"4376c6\"]\nmatches: \"4376c65d2f232afbe9b882a35baa4f6fe8667c4e...\"\nrejects: \"5c83da77...\"\n```\n\nMinimum recommended prefix length: 4 characters (8 hex digits).\n\n### Rule 6.3: Tag Filters\n\nTag filters use the format `#<single-letter>` as the field name.\n\n```\nfilter[\"#e\"] = [\"5c83da77...\", \"ae3f2a91...\"]\n```\n\nMatches events containing tags where:\n\n- First element (tag name) equals the letter\n- Second element (tag value) matches any value in the filter array\n\n**Example matching:**\n\n```\nEvent tags: [[\"e\", \"5c83da77...\"], [\"p\", \"91cf9...\"]]\nFilter: {\"#e\": [\"5c83da77...\"]}\nResult: MATCH (event has \"e\" tag with matching value)\n```\n\n### Rule 6.4: Time Range Filters\n\n- `since`: Event timestamp must be ≥ this value\n- `until`: Event timestamp must be ≤ this value\n\nBoth use Unix timestamps (seconds since 1970-01-01 00:00:00 UTC).\n\n### Rule 6.5: Limit\n\nThe `limit` field caps the number of events returned. Relays should return the most recent events when applying limits.\n\n## 7. Event Validation\n\n### Rule 7.1: Structure Validation\n\nCheck that:\n\n1. All seven required fields exist\n2. Field types match specification\n3. Hex fields have correct length\n4. Tags is an array of arrays\n5. Each tag has at least two elements\n6. Timestamp is an integer\n\n### Rule 7.2: ID Validation\n\nRecompute the event ID and verify it matches the stored `id` field:\n\n```\ncomputed_id = sha256_hex(serialize([0, pubkey, created_at, kind, tags, content]))\nis_valid = (computed_id == event.id)\n```\n\n### Rule 7.3: Signature Validation\n\nVerify the Schnorr signature:\n\n```\nis_valid = schnorr_verify(\n  pubkey_bytes,\n  id_bytes,\n  sig_bytes\n)\n```\n\nAn event must pass both ID validation and signature validation to be considered authentic.\n\n---\n\n## Optional Features\n\nThe following features are implemented by some clients and relays but are not required for basic protocol operation.\n\n## Optional: Event Kind Classification\n\nSome implementations categorize event kinds by behavior.\n\n### Optional Rule 8.1: Kind Ranges\n\n**Regular events** (default): Stored permanently with unique IDs.\n\n**Replaceable events** (10000-19999): Newest event per (kind, pubkey) replaces all previous.\n\n- Also: kinds 0, 3 are treated as replaceable\n\n**Ephemeral events** (20000-29999): Not stored by relays, only forwarded.\n\n**Parameterized replaceable events** (30000-39999): Newest event per (kind, pubkey, d-tag-value) replaces previous.\n\n### Optional Rule 8.2: Replaceable Event Behavior\n\nWhen a relay receives a replaceable event:\n\n1. Find existing events with same (kind, pubkey)\n2. Compare timestamps\n3. Keep only the newest event\n4. Delete older events\n\n**Example:**\n\n```\nExisting: kind=0, pubkey=\"abc123\", created_at=1000\nNew:      kind=0, pubkey=\"abc123\", created_at=1500\nAction:   Delete existing, store new\n```\n\n### Optional Rule 8.3: Parameterized Replaceable Events\n\nUses a `d` tag to create unique identifiers:\n\n```json\n{\n  \"kind\": 30023,\n  \"pubkey\": \"abc123...\",\n  \"tags\": [[\"d\", \"my-article\"]],\n  \"content\": \"...\"\n}\n```\n\nReplacement key: `(kind=30023, pubkey=\"abc123...\", d=\"my-article\")`\n\n## Optional: Address Format\n\nParameterized replaceable events can be referenced by address instead of ID.\n\n### Optional Rule 9.1: Address Construction\n\n```\naddress = \"<kind>:<pubkey>:<d-tag-value>\"\n```\n\n**Example:**\n\n```\n\"30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:my-article\"\n```\n\n### Optional Rule 9.2: Address Tag\n\nReference addresses in tags:\n\n```json\n[\"a\", \"30023:6e4684...:my-article\", \"wss://relay.example.com\"]\n```\n\n## Optional: Proof of Work\n\nEvents can include proof-of-work by mining a nonce.\n\n### Optional Rule 10.1: Difficulty Measurement\n\nCount leading zero bits in the event ID:\n\n```\ndifficulty = count_leading_zero_bits(event.id)\n```\n\n**Example:**\n\n```\nevent.id = \"000000af4b3c2...\" (24 leading zero bits = difficulty 24)\n```\n\n### Optional Rule 10.2: Nonce Tag\n\nInclude a nonce tag:\n\n```json\n[\"nonce\", \"12345678\", \"24\"]\n```\n\nWhere:\n\n- First value: nonce counter\n- Second value: target difficulty\n- Third value (optional): commitment\n\n### Optional Rule 10.3: Mining Process\n\n```\ntarget_difficulty = 24\nnonce = 0\nloop:\n  event.tags = [[\"nonce\", string(nonce), string(target_difficulty)]]\n  event.id = compute_event_id(event)\n  if count_leading_zero_bits(event.id) >= target_difficulty:\n    break\n  nonce = nonce + 1\n```\n\n## Optional: Full-Text Search\n\nFilters may include a search field.\n\n### Optional Rule 11.1: Search Filter\n\n```json\n{\n  \"kinds\": [1],\n  \"search\": \"bitcoin protocol\"\n}\n```\n\nMatches events where the `content` field contains the search terms. Implementation-specific (case sensitivity, word boundaries, etc.).\n\n## Optional: Event Counting\n\nRequest event counts without retrieving full events.\n\n### Optional Rule 12.1: COUNT Command\n\nClient sends:\n\n```json\n[\"COUNT\", <subscription-id>, <filter-object>, ...]\n```\n\nRelay responds:\n\n```json\n[\"COUNT\", <subscription-id>, {\"count\": 47}]\n```\n\n## Optional: Event Relationships\n\nTrack parent-child relationships between events.\n\n### Optional Rule 13.1: Reply Markers\n\nThe `e` tag can include a marker:\n\n```json\n[\"e\", \"<parent-event-id>\", \"<relay-hint>\", \"reply\"]\n```\n\nMarkers: `reply`, `root`, `mention`\n\n### Optional Rule 13.2: Parent Extraction\n\n```\nget_parent_ids(event):\n  return [tag[1] for tag in event.tags where tag[0] == \"e\"]\n\nget_parent_addresses(event):\n  return [tag[1] for tag in event.tags where tag[0] == \"a\"]\n```\n\n## Optional: Event Expiration\n\nEvents can specify an expiration time.\n\n### Optional Rule 14.1: Expiration Tag\n\n```json\n[\"expiration\", \"1673433737\"]\n```\n\nValue is a Unix timestamp. Relays should delete the event after this time.\n\n### Optional Rule 14.2: Expiration Check\n\n```\nis_expired(event, current_time):\n  for tag in event.tags:\n    if tag[0] == \"expiration\":\n      return current_time > parse_int(tag[1])\n  return false\n```\n\n## Optional: Event Sorting\n\nCanonical ordering for event lists.\n\n### Optional Rule 15.1: Sort Order\n\nPrimary sort: `created_at` descending (newest first)  \nTiebreaker: `id` ascending (lexicographic)\n\n```\nsort_events(events):\n  return sorted(events, key=lambda e: (-e.created_at, e.id))\n```\n\nThis gives a deterministic ordering when multiple events have the same timestamp.","sig":"24c4e026cfbd1eb0ca1da35d6548fd47116e9a2fe966f44ba9d522e6a392b0c44972fef88930640a86feb7f3bd2e08e9398c977164ee1350959653c98d32a7a8"}