P2PKH Pubkey Indexer API

Zero-knowledge scanning infrastructure for BCH privacy protocols • v2.0 • 0penw0rld.com

Base URL: https://0penw0rld.com/api • Single file: pubkey-indexer.js

Overview

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.

Key Properties:

Supported Protocols

ProtocolHow It Uses The IndexerStatus
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

Endpoints

GET /api/pubkeys?from={height}&to={height}

Returns all P2PKH input compressed pubkeys for blocks in the specified range (inclusive).

ParameterTypeRequiredDescription
fromintegerYesStart block height (inclusive)
tointegerYesEnd block height (inclusive)

Response

{
  "from": 943370,
  "to": 943372,
  "count": 660,
  "pubkeys": [
    {
      "height": 943370,
      "txid": "3a1b2d74d9c0eaee02d612599fdb55cd9461b77c9535759c853a3d75c2da2c84",
      "vin": 0,
      "pubkey": "02c72ca3449d847bc75133d9b29461237d12fb90c1adbf91a92ca6331a50b6d07d",
      "outpoint": "84...0000000000"
    },
    ...
  ]
}
FieldTypeSizeDescription
heightintegerBlock height containing the transaction
txidstring64 hexTransaction ID
vinintegerInput index within the transaction
pubkeystring66 hexCompressed secp256k1 public key (33 bytes)
outpointstring72 hexPrevious TX hash (LE, 32 bytes) + previous vout (LE, 4 bytes)

Limits

ConstraintValue
Max blocks per request100
Rate limitNone (cached responses are instant)
CachePermanent per block (blocks are immutable)
CORSAllowed from all origins
GET /api/health

Returns server status, source mode, and connection state.

{
  "status": "ok",
  "source": "fulcrum",
  "connected": true,
  "cached": 42
}
GET /api/pubkeys?from={height}&to={height}&format=binary

Same as above but returns compact binary format (106 bytes per entry). Use format=binary query parameter.

FieldSizeTypeDescription
height4 bytesuint32 LEBlock height
txid32 bytesrawTransaction ID
vin1 byteuint8Input index
pubkey33 bytescompressed secp256k1Public key
outpoint36 bytestxid (32) + vout (4) LEPrevious output reference

106 bytes/entry vs ~250 bytes JSON = ~60% less data for bulk scanning.

GET /api/stats

Returns cache statistics: number of indexed blocks, height range, and source mode.

{
  "cached_blocks": 42,
  "range": { "from": 943000, "to": 943042 },
  "source": "fulcrum",
  "connected": true
}

Live Test

Try it: fetch pubkeys for a block range

FROM TO

Client-Side Scanning

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 });
  }
}
Privacy guarantee: The server sees the same request from everyone — "give me all pubkeys for blocks X to Y." It cannot distinguish a stealth scan from an RPA scan, a researcher downloading data, or a wallet syncing. The ECDH computation happens entirely in the browser.

Data Size Estimates

MetricValue
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
For initial sync of old wallets: scanning months of history requires downloading significant data. Consider implementing a progress bar and background download. For real-time monitoring, subscribe to new blocks via Fulcrum (blockchain.headers.subscribe) and fetch only the latest block's pubkeys.

Comparison With Alternatives

MethodPrivacyData SizeServer 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

Architecture

Two Source Modes

ModeSourceRequirementBest 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

Three Output Targets

TargetCommandUse 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        |
          +-------------------+

Self-Host: How to Run Your Own Indexer

The more indexers run by different operators, the more resilient the privacy infrastructure. Running your own takes 5 minutes.

Requirements

RequirementVersionNotes
Node.js18+apt install nodejs or nodejs.org
npmanyComes with Node.js
Fulcrum accessPublic servers work (no local node needed)
Disk space~1 GB/yearFor cached block data
RAM~50 MBMinimal footprint

Step 1: Download

# 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

Step 2: Configure (optional)

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',
];
No BCH node required. The indexer connects to public Fulcrum servers via WebSocket. You can also use --source node to connect to your own BCHN full node for maximum sovereignty.

Step 3: Run

# 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

CLI Options

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

Step 4: Run as Service (production)

# 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

Step 5: Reverse Proxy (nginx)

# 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

Step 6: Pre-warm Cache (optional)

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
First indexing is slow: each block requires fetching every TX via Fulcrum. Expect ~5-15 seconds per block on first fetch. Subsequent requests for the same blocks are instant (cached).

Federation

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');
}
The more operators run indexers, the better. Each indexer serves identical data (blocks are immutable). There's no coordination needed between operators. Wallets can query any indexer and get the same results. This is infrastructure-level decentralization — like running a Fulcrum server benefits all Electrum wallets.

Docker

# 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

Dockerfile

FROM node:18-alpine
WORKDIR /app
COPY pubkey-indexer.js .
RUN npm install ws
EXPOSE 3847
CMD ["node", "pubkey-indexer.js", "--mode", "serve"]

Library Mode

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();
No HTTP overhead. No JSON parsing. Direct iteration over cached binary data. This is what an Electron Cash plugin would use.

Source Code

The indexer is a single Node.js file: pubkey-indexer.js (~450 lines)

00 Protocol • 0penw0rld.comStealth SpecWizardConnect Spec