Add encryption chapter
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user