The browser cannot open a Bitcoin P2P socket, and a home gateway usually has
no inbound port. The two ends meet at a public relay. The relay is the control
and data plane; the WebSocket "bridge" from docs/NEXT_PHASE.md can be added
later as an alternative transport without changing anything above net.Conn.
The client is a P2P peer. The gateway is a byte-stream proxy. Keeping the Bitcoin handshake end to end means the gateway does not need to emulate a peer, and the client's verification does not depend on the gateway's honesty.
pkg/btcwire is a pure codec: framing, compact sizes, the version and
handshake messages, headers, the BIP157 messages, inventory and raw
transactions. It has no network imports and builds for wasm.
pkg/p2p is the peer: version/verack, frame I/O, automatic pong, and a
deadline-bounded read. It works over net.Conn, which is either a TCP socket or
a tunnel stream.
pkg/filter is BIP158 basic filters: SipHash-2-4, Golomb-Rice coding, build
and match, validated against the ten official bip-0158/testnet-19.json
vectors in pkg/filter/testdata.
pkg/neutrino is the client loop:
getheaders from a block locator (ten single steps, then doubling) untilthe peer's advertised height or an empty response. Each header must link to the previous and satisfy its target.
getcfheaders from the next height. The response's previous filter headermust match the stored chain; each hash is folded into a new filter header; the response's stop hash must be the last block's hash. Both the filter hash and the filter header are stored, because the filter is later checked against the hash the header commits to.
getcfilters in ranges. Every filter's block hash and SHA256d(filter) areverified before storing.
pkg/neutrino also defines Store. An Anchor is the trusted starting point:
the block hash at a height and, for a checkpoint, the filter header at that
height. A genesis anchor leaves the filter height at -1 because filter zero's
hash is not known until it is downloaded. Headers and filter headers are
contiguous from the anchor; filters may begin later, which is what a swap
wallet wants.
Memory is for tests. FileStore is three append-only files
(headers.bin, cfheaders.bin, filters.bin) plus anchor.bin, with a
height in every record, truncation of a partial tail on open, and a Sync
after each batch. A reorg calls Truncate, which rewrites the three files from
the in-memory state; reorgs are rare enough that a rewrite is simpler and less
error-prone than an append-only undo log.
pkg/idbstore is the browser store. It opens the database, loads the anchor,
headers, filter headers and filters (optionally from a birth height) into a
Memory, and writes every change behind the sync loop in batched
transactions. Flush waits for the queue; the wasm command flushes before it
reports a sync done and again when a session ends. A crash between batches
loses at most one batch, and the next sync resumes from the last committed
filter tip.
pkg/tunnel is the transport. session owns two sequence spaces, credit-based
flow control, retransmission, the read queue, heartbeats, close and reset. The
client also owns a relay subscription; the gateway listener routes decrypted
frames from one shared subscription into per-session inboxes. The listener
reconnects and resubscribes with backoff, and the WebSocket transport pings
idle connections.
The receive side advertises free frames in every ACK. The send side blocks on
credit, so a frozen worker stops the gateway's node-to-tunnel pump and the
node's own TCP window takes over. reset aborts a stream; close ends it in
order. A client switching gateway or abandoning a range uses reset.
pkg/bridge accepts either a tunnel stream or a WebSocket stream. Both end at
the same handleConn: check the chain name, dial the node's P2P port, and copy
frames in both directions after checking the command against the whitelist and
re-encoding the frame. It never interprets a payload
and never performs a handshake of its own. A token bucket limits each
direction's byte rate, the tunnel bounds sessions per client, and a presence
loop publishes the addressable gateway announcement with the node tip from a
periodic handshake.
The WebSocket path is pkg/wsserver on the gateway and pkg/ws/pkg/jsws
StreamConn on the client. It is a raw P2P byte stream, not the tunnel
envelope: TLS replaces NIP-44 on that path, and the same bridge policy applies.
The client's dialer tries bridge URLs in order and then the relay tunnel, so
a blocked WebSocket degrades to the encrypted relay path.
cmd/nostrino-wasm exposes the client to a page:
nostrino.sync(uri, chain) // starts a background sync
nostrino.status() // JSON: running, status, error, tip, tipHash, added, filters, bytes, elapsed, matches
nostrino.match(scriptHex) // JSON array of matching heights
The current wasm store is memory, retained for the life of the module. The page
can sync incrementally while it stays loaded; a reload rescans. IndexedDB is
the next layer and does not change the sync code: the Store interface is the
seam.
The intended host is a dedicated Web Worker, not a service worker. A service
worker is evicted aggressively and is not an always-on daemon. A dedicated
worker lives with the document, may be paused, and is pull-based, so a pause
only means the next request is later. The web demo in web/ shows the shape:
page -> worker -> wasm -> nostrino.sync, with cancel mapped to the reset
path.
Settlement deadlines are explicitly outside this layer. The tunnel reports heights and lets the wallet recompute a deadline from height and wall clock on resume; it never owns a timer whose non-firing could lose money.
Everything is in-tree, because the target is TinyGo and the repository has no external modules:
pkg/crypto/secp256k1 is BIP-340 signing and verification. Verification andsigning use the btcec code; the math/big implementation covers arbitrary message lengths and is the reference in tests.
pkg/nip44 is NIP-44 v2 with the official vectors.pkg/chacha20 is RFC 8439 ChaCha20 with the RFC test vector.pkg/nostr is the NIP-01 event codec, canonical serialization and filters,derived from torly.
The wasm target has no net and no os file system. The client therefore
takes a Dial function, and the browser build supplies one backed by
pkg/jsws, which wraps the page's WebSocket through syscall/js. The
gateway build supplies TCP. The store is an interface for the same reason.
TinyGo's wasm target uses GOOS=js; pkg/jsws is behind //go:build js && wasm
and native builds use pkg/ws, which adds TLS for wss://.
Headers are checked for linkage, proof of work, a future timestamp bound,
median-time-past and the expected difficulty. The retarget follows the 2016-
block (configurable) Bitcoin rule and the testnet minimum-difficulty exception;
regtest is fixed. A header chain that forks below the tip is resolved by
walking back to the fork point (bounded), calling Store.Truncate and
re-downloading the new branch. The first retarget boundary after a checkpoint
is not verifiable because the window's first block is not stored; that one
boundary is accepted from the peer and every later one is checked.
A mainnet first sync still needs a trusted checkpoint, because a sync from
genesis downloads the whole header and filter-header history. Checkpoints are
data: chain.Checkpoint and the profile JSON carry height, block hash and
filter header, and -checkpoint-* flags override them. The repository does not
ship a mainnet checkpoint value; an operator must obtain one from a node they
trust.
The XBT profile is intentionally blank in the binary. Guessing a fork's magic,
genesis or proof-of-work limit would silently point a client at the wrong
chain. -profile supplies the full JSON, including the difficulty schedule and
an optional checkpoint.
official BIP158 and NIP-44 vectors and the BIP-340 suite.
pkg/tunnel has an in-process relay that can drop every Nth delivery, whichexercises the NACK/retransmit path without a network.
internal/e2e runs test node -> bridge -> in-process relay -> tunnel ->neutrino and checks every filter, local matching, and file-store resume.
scripts/wasm-smoke.sh repeats the whole stack with the real relay (torly)and the real wasm bundle in Node, which is the acceptance test for the browser target.
nostr+chaindata, 23196/23197).
bridge= data plane is implemented as a raw P2Pstream over TLS; the relay stream remains the encrypted control/fallback and is still the default when no bridge is advertised.
carries the P2P bytes directly. The bridge's whitelist is the equivalent policy boundary.
chain data and must not enter the BIP157 path. Presence/registry events and the wallet layer belong to the next phases and are not in this repository.