# nostrino 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) | +----------------------------------------------------+ ``` ## Status Working and tested: - the light client builds with `tinygo -target wasm`, and syncs headers, filter headers and compact filters through a real relay and bridge; - the tunnel is NIP-44 v2 encrypted, sequenced, acknowledged, credit-flow controlled, retransmitting, resettable and cancellable; a lost relay event is recovered; - the client verifies proof of work, header linkage, the filter-header chain, each filter hash against its committed filter header, and matches wallet scripts locally; - the bridge has a P2P command whitelist and no wallet surface; - the client reconnects its relay subscription; the bridge keeps relays alive with WebSocket pings and reconnects with backoff; - the browser store is IndexedDB: a reload resumes at the last verified filter tip without re-downloading, validated across two wasm instances; - a genesis or checkpoint anchor (`-profile`, `-checkpoint-*`) lets a first sync 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; - NIP-42 AUTH is answered automatically; the client publishes on every relay in the URI and survives losing one; the gateway enforces per-stream byte rates and per-client session limits; - the gateway publishes a discoverable announcement (kind `30333`) and `nostrino-discover` lists gateways on a relay; - a WebSocket data plane (`bridge=ws://...`) carries the raw P2P stream with TLS 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`. ## Quick start Requirements: Go 1.25+, TinyGo 0.43+, Node 21+ (or Node 20 with `--experimental-websocket`), and [torly](../torly). ```sh 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: ```sh (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. ## The two parts **`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](../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](../torly), a TinyGo relay. ## Connection string The gateway is named by its **public key**, not by a shared secret: ``` nostr+chaindata://?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`. ## Packages | 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 | ## Build ```sh # 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. ## Security model - The client signs every event; the gateway signs only its own transport events and never has wallet key material. - NIP-44 v2 encrypts every frame; the relay sees ciphertext, addressees and traffic shape. - The client verifies the header chain and the filter-header chain. A gateway can withhold data; it cannot forge it. - Filters are matched locally. The gateway learns the requested heights and hashes, not the wallet's script set. - The bridge forwards P2P only, and only whitelisted commands. There is no `listunspent`, no signing, no address query. - The browser is the custody surface. The seed and the wallet are a later layer; this repository stops at the chain-data transport. ## Attribution The secp256k1/BIP-340 code, the Nostr event codec and the license text are derived from [torly](https://git.smesh.lol/torly) (AGPL-3.0). The btcec and Decred secp256k1 trees carry their original ISC licenses. See `NOTICE`. ## License AGPL-3.0. See `LICENSE`.