torly

torly - a TinyGo Nostr relay

git clone https://git.smesh.lol/torly.git

torly

A Nostr relay compiled by TinyGo, ported clean-room from the relay semantics of orly onto the storage model used by morly: an append-only write-ahead log for immutable signed events, and sorted flat-file indexes over it. No Badger, no SQLite, no gRPC, no Neo4j, no cgo.

torly 0.1.0
listening on http://127.0.0.1:3334
  • Relay: NIP-01 (EVENT, REQ, CLOSE, EOSE, OK, NOTICE,

CLOSED), NIP-09 deletion, NIP-11 relay information, NIP-40 expiration, NIP-42 authentication, NIP-45 COUNT, NIP-50 search, NIP-70 protected events, replaceable and addressable event semantics.

  • Social graph: materialized epg/peg/eeg/gee/ppg/gpp indexes and

the _graph filter extension. pp traverses kind-3 follows, pe the pubkey↔event edges (authored events and p-tag mentions), and ee the e-tag thread graph. Results are returned as relay-signed kind-39000 events.

  • Blossom: BUD-01 (GET/HEAD/DELETE), BUD-02 (upload), BUD-06 (upload

check) and BUD-12 (list by pubkey), with kind:24242 authorization.

  • Storage: segmented WAL plus immutable sorted index runs merged into

fixed-width index files. Crash recovery replays the log after the last checkpoint; NIP-09 tombstones survive restarts. Graph edges are stored in both directions, and a reply that arrives before its root is backfilled from the tag index when the root is saved.

  • Transport: a poll-based acceptor plus fixed reader/writer worker pairs.

HTTP/1.1 and RFC 6455 parsing live in the reader domain; only the writer writes and closes. TinyGo's net/http Hijack is unusable, so the WebSocket path is implemented directly over the socket.

  • Run model: a fixed tree of domains built on

tiny-spawn. Each domain owns its state and communicates only over channels bound at spawn; there is no shared mutable state between goroutines.

Out of scope, deliberately: Tor, WireGuard, ACME, SQL databases, Badger, Neo4j, gRPC, the payment/bridge/bunker subsystems, and the browser UI (which can be a TinyGo WASM application later; the relay already serves a static directory).

Build

TinyGo still needs the Go toolchain for its standard-library sources. On this machine both are provisioned locally:

. ./scripts/env.sh      # puts ~/.local/goroot/bin on PATH
make build              # -> ./torly, built by tinygo

Requirements: TinyGo 0.43+ (built with Go 1.27), Go 1.26+.

There are no external modules and no vendor directory. The implementations live in this repository: pkg/tinyspawn and pkg/tinyactor for the run model, pkg/crypto/btcec and pkg/crypto/decred/secp256k1 (copied ISC code) for BIP-340, and pkg/unix for the socket/poll calls the acceptor uses. go.mod has no require block; a clone builds offline. There is no cgo and no SQL/mmap dependency.

Run model

The relay is a fixed tree, spawned once at startup and cancelled by one breaker. No domain is created per connection or per job.

DomainOwns
root (main)configuration copy, channel wiring, signal handling, shutdown
acceptorthe listening fd, non-blocking poll, connection assignment
reader[0..N-1]read side + HTTP/WebSocket parsing for one connection at a time
writer[0..N-1]write side, framing, and the close of that connection
netconnections, subscriptions, authentication, pending queries
dbthe store engine, WAL, indexes, expiry ticker
verify[0..V-1]one BIP-340 check at a time, rotating from net
blossomblob index, blob files, BUD authorization

N is TORLY_CONN_WORKERS (default 64) and V is TORLY_VERIFY_WORKERS (default 2). A reader and its paired writer split the socket capability: only the reader reads, only the writer writes and closes. Messages are values whose slice/map ownership moves with the message; the sender never touches them again. Package variables are read-only constants or immutable configuration. On native Moxie the same tree becomes processes; on TinyGo it is a fixed set of cooperative goroutines.

Run

TORLY_DATA_DIR=./data TORLY_PORT=3334 ./torly serve

To load events from a JSONL export without starting the relay:

torly import --data-dir ./data --file events.jsonl

torly import writes the WAL and indexes in bulk and builds the social graph once at the end.

