The Read Side: How a Private Mobile Bitcoin Wallet Learns What’s Its Own
Winnow downloads public block filters and checks them against your wallet on the phone. It fetches matching blocks to find payments without sending peers its watch list.
Numbers marked “approx.” are estimates, not measurements. Historical signet timings remain in screenshots/timings.json; the current journey does not update them.
1. The problem
A Bitcoin wallet is, at its core, a holder of private keys plus the ability to answer four questions about the chain:
- “What’s mine?” — which transaction outputs are spendable by my keys. From this follows everything the wallet displays (balance, history) and everything it needs to sign (the UTXOs, their amounts, and their scriptPubKeys — required inputs to the BIP341 signature hash).
- “Where’s the tip?” — the current block height, so “confirmed” can mean something.
- “What fee should I pay?” — fee-market information.
- “Did my transaction get out, and did it get mined?” — broadcast and confirmation tracking.
Question 1 is the entire difficulty, and the reason is structural: the Bitcoin P2P protocol has no address index. Full nodes validate every transaction but do not maintain a queryable mapping from addresses or scripts to transactions. There is no getbalance(address) message on the wire. So for a light client, someone has to scan the blockchain on the wallet’s behalf. The design question of this paper is: who scans, what do they learn, and what does it cost?
Every realistic answer falls into one of two families:
- A server scans for you. You hand the server your addresses (or extended public keys); it queries its index and returns your history. Fast and cheap — and the server learns, and can log, every address you care about, linkable to your IP address.
- Your phone scans for itself. The phone downloads a compact per-block summary (a filter), checks it locally against its own scripts, and downloads a full block only when a filter indicates the block contains its transactions. Nobody learns anything — but the phone does the work and the bandwidth.
This paper walks through every realistic mechanism in both families, with honest cost/privacy/trust accounting, then walks the actual use cases of this product and concludes which mechanism serves each one — and why the answer, for this product, is client-side scanning by default, with a server only ever as an explicit, warned, user-initiated opt-in.
Scope note: Winnow is a fresh-wallet product. Wallets are created new in the app; a new wallet has no history, so scanning runs forward from the moment of creation. Existing wallets may be imported only with their history included — the user supplies an export bundle (descriptor/keys + known transactions and UTXOs + a last-known height) from their previous wallet software, and the app verifies and updates that history by scanning filters forward from the bundle’s height. There is no historical back-scan machinery at all: catch-up cost is proportional to how stale the bundle is, and a bundle exported at the tip costs nothing. This constraint — chosen deliberately — is what makes the pure-P2P answer not merely acceptable but cheap. The bundle format, the verification algorithm, and what a lying file can still do are specified in import, not here.
2. The candidate mechanisms
Other ways to find payments
2.1 Full node on the phone
Run Bitcoin Core (or equivalent) on the device: download and validate every block.
- Cost: the full chain is several hundred GB and growing (exact current size depends on date and pruning; even pruned, initial block download must process the entire history — days of CPU and hundreds of GB of transfer). Ruled out for a phone by bandwidth, storage, battery, and App Store reality. Not discussed further.
2.2 Central indexer (“esplora”-family APIs)
Public servers (e.g. mempool.space, blockstream.info) run a full node plus an address index, and expose REST endpoints: GET /address/{addr}/utxo, GET /address/{addr}/txs, GET /fee-estimates, POST /tx.
- Cost to user: kilobytes per query; instant answers; mempool-stage visibility (unconfirmed transactions appear immediately).
- What leaks: everything that matters. The operator sees every address you query, the set of your addresses (wallet fingerprint), your balance, your counterparties, and your IP address linking them together. “We don’t keep logs” is a policy, not a mechanism — the query stream itself is the sensitive data, and it exists the moment you ask.
- Trust: you must trust the server’s answers about your history. (Partial mitigation exists: when spending, the BIP341 sighash commits to input amounts, so a server lying about a UTXO’s amount produces an invalid transaction rather than a theft — but a server can still hide payments from you or invent history.)
- Role in this product: external website links only. Settings selects mempool.space or a custom Esplora-compatible website. Winnow never queries it for wallet state, fees, or broadcast. A deliberate address/transaction tap shows the exact host and disclosure warning before iOS opens the URL; the website’s answer is not imported as wallet truth.
2.3 Electrum protocol
The Electrum server protocol (address scripthash subscriptions over TCP/SSL) is the same shape as §2.2 — a server-side address index queried with your scripthashes — with the same privacy properties: the server learns every scripthash you subscribe to. Noted for completeness; nothing about it improves on §2.2 for this product’s goals.
2.4 BIP37 bloom filters — and why they’re dead
BIP37 (2012) let a light client upload a bloom filter of its keys to a full-node peer, which then forwarded only matching transactions. This is the historically important wrong answer:
- Privacy catastrophe. Bloom filters leak far more than intended: false-positive rates are chosen small, filters for multiple addresses correlate, and an observer (or the peer itself) can statistically extract which addresses are actually yours. The research literature demolished this repeatedly (e.g. Gervais et al., “On the Privacy Provisions of Bloom Filters in Lightweight Bitcoin Clients”, ACSAC 2014).
- DoS vector. Serving BIP37 lets any client force a full node to scan entire blocks against arbitrary filters — CPU for free, on demand.
- Consequence: Bitcoin Core has served BIP37 to peers only when explicitly enabled since v0.19 (2019); on today’s network it is effectively unavailable. Any design that needs “ask a node about my address” over P2P is a design from 2013.
BIP37 matters here for one reason: it establishes that server-side matching is inherently leaky, which is why the modern design inverts it — the data moves to the client, the matching happens on the client.
2.5 BIP157/158 compact block filters (client-side filtering)
This is the inversion, and the mechanism this product uses by default. Mechanics, precisely:
- BIP158 defines, for each block, a basic filter: take every output scriptPubKey in the block (excluding OP_RETURN outputs) and the scriptPubKeys of all outputs spent by the block’s inputs; hash each with SipHash keyed by the first 16 bytes of the block hash; map into a Golomb-Rice-coded set (GCS) with parameters P=19, M=784931. Result: a per-block data structure of approx. 15–20 KB (recent blocks; smaller for older, emptier blocks) that answers “might script S be in this block?” with zero false negatives and a false-positive rate the GCS construction leaves tunable in general but the basic filter fixes at 1/784931.
- BIP157 wires it into P2P: peers signal
NODE_COMPACT_FILTERS(service bit 6); clients requestgetcfheaders(a 32-byte commitment chain over the filters),getcfcheckpt(checkpoint hashes, for cross-peer comparison), andgetcfilters(the filters themselves). - Client flow: the wallet derives its own scriptPubKeys locally → downloads the filter for each new block → checks each filter on the device against its scripts → on a (rare) match, downloads that one full block (
getdata) and extracts its transactions. Nothing about the wallet’s keys or addresses is ever transmitted.
Costs, stated plainly:
- Block headers (needed to know the chain and anchor filter headers): exactly 80 bytes per block — approx. 75 MB for the entire historical chain, and zero for a fresh wallet that starts at the tip.
- Filters, steady state: ~144 blocks/day × approx. 15–20 KB ≈ 3 MB/day (approx.) of filter download. A once-daily app open pulls a few MB — comparable to loading a couple of web pages.
- Matched blocks: only blocks that actually contain the wallet’s transactions, ~1–2 MB each (exact: block size varies up to 4M weight units). For typical personal use this is rare — days to weeks apart.
- CPU/battery: hashing and matching a filter against a watch list of thousands of scripts is milliseconds of work per block on a modern phone. The signet UI suite measured mined→filter-detected at 0.69s on an iPhone 17 Pro Max simulator (2026-08-17,
screenshots/timings.json) — that figure includes mining-propagation and the match, not just GCS lookup, and is a measurement on signet, not a mainnet bandwidth claim.
Server privacy designs considered
2.6 Server-side privacy designs (considered, and why none is the default)
Since the leak in §2.2/§2.3 is the server observing queries, can we build a server that can’t observe? The candidates:
- Self-hosting. Run your own indexer on your own hardware and query only it. Privacy: excellent (you trust yourself). Cost: hardware, setup, and maintenance — outside this product’s premise of a self-contained mobile client. A user can select that instance for Winnow’s warned external links, but the app still does not make automatic wallet queries.
- Tor / VPN to a public indexer. Hides the client’s IP from the operator. The operator still sees — and can correlate and log — the content: the set of addresses, which is itself the wallet fingerprint. Partial mitigation only.
- Oblivious HTTP (OHTTP, RFC 9458). A two-party relay/gateway split: the relay sees the client’s IP but not the query; the gateway sees the query but not the IP. This is Apple Private Relay’s architecture and it is genuinely deployable. But the gateway still aggregates the address sets of all users it serves, trust now rests on the relay/gateway non-collusion assumption, and someone must operate both parties. Better than Tor, still a server to trust.
- TEE + no-logs + ORAM (“enclave esplora”). Run the indexer inside a hardware enclave (Intel TDX, AMD SEV-SNP, AWS Nitro) with remote attestation, so the operator cannot read queries even with root; use ORAM (Oblivious RAM) inside the enclave so memory-access patterns don’t reveal which addresses are being looked up. Intellectual honesty requires listing the catches:
- The enclave boundary ends at the network card: the host still observes client IPs, connection timing, and request sizes — traffic analysis outside the enclave — unless expensive padding/batching is added.
- TEEs have a decade-long side-channel track record (Spectre-class transient execution, controlled-channel/page-fault attacks against SGX, cache attacks). ORAM addresses one class (access patterns); it does not fix microarchitectural leakage, and ORAM over a multi-GB address index carries a real bandwidth/latency multiplier per query.
- Remote attestation roots trust in the CPU vendor’s signing infrastructure and in the reproducible-build chain of the enclave image — a different trust assumption, not the absence of one.
- Someone must procure enclave-capable hardware and operate it forever. “Can it keep no logs and use ORAM?” — yes, approximately, at real cost; but the product question is whether a mobile wallet should depend on anyone’s server at all. For this product: not by default, ever.
The pattern across §2.6: every server-side fix moves or shrinks the trust rather than deleting it, and every one requires operating infrastructure. Client-side filtering (§2.5) is the only mechanism that requires trusting no one with the read path.
2.7 Honest weaknesses of compact filters
Filters win the privacy argument; they lose elsewhere. Said louder than the strengths:
- Filters are not consensus-committed. A malicious peer can serve a filter that omits your transaction (lying by omission), causing the wallet to miss a payment. Mitigation: fetch
cfcheckpt/cfheadersfrom ≥2 independent peers and disconnect peers that disagree — disagreement is detectable, though the protocol cannot by itself prove which peer lied. A future consensus change committing filters to blocks would close this; it does not exist today. Residual risk: a partitioned client (all reachable peers colluding) can be lied to — the standard eclipse-attack caveat for all light clients. - Mempool view: bounded by design, not absent. Filters cover confirmed blocks only, so the steady state is confirmation-time visibility. But the client can open a bounded mempool window (§2.8): while the Receive screen is open — i.e., while a payment is actively expected — the app subscribes to full transaction relay and matches locally, so the payment appears as unconfirmed within seconds of broadcast. The window exists only while the screen is open, anything relayed before it opened is missed, and 0-conf is never final against RBF/double-spend — the app says “unconfirmed”, never “received”. A user who wants a second, faster manual check can open an address or transaction at the selected explorer after a warning; Winnow does not consume that answer. (The one answer Winnow does consume is the warned, single-tap “Infer sender” lookup on a received payment, which returns funding addresses only — never balance, history, or identity.)
- Bandwidth is real, if modest. ~3 MB/day (approx.) steady state is trivial on Wi-Fi and fine on cellular, but it is not zero, and a phone that hasn’t synced in a month downloads ~100 MB (approx.) of filters to catch up. Mempool windows add ~180 KB/min (approx.) while open — bounded by a screen session.
- Fee estimation is blind. Without a persistent mempool view, the wallet cannot see the current fee market — and relayed transactions alone don’t help, since a feerate needs input amounts, which means recursively fetching parent transactions (the bandwidth blowup returns through the back door). Mitigations: BIP133
feefiltermessages from peers give the network’s minimum relay fee floor; feerates of transactions in matched blocks give some signal; beyond that, conservative static presets with user override. The result is cruder than any mempool-aware estimator — the price of asking no one. Owned. - Fresh-wallet scope is what makes this viable. Filters are cheap because scanning starts at creation and runs forward. Recovering an old wallet privately from the chain would mean back-scanning gigabytes of historical filters (approx.; multiple GB for a multi-year-old mainnet wallet) — so this product doesn’t do that. Instead, import requires the history to come with the wallet. The format and the residual lies a bundle can still tell (omitted old coins, a too-high height) are import.
2.8 Bounded mempool windows
The refinement that removes §2.7.2’s sting:
- What: a time- and screen-bounded subscription to full transaction relay. The client sets the relay bit in its
versionhandshake for the window’s duration, answers every transactioninvwithgetdata, and matches the results locally against a watch set (typically the one address currently displayed). - Why it’s private: the watch list stays on the device while the client receives public transaction gossip. Connection timing, request patterns and relay behavior can still reveal information; this does not make a mobile client indistinguishable from a full node.
- Cost: ~180 KB/min (approx.: ~5 tx/s × ~600 B) for the duration of the window — a few MB per receive session. Only ever while the relevant screen is open.
- Used for: (a) receive-side 0-conf visibility while expecting a payment (“unconfirmed — awaiting confirmation” within seconds of the sender broadcasting); (b) send-side propagation confidence (peers echoing our txid back = the network has it).
- Still can’t: fee histograms (feerates need prevout amounts → recursive parent fetches → the bandwidth blowup returns — fee policy lives on the write side); anything relayed before the window opened; safety of unconfirmed funds against RBF/double-spend — which is why the UI never presents 0-conf as final.
2.9 Threat model (read path)
What an adversary can do to this paper’s mechanism, not to keys or broadcast (those are mobile §5 and write-side §8).
| Adversary | Sees | Can do | Mitigation | Residual |
|---|---|---|---|---|
| Honest-but-curious peer | Your IP; that you are a compact-filter client syncing from height H; the same filter bytes everyone else downloads | Log the connection; infer “this IP runs a light client” | Nothing about scripts or addresses is sent | The connection itself |
| Lying-by-omission peer | Same | Serve a filter that drops your transaction | getcfcheckpt / getcfheaders from ≥2 peers; disagreement disconnects the minority; each cfilter must reproduce the pinned filter-header chain |
Disagreement is detectable; which peer lied is not provable |
| Fully eclipsed peer set | Same, and they agree with each other | Show a consistent false filter-header chain, hide payments, or stall the tip | Manual peers (Settings) so a user who has a node they trust can skip DNS seeds; seeds themselves resolve over DoH (not on-path UDP) with getaddrinfo fallback, and RFC1918/loopback answers are dropped; headers still need PoW + chainwork |
A partitioned phone can be lied to — the standard light-client eclipse caveat. DoH removes the unauthenticated-UDP seed rewrite; it does not remove a determined eclipse. |
| Network observer (not a peer) | Timing, sizes, destination IPs of the outbound pool | Infer that this IP is syncing filters from height H | No addresses on the wire | Height H plus “is a BIP157 client” |
Filters are not consensus-committed. A future soft fork committing the filter header into the block would collapse the first three rows’ residual column (§5.1). Until then this table is the honest one.
2.10 The two shortcuts, said plainly
Everything above concerns filters. Headers are the other half, and between them there are exactly two places this wallet does not derive everything for itself. Both are listed here rather than left to be found.
One: the chain before the checkpoint. Validating mainnet from genesis means checking the proof of work on ~963,000 headers — about 8.5 minutes of a phone at full CPU, and effectively the whole first-launch experience, since filter scanning for a new wallet is a few hundred blocks. By default Winnow starts instead from a checkpoint compiled into the app: a height, that block's 80-byte header, and the cumulative chainwork at that point. Headers after it are validated exactly as before — proof of work, linkage, most-work fork choice, all unchanged.
Two: history does not come from the chain. Filters are cheap because scanning starts at the wallet's creation and runs forward, so recovering an old wallet privately would mean back-scanning gigabytes of historical filters. Winnow does not do that; an imported wallet brings its history with it (import). That is the same trade in a different place, and §2.7 covers what a bundle can still misstate.
The checkpoint adds no new party. This is the part worth being precise about. The constant ships inside the same signed binary as the code that derives your keys, matches your filters and computes your balance. Anyone able to change the checkpoint is equally able to change any of those — so trusting it is not a further act of trust, it is the one already performed by installing the app. That is a different thing from fetching a checkpoint from a server, which would introduce a party who could serve one value to everybody and another to you. Nothing here is fetched, and every install carries the same value.
And a checkpoint is not only a saving — it is also a defence. A light client with no checkpoint chooses between competing chains on accumulated work alone, which is precisely what a long-range attack exploits: a branch taken near genesis, when difficulty was trivial, can be extended cheaply into something a naive comparison finds plausible. A checkpoint forecloses that entire class. Bitcoin Core shipped checkpoints for the same reason for years. So the honest accounting is not "we gave up verification to be fast" — it is that the shortcut removes one attack while accepting a different, narrower one.
What is still verified, and by how many. Every header after the checkpoint is checked on the device. Filter commitments are cross-checked across several peers. Network-prefix and source diversity reduce concentration, but do not prove independent operators. Tor and I2P names have no prefix, so while clearnet is selected they may not fill every slot, and with only Tor and I2P selected neither overlay may. No peer-address list is bundled. Fresh installs use DNS seeds; saved working peers and signed census downloads provide other candidates. Census validation checks the publisher signature, schema, size, original date (at most seven days old), addresses, duplicate endpoints, clearnet prefix diversity and heights within 100 blocks of the reference tip. Handshakes and reported heights are candidate-selection evidence, not proof of valid filters or chain identity. DNS-only discovery can occupy two of the three default slots under the existing source ceiling.
Refreshing candidates. Mainnet automatically downloads the signed census when its saved catalog is missing or expired. Saved-peer and DNS discovery continue independently. Advanced → Refresh peer list can also download the canonical census artifact and atomically replaces the saved catalog only after validation. Active connections remain. An expired download stops supplying new candidates; saved peers and DNS discovery remain available. Reset and shuffle peers clears learned and transient state while preserving manual settings and the downloaded catalog, and prefers different automatic peers where possible. Downloaded census candidates count as one trust source.
Connections. With clearnet selected, peers, DNS discovery, catalog downloads and consented explorer lookups are made directly from the device. Automatic routing, the default, asks Tailscale’s resolver for the user’s own winnow-tor-gateway and winnow-i2p-gateway; when either answers, clearnet is dropped and every peer is reached through a gateway, never dialled directly, and when neither answers the wallet falls back to clearnet. Manual routing without clearnet skips DNS seeds and sends catalog and explorer requests through the Tor gateway; I2P alone fetches the catalog from its I2P mirror and turns explorer lookups off. The census’s validated onion and I2P entries are kept for those routes (gateway setup). Winnow bundles no Tor or I2P client; the embedded Tor client of 0.6–0.7.0 was removed in 0.7.1 (see the roadmap). Networking stops in the background and is rebuilt in the foreground, or for one bounded check when iOS starts a background task. That check uses the same routing, except that Automatic routing which last reached peers through a gateway skips the check rather than connect directly.
What would still go wrong. If the constant were wrong, the wallet would follow a chain that is not Bitcoin's, and later validation would not notice — every header after a false starting point can be perfectly valid relative to it. What stands behind it: the value carries its provenance in the source, including the tip it was taken from and the command that recomputes it, and the build fails if a chain synced from genesis and a chain synced from the checkpoint ever disagree on tip hash or total chainwork. That test runs through the same loading path the app uses, not a special one.
How to turn it off. Settings → Verify the chain from genesis. On, the app ignores the checkpoint and spends the minutes. Switching rebuilds the stored chain rather than mixing the two, so an install cannot end up half-validated.
3. Use-case walkthrough: what serves what, and why
| # | Use case | Default mechanism | One-sentence rationale |
|---|---|---|---|
| 1 | Fresh wallet, first launch | Nothing to scan; record creation height; sync filters forward from tip | A new key has no past — the read side starts empty and cheap by construction. |
| 2 | Daily open / ongoing sync | getcfilters for blocks since stored checkpoint (~3 MB/day, approx.), match locally, fetch matched blocks only |
Client-side matching avoids sending an address watch list. Subsequent block requests, relay traffic and timing can still reveal information. |
| 3 | Receiving a payment | While the Receive screen is open: mempool window (§2.8) shows the payment as unconfirmed within seconds; finality arrives via filter match at block confirmation | When you’re actively expecting money, a short full-relay subscription is cheap, private, and exactly as honest as “unconfirmed” implies. |
| 4 | Sending | UTXOs/amounts/scripts already local from scanning; the rest is the write side | The read side’s job ends when the coins and their scripts are on the device. |
| 5 | Watching a single address | One more scriptPubKey in the local match list — same filter stream, zero extra bandwidth | The P2P protocol has no per-address query (BIP37 is dead); the granularity is per-block filters regardless of watch-list size. |
| 6 | Multisig vault (k-of-n or MuSig2 n-of-n) | Identical machinery — watch list derived from the vault’s tr() descriptor |
A vault is just a different set of scripts; the read side doesn’t care. Ceremony is vaults. |
| 7 | Balance & history display | Local storage, populated by §2.5 matches | After sync, display is a database read — no network at all. |
| 8 | “Where’s the tip?” | 80-byte block headers over P2P (getheaders), PoW + chainwork-checked |
Headers are the sync clock and the anchor for the filter-header chain. |
| 9 | “Did my tx get out?” | Peer inv gossip — peers echoing our txid back prove propagation; confirmation observed via filter match |
Relay acceptance is write-side §7; this paper observes the confirmation. |
| 10 | Importing an existing wallet | History bundle, verified by forward filter-scan from its height | Specified in import. |
| 11 | Optional manual check | A warned tap opens an exact address or transaction in an external browser. A separate per-lookup consent discloses a transaction ID and the IP disclosure before requesting funding-address suggestions. Every suggestion requires explicit selection, even if only one exists, and retains unverified provenance. A local name-only label makes no request and cannot send until a destination is attached. | Useful while P2P sync catches up, but the explorer learns the exact item and the connection’s IP address. It is a user action, not a fast read path. |
One use case is deliberately absent from the default path:
- Always-on mempool awareness (instant notification of unexpected payments, background 0-conf, live fee markets) — all require either a persistent relay subscription (bandwidth/battery cost the product rejects) or a server (privacy cost the product rejects). Bounded windows (§2.8) cover the cases where the user is present and expecting something; everything else waits for confirmation.
4. Conclusion
For a fresh-wallet product, the read side reduces to a steady-state stream of ~3 MB/day (approx.) of compact filters, matched on-device, with full blocks fetched only on hits — plus bounded mempool windows (§2.8) for the moments a user is actively sending or expecting a payment. Winnow does not query a wallet-history server automatically. Discovery services still receive network requests; peers see protocol traffic, and direct connections expose the device’s IP. The costs — confirmation-time visibility for unexpected payments, crude fee estimation, reliance on honest filter peers cross-checked by header comparison — are real, bounded, and stated to the user instead of hidden. Warned explorer links let the user make a one-off disclosure without turning it into wallet infrastructure.
v1 on the read side is therefore: block headers + BIP157/158 compact filters + bounded mempool windows, over Network.framework, talking only to full-node peers that signal NODE_COMPACT_FILTERS. Esplora is an external, warned link—not a wallet backend. The phone those bytes land on, the spend, the vault, and the import are the other papers.
Future hardening and validation details
5. Future hardening
Three known paths would strengthen this design further. None is buildable today on this product’s constraints; all are worth stating so the current trade-offs are legible against them.
5.1 Consensus-committed block filters
- What: a soft fork committing each block’s BIP158 filter (or filter header) into the block itself, so a peer serving a wrong filter is provably wrong against the header chain.
- Fixes: §2.7.1 outright — lying by omission becomes impossible, not merely detectable-by-disagreement.
- Costs: a consensus change; outside anyone’s roadmap control. Until then, multi-peer
cfcheckpt/cfheaderscomparison is the mitigation.
5.2 PoW fraud proofs
- What: a compact, relayable proof that some block in a chain is invalid, so light clients needn’t accept majority chainwork on faith.
- Fixes: the eclipse-amplified trust assumptions that all light clients carry — a partitioned client fed a fabricated chain tip.
- Costs: producing or consuming fraud proofs requires validation infrastructure (a script engine, chain state) that this product deliberately does not carry; and full validation on-device means downloading every block (~150–300 MB/day approx. vs. ~3 MB/day of filters) — bandwidth, not storage, is the mobile constraint. Utreexo-style accumulators solve UTXO-set storage; they do not shrink the blocks. If a future revision ever embeds real validation, reusing an existing consensus engine rather than reimplementing one is the only sane path.
5.3 Utreexo proof-based import verification
- What: with the draft Utreexo peer services (BIP181/182/183, work in progress), a peer could serve a Merkle proof that an imported output is still in the accumulator — i.e., still unspent.
- Fixes: the import flow’s one remaining cost — verifying a history bundle by forward-scanning filters from its height (§2.7.5). Proof-based verification is O(1) per UTXO and independent of bundle staleness.
- Costs: the BIPs are drafts and no serving network exists. The import-bundle format is versioned, so a
prooffield can be added non-destructively when the ecosystem arrives.
Appendix A. How the implementation is validated
Nothing in this paper asks to be taken on faith — the code is checked against independent ground truth at every layer:
- Official BIP test vectors for every cryptographic surface: BIP39/32/86 (keys), BIP340 (Schnorr), BIP341 (Taproot trees, tweaks, control blocks, sighash — script-path vectors generated via Bitcoin Core’s own functional-test framework where the official file is silent), BIP350 (bech32m), BIP158 (GCS filters, byte-for-byte against real blocks), BIP327 (MuSig2), BIP380/387/389/390 (descriptors).
- Loopback fake-node tests for the P2P layer: handshakes, filter sync, lying-filter detection, relay state machine — deterministic, no network.
- One end-to-end UI test makes ordinary, MuSig2, and script-path 2-of-3 payments against a private signet node. Core supplies the external cosigners, accepts the transactions, and mines blocks automatically for prompt confirmation in the app. The host records one continuous video; detailed failures stay in focused tests.
- The crypto boundary: all secret-key operations delegate to libsecp256k1 (via P256K) — Bitcoin Core’s constant-time, exhaustively tested curve library. No Swift code does raw curve math on secrets.
Inspect the current test runs and artifacts. The payment recording identifies the source and run behind that journey.
6. References
- BIP37: Connection Bloom filtering (and its privacy critique: Gervais et al., ACSAC 2014)
- BIP133: feefilter message
- BIP157: Client Side Block Filtering
- BIP158: Compact Block Filters for Light Clients
- RFC 9458: Oblivious HTTP
- Neutrino (lightninglabs/neutrino) — reference BIP157/158 light-client implementation
- BIP341: Taproot · BIP340: Schnorr · BIP86 · BIP327: MuSig2 · BIP388 · BIP370: PSBTv2