Set up literate programming structure

This commit is contained in:
Jon Staab
2026-04-07 09:55:41 -07:00
commit 2d36400ba0
16 changed files with 933 additions and 0 deletions
+49
View File
@@ -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.
+202
View File
@@ -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.
+4
View File
@@ -0,0 +1,4 @@
# Summary
- [Introduction](01-introduction.md)
- [Events](02-events.md)
+8
View File
@@ -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"