Blossom has offline backup and live restore commands:

torly blossom-backup  --data-dir ./data --out blossom.tar
torly blossom-restore --data-dir ./data --file blossom.tar

See docs/DEPLOYMENT.md for the torly.smesh.lol deployment: service layout, Caddy, the five-minute health monitor, and Blossom backup/restore.

Configuration is by environment variable. Every TORLY_* name also accepts an ORLY_* fallback, so an existing deployment only has to change its binary.

VariableDefaultMeaning
TORLY_APP_NAMEtorlyNIP-11 name
TORLY_DESCRIPTIONtorly - a TinyGo Nostr relayNIP-11 description
TORLY_CONTACT / TORLY_ICONemptyNIP-11 contact and icon
TORLY_LISTEN127.0.0.1bind address
TORLY_PORT3334bind port
TORLY_DATA_DIR~/.local/share/torlydatabase and blobs
TORLY_STATIC_DIRemptyoptional web root
TORLY_RELAY_URLderivedURL used by NIP-42 checks
TORLY_SECRET_KEYDATA_DIR/relay.keyrelay signing key, 64 hex chars
TORLY_AUTH_REQUIREDfalserequire NIP-42 AUTH to publish
TORLY_MAX_CONNECTIONS0cap on concurrent connections, 0 = worker count
TORLY_CONN_WORKERS64fixed reader/writer pairs
TORLY_VERIFY_WORKERS2signature verification domains
TORLY_BLOSSOM_ENABLEDtrueserve Blossom
TORLY_BLOSSOM_MAX_BLOB_MB50largest accepted blob
TORLY_MAX_EVENT_BYTES131072largest event JSON
TORLY_MAX_SUBS_PER_CONN64subscriptions per connection
TORLY_MAX_FILTERS16filters per REQ/COUNT
TORLY_MAX_LIMIT5000largest accepted filter limit
TORLY_QUERY_LIMIT5000cap for unbounded filters
TORLY_MAX_FUTURE_SECS900allowed created_at skew
TORLY_EXPIRY_INTERVAL60seconds between NIP-40 purges
TORLY_LOG_LEVELinfoerror, warn, info, debug

torly version prints the version, torly help prints the same table.

Layout

torly/
  cmd/torly/            entry point, signal handling, tree startup/shutdown
  pkg/config/           environment configuration and NIP-11 document
  pkg/nostr/            events, tags, copied JSON codec, filters, _graph types
  pkg/crypto/           copied BIP-340 implementation + torly's wrapper
  pkg/crypto/btcec/     copied btcec/decred schnorr and curve code
  pkg/crypto/decred/    copied decred secp256k1 field/scalar/group code
  pkg/tinyspawn/        copied tiny-spawn run model
  pkg/tinyactor/        copied tiny-actor lifecycle primitives
  pkg/unix/             the socket/poll calls the acceptor needs
  pkg/store/            WAL, sorted index runs, query planner, graph traversal, recovery
  pkg/domain/db/        database domain (sole owner of the store)
  pkg/domain/verify/    signature verification workers
  pkg/domain/net/       protocol state: connections, subs, auth, routing
  pkg/domain/io/        acceptor, reader/writer workers, HTTP/WS parsing
  pkg/domain/blob/      Blossom domain
  pkg/wire/             value messages shared between domains
  pkg/httpsrv/          HTTP request parser (transport-neutral)
  pkg/ws/               RFC 6455 reader, handshake and frame builder
  pkg/relay/            spawn-tree root and integration tests
  deploy/               systemd units, env file and Caddy site block
  scripts/torly-monitor.sh   health and NIP-11 check
  pkg/blossom/          blob store, index log, BUD endpoints
  pkg/sigtrap/          TinyGo prompt-signal workaround
  docs/GRAPH.md         `_graph` request/response and edge semantics
  docs/GRAPH-COMPATIBILITY.md  comparison with the original orly graph code
  docs/DEPLOYMENT.md    service layout, Caddy, monitor and Blossom backup
  scripts/e2e.sh        end-to-end check against the `nak` client

Data directory:

