Architecture
zig-nostr is built in layers, each shipped and tested on its own. Everything
above the crypto core is written in plain Zig; the two C dependencies
(libsecp256k1 and LMDB) are compiled from pinned source.
Library core
The credibility anchor. secp256k1 keys and BIP-340 Schnorr signatures come from
bitcoin-core’s libsecp256k1, bound via translate-c and passing the full
official BIP-340 test-vector suite. On top of it:
- NIP-01 event model: canonical serialization, sha256 id computation, and wire JSON encode/decode, verified against sha256 oracle vectors.
- NIP-19 / NIP-21: bech32 entity encoding (
npub/nsec/noteand the TLV formsnprofile/nevent/naddr/nrelay) plusnostr:URIs. - NIP-06: BIP-39 mnemonics and BIP-32 HD derivation, byte-for-byte against the official vectors.
- NIP-49: private-key encryption at rest (
ncryptsec): scrypt + XChaCha20-Poly1305, with NFKC password normalization for cross-implementation interop.
Transport & outbox
- RFC 6455 WebSocket framing and handshake, and a stream-generic relay connection state machine with NIP-01 subscriptions.
- A live TCP/TLS dialer. Hostnames resolve through libc
getaddrinfoon macOS and Linux and through std’s own resolver on Windows, and an IP literal is parsed first so it never reaches a resolver.dialDiagnosedisdialthat also records the HTTP status the relay answered the upgrade with, so a relay that says 503 when busy, 401 or 429 can be told apart from one that is down. - NIP-50 search on the filter:
Filter.searchis sent to relays as"search", and the local store applies it as a case-insensitive substring of the content. - NIP-65 outbox model: relay lists with read/write routing and zero hardcoded relays, so events are published and fetched where they actually live.
Local-first store
A zero-copy, memory-mapped LMDB event store (store.zig):
- A compact binary event record with secondary indexes (author, kind,
created_at, tag). - A bounded, newest-first query planner: it walks the indexes via reverse
cursors and a k-way merge that takes the newest candidate from a binary heap,
stopping at
limit, so feed latency stays flat as the store grows (see Performance). - Validate-on-insert ingestion: dedup, replaceable / parameterized-replaceable upserts, and NIP-09 deletions.
- A direct-message conversation index, local-first reconciliation, and a size-cap cache.
Signer protocol
The layer the native signer is built on:
-
NIP-44 v2 payload encryption (ChaCha20 + HMAC-SHA256 over a libsecp256k1 ECDH secret), verified against the official vectors.
-
NIP-46 remote signing, the request/response messages, the
kind:24133envelope, a transport-agnostic dispatcher behind an approval policy, and thebunker:///nostrconnect://connection URIs. -
NIP-42 client authentication, so a signer can serve over relays that gate access behind auth.
-
A NIP-46 client (
nip46_client, experimental): the app side of a bunker connection, overbunker://ornostrconnect://, with one reader per relay. Its shape may still change.
The native signer built on this layer is Notary.
Messages, sync and HTTP
Codecs and protocols the command line is built from. Where a spec needs HTTP, the library builds and reads the messages and leaves the transport to the caller.
- NIP-59 gift wrap and NIP-17 messages: rumor, seal and wrap, with the sender proven by the seal alone.
- NIP-04 legacy messages, for older clients. It has no authentication.
- NIP-77 negentropy: protocol version 1, checked against transcripts recorded
from the reference implementation, with the
NEG-*and NIP-45COUNTmessages encoded and parsed. - NIP-13 proof of work: counting, and mining to a target.
- NIP-05 identifiers and NIP-11 relay documents, parsed.
- NIP-98 HTTP authentication, the NIP-86 relay management API, and Blossom authorization and blob descriptors.
Platforms
The library builds for macOS, Linux and Windows, on x86_64 and aarch64, and CI builds and tests it on all three. On Windows the dialer waits for write room with a poll request to the AFD driver, because std opens its sockets on that driver and never registers them with Winsock, and a key file takes its directory’s default access list because there are no mode bits. macOS and Linux keep getaddrinfo and poll.
How it is tested
Tests sit beside the code and zig build test runs them all. The cryptography is checked against the official vectors (the BIP-340 suite, the NIP-44 v2 vectors, and the NIP-19, NIP-49 and RFC 6455 worked examples). The transport is driven by a scripted in-memory relay, with a few tests on a real loopback socket for the parts a fake cannot show. The store runs on a real LMDB file in a temporary directory.
The parsers that read bytes someone else wrote have fuzz targets: WebSocket frames and handshake responses, relay messages, events, bech32 and NIP-19, NIP-44 payloads, NIP-46 messages and URIs, relay lists and URLs, mnemonics, ncryptsec files and filters. Each asserts that nothing crashes or leaks and, where one exists, an invariant, such as an event parsed, serialized and parsed again being the same event. zig build test runs each target over a seed corpus, and zig build test --fuzz -Doptimize=ReleaseSafe fuzzes them until stopped.
The repository’s ARCHITECTURE.md goes through the modules file by file, how an event travels from a key to a relay to the store, and the threading and cancellation rules.