* [bitcoindev] spscand and libspscan: a watch-only BIP352 scanner
@ 2026-11-14 8:40 Ali Sherief
0 siblings, 0 replies; only message in thread
From: Ali Sherief @ 2026-11-14 8:40 UTC (permalink / raw)
To: Bitcoin Development Mailing List
[-- Attachment #1.1: Type: text/plain, Size: 5014 bytes --]
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
--
You received this message because you are subscribed to the Google Groups "Bitcoin Development Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bitcoindev+unsubscribe@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/bitcoindev/05fbac98-5a91-4224-ad9b-745441252e26n%40googlegroups.com.
[-- Attachment #1.2: Type: text/html, Size: 5486 bytes --]
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-11-14 12:34 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-14 8:40 [bitcoindev] spscand and libspscan: a watch-only BIP352 scanner Ali Sherief
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox