Zero-knowledge scanning infrastructure for BCH privacy protocols • v2.0 • 0penw0rld.com
Base URL: https://0penw0rld.com/api • Single file: pubkey-indexer.js
The P2PKH Pubkey Indexer serves all compressed public keys from P2PKH transaction inputs for any Bitcoin Cash block range. It enables privacy-preserving scanning for multiple protocols without leaking information to the server.
| Protocol | How It Uses The Indexer | Status |
|---|---|---|
| Stealth Addresses (00 Protocol) | Download pubkeys, compute ECDH(scan_priv, pubkey) for each, derive one-time address, check against UTXO set | Live |
| RPA (Reusable Payment Addresses) | Download pubkeys, filter by notification prefix, compute ECDH for candidates | Compatible |
| Confidential Txs | Download pubkeys, use covenant scanning logic to identify privacy outputs | Compatible |
Returns all P2PKH input compressed pubkeys for blocks in the specified range (inclusive).
| Parameter | Type | Required | Description |
|---|---|---|---|
from | integer | Yes | Start block height (inclusive) |
to | integer | Yes | End block height (inclusive) |
{
"from": 943370,
"to": 943372,
"count": 660,
"pubkeys": [
{
"height": 943370,
"txid": "3a1b2d74d9c0eaee02d612599fdb55cd9461b77c9535759c853a3d75c2da2c84",
"vin": 0,
"pubkey": "02c72ca3449d847bc75133d9b29461237d12fb90c1adbf91a92ca6331a50b6d07d",
"outpoint": "84...0000000000"
},
...
]
}
| Field | Type | Size | Description |
|---|---|---|---|
height | integer | — | Block height containing the transaction |
txid | string | 64 hex | Transaction ID |
vin | integer | — | Input index within the transaction |
pubkey | string | 66 hex | Compressed secp256k1 public key (33 bytes) |
outpoint | string | 72 hex | Previous TX hash (LE, 32 bytes) + previous vout (LE, 4 bytes) |
| Constraint | Value |
|---|---|
| Max blocks per request | 100 |
| Rate limit | None (cached responses are instant) |
| Cache | Permanent per block (blocks are immutable) |
| CORS | Allowed from all origins |
Returns server status, source mode, and connection state.
{
"status": "ok",
"source": "fulcrum",
"connected": true,
"cached": 42
}
Same as above but returns compact binary format (106 bytes per entry). Use format=binary query parameter.
| Field | Size | Type | Description |
|---|---|---|---|
| height | 4 bytes | uint32 LE | Block height |
| txid | 32 bytes | raw | Transaction ID |
| vin | 1 byte | uint8 | Input index |
| pubkey | 33 bytes | compressed secp256k1 | Public key |
| outpoint | 36 bytes | txid (32) + vout (4) LE | Previous output reference |
106 bytes/entry vs ~250 bytes JSON = ~60% less data for bulk scanning.
Returns cache statistics: number of indexed blocks, height range, and source mode.
{
"cached_blocks": 42,
"range": { "from": 943000, "to": 943042 },
"source": "fulcrum",
"connected": true
}
Try it: fetch pubkeys for a block range
The indexer only provides raw data. All cryptographic operations happen client-side in the wallet. Here's how a stealth scan works:
// 1. Fetch pubkeys from indexer
const resp = await fetch(`https://0penw0rld.com/api/pubkeys?from=${fromHeight}&to=${toHeight}`);
const { pubkeys } = await resp.json();
// 2. For each pubkey, test ECDH locally
for (const entry of pubkeys) {
const senderPub = h2b(entry.pubkey);
const outpoint = h2b(entry.outpoint);
// ECDH: shared secret = scanPriv * senderPub
const shared = secp256k1.getSharedSecret(scanPriv, senderPub);
const sharedX = shared.slice(1, 33);
// Derive tweak
const c = sha256(concat(sha256(sharedX), outpoint));
const cBig = BigInt('0x' + b2h(c)) % N;
// Derive stealth pubkey: P = spendPub + c*G
const stealthPub = spendPoint.add(G.multiply(cBig)).toRawBytes(true);
const stealthAddr = toCashAddr(ripemd160(sha256(stealthPub)));
// 3. Check if this address has UTXOs
const utxos = await checkAddress(stealthAddr);
if (utxos.length > 0) {
// STEALTH PAYMENT FOUND!
const spendKey = (spendPriv + c) % N;
saveStealthUtxo({ txid: entry.txid, addr: stealthAddr, priv: spendKey });
}
}
| Metric | Value |
|---|---|
| Bytes per pubkey entry (JSON) | ~250 bytes |
| Average pubkeys per block | ~200-400 |
| Data per block | ~50-100 KB |
| Data per day (~144 blocks) | ~7-15 MB |
| Data per week | ~50-100 MB |
blockchain.headers.subscribe) and fetch only the latest block's pubkeys.
| Method | Privacy | Data Size | Server Knowledge |
|---|---|---|---|
| Full block download | Perfect | ~1 MB/block | None |
| Pubkey Indexer (this) | Perfect | ~50-100 KB/block | None (serves same data to all) |
| RPA prefix filter (Fulcrum) | Reduced | ~1-5 KB/block | Knows your prefix (derived from paycode) |
| Electrum scripthash | Low | ~0.5 KB/query | Knows exact addresses you're watching |
| Mode | Source | Requirement | Best For |
|---|---|---|---|
| Mode A: Fulcrum (WSS) | Public Fulcrum/ElectrumX servers | No local node needed | Quick setup, public infrastructure |
| Mode B: BCHN (JSON-RPC) | Local BCHN full node | bitcoind running locally |
Full sovereignty, self-hosters |
| Target | Command | Use Case |
|---|---|---|
| HTTP/JSON API | pubkey-indexer serve |
Browser wallets (00-Wallet), remote clients |
| CLI / stdout | pubkey-indexer scan --from 943000 |
Piping, desktop apps, EC plugin |
| Library import | require('pubkey-indexer') |
Direct integration in Node.js wallets |
Source Layer Output Layer
+-------------------+ +--------------------+
| Fulcrum (WSS) | | HTTP/JSON API |
| - Public servers |---+ +-->| port 3847 |
| - No node needed | | | +--------------------+
+-------------------+ | |
+-->---+ +--------------------+
+-------------------+ | P | | CLI / stdout |
| BCHN (JSON-RPC) | | A +-->| json or binary |
| - Local node |---+ R | +--------------------+
| - Full sovereignty| S |
+-------------------+ E | +--------------------+
R +-->| Library (require) |
+-------------------+ | | async generators |
| Cache Layer |<--+ +--------------------+
| JSON + Binary |
| per block |
+-------------------+
The more indexers run by different operators, the more resilient the privacy infrastructure. Running your own takes 5 minutes.
| Requirement | Version | Notes |
|---|---|---|
| Node.js | 18+ | apt install nodejs or nodejs.org |
| npm | any | Comes with Node.js |
| Fulcrum access | — | Public servers work (no local node needed) |
| Disk space | ~1 GB/year | For cached block data |
| RAM | ~50 MB | Minimal footprint |
# Clone or download the single file
mkdir pubkey-indexer && cd pubkey-indexer
wget https://0penw0rld.com/pubkey-indexer.js
# Install dependency (just one: ws for WebSocket)
npm init -y
npm install ws
Edit the top of pubkey-indexer.js to customize:
const PORT = 3847; // HTTP port
const CACHE_DIR = './cache/pubkeys'; // where block data is stored
const MAX_RANGE = 100; // max blocks per request
// Fulcrum servers (public, no auth needed)
const FULCRUM_URLS = [
'wss://bch.imaginary.cash:50004',
'wss://electroncash.de:50004',
'wss://bch.loping.net:50004',
];
--source node to connect to your own BCHN full node for maximum sovereignty.
# HTTP API server (default)
node pubkey-indexer.js
node pubkey-indexer.js serve # same
# With BCHN full node instead of Fulcrum
node pubkey-indexer.js serve --source node --rpc http://localhost:8332
# CLI scan mode (stream to stdout)
node pubkey-indexer.js scan --from 943000 # JSON lines to stdout
node pubkey-indexer.js scan --from 943000 --format binary # compact binary
# With config file
node pubkey-indexer.js --config config.yaml
# Verify it works
curl http://localhost:3847/api/health
# {"status":"ok","source":"fulcrum","connected":true,"cached":0}
# Fetch some pubkeys
curl "http://localhost:3847/api/pubkeys?from=943370&to=943372"
# {"from":943370,"to":943372,"count":660,"pubkeys":[...]}
# Fetch as binary
curl "http://localhost:3847/api/pubkeys?from=943370&to=943372&format=binary" > block.bin
Options:
--mode serve|scan Operation mode (default: serve)
--source fulcrum|node Data source (default: fulcrum)
--format json|binary Output format for scan mode (default: json)
--from <height> Start block for scan mode
--to <height> End block (default: chain tip)
--port <port> HTTP port for serve mode (default: 3847)
--cache-dir <path> Cache directory (default: ./cache/pubkeys)
--max-range <n> Max blocks per HTTP request (default: 100)
--rpc <url> BCHN RPC URL (default: http://localhost:8332)
--rpc-user <user> BCHN RPC username
--rpc-pass <pass> BCHN RPC password
--config <file> Load config from YAML file
--help Show help
# Create systemd service
sudo cat > /etc/systemd/system/pubkey-indexer.service << 'EOF'
[Unit]
Description=BCH P2PKH Pubkey Indexer
After=network.target
[Service]
Type=simple
WorkingDirectory=/path/to/pubkey-indexer
ExecStart=/usr/bin/node pubkey-indexer.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
EOF
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable pubkey-indexer
sudo systemctl start pubkey-indexer
# Check status
sudo systemctl status pubkey-indexer
# Add to your nginx server block:
location /api/ {
proxy_pass http://127.0.0.1:3847;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 120s;
add_header Access-Control-Allow-Origin * always;
}
# Test and reload
sudo nginx -t && sudo systemctl reload nginx
To pre-index historical blocks for faster initial client syncs:
# Index blocks 940000 to 943000 (fetches and caches each block)
for i in $(seq 940000 100 943000); do
curl -s "http://localhost:3847/api/pubkeys?from=$i&to=$((i+99))" | jq .count
sleep 2 # be nice to Fulcrum
done
Wallets should support multiple indexer URLs for redundancy. If one indexer is down, the wallet falls back to others:
// Client-side: try multiple indexers
const INDEXERS = [
'https://0penw0rld.com/api',
'https://your-indexer.example.com/api',
'https://community-indexer.bch.info/api',
];
async function fetchPubkeys(from, to) {
for (const base of INDEXERS) {
try {
const r = await fetch(`${base}/pubkeys?from=${from}&to=${to}`);
if (r.ok) return await r.json();
} catch { continue; }
}
throw new Error('All indexers unreachable');
}
# Build
docker build -f Dockerfile.indexer -t pubkey-indexer .
# Run (Fulcrum mode, default)
docker run -d -p 3847:3847 -v ./cache:/app/cache pubkey-indexer
# Run (BCHN mode)
docker run -d -p 3847:3847 -v ./cache:/app/cache \
pubkey-indexer node pubkey-indexer.js --source node --rpc http://host.docker.internal:8332
FROM node:18-alpine
WORKDIR /app
COPY pubkey-indexer.js .
RUN npm install ws
EXPOSE 3847
CMD ["node", "pubkey-indexer.js", "--mode", "serve"]
For direct integration in Electron Cash plugins or any Node.js wallet:
const { createScanner } = require('./pubkey-indexer');
const scanner = createScanner({
source: 'fulcrum', // or 'node'
cacheDir: './cache',
});
await scanner.connect();
// Get all pubkeys for a block range
const entries = await scanner.getPubkeys(943000, 943100);
// Or stream block by block (async generator)
for await (const entry of scanner.pubkeys(943000, 943100)) {
// entry = { height, txid, vin, pubkey, outpoint }
const shared = secp256k1.getSharedSecret(scanPriv, h2b(entry.pubkey));
// ... ECDH check ...
}
// Get chain tip
const tip = await scanner.getTip();
The indexer is a single Node.js file: pubkey-indexer.js (~450 lines)
module.exportsws00 Protocol • 0penw0rld.com • Stealth Spec • WizardConnect Spec