The problem: BlindBit Oracle is the
server that hands a wallet the per-block data it needs to find its own
BIP-352 silent payments. Its HTTP API was rewritten this year, and
the old one was removed. Every wallet-shaped client that exists today still speaks the old
one: the Dana Android APK, spdk's backend-blindbit-v1
and its WASM backend, and BlindBit's own mobile apps. Only blindbit-cli and
blindbit-desktop speak v2. So a v2 endpoint is unusable by almost every client ever
written against it.
What this is: a translation layer. Point it at a v2 oracle, point an old client at it, and the client works unchanged.
wallet (v1 API) -> shim :8020 -> oracle v2 :8010
One /full-block/{h} fetch per block, cached and single-flighted, serves all nine v1
routes for that block.
go build -o v1shim . && ./v1shim -listen 127.0.0.1:8020 -oracle http://127.0.0.1:8010
This is the one thing to read before pointing a wallet with real money at it.
The v1 API identifies a spent output by sha256(txid || vout || blockhash)[:8], salted per
block. v2 identifies it by the first 8 bytes of the spent output's x-only pubkey, unsalted.
They are not interconvertible, so the v1 form has to be rebuilt from outpoints.
A stock /full-block/{h} only returns transactions that have outputs in the index, so
it exposes outpoints only for transactions that themselves create silent payments.
Everything else is dropped, including the outpoints the handler just read from the
database. Measured against a full mainnet index over blocks 965060-965089, that is
13,276 of 16,745 spends, 79.3%. The gap is biased against the case that matters: an
ordinary spend of a received silent payment, by a transaction that does not itself create
one. Concretely, 7 of the 19 spends of block 965060's indexed outputs are missing from
/full-block/965061.
Consequence: a wallet on a stock oracle can show an already-spent output as spendable.
So the shim requests /full-block/{h}?include_input_only=1, and measures at startup
whether the oracle honours it, by comparing its reconstructed spent set for one block
against the oracle's own count:
$ curl -s localhost:8020/shim/status
{"spent_coverage":"complete","probe_height":965204,"shim_spent":634,"oracle_spent":634}
The same result is logged, as a warning when coverage is partial. Against a patched oracle the reconstruction is exact: 16,745 of 16,745 over the same 30 blocks.
That parameter is not upstream yet. It is
branch feat/full-block-include-input-only,
and the upstream question about the dropped outpoints is
setavenger/blindbit-oracle#58.
It is opt-in rather than the default for a concrete reason: an input-only transaction
carries an all-zero tweak, and spdk's v2 backend parses that field with
PublicKey::from_slice, which rejects the whole response. Making completeness the default
would break existing v2 clients.
| v1 route | notes |
|---|---|
GET /info |
passthrough, the shape did not change |
GET /block-height |
v2 dropped the route, kept the field on /info |
GET /block-hash/{h} |
|
GET /tweaks/{h}?dustLimit= |
full index, see "Cut-through" |
GET /tweak-index/{h}?dustLimit= |
|
GET /filter/new-utxos/{h} |
rebuilt, see "Filters" |
GET /filter/spent/{h} |
rebuilt, see "Spent coverage" |
GET /utxos/{h} |
|
GET /spent-index/{h} |
|
POST /forward-tx |
501, this shim does not broadcast |
GET /shim/status |
not v1, the coverage probe result |
Filters. v2 removed them. The builder is still in its tree but nothing calls it and
nothing is stored, so the shim rebuilds both per block: the same btcsuite GCS code, the
same DefaultP and DefaultM, the same key derivation from the internal-order block hash.
The btcsuite versions in go.mod are pinned to the ones blindbit-oracle itself uses, so
the bytes match by construction rather than by reimplementation.
One deliberate divergence: v1's new-utxos filter covered every taproot output in the
block, this one covers every taproot output of the indexed transactions. That is a
subset, so the filter is smaller with fewer false positives, and it cannot produce a false
negative for silent payment scanning, because a payment to the user is by definition in an
indexed transaction.
Byte order, twice. v2 serialises an input as a display-order txid followed by a big-endian vout. v1 hashed an internal-order txid followed by a little-endian vout, salted with the internal-order block hash. Both flip. Getting either wrong produces identifiers that look fine and that no client ever matches.
Cut-through. v1 /tweaks served the cut-through list and /tweak-index the full one.
v2 exposes no cut-through tweak list over HTTP, so both routes return the full index. That
over-delivers tweaks, which costs the client work and never hides a payment from it.
utxos.timestamp is always 0 and utxos.spent is always false. v1 marked both "not
used" in its own source, and spdk parses them but drops them when converting to UtxoData.
The conformance tests run against a live v2 oracle. They are anchored to mainnet block 965085, which carries a 50,000-sat silent payment found blind with the BIP-352 reference implementation.
ORACLE=http://127.0.0.1:8010 go test -v ./...
They assert that the new-utxos filter matches every indexed output in the block, that it
rejects an unrelated key, and that the spent filter matches every identifier the shim
publishes. A filter that silently matches nothing is the failure mode worth guarding.
main.go: flags, startup coverage probe.v2client.go: the v2 client, the per-block view every route is derived from, and the v1 spent-identifier reconstruction.filters.go: GCS filter construction, byte-compatible with v1.v1api.go: the nine v1 routes plus/shim/status.conformance_test.go: the checks above.
MIT. From the operators of https://silentpayments.net. Related: silentpayments-measurements, which measures what each light-client design actually costs and carries a converged protocol pre-draft.