README.md raw

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:

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;

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;

in the URI and survives losing one; the gateway enforces per-stream byte rates and per-client session limits;

nostrino-discover lists gateways on a relay;

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

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.

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.