Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

Alternative runtime + distribution: scriptc-compiled single-binary relay (POC, load-test data, optimistic verification)

未关闭
#902 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
25/100
Issue 类型
功能
描述清晰度
基本清楚
活跃度
活跃
技术栈
postgresql, redis, sqlite, typescript

调研方向

Start by reading the linked issue #147 and the fork PRs, especially #1 on the storage/cache seam and #6 on optimistic verification; the POC details and measurements are on the fork, not identified as upstream code. The issue asks maintainers to choose among several separate proposals, so first clarify which, if any, has an agreed scope. Done would mean a narrowly defined upstream change or documentation task with acceptance criteria; none is specified yet.

由索引模型根据 Issue 内容生成。

描述

Title: Alternative runtime + distribution: scriptc-compiled single-binary relay (POC, load-test data, optimistic verification)


Following up on #147 (alternative storage adapters): we explored the adjacent axis, alternative runtime + distribution, and have a working POC on our fork with measured data before proposing anything for the tree. The short version: the relay's protocol core compiles with vercel-labs/scriptc into a single ~5 MB static Linux binary with no Node.js runtime and no embedded JS engine, and a deferred (optimistic) signature-verification mode built on top of it reaches 3,908 events/s sustained on the bench box, 4.6x the stock relay in the same test, while using 9.4 MB of RSS instead of 2.3 GB.

What we built (POC, on our fork)

The work is a chain of focused PRs on espetro/nostream, each independently reviewable:

  • #1 answers #147's storage-adapter ask directly: a thin StorageDialect / CacheDriver seam (~400 LOC in-tree) with DB_ADAPTER / CACHE_ADAPTER module loading, so backend specifics can live in community-maintained packages. It ships a self-contained SQLite adapter (DB_CLIENT=sqlite + CACHE_DRIVER=sqlite, better-sqlite3, FTS5 for NIP-50, Redis-API-compatible cache shim including the broadcast stream and the two Lua rate-limit scripts) that runs the whole stock relay on one SQLite file with zero daemons. The Postgres path is verbatim upstream code.
  • #2 is the feasibility probe: nostream-validate, a NIP-01 event validator / filter matcher (vendored BIP-340 + SHA-256, no @noble) compiled to a ~460 KB static ELF, scriptc coverage 306/306, no dynamic remainder.
  • #3 is the actual relay: NIP-01 (EVENT/REQ/CLOSE/EOSE, replaceable + ephemeral events, NIP-33 upserts), filter matching, SQLite storage, WebSocket transport, in-process fanout, authored in the scriptc TS subset on @chttp/router + @chttp/ws + @cutils/sqlite, compiling to a 5.0 MB static ELF at 100% static coverage (2839/2839 statements).
  • #4 ports the HTTP surface (healthz, stats, NIP-11 document) onto the already-vendored @chttp/router inside the same binary, replacing express for this deployment shape.
  • #6 adds optimistic signature verification (its own section below), and #7 adds an optional sampled variant with a documented trust tradeoff, kept separate so the safe mode can be evaluated on its own.

Things deliberately left out of the compiled surface (knex migrations as operator tooling, pino, zod, worker_threads) are listed with reasons in native/relay/README.md on the fork.

Why scriptc

