Files
bal-server/docs/05_api_reference.md
svatantrya 707d5b1032 feat(contrib): add ping key revocation helper
- Add make_ping_key_revocation.py: onion revocations are signed by the
  identity key, DNS TXT revocations are self-signed by the revoked key
- Document the revocation format, endpoint, helper usage, lifetime and
  no-revocation-of-identity-key limits
- Update the security audit, references and env docs for the new flow and
  the v2/onion-only scope
2026-11-09 00:45:43 -04:00

9.3 KiB

API Reference

Quick Reference


HTTP API (provided by bal-server)

Rate Limiting

All endpoints are rate-limited via actix-governor with a token-bucket algorithm. The rate limit key is the real client IP address, extracted from X-Real-IP / X-Forwarded-For headers when the request comes from a trusted proxy (default 127.0.0.1, configurable via BAL_SERVER_TRUSTED_PROXY). Direct connections (non-proxy) use the TCP peer IP.

Default: 1 req/s with burst of 3 (configurable via BAL_SERVER_ACTIX_PUSHTXS_PER_SEC / BAL_SERVER_ACTIX_PUSHTXS_BURST).

When behind Nginx, ensure proxy_set_header X-Real-IP $remote_addr is set so the server can identify individual clients.

GET /

  • Description: Returns a static identification string (default: "Will Executor Server").
  • Response: Plain text 200 OK.

GET /version

  • Description: Returns the Cargo package version.
  • Response: text/plain (e.g., 0.3.2).

GET /.pub_key.pem

  • Description: Returns the static Ed25519 public key PEM file for signature verification of remote stats.
  • Response: text/plain with the PEM file content.
  • File: public_key.pem in the project root (path configurable via BAL_SERVER_PUB_KEY_PATH).

GET /:network/info

  • Description: Returns JSON with the server's configuration for that specific network.
  • Supported Networks: bitcoin, testnet, testnet4, signet, regtest.
  • Response (200 OK):
    {
      "address": "bcrt1q...",
      "base_fee": 50000,
      "chain": "regtest",
      "info": "Will Executor Server",
      "version": "0.3.2"
    }
    
  • In xpub mode, the address field contains a freshly derived P2WPKH address unique to the requesting IP.
  • Error: 404 if the network is not configured or unknown.

GET /:network/stats

  • Description: Returns statistics for the given network. Guarded by the expose_stats configuration flag.
  • Response (200 OK): A JSON array of StatsResponse objects:
    [
      {
        "report_date": "2024-07-20T12:00:00Z",
        "chain": "regtest",
        "totals": 42,
        "waiting": 10,
        "sent": 30,
        "failed": 2,
        "waiting_profit": 10000,
        "sent_profit": 30000,
        "missed_profit": 5000,
        "unique_inputs": 15
      }
    ]
    
  • Error: 403 or 400 if stats are not enabled or the network is unknown.

POST /:network/pushtxs

  • Description: Accepts one or more raw hex Bitcoin transactions (newline-separated). The server deserializes each transaction, validates that the fee is paid to the correct our_address for that network, and stores valid transactions in the database.
  • Request Body: Newline-separated raw hex transactions.
    02000000000101...hex...\n
    02000000000101...hex...\n
    
  • Response (200 OK): A JSON object with the results for the batch:
    {
      "accepted": 2,
      "rejected": 0,
      "details": [...]
    }
    
  • Response (400 Bad Request): If all transactions are invalid, the fee is missing, or the locktime is not acceptable.
  • Response (413 Payload Too Limited): If the request body exceeds the configured max size (default 1 MiB).
  • Security Note: Invalid transactions or those not paying the required fees are not inserted into the database.

POST /searchtx

  • Description: Searches for a transaction by its txid. The request body must contain exactly 64 hex characters.
  • Request Body:
    {
      "txid": "abc123def456..."
    }
    
  • Response (200 OK):
    {
      "txid": "abc123...",
      "status": 1,
      "tx": "020000000...",
      "our_address": "bcrt1q...",
      "our_fees": 1000,
      "reqid": "192.168.1.1"
    }
    
  • Response (400 Bad Request): If the txid is not exactly 64 hex characters.
  • Response (404 Not Found): If the transaction is not found in the database.

ZMQ Messages (consumed by bal-pusher)

