Skip to content

Releases: zig-nostr/nostr

v0.3.5 — NIP-42 relay authentication

Choose a tag to compare

@sepehr-safari sepehr-safari released this 12 Jul 15:43
501f0f0

Added

  • NIP-42 authentication of clients to relays (src/nip42.zig). A connection can now authenticate to relays that require it — to protect DMs, deliver ephemeral events, etc. — so a signer can serve NIP-46 over such relays.
    • nip42.authEvent(...) builds and signs the kind:22242 event (a relay tag + the relay's challenge, empty content).
    • message.zig parses the relay's ["AUTH", <challenge>] into a new RelayMessage.auth variant (it previously returned InvalidMessage, which dropped the connection); encodeAuth emits the client's ["AUTH", <event>] reply; Connection.authenticate / Relay.authenticate send it.

Fixed

  • The WebSocket opening handshake now includes a non-default port in the Host header (RFC 9110 §7.2): Host: relay.example.com:8443, not Host: relay.example.com. Relays that derive their canonical URL from Host compare it against a NIP-42 auth event's relay tag; omitting the port made them mismatch and reject the authentication. Default ports (80/443) stay omitted, so standard ws:///wss:// relays are unaffected. Adds relay.Url.hostHeader.

Verified live against a relay requiring NIP-42 (nak serve --auth --eager-auth): the signer answers the challenge, is accepted, re-subscribes, and a full NIP-46 round-trip completes — the client receives a valid signed event (nak verify clean).

Full changelog: https://github.com/zig-nostr/nostr/blob/main/CHANGELOG.md

v0.3.4 — wss:// delivery stall fix (works on damus)

Choose a tag to compare

@sepehr-safari sepehr-safari released this 11 Jul 23:42
0f60bf5

Fixed

A running signer connected and subscribed to a public wss:// relay but requests published by a client arrived late or never, so the client hung. This was not NIP-42 AUTH (damus delivers kind:24133 unauthenticated) — it was a receive-path stall.

The std TLS reader decrypts one record at a time into its own buffer. IoStream.read returned bytes via the generic readVec into the full 4 KiB receive buffer, which greedily keeps reading until that buffer fills — so a relay message that had already arrived was drained into the buffer and then the read blocked on the next TLS record to fill the rest, withholding the message until unrelated later traffic (a client retry, a relay ping) happened to arrive. Every NIP-46 request stalled behind the following record.

read now serves already-buffered bytes and otherwise does exactly one underlying read (fillMore), so each message surfaces the moment its record lands — on both ws:// and wss://. Supersedes the #44 / #46 read iterations, which fixed the handshake but left this stall. Adds regression tests modeling the real fill-your-own-buffer reader contract.

Verified live against relay.damus.io: a full NIP-46 round-trip (client connect → signer ACK → sign_event → signed reply) completes in ~3 s at sub-second per-request latency, returning a valid signed event. (#48, #49)

Full changelog: https://github.com/zig-nostr/nostr/blob/main/CHANGELOG.md

v0.3.3 — TLS handshake fix (wss://)

Choose a tag to compare

@sepehr-safari sepehr-safari released this 11 Jul 22:22
996e8a2

Fixed

Follow-up to v0.3.2. That release fixed the readSliceShort fill-deadlock by reading once with readVec, but a readVec of zero bytes means "no application data yet", not end-of-stream — a TLS record can carry none. IoStream.read reported that zero as EOF, so the websocket handshake against a real wss:// relay aborted with HandshakeFailed (plaintext ws:// was unaffected).

read now retries past a bare zero read and returns only on the first real bytes or a genuine end of stream. Verified live: ws:// (local relay) completes a full NIP-46 request round-trip, and wss:// (relay.damus.io) now completes the TLS handshake. Adds a regression test for the zero-then-data case. (#46)

Full changelog: https://github.com/zig-nostr/nostr/blob/main/CHANGELOG.md

v0.3.2 — live relay handshake deadlock fix

Choose a tag to compare

@sepehr-safari sepehr-safari released this 11 Jul 21:56
086299f

Fixed

A running signer connected to relays but never received any requests. The live relay connection deadlocked the websocket opening handshake: IoStream.read used readSliceShort, which blocks until it fills the whole read buffer, so reading the short 101 Switching Protocols response into a 4 KiB buffer waited forever for bytes the relay only sends after we subscribe — dial never returned and no subscription was sent.

It now reads once and returns whatever is available (readVec), like a POSIX read. Verified live end-to-end: the signer now receives a NIP-46 request over a relay, holds it for approval, signs on approval, and returns a valid signed event — the key never leaving the signer. Adds a regression test that pins the read primitive. (#44, closes #43)

Full changelog: https://github.com/zig-nostr/nostr/blob/main/CHANGELOG.md

v0.3.1 — macOS relay hostname fix

Choose a tag to compare

@sepehr-safari sepehr-safari released this 11 Jul 11:21
e73a3dd

A patch release fixing hostname resolution in the live relay dialer on macOS.

What changed

  • relay.dial resolves relay hostnames with the system resolver (libc getaddrinfo) instead of std's built-in DNS resolver. std reads nameservers from /etc/resolv.conf, which is empty on macOS (name resolution goes through the system configuration framework, not resolv.conf), so it fell back to querying a dead 127.0.0.1:53 and a hostname lookup hung indefinitely. getaddrinfo uses the OS resolver and works on both macOS and Linux; an IP-literal host flows through the same path. (#41)

The live dialer isn't reachable from CI (no relay to connect to), so this had gone unnoticed until the signer connected to a real relay.

Verification

Verified by hand against a live relay: the signer resolves relay.damus.io, opens the socket, completes the TLS + WebSocket handshake, and holds an ESTABLISHED connection subscribed for NIP-46 requests. resolveAndConnect is referenced in the "semantically analyzed" test so CI keeps type-checking it; 132 tests pass, CI green on Linux and macOS.

Install

zig fetch --save https://github.com/zig-nostr/nostr/archive/refs/tags/v0.3.1.tar.gz

What's next

Milestone A5 continues in zig-nostr/signer: NIP-49 encrypted key storage at rest and a per-request approval flow, then a native macOS build.

v0.3.0 — NIP-46 remote signing

Choose a tag to compare

@sepehr-safari sepehr-safari released this 11 Jul 09:44
db437fa

Milestone A5 groundwork — the protocol layer a native signer needs: NIP-44 v2 encryption and the NIP-46 remote-signing ("bunker") protocol, so a signer can hold the user's key and sign for remote clients over a relay without the key ever reaching the client.

Highlights

NIP-44 v2 payload encryption (src/nip44.zig)

  • ChaCha20 + HMAC-SHA256 with HKDF-derived per-message keys over a libsecp256k1 ECDH shared secret, NIP-44 padding, and a constant-time MAC that fails closed.
  • Verified against the official NIP-44 test vectors — conversation keys, message keys, padding boundaries, and encrypt/decrypt round-trips.
  • Adds keys.Signer.sharedSecretX — raw-x ECDH via a custom libsecp256k1 hash callback (the ECDH module is now enabled in the pinned secp256k1 build).

NIP-46 remote signing (src/nip46.zig)

  • The request/response messages and their JSON, and the kind:24133 NIP-44 envelope (seal / open).
  • A transport-agnostic Bunker dispatcherconnect, sign_event, ping, get_public_key, nip44_encrypt, nip44_decrypt — behind an injectable approval Policy, keeping the connection key separate from the user key per spec.
  • bunker:// and nostrconnect:// connection URIs: parse and build with RFC 3986 percent-coding, verified against the spec's example token.

Relay I/O is deliberately left to the application — the library stays transport-agnostic; the native signer that wires this to a relay is being built in zig-nostr/signer.

Quality

  • 132 tests passing, CI green on Linux and macOS.
  • No hand-rolled cryptography: encryption binds to audited bitcoin-core/secp256k1 (compiled from a pinned source) and Zig std's ChaCha20/HMAC/HKDF, checked against the official spec vectors.
  • This release also brings CHANGELOG.md back in sync with every release tag (the [0.2.0] and [0.2.1] sections were back-filled).

Install

zig fetch --save https://github.com/zig-nostr/nostr/archive/refs/tags/v0.3.0.tar.gz

What's next

Milestone A5 continues in zig-nostr/signer: the relay listen/sign loop, NIP-49 encrypted key storage at rest, and a per-request approval flow, then a native macOS build.

v0.2.1 — bounded query performance patch

Choose a tag to compare

@sepehr-safari sepehr-safari released this 11 Jul 00:38
1e16c1b

A performance patch for the local-first event store: queries are now answered by a bounded newest-first index merge instead of collecting every matching event before applying the limit.

What changed

  • Store.query opens one reverse cursor per index prefix (per author / kind / tag value), parks each on its newest in-range key, and k-way merges on the indexes' order-preserving [time][id] key suffix — yielding results globally newest-first and stopping at limit. Query cost is now proportional to the events returned, not to the total matching history. (#33, #35)
  • The store benchmark now measures the hottest client shape — a 20-author, kind-1, 500-note home feed — alongside the single-author profile query. (#34)

Numbers

BENCH_N=100000 zig build bench -Doptimize=ReleaseFast (100k stored events, warm cache, Apple M2 Pro):

Query v0.2.0 v0.2.1
Home feed — 20 authors, kind 1, limit 500 ~25.8 ms ~0.28 ms (~93×)
Profile — 1 author, limit 500 ~0.67 ms ~0.24 ms (~2.8×)

Ordering and filter semantics are unchanged (created_at desc, id desc; Filter.matches still validates every result) — the full existing test suite passes untouched, with new tests pinning cross-stream tie-breaks and tag key-region edge cases. 109 tests passing, CI green on Linux and macOS.

Install

zig fetch --save https://github.com/zig-nostr/nostr/archive/refs/tags/v0.2.1.tar.gz

What's next

Milestone A5: a native signer with NIP-49 at-rest encryption and a NIP-46 bunker (remote signing with per-request approval).

v0.2.0 — Transport & local-first store

Choose a tag to compare

@sepehr-safari sepehr-safari released this 10 Jul 21:06
ed08c61

Milestones A3 (relay transport) and A4 (the local-first event store) — the transport layer and the performance spine that make a Nostr client feel instant.

Highlights

Relay transport (A3)

  • RFC 6455 WebSocket framing and handshake, verified against the spec's worked examples.
  • A relay connection state machine — publish / subscribe / unsubscribe, ping→pong, fragmentation reassembly — generic over its byte stream, so the entire protocol flow is proven in CI against an in-memory stream, plus a live TCP/TLS dialer with system-CA certificate verification.
  • NIP-01 wire types: the filter model, client REQ/EVENT/CLOSE encoders, and relay EVENT/OK/EOSE/CLOSED/NOTICE parsing.
  • NIP-65 outbox routing: parse kind:10002 relay lists and route reads/writes by relay list, with zero hardcoded relays.

Local-first event store (A4) — modeled on nostrdb

  • A zero-copy, memory-mapped LMDB store: events live in a compact binary record whose scalar fields are read straight from the memory map, with no JSON re-parse on read.
  • Secondary indexes (author, kind, created_at, single-letter tags) and a filter-driven query API that reuses the exact subscription matching semantics, returning results newest-first with pagination.
  • Validate-on-insert ingestion: optional signature verification, replaceable and parameterized-replaceable "latest-wins" upserts, and NIP-09 deletion with author-scoped tombstones.
  • A direct-message conversation index (keyed by the canonical participant pair) for instant DM threads, local-first reconciliation helpers, a size-cap cache, and batched bulk insert.

Performance

Benchmark (zig build bench, ReleaseFast, 100k events across 100 authors):

  • ingest ~149,000 events/s
  • warm 500-note feed query in ~0.6 ms

Quality

  • 107 tests passing, CI green on Linux and macOS.
  • The relay protocol flow is tested hermetically over an in-memory stream; the store is verified end-to-end against real LMDB.
  • No hand-rolled cryptography or storage engine — signing binds to audited bitcoin-core/secp256k1, storage to LMDB, both compiled from pinned sources.

Install

zig fetch --save https://github.com/zig-nostr/nostr/archive/refs/tags/v0.2.0.tar.gz

What's next

Milestone A5: a native signer with NIP-49 at-rest encryption and a NIP-46 bunker (remote signing with per-request approval).

v0.1.0 — Library core

Choose a tag to compare

@sepehr-safari sepehr-safari released this 10 Jul 13:47
62692b5

Milestone A2: keys, encoding, events, and signatures — the cryptographic and data foundation the rest of the library builds on.

Highlights

  • secp256k1 keys and BIP-340 Schnorr signatures, bound to bitcoin-core's audited libsecp256k1 (compiled from source, pinned by exact commit). Passes the full official BIP-340 test-vector suite — all 19 vectors, both signing (exact signature reproduction) and verification (correct accept/reject on every case, including the adversarial ones).
  • NIP-01 event model: canonical [0,pubkey,created_at,kind,tags,content] serialization with the spec's exact escaping rule, sha256 id hashing, wire-format JSON encode/decode, and event-level create/verify.
  • NIP-19 bech32 entity encoding (npub/nsec/note bare, nprofile/nevent/naddr/nrelay TLV) and NIP-21 nostr: URIs, verified against the official spec vectors.
  • NIP-06 key derivation: BIP-39 mnemonic (embedded official English wordlist, checksum validation) + BIP-32 HD derivation for m/44'/1237'/<account>'/0/0, verified against both official test vectors byte-for-byte.
  • NIP-49 encrypted private key storage (ncryptsec): scrypt + XChaCha20-Poly1305, verified against the official decryption vector.

Quality

  • 39/39 tests passing, CI green on Linux and macOS.
  • Every cryptographic primitive verified against official spec test vectors, not just internal self-consistency.
  • No hand-rolled cryptography — signing/verification binds to audited bitcoin-core/secp256k1.

Install

zig fetch --save https://github.com/zig-nostr/nostr/archive/refs/tags/v0.1.0.tar.gz

What's next

Milestone A3: relay transport (websocket client, reconnect/backoff), NIP-01 message handling, subscription management, and NIP-65 outbox routing.