Add events chapter
This commit is contained in:
+449
-165
@@ -1,202 +1,486 @@
|
||||
# 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.
|
||||
Almost everything else in this book is built out of events. Profiles, follow
|
||||
lists, direct messages, zap receipts, emoji reactions, long-form articles —
|
||||
they are all the same data type with different values in a handful of fields.
|
||||
If you understand events, you understand most of what a nostr library does.
|
||||
|
||||
## The event structure
|
||||
An event is a JSON object with seven fields. What makes it interesting is
|
||||
that the `id` field is a hash of the other fields and the `sig` field is a
|
||||
Schnorr signature over that id. Anyone can verify, offline, that a given
|
||||
event was produced by the holder of a particular secret key and has not been
|
||||
altered since. There is no database row to update, no server to consult. The
|
||||
event either hashes to its claimed id and verifies under its claimed pubkey,
|
||||
or it doesn't — and that property, referential transparency in the functional
|
||||
sense, is why nostr can work at all.
|
||||
|
||||
Here's what a nostr event looks like as JSON:
|
||||
This chapter builds the Rust types that represent events and the two
|
||||
primitives that make them go: computing the id and verifying the signature.
|
||||
Construction is layered across six structs so that each stage of an event's
|
||||
life is a distinct type; you can't sign something that hasn't been hashed,
|
||||
and you can't hash something that doesn't know its author.
|
||||
|
||||
## The module
|
||||
|
||||
```rust {file=coracle-lib/src/lib.rs}
|
||||
pub mod events;
|
||||
```
|
||||
|
||||
```rust {file=coracle-lib/src/events.rs}
|
||||
//! The nostr `Event` type, built up through a layered hierarchy of structs
|
||||
//! that each add a single field: [`EventContent`], [`EventTemplate`],
|
||||
//! [`StampedEvent`], [`OwnedEvent`], [`HashedEvent`], and [`Event`].
|
||||
|
||||
use serde::de::{self, MapAccess, Visitor};
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt;
|
||||
|
||||
use crate::keys::PublicKey;
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
```rust {file=coracle-lib/src/events.rs}
|
||||
/// Errors that can occur when parsing or verifying an event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EventError {
|
||||
/// The recomputed id did not match the event's `id` field.
|
||||
InvalidId,
|
||||
/// The Schnorr signature did not verify against the event's pubkey and id.
|
||||
InvalidSignature,
|
||||
}
|
||||
|
||||
impl fmt::Display for EventError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
EventError::InvalidId => write!(f, "event id does not match its contents"),
|
||||
EventError::InvalidSignature => write!(f, "event signature failed to verify"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EventError {}
|
||||
```
|
||||
|
||||
## The wire format
|
||||
|
||||
A signed event on the wire looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "4376c65d2f232afbe9b882a35baa4f6fe8bce184396e157b1a35b3f01b3e285c",
|
||||
"id": "4376c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65",
|
||||
"pubkey": "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
|
||||
"created_at": 1673347337,
|
||||
"kind": 1,
|
||||
"tags": [
|
||||
["e", "3da979448d9ba263864c4d6f14984c423a3838364ec255f03c7904b1ae77f206"],
|
||||
["p", "bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce"]
|
||||
],
|
||||
"content": "Hello, nostr!",
|
||||
"sig": "908a15e46fb4d8675bab026fc230a0e3542bfade63da02d542fb78b2a8513fcd..."
|
||||
"tags": [["t", "nostr"]],
|
||||
"content": "hello nostr",
|
||||
"sig": "908a15e46fb4d8675bab026fc230a0e3542bfade63da02d542fb78b2a8513fcd0092619a2c8c1221e581946e0191f2af505dfdf8657a414dbca329186f009262"
|
||||
}
|
||||
```
|
||||
|
||||
Let's define this in Rust. We'll start with the module declaration:
|
||||
`id`, `pubkey`, and `sig` are hex strings of fixed length. `created_at` is a
|
||||
Unix timestamp in seconds. `kind` is a 16-bit integer. `tags` is a list of
|
||||
lists of strings. `content` is a string whose meaning depends on the kind.
|
||||
|
||||
```rust {file=coracle-lib/src/lib.rs}
|
||||
pub mod event;
|
||||
```
|
||||
## Building up an event
|
||||
|
||||
And now the `Event` struct itself. Each field maps directly to the JSON above:
|
||||
Rather than one `Event` struct that may or may not have its id and signature
|
||||
filled in, we define six structs, each of which adds exactly one field to the
|
||||
previous. A value's type tells you which steps it has been through, and a
|
||||
`.foo()` method takes you from one stage to the next.
|
||||
|
||||
```rust {file=coracle-lib/src/event.rs}
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Sha256, Digest};
|
||||
### `EventContent`
|
||||
|
||||
/// 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.
|
||||
The starting point is the payload the caller wants to publish: a
|
||||
human-readable `content` string plus the structured `tags` that reference
|
||||
other events, pubkeys, topics, and whatever else. Tags and content travel
|
||||
together because tags are part of what the hash commits to — they are not
|
||||
metadata in the "could be added later" sense.
|
||||
|
||||
```rust {file=coracle-lib/src/events.rs}
|
||||
/// The content of an event: the human-readable body plus its tags.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EventContent {
|
||||
pub content: String,
|
||||
/// A 64-byte hex-encoded Schnorr signature of the event ID.
|
||||
pub sig: String,
|
||||
pub tags: Vec<Vec<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()
|
||||
impl EventContent {
|
||||
pub fn new(content: impl Into<String>, tags: Vec<Vec<String>>) -> Self {
|
||||
EventContent { content: content.into(), tags }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verifying signatures
|
||||
### `EventTemplate`
|
||||
|
||||
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.
|
||||
A template adds the numeric `kind` that decides how the content should be
|
||||
interpreted. A kind-1 template is a short text note; a kind-6 template is a
|
||||
repost; a kind-1984 template is a report. This chapter stays agnostic about
|
||||
which kinds mean what — that's the next layer of the stack.
|
||||
|
||||
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/events.rs}
|
||||
/// Event content plus its kind.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EventTemplate {
|
||||
pub content: String,
|
||||
pub tags: Vec<Vec<String>>,
|
||||
pub kind: u16,
|
||||
}
|
||||
|
||||
```rust {file=coracle-lib/src/event.rs}
|
||||
impl EventContent {
|
||||
/// Tag the content with a kind, producing a publishable template.
|
||||
pub fn kind(self, kind: u16) -> EventTemplate {
|
||||
EventTemplate { content: self.content, tags: self.tags, kind }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `StampedEvent`
|
||||
|
||||
Stamping adds the `created_at` timestamp. Callers usually pass
|
||||
`SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()`, but the
|
||||
library stays out of clock policy — the timestamp is whatever the caller says
|
||||
it is.
|
||||
|
||||
```rust {file=coracle-lib/src/events.rs}
|
||||
/// A template with a `created_at` timestamp attached.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StampedEvent {
|
||||
pub content: String,
|
||||
pub kind: u16,
|
||||
pub tags: Vec<Vec<String>>,
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
impl EventTemplate {
|
||||
pub fn stamp(self, created_at: u64) -> StampedEvent {
|
||||
StampedEvent {
|
||||
content: self.content,
|
||||
kind: self.kind,
|
||||
tags: self.tags,
|
||||
created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `OwnedEvent`
|
||||
|
||||
Ownership means attaching an author. This is where the `PublicKey` from
|
||||
chapter 2 shows up: the event is about to be something that some specific key
|
||||
claims responsibility for.
|
||||
|
||||
```rust {file=coracle-lib/src/events.rs}
|
||||
/// A stamped event with its author's public key attached.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OwnedEvent {
|
||||
pub content: String,
|
||||
pub kind: u16,
|
||||
pub tags: Vec<Vec<String>>,
|
||||
pub created_at: u64,
|
||||
pub pubkey: PublicKey,
|
||||
}
|
||||
|
||||
impl StampedEvent {
|
||||
pub fn own(self, pubkey: PublicKey) -> OwnedEvent {
|
||||
OwnedEvent {
|
||||
content: self.content,
|
||||
kind: self.kind,
|
||||
tags: self.tags,
|
||||
created_at: self.created_at,
|
||||
pubkey,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `HashedEvent`
|
||||
|
||||
Hashing produces the event id — the 32-byte SHA-256 that NIP-01 defines as
|
||||
the hash of a specific canonical serialization. The canonical form is a JSON
|
||||
*array*, not an object:
|
||||
|
||||
```text
|
||||
[0, pubkey, created_at, kind, tags, content]
|
||||
```
|
||||
|
||||
The leading `0` is a version marker reserved by the protocol. The order is
|
||||
fixed. Every nostr implementation on the network has to produce byte-
|
||||
identical output from the same fields, or ids wouldn't match.
|
||||
|
||||
```rust {file=coracle-lib/src/events.rs}
|
||||
fn canonical(
|
||||
pubkey: &PublicKey,
|
||||
created_at: u64,
|
||||
kind: u16,
|
||||
tags: &[Vec<String>],
|
||||
content: &str,
|
||||
) -> String {
|
||||
serde_json::json!([
|
||||
0,
|
||||
pubkey.to_hex(),
|
||||
created_at,
|
||||
kind,
|
||||
tags,
|
||||
content,
|
||||
])
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// An owned event with its computed id.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HashedEvent {
|
||||
pub content: String,
|
||||
pub kind: u16,
|
||||
pub tags: Vec<Vec<String>>,
|
||||
pub created_at: u64,
|
||||
pub pubkey: PublicKey,
|
||||
pub id: [u8; 32],
|
||||
}
|
||||
|
||||
impl OwnedEvent {
|
||||
pub fn hash(self) -> HashedEvent {
|
||||
let id: [u8; 32] = Sha256::digest(
|
||||
canonical(&self.pubkey, self.created_at, self.kind, &self.tags, &self.content)
|
||||
.as_bytes(),
|
||||
)
|
||||
.into();
|
||||
HashedEvent {
|
||||
content: self.content,
|
||||
kind: self.kind,
|
||||
tags: self.tags,
|
||||
created_at: self.created_at,
|
||||
pubkey: self.pubkey,
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `Event`
|
||||
|
||||
The final step attaches the signature and yields the type that goes on the
|
||||
wire.
|
||||
|
||||
```rust {file=coracle-lib/src/events.rs}
|
||||
/// A fully-formed nostr event: hashed and signed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Event {
|
||||
pub content: String,
|
||||
pub kind: u16,
|
||||
pub tags: Vec<Vec<String>>,
|
||||
pub created_at: u64,
|
||||
pub pubkey: PublicKey,
|
||||
pub id: [u8; 32],
|
||||
pub sig: [u8; 64],
|
||||
}
|
||||
|
||||
impl HashedEvent {
|
||||
pub fn sign(self, sig: [u8; 64]) -> Event {
|
||||
Event {
|
||||
content: self.content,
|
||||
kind: self.kind,
|
||||
tags: self.tags,
|
||||
created_at: self.created_at,
|
||||
pubkey: self.pubkey,
|
||||
id: self.id,
|
||||
sig,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The two-step split between `hash` and `sign` is deliberate. `hash` is pure
|
||||
and knows nothing about keys; `sign` takes an already-computed signature as
|
||||
input rather than doing the signing itself. That keeps the signing primitive
|
||||
out of the event module, which is where we need it next.
|
||||
|
||||
## Signing a 32-byte message
|
||||
|
||||
We need exactly one new capability to drive the pipeline above end-to-end: a
|
||||
way for a `SecretKey` to produce a Schnorr signature over 32 arbitrary bytes.
|
||||
Not "sign an event" — just "sign these bytes." A future chapter will wrap
|
||||
this in a `Signer` trait that knows about events; we want only one place in
|
||||
the library where the cryptographic operation actually happens.
|
||||
|
||||
```rust {file=coracle-lib/src/keys.rs}
|
||||
impl SecretKey {
|
||||
/// Schnorr-sign a 32-byte message under this secret key. The digest
|
||||
/// length is enforced at the type level; callers have to decide what
|
||||
/// 32 bytes they're signing. Deterministic under BIP-340.
|
||||
pub fn sign(&self, message: &[u8; 32]) -> [u8; 64] {
|
||||
let keypair = secp256k1::Keypair::from_secret_key(SECP256K1, &self.0);
|
||||
let msg = secp256k1::Message::from_digest(*message);
|
||||
SECP256K1.sign_schnorr_no_aux_rand(&msg, &keypair).serialize()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With that in place, the full pipeline from "hello nostr" to a signed event is
|
||||
one expression:
|
||||
|
||||
```rust
|
||||
let hashed = EventContent::new("hello nostr", vec![])
|
||||
.kind(1)
|
||||
.stamp(1_700_000_000)
|
||||
.own(secret.public_key())
|
||||
.hash();
|
||||
let event = hashed.clone().sign(secret.sign(&hashed.id));
|
||||
```
|
||||
|
||||
The pipeline is illustrative, not tangled — the library does not ship an
|
||||
`Event::sign_with` or `Signer` convenience yet. The later chapter on signers
|
||||
wraps these inner two lines in a `Signer::sign_event` default so that remote
|
||||
signers (NIP-07, NIP-46) plug into the same shape.
|
||||
|
||||
## Verification
|
||||
|
||||
Verification reverses the pipeline. Given an `Event`, recompute the canonical
|
||||
form from its current fields, compare the resulting hash against the stored
|
||||
`id`, and check the Schnorr signature.
|
||||
|
||||
```rust {file=coracle-lib/src/events.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;
|
||||
/// Recompute the event id and compare it to the stored `id`.
|
||||
pub fn verify_id(&self) -> bool {
|
||||
let expected: [u8; 32] = Sha256::digest(
|
||||
canonical(&self.pubkey, self.created_at, self.kind, &self.tags, &self.content)
|
||||
.as_bytes(),
|
||||
)
|
||||
.into();
|
||||
expected == self.id
|
||||
}
|
||||
|
||||
/// Verify both the id and the Schnorr signature.
|
||||
pub fn verify(&self) -> Result<(), EventError> {
|
||||
if !self.verify_id() {
|
||||
return Err(EventError::InvalidId);
|
||||
}
|
||||
let msg = secp256k1::Message::from_digest(self.id);
|
||||
let sig = secp256k1::schnorr::Signature::from_slice(&self.sig)
|
||||
.map_err(|_| EventError::InvalidSignature)?;
|
||||
let xonly = secp256k1::XOnlyPublicKey::from_slice(&self.pubkey.as_bytes())
|
||||
.map_err(|_| EventError::InvalidSignature)?;
|
||||
sig.verify(&msg, &xonly).map_err(|_| EventError::InvalidSignature)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The canonical-form helper is the single source of truth for "what bytes go
|
||||
into the hash." Both `OwnedEvent::hash` and `Event::verify_id` call it, so
|
||||
there is no way for the signing and verification paths to disagree about the
|
||||
serialization.
|
||||
|
||||
## JSON
|
||||
|
||||
On the wire an event is the JSON object from the start of the chapter. The
|
||||
binary fields — `id`, `pubkey`, and `sig` — are hex strings there, and
|
||||
fixed-size byte arrays in memory. We hand-write `Serialize` and `Deserialize`
|
||||
to bridge the two without pulling in `serde_with`.
|
||||
|
||||
```rust {file=coracle-lib/src/events.rs}
|
||||
impl Serialize for Event {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let mut state = serializer.serialize_struct("Event", 7)?;
|
||||
state.serialize_field("id", &hex::encode(self.id))?;
|
||||
state.serialize_field("pubkey", &self.pubkey.to_hex())?;
|
||||
state.serialize_field("created_at", &self.created_at)?;
|
||||
state.serialize_field("kind", &self.kind)?;
|
||||
state.serialize_field("tags", &self.tags)?;
|
||||
state.serialize_field("content", &self.content)?;
|
||||
state.serialize_field("sig", &hex::encode(self.sig))?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The deserializer walks the fields into `Option`s, validates hex shape at the
|
||||
end, and silently ignores unknown keys — relays and clients sometimes attach
|
||||
their own, and rejecting them would make this parser less permissive than
|
||||
the network.
|
||||
|
||||
```rust {file=coracle-lib/src/events.rs}
|
||||
impl<'de> Deserialize<'de> for Event {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
deserializer.deserialize_map(EventVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct EventVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for EventVisitor {
|
||||
type Value = Event;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("a nostr event object")
|
||||
}
|
||||
|
||||
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Event, M::Error> {
|
||||
let mut id: Option<String> = None;
|
||||
let mut pubkey: Option<String> = None;
|
||||
let mut created_at: Option<u64> = None;
|
||||
let mut kind: Option<u16> = None;
|
||||
let mut tags: Option<Vec<Vec<String>>> = None;
|
||||
let mut content: Option<String> = None;
|
||||
let mut sig: Option<String> = None;
|
||||
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
match key.as_str() {
|
||||
"id" => id = Some(map.next_value()?),
|
||||
"pubkey" => pubkey = Some(map.next_value()?),
|
||||
"created_at" => created_at = Some(map.next_value()?),
|
||||
"kind" => kind = Some(map.next_value()?),
|
||||
"tags" => tags = Some(map.next_value()?),
|
||||
"content" => content = Some(map.next_value()?),
|
||||
"sig" => sig = Some(map.next_value()?),
|
||||
_ => {
|
||||
let _: serde::de::IgnoredAny = map.next_value()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 id = decode_32(&id.ok_or_else(|| de::Error::missing_field("id"))?)
|
||||
.map_err(|_| de::Error::custom("invalid hex in field `id`"))?;
|
||||
let sig = decode_64(&sig.ok_or_else(|| de::Error::missing_field("sig"))?)
|
||||
.map_err(|_| de::Error::custom("invalid hex in field `sig`"))?;
|
||||
let pubkey = PublicKey::from_hex(
|
||||
&pubkey.ok_or_else(|| de::Error::missing_field("pubkey"))?,
|
||||
)
|
||||
.map_err(|_| de::Error::custom("invalid hex in field `pubkey`"))?;
|
||||
|
||||
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()
|
||||
Ok(Event {
|
||||
id,
|
||||
pubkey,
|
||||
created_at: created_at.ok_or_else(|| de::Error::missing_field("created_at"))?,
|
||||
kind: kind.ok_or_else(|| de::Error::missing_field("kind"))?,
|
||||
tags: tags.ok_or_else(|| de::Error::missing_field("tags"))?,
|
||||
content: content.ok_or_else(|| de::Error::missing_field("content"))?,
|
||||
sig,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_32(s: &str) -> Result<[u8; 32], ()> {
|
||||
let bytes = hex::decode(s).map_err(|_| ())?;
|
||||
bytes.try_into().map_err(|_| ())
|
||||
}
|
||||
|
||||
fn decode_64(s: &str) -> Result<[u8; 64], ()> {
|
||||
let bytes = hex::decode(s).map_err(|_| ())?;
|
||||
let arr: [u8; 64] = bytes.try_into().map_err(|_| ())?;
|
||||
Ok(arr)
|
||||
}
|
||||
```
|
||||
|
||||
## Creating events
|
||||
Deserialization does not verify the signature. Parsing is cheap; `verify()`
|
||||
is not, and there are many places where you want to read an event off disk
|
||||
without paying for a signature check.
|
||||
|
||||
To create an event, you provide the content fields and sign with a secret key. The process is:
|
||||
## What's next
|
||||
|
||||
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.
|
||||
The next chapter gives `tags` a proper type. Tags are the structured half of
|
||||
every event — references to other events, pubkeys, topics, relay hints — and
|
||||
treating them as bare `Vec<Vec<String>>` gets tiresome fast.
|
||||
|
||||
Reference in New Issue
Block a user