Topic: hashblock

  • Format: A multipart ZMQ message. The first frame is the topic name (hashblock), the second frame is the 32-byte block hash.
  • Trigger: When a new Bitcoin block is found by the local node.
  • Action: The pusher fetches getblockchaininfo from the RPC, gets the updated mediantime and blocks height, then queries and pushes pending transactions.
  • Endpoint: Per-network (e.g., tcp://127.0.0.1:28332 for mainnet).
  • Timeout: 5 seconds (ZMQ_RCVTIMEO). The pusher logs a warning after ~720 consecutive timeouts (~1 hour).

Bitcoin Core RPC Usage (used by bal-pusher)

sendrawtransaction

  • Method: sendrawtransaction (RPC 2)
  • Parameters: hexstring (the raw hex of the transaction to broadcast).
  • Description: Broadcasts the transaction to the Bitcoin network. If the transaction is invalid (e.g., bad-txns-inputs-missingorspent), the RPC will return an error with a negative code (e.g., -25).
  • Error Handling: The pusher catches these errors, logs them, and updates the database status to 2 (failed) with the error in push_err.

getblockchaininfo

  • Method: getblockchaininfo (RPC 1)
  • Parameters: None.
  • Description: Returns the current blockchain state, including mediantime (median timestamp of the last 11 blocks) and blocks (best block height). Used to evaluate nLockTime of pending transactions.

getblock

  • Method: getblock (RPC 1)
  • Parameters: blockhash, verbosity (set to 1 for JSON with timestamp).
  • Description: Fetches block details. Used as an alternative for median time calculation.

RPC Authentication

Authentication is done via bitcoincore-rpc using either:

  • UserPass: BAL_PUSHER_{NETWORK}_RPC_USER and BAL_PUSHER_{NETWORK}_RPC_PASSWORD.
  • CookieFile: $HOME/.bitcoin/{dir_path}.cookie or custom path via BAL_PUSHER_{NETWORK}_COOKIE_FILE.

The client tries username/password auth first, then falls back to cookie file auth.


Ed25519 WeTime Ping Signing

The pusher signs the report before sending it to the welist server. The scheme is selected with WELIST_PING_SIG_VERSION:

Version 2 (canonical JSON, preferred)

  1. Fetches info, base_fee and version from the local GET /{chain}/info endpoint (failure falls back to null).
  2. Builds a compact JSON object with lexicographically sorted keys, every field present, and null for absent optional values: base_fee, chain, height, info, last_block_hash, median_time, sig_version, url, version.
  3. Signs WELIST-PING-V2\n followed by that JSON with the Ed25519 private key (SSL_KEY_PATH).
  4. POSTs the same fields plus the base64 signature and "sig_version": 2 to {WELIST_SERVER_URL}/ping.
  5. The welist server discovers the public key from a DNSSEC-validated _welist-key.<host> TXT record (v=we1; alg=ed25519; key=<base64 SPKI DER>) or from a v3 .onion address. No HTTP callback to /.pub_key.pem is made.

For .onion URLs the pusher includes a delegation object when WELIST_PING_DELEGATION_FILE is set and WELIST_PING_SIG_VERSION=2. The onion identity key signs that certificate offline; SSL_KEY_PATH is then the dedicated ping key, and the service verifies the certificate before checking the ping signature. The service normalizes onion (lowercase, trailing dot removed) before comparing it with the ping host; the pusher's startup check uses the exact configured host, so issue lowercase certificates.

The pusher validates the certificate at startup: onion must match the configured host, valid_until must be in the future, and key must match SSL_KEY_PATH. If the file is missing or malformed, or any check fails, the pusher logs a warning and sends no delegation object. It then signs with SSL_KEY_PATH directly; in the recommended setup that key is the dedicated ping key, so the service rejects the pings until delegation is restored.

The url is trimmed of surrounding whitespace and trailing slashes, and the chain is lowercase, matching the service-side canonicalization.

To stop accepting a ping key, run contrib/make_ping_key_revocation.py and submit the result to {WELIST_SERVER_URL}/ping-keys/revoke. Onion revocations are signed by the onion identity key; DNS TXT revocations are self-signed by the key being revoked. See 10_dnssec_key_publishing.md for the operator guide on publishing _welist-key.<host>, onion delegation and revocation.

Version 1 (legacy)

  1. Concatenates url + chain + height + median_time + last_block_hash.
  2. Signs that string with the Ed25519 private key.
  3. POSTs those five fields plus the base64 signature to {WELIST_SERVER_URL}/ping. No info, base_fee or version is sent.
  4. The welist server verifies the signature with the public key served at GET /.pub_key.pem.
  5. The v1 signature authenticates only liveness. The service refreshes last_update for an accepted ping but does not persist executor metadata. Because any metadata added to a v1 ping would be outside the signature, use v2 when info, base_fee and version must be published and authenticated.