Add Nostr Protocol Specification
591
Nostr-Protocol-Specification.md
Normal file
591
Nostr-Protocol-Specification.md
Normal file
@@ -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", <event-object>]
|
||||
```
|
||||
|
||||
**REQ** - Subscribe to events:
|
||||
|
||||
```json
|
||||
["REQ", <subscription-id>, <filter-object>, <filter-object>, ...]
|
||||
```
|
||||
|
||||
**CLOSE** - End subscription:
|
||||
|
||||
```json
|
||||
["CLOSE", <subscription-id>]
|
||||
```
|
||||
|
||||
**AUTH** - Authenticate with relay:
|
||||
|
||||
```json
|
||||
["AUTH", <signed-event>]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
["REQ", "my-sub-1", {"kinds": [1], "limit": 10}]
|
||||
```
|
||||
|
||||
### Rule 5.2: Relay to Client Messages
|
||||
|
||||
**EVENT** - Deliver matching event:
|
||||
|
||||
```json
|
||||
["EVENT", <subscription-id>, <event-object>]
|
||||
```
|
||||
|
||||
**EOSE** - End of stored events:
|
||||
|
||||
```json
|
||||
["EOSE", <subscription-id>]
|
||||
```
|
||||
|
||||
**OK** - Confirm event acceptance:
|
||||
|
||||
```json
|
||||
["OK", <event-id>, <true|false>, <message>]
|
||||
```
|
||||
|
||||
**CLOSED** - Subscription ended:
|
||||
|
||||
```json
|
||||
["CLOSED", <subscription-id>, <message>]
|
||||
```
|
||||
|
||||
**NOTICE** - Human-readable message:
|
||||
|
||||
```json
|
||||
["NOTICE", <message>]
|
||||
```
|
||||
|
||||
**AUTH** - Request authentication:
|
||||
|
||||
```json
|
||||
["AUTH", <challenge-string>]
|
||||
```
|
||||
|
||||
**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
|
||||
#<letter>: [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 `#<single-letter>` 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 = "<kind>:<pubkey>:<d-tag-value>"
|
||||
```
|
||||
|
||||
**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", <subscription-id>, <filter-object>, ...]
|
||||
```
|
||||
|
||||
Relay responds:
|
||||
|
||||
```json
|
||||
["COUNT", <subscription-id>, {"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", "<parent-event-id>", "<relay-hint>", "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.
|
||||
Reference in New Issue
Block a user