# torly A Nostr relay compiled by **TinyGo**, ported clean-room from the relay semantics of [orly](../orly) onto the storage model used by [morly](~/moxie/git.smesh.lol/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](../../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: ```sh . ./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. | Domain | Owns | |---|---| | root (`main`) | configuration copy, channel wiring, signal handling, shutdown | | acceptor | the 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 | | net | connections, subscriptions, authentication, pending queries | | db | the store engine, WAL, indexes, expiry ticker | | verify[0..V-1] | one BIP-340 check at a time, rotating from `net` | | blossom | blob 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 ```sh TORLY_DATA_DIR=./data TORLY_PORT=3334 ./torly serve ``` To load events from a JSONL export without starting the relay: ```sh 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: ```sh torly blossom-backup --data-dir ./data --out blossom.tar torly blossom-restore --data-dir ./data --file blossom.tar ``` See [docs/DEPLOYMENT.md](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. | Variable | Default | Meaning | |---|---|---| | `TORLY_APP_NAME` | `torly` | NIP-11 name | | `TORLY_DESCRIPTION` | `torly - a TinyGo Nostr relay` | NIP-11 description | | `TORLY_CONTACT` / `TORLY_ICON` | empty | NIP-11 contact and icon | | `TORLY_LISTEN` | `127.0.0.1` | bind address | | `TORLY_PORT` | `3334` | bind port | | `TORLY_DATA_DIR` | `~/.local/share/torly` | database and blobs | | `TORLY_STATIC_DIR` | empty | optional web root | | `TORLY_RELAY_URL` | derived | URL used by NIP-42 checks | | `TORLY_SECRET_KEY` | `DATA_DIR/relay.key` | relay signing key, 64 hex chars | | `TORLY_AUTH_REQUIRED` | `false` | require NIP-42 AUTH to publish | | `TORLY_MAX_CONNECTIONS` | `0` | cap on concurrent connections, 0 = worker count | | `TORLY_CONN_WORKERS` | `64` | fixed reader/writer pairs | | `TORLY_VERIFY_WORKERS` | `2` | signature verification domains | | `TORLY_BLOSSOM_ENABLED` | `true` | serve Blossom | | `TORLY_BLOSSOM_MAX_BLOB_MB` | `50` | largest accepted blob | | `TORLY_MAX_EVENT_BYTES` | `131072` | largest event JSON | | `TORLY_MAX_SUBS_PER_CONN` | `64` | subscriptions per connection | | `TORLY_MAX_FILTERS` | `16` | filters per `REQ`/`COUNT` | | `TORLY_MAX_LIMIT` | `5000` | largest accepted filter limit | | `TORLY_QUERY_LIMIT` | `5000` | cap for unbounded filters | | `TORLY_MAX_FUTURE_SECS` | `900` | allowed `created_at` skew | | `TORLY_EXPIRY_INTERVAL` | `60` | seconds between NIP-40 purges | | `TORLY_LOG_LEVEL` | `info` | `error`, `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// blob content index.log ownership and content-type log ``` ## Tests ```sh 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. | Package | Coverage | What is exercised | |---|---|---| | `pkg/crypto/secp256k1` | 88.8% | official BIP-340 vectors, signing round trips, tamper rejection | | `pkg/nostr` | 80.4% | canonical JSON vs Node, ids/signatures, parsing failures, tags, filters, `_graph` parsing | | `pkg/config` | 88.2% | defaults, `ORLY_*` fallback and `TORLY_*` precedence, bounds, booleans | | `pkg/store` | 77.0% | save/query planner, replacement, deletion, expiry, torn WAL tail, stale checkpoint, multi-run reopen, result caps, graph edges and traversal | | `pkg/httpsrv` | 72.9% | request parsing, bodies, duplicate headers, `100-continue`, malformed requests, oversized bodies, hijack | | `pkg/ws` | 46.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/relay` | 74.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/blossom` | 72.9% | BUD auth matrix, upload/check/download/list/delete, ownership, index reopen | | `pkg/domain/*`, `pkg/wire` | end-to-end | exercised by the relay integration suite through real sockets | | `cmd/torly` | 34.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`.