The stack stays TypeScript: scriptc compiles an ordinary TS subset to a small native executable, so the protocol code above is readable, type-checked TS, not a C rewrite. That mattered for feasibility: the whole NIP-01 core ported in days, not weeks, and scriptc coverage gives a checkable gate that a unit has no dynamic remainder (no hidden quickjs fallback), which is what makes the "single static binary" claim verifiable rather than aspirational. scriptc is developed under vercel-labs (Vercel's GitHub org), so the toolchain risk looks bounded. The dependencies it pushes you toward have familiar APIs: @chttp/router is a minimal HTTP router, @chttp/ws a WebSocket layer over it, @cutils/sqlite an embedded SQLite with a vendored amalgamation; all three are authored in the same compilable subset, which is why the binary stays static. Full disclosure: I am the maintainer of @chttp and @cutils, so weight that recommendation accordingly; the relay vendors their sources rather than taking them as runtime npm dependencies, so they are swappable. The subset has real limits (we hit and worked around a few: intersection types with no runtime shape, template-literal index signatures, strictNullChecks being required, so the fork scopes it to native/), and the README documents them honestly.

Load-test results (same box, 60k signed-event corpus, 4 sockets, 30s sustained)

config sustained ingest EVENT OK p50 cold start peak RSS artifact
stock relay, node/ts-node, sqlite mode (baseline) 858 ev/s 146 ms 9.7 s 2364 MB sources + node + node_modules
native ELF, sync verification 534 ev/s 236 ms 421 ms 9.3 MB 5.0 MB static ELF
native ELF, optimistic verification 3,908 ev/s 10.3 ms ~416 ms 9.4 MB 5.1 MB static ELF

Honest read: the native binary in sync mode is ~62% of stock throughput because it is a single OS thread doing signature verify + JSON parse + SQLite writes inline, while the stock relay fans storage out to worker threads and benefits from more warm JIT. In exchange you get one 5 MB deployable file, 254x less resident memory, and ~23x faster cold start. For the sub-1 GB VPS / embedded-operator segment that cannot run Postgres + Redis + Node (or does not want three processes to babysit), that trade looks favorable; for large public relays, stock is the right choice. REQ latencies are comparable between native and stock (p50 9.5 vs 7 ms for author queries, 29 vs 17 ms for single-char tag queries).

The SQLite adapter from #1 was benchmarked separately against Postgres + Redis on the same host: identical relay code, and the daemon-free mode lands at full query parity (ingest 644 vs 668 ev/s, REQ percentiles within noise or better) with ~2.7x less disk for the same 3k-event corpus. The bench also caught a real regression there: correlated tag-filter EXISTS probes that could not resolve in-index until the adapter gained a covering event_tags(event_id, tag_name, tag_value) index.

The bottleneck, and optimistic verification

Profiling the native relay's ingest path showed the obvious-in-retrospect thing: BIP-340 signature verification is ~94% of it. The same binary with verification skipped (bench aid only) sustains 9,081 ev/s, versus 534 verified. So the ceiling is not transport, JSON, or SQLite; it is schnorr, and it is paid inline before the store and before the OK frame in upstream semantics.

That motivated an optimistic verification mode (RELAY_VERIFY_MODE=optimistic, #6), and we think the idea is interesting independently of the native binary: it converts per-event latency into throughput by pipelining verification behind the ACK, the same way Postgres pipelines fsync behind synchronous_commit = off while never letting uncommitted data be read. The mechanics: an EVENT is stored with verified=0 and ACKed immediately; a bounded queue (4096, overflow degrades to inline sync verification, so backpressure is bounded) drains a few items after each inbound frame; REQ and fanout only ever touch verified=1 rows, so nothing invalid is ever served or broadcast; a failed check tombstones the event (verified=-1), NOTICEs the sender, and quarantines the pubkey, retracting its pending rows and forcing its future events to verify inline. Every event is still verified; adversarially the servable set is identical to sync mode, only OK ordering differs (an invalid event gets an OK first and a retraction after). On the bench that is 3,908 ev/s sustained with OK p50 10.3 ms, 7.3x the same binary in sync mode and 4.6x the stock baseline, at unchanged RSS and cold start. There is also a sampled mode (#7) that only verifies a percentage of traffic and reaches 7,536 ev/s at 25%, but it deliberately weakens the signature guarantee for unsampled events, so we stacked it separately and would only document it as an explicit tradeoff, never a default.

Worth stating plainly: optimistic verification does not make the single-threaded binary faster at verifying; it hides verification latency behind the ACK so ingest stops waiting on it. The Node relay could adopt the same pattern (its per-event cost profile is different, but the ACK-before-verify ordering and the verified=1 servability gate compose with worker-thread verification the same way).

Why this fits #147's frame, not a big diff

None of this asks the project to maintain a second runtime. The upstreamable surface is small: the storage/cache seam from #1 (which also serves the CockroachDB/MySQL/serverless interest already voiced in #147), and optionally the protocol-core carve-out the native work proved out. The scriptc relay itself lives entirely in a native/ subtree plus community packages and can stay a documented community distribution.

Questions for maintainers

  1. Would you accept the thin StorageDialect / CacheDriver seam (with DB_ADAPTER / CACHE_ADAPTER module loading) upstream, keeping SQLite and other backends community-maintained out-of-tree? That is roughly the smallest diff that unlocks alternative storage for #147.
  2. Is there interest in documenting a "community distributions" page, the way some projects list install variants, so a native-binary build can exist alongside docker/source without living in-tree?
  3. Is there appetite for optimistic verification (ACK-before-verify with a verified-only servability gate) as an upstream relay feature, independent of the native variant? We would propose it as its own issue + PR with the tradeoffs above.

Everything is public on the fork, and both benchmarks are reproducible from a clean checkout: native/relay/bench/run.sh (native vs stock, cold start + RSS + sustained ingest + percentiles) and benchmark/run.sh (Postgres+Redis vs SQLite adapter). Happy to PR whichever granularity is useful, or none of it, the data is the point.


(POC and measurements via Devin sessions, posted by @espetro.)

主要语言
TypeScript
星标
829
派生
234
平均合并
18 小时 43 分钟
30 天内合并 PR
67

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

cameri/nostream 的其他 Issue

查看 cameri/nostream 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。