git clone https://git.smesh.lol/nostrino.git
A compact-filter (BIP157/BIP158) Bitcoin light client and its relay bridge, with no TCP socket in the client. The client is built for js/wasm; the bridge runs on the gateway next to a chain node and connects the two over a public Nostr relay.
This is the M1 spike of docs/NEXT_PHASE.md in the swap project: prove that
the Neutrino subset can be a TinyGo wasm module, that the chain-data transport
can be a relay tunnel instead of a TCP socket, and that the client still
verifies everything it is given.
browser (js/wasm)
+----------------------------------------------------+
| pkg/neutrino -- P2P frames -- pkg/tunnel |
| | | |
| +-- headers/filters | NIP-44 |
| +-- local script matching | events |
+---------------------------------------|------------+
|
public relay (torly)
|
+---------------------------------------|------------+
| pkg/tunnel --> pkg/bridge --> TCP P2P |
| (bitcoind/knots) |
| gateway (native TinyGo) |
+----------------------------------------------------+
Working and tested:
tinygo -target wasm, and syncs headers,filter headers and compact filters through a real relay and bridge;
controlled, retransmitting, resettable and cancellable; a lost relay event is recovered;
each filter hash against its committed filter header, and matches wallet scripts locally;
with WebSocket pings and reconnects with backoff;
tip without re-downloading, validated across two wasm instances;
-profile, -checkpoint-*) lets a firstsync start near the tip; headers are checked for median-time-past, difficulty/retarget and linkage, and a reorg rolls back and re-downloads the new branch;
in the URI and survives losing one; the gateway enforces per-stream byte rates and per-client session limits;
30333) and nostrino-discover lists gateways on a relay;
bridge=ws://...) carries the raw P2P stream withTLS protection, the same command whitelist and limits, and the relay tunnel is the fallback when the WebSocket is unavailable.
It is meant to run in a dedicated Web Worker. make web assembles the page,
the worker and the wasm bundle in bin/web; serve that directory and open it
to drive a sync from a browser. The worker is pull-based, so browser pauses
are harmless; a reset closes the gateway's node socket immediately, and a half-delivered filter range
is re-requested from the last verified filter tip.
Not in this cut: pinned mainnet checkpoint values, the real XBT parameters,
built-in TLS termination for the WebSocket bridge (use a reverse proxy for
wss://), and the swap/wallet layer. The browser IndexedDB store loads its
indexes into memory on open; a full mainnet filter set needs a streaming
store, and that is the next storage step. See docs/STATUS.md.
Requirements: Go 1.25+, TinyGo 0.43+, Node 21+ (or Node 20 with
--experimental-websocket), and torly.
make build # native gateway commands
make wasm # browser bundle: bin/nostrino.wasm
make web # assemble bin/web (worker demo + wasm_exec.js)
./scripts/wasm-smoke.sh
For persistence, install fake-indexeddb once and run:
(cd /tmp && npm install fake-indexeddb)
NODE_PATH=/tmp/node_modules ./scripts/idb-smoke.sh
bin/nostrino-discover -relay wss://relay.example -chain btc lists gateways
announced on a relay.
The smoke script starts torly, a deterministic regtest test node, the bridge and the wasm client, then checks that the client synced 51 filters and matched the script embedded in block 7. It is the end-to-end acceptance test.
`pkg/neutrino` is the light client. It speaks BIP157 over any net.Conn,
verifies the header and filter chains, stores filters, and matches scripts
locally. cmd/nostrino-wasm exposes it to a page as nostrino.sync,
nostrino.status and nostrino.match. On native targets it runs over TCP for
tests and tooling.
`pkg/chaindata` is the seam between the client and a wallet: binary frames over a tiny-spawn duplex, with request ids, cancellation, a close that fails what is in flight, and a filter read that is one buffer instead of a list of messages. `pkg/chainsource` puts a light client behind it. The wallet that speaks it lives in ../tinywallet; neither side compiles the other, and neither needs to know what is underneath the channel.
`pkg/bridge` is the gateway. It listens on the relay under a persistent public key, accepts encrypted streams, dials the chain node's P2P port and forwards only the messages a compact-filter client needs. It holds no keys that can spend, signs nothing and exposes no wallet RPC.
The relay is any NIP-01 relay; the stack is developed against torly, a TinyGo relay.
The gateway is named by its public key, not by a shared secret:
nostr+chaindata://<gateway-pubkey>?relay=wss://relay.example&chains=btc&v=1
The client generates an ephemeral keypair per session. The relay sees signed
events with opaque NIP-44 content and the addressing tags; the gateway sees
the P2P bytes. The full protocol is in docs/PROTOCOL.md.
| Package | Purpose |
|---|---|
pkg/neutrino | header/filter sync, verification, store interface, matching |
pkg/p2p | Bitcoin version handshake and frame loop |
pkg/btcwire | Bitcoin P2P wire codec and BIP157 messages |
pkg/filter | BIP158 Golomb-coded filters (SipHash-2-4) |
pkg/chaindata | the wallet-facing channel: binary frames, ids, cancel, close |
pkg/chainsource | a light client answering on that channel |
pkg/tunnel | reliable encrypted stream over relay events; URI and keys |
pkg/relay | NIP-01 client over a transport |
pkg/ws / pkg/jsws | native and browser WebSocket transports and streams |
pkg/wsserver | gateway WebSocket upgrade and framing |
pkg/bridge | gateway: relay stream <-> node P2P, whitelist, limits, presence |
pkg/chain | chain profiles (magic, genesis, difficulty, checkpoint) |
pkg/registry | gateway announcements and discovery (kind 30333) |
pkg/idbstore | IndexedDB persistence for the wasm store |
pkg/nostr, pkg/nip44, pkg/chacha20, pkg/crypto | events, encryption, secp256k1 |
internal/testnode | deterministic BIP157 regtest peer |
internal/memrelay | in-process relay for tests |
# browser light client (the primary target)
tinygo build -target wasm -o bin/nostrino.wasm ./cmd/nostrino-wasm
# gateway, TinyGo
tinygo build -o bin/nostrino-bridge ./cmd/nostrino-bridge
# native development builds and tests
go build ./cmd/...
go test ./...
TinyGo needs a Go toolchain for its standard-library sources. If your GOROOT
is not the one TinyGo uses, run export GOROOT=$HOME/.local/goroot first; the
Makefile does this when it finds that install.
events and never has wallet key material.
traffic shape.
can withhold data; it cannot forge it.
hashes, not the wallet's script set.
listunspent, no signing, no address query.
layer; this repository stops at the chain-data transport.
The secp256k1/BIP-340 code, the Nostr event codec and the license text are
derived from torly (AGPL-3.0). The btcec and
Decred secp256k1 trees carry their original ISC licenses. See NOTICE.
AGPL-3.0. See LICENSE.