203 lines
7.0 KiB
Markdown
203 lines
7.0 KiB
Markdown
# Events
|
|
|
|
Everything in nostr is an **event**. A short note, a profile update, a reaction, a relay list —
|
|
they're all events. An event is a JSON object signed by its author's cryptographic key. Once
|
|
signed, it cannot be altered without invalidating the signature, which means any relay or client
|
|
can verify that an event is authentic without trusting anyone.
|
|
|
|
## The event structure
|
|
|
|
Here's what a nostr event looks like as JSON:
|
|
|
|
```json
|
|
{
|
|
"id": "4376c65d2f232afbe9b882a35baa4f6fe8bce184396e157b1a35b3f01b3e285c",
|
|
"pubkey": "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
|
|
"created_at": 1673347337,
|
|
"kind": 1,
|
|
"tags": [
|
|
["e", "3da979448d9ba263864c4d6f14984c423a3838364ec255f03c7904b1ae77f206"],
|
|
["p", "bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce"]
|
|
],
|
|
"content": "Hello, nostr!",
|
|
"sig": "908a15e46fb4d8675bab026fc230a0e3542bfade63da02d542fb78b2a8513fcd..."
|
|
}
|
|
```
|
|
|
|
Let's define this in Rust. We'll start with the module declaration:
|
|
|
|
```rust {file=coracle-lib/src/lib.rs}
|
|
pub mod event;
|
|
```
|
|
|
|
And now the `Event` struct itself. Each field maps directly to the JSON above:
|
|
|
|
```rust {file=coracle-lib/src/event.rs}
|
|
use serde::{Deserialize, Serialize};
|
|
use sha2::{Sha256, Digest};
|
|
|
|
/// A nostr event — the fundamental data type of the protocol.
|
|
///
|
|
/// Every interaction on nostr is represented as a signed event. The `id` is a
|
|
/// SHA-256 hash of the canonical serialization, and the `sig` is a Schnorr
|
|
/// signature over that hash using the author's private key.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Event {
|
|
/// The event ID: a 32-byte hex-encoded SHA-256 hash of the canonical serialization.
|
|
pub id: String,
|
|
/// The author's public key: a 32-byte hex-encoded secp256k1 public key (x-only).
|
|
pub pubkey: String,
|
|
/// Unix timestamp in seconds when the event was created.
|
|
pub created_at: u64,
|
|
/// The event kind determines how the event should be interpreted.
|
|
/// Kind 0 = metadata, kind 1 = short text note, kind 3 = contacts, etc.
|
|
pub kind: u16,
|
|
/// A list of tags. Each tag is an array of strings where the first element
|
|
/// identifies the tag type. Common tags: ["e", <event-id>], ["p", <pubkey>].
|
|
pub tags: Vec<Vec<String>>,
|
|
/// The event content. Interpretation depends on the kind.
|
|
pub content: String,
|
|
/// A 64-byte hex-encoded Schnorr signature of the event ID.
|
|
pub sig: String,
|
|
}
|
|
```
|
|
|
|
## Computing the event ID
|
|
|
|
The event ID is not chosen by the author — it's *derived* from the event's content via a
|
|
deterministic process. This is what makes events tamper-proof: change any field and the ID
|
|
changes, which invalidates the signature.
|
|
|
|
The ID is the SHA-256 hash of a specific JSON serialization called the **canonical form**:
|
|
|
|
```json
|
|
[0, <pubkey>, <created_at>, <kind>, <tags>, <content>]
|
|
```
|
|
|
|
That leading `0` is a version number reserved for future use. The array is serialized as
|
|
compact JSON (no whitespace) and UTF-8 encoded before hashing.
|
|
|
|
```rust {file=coracle-lib/src/event.rs}
|
|
impl Event {
|
|
/// Serialize this event into the canonical form used for ID computation.
|
|
/// The result is: `[0, pubkey, created_at, kind, tags, content]`
|
|
pub fn serialize(&self) -> String {
|
|
serde_json::to_string(
|
|
&serde_json::json!([0, self.pubkey, self.created_at, self.kind, self.tags, self.content])
|
|
).expect("event serialization should never fail")
|
|
}
|
|
|
|
/// Compute the event ID by SHA-256 hashing the canonical serialization.
|
|
pub fn compute_id(&self) -> String {
|
|
let serialized = self.serialize();
|
|
let hash = Sha256::digest(serialized.as_bytes());
|
|
hex::encode(hash)
|
|
}
|
|
|
|
/// Check whether the event's `id` field matches the computed hash.
|
|
pub fn id_is_valid(&self) -> bool {
|
|
self.id == self.compute_id()
|
|
}
|
|
}
|
|
```
|
|
|
|
## Verifying signatures
|
|
|
|
Nostr uses **Schnorr signatures** over the secp256k1 curve — the same curve used by Bitcoin.
|
|
The signature is computed over the event ID (which is already a hash), and can be verified
|
|
using the author's public key.
|
|
|
|
This means anyone can verify an event's authenticity without contacting the author or any
|
|
trusted authority. The math guarantees it.
|
|
|
|
```rust {file=coracle-lib/src/event.rs}
|
|
impl Event {
|
|
/// Verify the event's Schnorr signature against its public key and ID.
|
|
/// Returns `true` if the signature is valid and the ID matches the content.
|
|
pub fn verify(&self) -> bool {
|
|
if !self.id_is_valid() {
|
|
return false;
|
|
}
|
|
|
|
let id_bytes = match hex::decode(&self.id) {
|
|
Ok(b) => b,
|
|
Err(_) => return false,
|
|
};
|
|
let sig_bytes = match hex::decode(&self.sig) {
|
|
Ok(b) => b,
|
|
Err(_) => return false,
|
|
};
|
|
let pubkey_bytes = match hex::decode(&self.pubkey) {
|
|
Ok(b) => b,
|
|
Err(_) => return false,
|
|
};
|
|
|
|
let msg = match secp256k1::Message::from_digest_slice(&id_bytes) {
|
|
Ok(m) => m,
|
|
Err(_) => return false,
|
|
};
|
|
let sig = match secp256k1::schnorr::Signature::from_slice(&sig_bytes) {
|
|
Ok(s) => s,
|
|
Err(_) => return false,
|
|
};
|
|
let pubkey = match secp256k1::XOnlyPublicKey::from_slice(&pubkey_bytes) {
|
|
Ok(k) => k,
|
|
Err(_) => return false,
|
|
};
|
|
|
|
sig.verify(&msg, &pubkey).is_ok()
|
|
}
|
|
}
|
|
```
|
|
|
|
## Creating events
|
|
|
|
To create an event, you provide the content fields and sign with a secret key. The process is:
|
|
|
|
1. Fill in `pubkey`, `created_at`, `kind`, `tags`, and `content`
|
|
2. Compute the `id` from the canonical serialization
|
|
3. Sign the `id` with the secret key to produce `sig`
|
|
|
|
```rust {file=coracle-lib/src/event.rs}
|
|
impl Event {
|
|
/// Create a new signed event from the given fields and secret key.
|
|
///
|
|
/// The `pubkey`, `id`, and `sig` fields are computed automatically.
|
|
pub fn new(
|
|
kind: u16,
|
|
content: &str,
|
|
tags: Vec<Vec<String>>,
|
|
created_at: u64,
|
|
secret_key: &secp256k1::SecretKey,
|
|
) -> Self {
|
|
let secp = secp256k1::Secp256k1::new();
|
|
let keypair = secp256k1::Keypair::from_secret_key(&secp, secret_key);
|
|
let (pubkey, _parity) = keypair.x_only_public_key();
|
|
|
|
let mut event = Event {
|
|
id: String::new(),
|
|
pubkey: hex::encode(pubkey.serialize()),
|
|
created_at,
|
|
kind,
|
|
tags,
|
|
content: content.to_string(),
|
|
sig: String::new(),
|
|
};
|
|
|
|
event.id = event.compute_id();
|
|
|
|
let id_bytes = hex::decode(&event.id).expect("just computed, must be valid hex");
|
|
let msg = secp256k1::Message::from_digest_slice(&id_bytes)
|
|
.expect("SHA-256 output is always 32 bytes");
|
|
let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
|
|
event.sig = hex::encode(sig.serialize());
|
|
|
|
event
|
|
}
|
|
}
|
|
```
|
|
|
|
With this, you can create and verify events — the atomic unit of all nostr communication. In
|
|
the next chapter, we'll look at how keys work in more detail and introduce signing abstractions
|
|
that go beyond a raw secret key.
|