Set up literate programming structure
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Introduction
|
||||
|
||||
Nostr is a simple, open protocol for decentralized social networking. Unlike traditional
|
||||
platforms, nostr has no central server. Instead, users publish signed messages called
|
||||
**events** to a network of **relays** — simple WebSocket servers that store and forward
|
||||
events.
|
||||
|
||||
The protocol's power comes from its simplicity. At its core, nostr defines just one data
|
||||
structure (the event) and a handful of message types for communicating with relays. Everything
|
||||
else — social graphs, encrypted messaging, long-form content, marketplace listings — is built
|
||||
on top of this foundation through a system of **NIPs** (Nostr Implementation Possibilities),
|
||||
which are community-authored specifications.
|
||||
|
||||
## What this book covers
|
||||
|
||||
This book is both a tutorial and the source code for the `coracle` family of Rust crates:
|
||||
|
||||
- **coracle-lib** — Core types and stateless utilities: events, keys, tags, filters, and
|
||||
serialization. Everything you need to understand and manipulate nostr data.
|
||||
- **coracle-net** — Networking: connecting to relays, managing subscriptions, publishing
|
||||
events, and relay discovery.
|
||||
- **coracle-signer** — Signing abstractions: local key signing, NIP-46 remote signing,
|
||||
and browser extension integration.
|
||||
- **coracle-content** — Content handling: parsing note text, rendering mentions, handling
|
||||
media links, and working with NIP-27 references.
|
||||
- **coracle-storage** — Persistence: storing and querying events locally across different
|
||||
platforms and backends.
|
||||
|
||||
The chapters are ordered so that each concept builds on what came before. Code blocks marked
|
||||
with a file path are **tangled** — extracted and assembled into Rust source files that form the
|
||||
actual library. What you're reading *is* the source code.
|
||||
|
||||
## How literate programming works here
|
||||
|
||||
Throughout this book, you'll see code blocks annotated with an output file path like this:
|
||||
|
||||
```text
|
||||
```rust {file=coracle-lib/src/lib.rs}
|
||||
pub mod event;
|
||||
```
|
||||
```
|
||||
|
||||
These blocks are the real source code. The `coracle-tangle` tool extracts them in document
|
||||
order and writes them to the indicated file paths. Multiple blocks targeting the same file are
|
||||
concatenated, so a struct can be introduced in one section and have methods added later in the
|
||||
narrative.
|
||||
|
||||
Code blocks *without* a file annotation are illustrative — they show examples, intermediate
|
||||
states, or protocol concepts without contributing to the compiled output.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](01-introduction.md)
|
||||
- [Events](02-events.md)
|
||||
@@ -0,0 +1,8 @@
|
||||
[book]
|
||||
title = "Coracle: A Nostr Library"
|
||||
description = "A literate programming approach to building nostr applications in Rust"
|
||||
authors = ["Jonathan Staab"]
|
||||
language = "en"
|
||||
|
||||
[build]
|
||||
build-dir = "../../target/book"
|
||||
Reference in New Issue
Block a user