nostrino

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

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.

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.

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; 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.

Connection string

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.

Packages

PackagePurpose
pkg/neutrinoheader/filter sync, verification, store interface, matching
pkg/p2pBitcoin version handshake and frame loop
pkg/btcwireBitcoin P2P wire codec and BIP157 messages
pkg/filterBIP158 Golomb-coded filters (SipHash-2-4)
pkg/chaindatathe wallet-facing channel: binary frames, ids, cancel, close
pkg/chainsourcea light client answering on that channel
pkg/tunnelreliable encrypted stream over relay events; URI and keys
pkg/relayNIP-01 client over a transport
pkg/ws / pkg/jswsnative and browser WebSocket transports and streams
pkg/wsservergateway WebSocket upgrade and framing
pkg/bridgegateway: relay stream <-> node P2P, whitelist, limits, presence
pkg/chainchain profiles (magic, genesis, difficulty, checkpoint)
pkg/registrygateway announcements and discovery (kind 30333)
pkg/idbstoreIndexedDB persistence for the wasm store
pkg/nostr, pkg/nip44, pkg/chacha20, pkg/cryptoevents, encryption, secp256k1
internal/testnodedeterministic BIP157 regtest peer
internal/memrelayin-process relay for tests

Build

# 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 (AGPL-3.0). The btcec and Decred secp256k1 trees carry their original ISC licenses. See NOTICE.

License

AGPL-3.0. See LICENSE.

files