# design notes ## why this shape 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. ## packages `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: 1. Seed genesis from the chain profile (hash checked). 2. `getheaders` from a block locator (ten single steps, then doubling) until the peer's advertised height or an empty response. Each header must link to the previous and satisfy its target. 3. `getcfheaders` from the next height. The response's previous filter header must 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. 4. `getcfilters` in ranges. Every filter's block hash and `SHA256d(filter)` are verified before storing. 5. Matching is a local scan of stored filters over the supplied scripts. `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: ```js 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. ## crypto 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 and signing 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. ## wasm constraints 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://`. ## limits of the verification 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. ## test strategy - Codec, filter and crypto packages have table-driven tests, including the official BIP158 and NIP-44 vectors and the BIP-340 suite. - `pkg/tunnel` has an in-process relay that can drop every Nth delivery, which exercises 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. ## compatibility with NEXT_PHASE - The URI scheme and event kinds match the proposal (`nostr+chaindata`, `23196/23197`). - The proposal's WebSocket `bridge=` data plane is implemented as a raw P2P stream over TLS; the relay stream remains the encrypted control/fallback and is still the default when no bridge is advertised. - The proposal's chunked request/response RPC is not implemented; the stream carries the P2P bytes directly. The bridge's whitelist is the equivalent policy boundary. - The swap record belongs to the gateway half, outside the tunnel; it is not 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.