# Plan: Events ## Topic Summary Introduce the core nostr `Event` type: the seven wire fields, JSON (de)serialization, the NIP-01 canonical form used for ID computation, the unsigned-vs-signed lifecycle distinction, and minimal structural validation. Add a signing primitive on `SecretKey` that signs an arbitrary byte message (not an event), and show — in prose, not in tangled code — how to combine `UnsignedEvent::id()` and `SecretKey::sign()` to produce a complete signed `Event`. A richer event-signing API and the `Signer` trait are deferred to chapter 05. ## Chapter Outline 1. **Opening framing.** Events as the entire nostr substrate: signed, content-addressable, permissionless facts. Referential transparency. Why a regular event's id is `sha256` of its canonical form. Connects back to chapter 02 ("your public key is your name") and forward ("everything in this book is built out of events"). 2. **The module.** Register `events` in `lib.rs`. Imports. 3. **Errors.** `EventError` enum covering JSON failures, id mismatch, signature failure, and structural problems. 4. **The wire structure.** Walk through the seven fields with a minimal JSON example. Show the raw shape the network agrees on. 5. **The `Event` struct.** Define a plain struct with public fields, derive `Clone`, `Debug`, `PartialEq`, `Eq`. Field types: - `id: [u8; 32]` - `pubkey: PublicKey` (from chapter 02) - `created_at: u64` - `kind: u16` - `tags: Vec>` - `content: String` - `sig: [u8; 64]` Add `serde` impls (custom for hex-encoded binary fields, derived otherwise). 6. **Canonical form and the id.** Show `[0, pubkey, created_at, kind, tags, content]`. Produce it with `serde_json::json!`. Hash with `sha2::Sha256`. Return 32 bytes. Introduce `UnsignedEvent` struct (same fields minus `id` and `sig`) and put `fn id(&self) -> [u8; 32]` on it. 7. **Signing primitive on `SecretKey`.** Add `SecretKey::sign(&self, msg: &[u8]) -> [u8; 64]`. This belongs in `keys.rs` but is introduced *here* because this is where we first need it. It wraps `secp256k1`'s Schnorr signing over a 32-byte digest, using the shared `SECP256K1` context. Show a worked example — in prose, NOT tangled — of `secret.sign(&unsigned.id())` producing a `[u8; 64]` that can be combined with the `UnsignedEvent` fields to form an `Event`. This keeps orchestration out of the core so chapter 05 can build a proper `Signer` abstraction on top. 8. **Verification.** Two methods on `Event`: - `fn verify_id(&self) -> bool` — recomputes the id from the other fields and compares. - `fn verify(&self) -> Result<(), EventError>` — verifies id and Schnorr signature. Uses `secp256k1::Message::from_digest` + `verify_schnorr` with the stored pubkey. Note: no `PublicKey::verify` method yet — the verification goes directly through `secp256k1` inside `Event::verify`. A future signer chapter can pull that out. 9. **JSON round-tripping.** `Event` implements `Serialize`/`Deserialize`. Because we store id/sig as bytes, we implement serde by hand via an intermediate struct with hex-string fields. Highlight: incoming JSON is validated structurally by serde but the id/sig are NOT cryptographically validated on deserialization — that's what `verify()` is for. 10. **A worked example.** Build an unsigned text-note event, compute its id, sign it, assemble the `Event`, serialize to JSON, parse it back, verify. This block is illustrative — marked without a `{file=...}` annotation so it is NOT tangled. It exists only in the narrative to show how the pieces fit together. 11. **What's next.** Pointer forward to chapter 05 (Signing), which will wrap `SecretKey::sign` in a proper `Signer` abstraction that knows how to sign events directly. ## API Design New in `coracle-lib/src/events.rs`: ```rust pub enum EventError { Json(String), IdMismatch, InvalidSignature, InvalidHexField(&'static str), } pub struct Event { pub id: [u8; 32], pub pubkey: PublicKey, pub created_at: u64, pub kind: u16, pub tags: Vec>, pub content: String, pub sig: [u8; 64], } impl Event { pub fn verify_id(&self) -> bool; pub fn verify(&self) -> Result<(), EventError>; } impl Serialize for Event { ... } impl<'de> Deserialize<'de> for Event { ... } pub struct UnsignedEvent { pub pubkey: PublicKey, pub created_at: u64, pub kind: u16, pub tags: Vec>, pub content: String, } impl UnsignedEvent { pub fn id(&self) -> [u8; 32]; } ``` New in `coracle-lib/src/keys.rs` (addition): ```rust impl SecretKey { pub fn sign(&self, message: &[u8; 32]) -> [u8; 64]; } ``` Taking `&[u8; 32]` rather than `&[u8]` encodes Schnorr-over-a-digest at the type level and matches `secp256k1::Message::from_digest`. The chapter notes that this is "sign an arbitrary 32-byte message," not "sign an event." ## Code Organization - `coracle-lib/src/events.rs` — new file, all event-related types. - `coracle-lib/src/lib.rs` — register `pub mod events;`. - `coracle-lib/src/keys.rs` — append a small block adding `SecretKey::sign`. - `coracle-lib/tests/events.rs` — hand-written integration tests (not tangled): round-trip a known event, verify a pre-signed fixture, reject tampered id/sig. ## Dependencies All already in `coracle-lib/Cargo.toml`: - `serde`, `serde_json` — JSON and canonical form - `sha2` — event id hash - `secp256k1` — Schnorr signing and verification - `hex` — id/sig hex encoding in serde impls No new dependencies. ## Narrative Notes - **Open with referential transparency.** The single most important concept in this chapter is that the event id is a function of the content. Lead with that, not with field enumeration. - **Canonical form deserves its own subsection.** Readers coming from conventional APIs will expect "just JSON"; it's worth stopping to explain why nostr uses a positional array, not an object, to compute the hash. - **Two separate structs, not `Option`.** Explain the choice: making the lifecycle visible in types is worth the slightly larger API surface in a teaching library. Reference rust-nostr's similar approach. - **Why `sign` is on `SecretKey`, not `Event`.** Keep signing as a low-level primitive over bytes. Chapter 05 will introduce a `Signer` trait that abstracts over local/remote/browser signers; only that trait will have an `sign_event` method. Keeping `Event` signer-agnostic now avoids having to retrofit it later. - **Show the worked example early enough that readers can see the pieces click.** Put it right after verification so they see create → hash → sign → verify in one flow. - **Structural validation is shallow.** Point out explicitly that `verify()` does the real work and structural validation is about rejecting garbage, not about enforcing semantics. Tie back to the "validation philosophy" from chapter 01's framing and from the building-nostr philosophy. - **Custom serde for binary fields.** Walk through why we hand-write `Serialize`/`Deserialize`: the wire format expects id/sig as lowercase hex strings, but we want to hold them as `[u8; 32]`/`[u8; 64]` internally. An intermediate struct with hex `String` fields + `From` conversions is the cleanest approach. ## Design Decisions 1. **Plain struct with public fields.** Matches rust-nostr. No getters, no setters, no builder — the teaching goal is clarity about what an event *is*. A builder can come later if it earns its place. 2. **Separate `UnsignedEvent`.** Option 1 from the research. Clearer lifecycle in types; avoids `Option` juggling. 3. **`[u8; 32]` / `[u8; 64]` for id/sig, not newtypes.** Keep the type surface small for this chapter. A later chapter can promote them to `EventId` etc. if it's worth it. The serde impl hides the raw array at the JSON boundary. 4. **Custom serde impls via an intermediate hex-string struct.** Mirrors rust-nostr's `EventIntermediate` approach but simpler: one owned struct for deserialization, hand-written `Serialize` for output. Avoids pulling in `serde_with` or writing `serialize_with` attributes. 5. **`SecretKey::sign` takes `&[u8; 32]`, not `&[u8]`.** Schnorr signs a 32-byte digest; the fixed-size array makes that contract visible and avoids runtime errors. Matches `secp256k1::Message::from_digest`. 6. **No `PublicKey::verify` yet.** One primitive at a time. Verification lives inside `Event::verify` for now, which hides the secp256k1 details. Chapter 05 can pull it out when the `Signer` trait emerges. 7. **`u64` for `created_at`.** Unix seconds fit comfortably. A signed int would suggest pre-epoch events are meaningful; they aren't in nostr. 8. **`u16` for `kind`.** nostr kinds fit in 16 bits and the canonical form serializes them as bare integers. No enum yet — that's chapter 07. 9. **Tags as `Vec>`.** Matches every reference. Richer tag types wait for chapter 06. 10. **Worked example is NOT tangled.** Per user's explicit instruction: show event signing as a prose example using the `sign` primitive, but do not generate a `sign_event` or similar helper in the library. ## Open Questions - **Should `Event::verify` validate structural invariants (pubkey length, hex shape of tags, etc.) in addition to id and signature?** Leaning no — those are caught at deserialization by serde. `verify()` should be *cryptographic* verification only. Will decide in writing. - **Error granularity for JSON failures.** Keep a single `Json(String)` variant, or split into `InvalidJson` / `MissingField` / `WrongType`? Leaning on a single variant to match the style of the keys chapter's `KeyError`. - **Should `UnsignedEvent` have a convenience constructor for kind-1 text notes with `created_at = now()`?** Leaning no — this chapter should show the raw fields; builders and helpers can come later without breaking anything.