Hi all,
I have published spscand, a watch-only scanner for BIP352 silent payments (version 1.1.2), and libspscan, the C library it scans with:
https://github.com/ZenulAbidin/spscand (MIT)
What it does
spscand holds the scan private key and the spend public key. It finds outputs paid to an sp1 address and its labels, tracks spends and reorgs, and reports them over a local read-only HTTP API and a webhook. For each output it stores the tweak t_k, so a separate signer can derive d = b_spend + t_k. It handles confirmed blocks only.
How scanning stays private
It never queries a server about an address, output or transaction. Also there are three block sources:
1. Bitcoin Core (25+) over RPC. Tweaks are computed locally from getblock verbosity 3. Nothing leaves the machine.
2. A BlindBit Oracle server. For every block from the wallet birthday to the tip it downloads all tweaks, all taproot outputs and the spent-key prefixes, and matches locally. Every block is requested the same way whether or not it pays the wallet. The server learns the client's IP address (unless Tor is used), the birthday and when the client is online or rescans. It can also withhold data and so hide a payment.
3. Filters, the strategy in BIP352's appendix. Tweaks still come from BlindBit, for every block. The block's BIP158 basic filter comes from P2P peers (BIP157). The client computes its candidate k = 0 output keys, tests them and its unspent outputs against the filter, and downloads the block only on a match. The peer serving that block learns it was of interest, so each block is fetched from a randomly chosen peer over a connection of its own.
A peer could hide a payment by serving a false filter. Filter headers are therefore compared across all reachable peers, and the user is told to configure at least two. The P2P transport is v1 (no BIP324 yet).
With a SOCKS5 proxy, names are resolved by the proxy, proxy environment variables are ignored, and each process, and in filter mode each P2P connection, uses random SOCKS credentials so that Tor with IsolateSOCKSAuth gives it circuits of its own.
libspscan
libspscan does the BIP352 receiving side and the BIP158 filter checks. It does no I/O, never takes a spend private key, and depends only on the C standard library, so it can be used under other wallets or indexers. spscand calls it through ctypes.
- It bundles libsecp256k1 v0.8.0, unmodified (the README gives the commit and a diff command). Shared secrets, output tweaks and labels come from its silentpayments module.
- libsecp256k1 is compiled into the same translation unit so that label matching can use its field arithmetic. Only spscan_* symbols are exported.
- A tweak server does not say which transaction a tweak belongs to, so a block's tweaks are tried against all of its taproot outputs. Each label adds a candidate x(P_k + L) per tweak. These are added in affine coordinates with one field inversion per batch of 1024. When labels outnumber twice the outputs, output - P_k is looked up among the labels instead, as BIP352 suggests.
- The shared secret b_scan * (input_hash * A) is computed in constant time. Matching after that branches on public output keys and on P_k, as libsecp256k1's own scanner does.
- Lookup tables are keyed with SipHash under a key derived from the scan key, so a sender cannot choose output keys that collide.
Measurements, mainnet block 833044 (721 eligible transactions, 2105 taproot outputs):
- BlindBit: 135 kB downloaded; 0.05 s CPU, 0.16 s with 1000 labels.
- Filters: 28 kB of tweaks and a 31 kB filter; 0.05 s CPU.
- The filter check tests tweaks * (1 + labels) items. The chance of downloading this block for nothing is 0.2% with no labels, 1% with 10 and 9% with 100. With 1000 labels most blocks match.
Tests
- The BIP352 receiving test vectors, in both the C and the Python code, with every result compared between the two.
- Bitcoin Core's BIP158 vectors. libspscan builds the same filters byte for byte.
- Mainnet block 833044 checked against a real BlindBit index.
- The filters backend against fake peers, including peers that serve false filters.
- An end-to-end run on regtest with Bitcoin Core 27.1: payments from the BIP352 reference sender with P2WPKH, P2SH-P2WPKH, P2PKH and P2TR inputs, then labels, spending, spend detection, reorgs and rescans. In filter mode it checks that only blocks paying or spending the wallet's outputs were downloaded, and with a SOCKS proxy that nothing bypassed it.
Corrections and criticism are welcome, on the list or as GitHub issues.
Ali Sherief
bitmixlist.org