data/
  relay.key                 relay signing key (TORLY_SECRET_KEY overrides)
  db/
    wal/seg-000.dat         append-only event log
    idx/*.dat               merged sorted indexes
    idx/*.dat.buf.NNNNNN    immutable sorted runs (not yet merged)
    checkpoint.dat          last durable WAL position + counters
  blossom/
    blobs/<xx>/<sha256>     blob content
    index.log               ownership and content-type log

Tests

make test        # unit and integration tests with the Go toolchain
make test-tinygo # the same suite compiled and run by TinyGo
make e2e         # builds with TinyGo and drives it with a real client (nak)

106 test functions in 17 files. End-to-end coverage with -coverpkg=./... is 74.5%; the domain packages are exercised through the relay integration tests rather than direct unit tests.

PackageCoverageWhat is exercised
pkg/crypto/secp256k188.8%official BIP-340 vectors, signing round trips, tamper rejection
pkg/nostr80.4%canonical JSON vs Node, ids/signatures, parsing failures, tags, filters, _graph parsing
pkg/config88.2%defaults, ORLY_* fallback and TORLY_* precedence, bounds, booleans
pkg/store77.0%save/query planner, replacement, deletion, expiry, torn WAL tail, stale checkpoint, multi-run reopen, result caps, graph edges and traversal
pkg/httpsrv72.9%request parsing, bodies, duplicate headers, 100-continue, malformed requests, oversized bodies, hijack
pkg/ws46.2% (reader/frame code; integration covers the rest)handshake accept/reject, masked frames, fragmentation, ping/pong, close, UTF-8 and size limits, frame lengths
pkg/relay74.3%NIP-01/09/40/42/45/50/70 flows, duplicate and ephemeral events, limits, malformed messages, live delivery, concurrency, Blossom, static files, signed _graph responses
pkg/blossom72.9%BUD auth matrix, upload/check/download/list/delete, ownership, index reopen
pkg/domain/*, pkg/wireend-to-endexercised by the relay integration suite through real sockets
cmd/torly34.6%importer and reindex-graph unit tests; start/publish/restart/SIGTERM/SIGKILL covered end to end
pkg/logx, pkg/sigtrap—thin wrappers; the signal path is covered by make e2e

make e2e drives the TinyGo binary with nak: publish, query, count, search, replace, delete, upload/remove a blob, graceful restart, then SIGKILL and WAL recovery.

Deliberately not unit-tested: disk-full and permission faults, physical network loss, and features that are not implemented (NIP-77 sync, NIP-86 management, NIP-46 bunker, payments). Those would test the operating system, not torly.

Limits and notes

  • TLS/ACME is not implemented; terminate TLS in a reverse proxy.
  • Single process. The database domain is the only owner of the store and

services requests sequentially; verification runs in separate domains and the network domain never blocks on it. The connection limit is the fixed worker pool: a reader/writer pair serves one connection at a time.

  • The listener is a non-blocking IPv4 socket polled every 200 ms. TORLY_LISTEN

must be an IPv4 address (or empty/0.0.0.0).

  • Verification of 32-byte messages, which is every Nostr event, runs through

btcec's pure-Go 5×52 BIP-340 implementation: about 0.3 ms per event under TinyGo here, versus 6.4 ms for the math/big reference it replaced. The reference remains for BIP-340's variable-length message vectors and as the independent cross-check exercised by the tests.

  • COUNT with several filters sums their counts and marks the reply

approximate; there is no NIP-77 negentropy, NIP-29 groups, NIP-46 bunker, NIP-86 management API, payments or media proxy.

  • _graph accepts the current orly four-element array and its object form.

pp is kind-3 follows, pe is one hop by design, and ee seeds from the events authored by the given pubkey. A graph filter is answered once; any other filters in the same REQ are ignored. COUNT rejects _graph.

  • Prefix filters for ids/authors are not supported; filters must carry full

64-character hashes.

  • TinyGo's cooperative scheduler does not drain the signal mask while a socket

is parked in netpoll. pkg/sigtrap calls the runtime's checkSignals from a 200 ms ticker so SIGTERM shuts the relay down promptly.

  • Domain messages are Go values; Go cannot enforce the copy/ownership rule the

way Moxie's codec does. The boundaries are documented in pkg/wire, and the tests run under -race.

License

AGPL-3.0-or-later. See LICENSE.

files