Files
bal-server/docs/04_modules_detail.md
svatantrya 69d877a360 docs: add comprehensive knowledge base and security audit
- Add docs/INDEX.md with navigable index and quick reference guides
- Add 9 knowledge base files covering project overview, Bitcoin domain,
  architecture, modules, API reference, database schema, deployment/security
- Update AGENTS.md with knowledge base reference and update policy
- Add tests/sql_injection_tests.rs with regression tests for SQL injection
- Fix SQL injection vulnerabilities in bal-pusher.rs:
  * Replace string-formatted UPDATE IN with loop + parameterized queries
  * Replace string-formatted UPDATE push_err with parameterized query
  * Add chain name validation in calculate_stats to prevent env var tampering
- Update .gitignore to exclude bal-pusher.env and bal-pusher.sh
2026-07-16 14:11:18 -04:00

8.1 KiB

Module Details

Quick Reference


lib.rs

Location: src/lib.rs

This is the root of the library crate. It simply exports two public modules:

  • pub mod db; — the database interface
  • pub mod xpub; — the extended public key utilities

It contains no application logic.


db.rs (Database Interface)

Location: src/db.rs

This module contains all the logic for interacting with the SQLite database.

Key Functions

  • create_table: Creates the full database schema if it does not exist. See src/db.rs for the CREATE TABLE statements.
  • execute_insert: A batched, atomic SQL wrapper function that performs multiple insert operations inside a transaction.
  • insert_tx: Inserts a transaction into tbl_tx.
  • insert_inp: Inserts an input into tbl_inp.
  • insert_out: Inserts an output into tbl_out.
  • insert_xpub: Inserts an xpub into tbl_xpub.
  • insert_address: Inserts a new derived address into tbl_address.
  • get_pending_txs: Queries tbl_tx for transactions with status=0 and valid locktime conditions.
  • update_tx_status: Updates status to 1 (sent) or 2 (failed) after a broadcast attempt.
  • get_stats: Aggregates statistics for the tbl_stats table.
  • get_address_by_ip: A query that joins tbl_address with tbl_xpub to find addresses by IP for rate limiting or reuse logic.

Design Notes

SQL queries are built using format! in many places. The execute_insert function attempts to batch inserts to reduce transaction overhead, but this is dependent on the SQLite version.


xpub.rs (Extended Public Key Utilities)

Location: src/xpub.rs

This module handles the derivation of Bitcoin addresses from extended public keys (xpub/zpub) and the creation of P2WPKH descriptors.

Key Functions

  • parse_xpub: Parses a Base58-encoded xpub/zpub string into a bitcoin::bip32::Xpub.
  • derive_address: Derives a P2WPKH (Bech32) address at a given address index from the xpub. Uses the BIP-84 path (m/84'/coin_type'/account'/0/index). Uses Secp256k1 from the secp256k1 crate for elliptic curve math.
  • get_descriptor: Generates a Bitcoin descriptor string for the xpub (e.g., wpkh(.../0/*)), which is useful for wallet integration.
  • checksum_verify: Verifies the Base58 checksum of an xpub/zpub string to prevent data corruption during entry.

Dependencies

  • bitcoin::bip32::Xpub
  • secp256k1::Secp256k1
  • bs58 for Base58 decoding
  • bitcoin::Address::p2wpkh for address creation

bal-server.rs (HTTP Server / API)

Location: src/bin/bal-server.rs

The main application binary that provides an async HTTP server.

Architecture

  • Runtime: tokio::main with rt-multi-thread.
  • HTTP Framework: hyper (low-level) + hyper-util + http-body-util. Each connection is spawned as a new tokio::task.
  • Routing: Routes are matched using path regex and a simple match on the HTTP method. The router is implemented manually in main.

Key Routes (implemented in source code)

  • GET /, GET /version: Returns static strings (name and version).
  • GET /.pub_key.pem: Returns the Ed25519 public key PEM file for signature verification.
  • GET /:network/info: Returns JSON with fee, address, and chain info. Networks: bitcoin, testnet, testnet4, signet, regtest.
  • GET /:network/stats: Returns per-chain statistics if expose_stats is enabled.
  • POST /:network/pushtxs: Accepts one or more raw hex transactions. It validates them, checks the fee output to the our_address for that network, and stores the transaction in the database. See src/bin/bal-server.rs for the pushtxs request body parsing logic.
  • POST /searchtx: Accepts a txid in the request body and returns the transaction details, status, and fee breakdown.

Configuration

  • The server reads environment variables and/or a config file (confy). Default config is hardcoded for regtest development.
  • db_file: The path to the SQLite database (e.g., bal.db).
  • bind_address: The address to listen on (e.g., 127.0.0.1:3031).
  • expose_stats: A boolean flag to enable/disable the stats endpoint.

Error Handling

  • WARNING: This binary uses unwrap() and expect() on many critical paths (e.g., sqlite::open, Regex::new, req.collect()). A malformed request could crash the async task or even the entire runtime. This is a known vulnerability.

Static Public Key (/.pub_key.pem)

The server serves a static public_key.pem file. The corresponding private key (privkey.pem) is used by the pusher to sign statistics before sending them to the welist server. This file is located in the project root directory.


bal-pusher.rs (Async Transaction Pusher)

Location: src/bin/bal-pusher.rs

This is the async daemon that monitors the blockchain and pushes pending transactions.

Architecture

  • Runtime: tokio::main.
  • ZMQ: zmq::Context with a SUB socket that listens to tcp://127.0.0.1:28332 (or similar per-network port). The topic is hashblock (32-byte block hash).
  • RPC: It uses the bitcoincore-rpc client to call getblockchaininfo (to get the mediantime) and sendrawtransaction for each transaction.
  • HTTP Client: reqwest with the json feature. It sends a signed JSON POST to the welist server.

Key Logic

  1. On every hashblock message, it calls main_result().
  2. main_result creates a bitcoincore-rpc client. If it fails, it panics (panic!("impossible to get client {}", e)), crashing the entire process.
  3. It fetches getblockchaininfo to get the mediantime.
  4. It queries the database for transactions with status=0 and locktime < mediantime.
  5. For each pending transaction, it calls sendrawtransaction.
  6. If send_stats is enabled, it collects statistics, signs them with privkey.pem, and sends them to the configured welist URL via reqwest.
  7. It updates the database with the new status.

Configuration

  • zmq_endpoint: The ZMQ endpoint (e.g., tcp://127.0.0.1:28332).
  • rpc_url: The URL of the Bitcoin RPC (e.g., http://127.0.0.1:18443).
  • rpc_auth: user_pass or cookie_file. The cookie path is constructed from the HOME environment variable (e.g., ~/.bitcoin/.cookie).
  • send_stats: A boolean that enables the remote server reporting.
  • welist_url: The URL to POST to.
  • ssl_key_path: The path to the Ed25519 private key (privkey.pem) for signing stats.

bal-pusher-enhanced.rs (Synchronous Transaction Pusher)

Location: src/bin/bal-pusher-enhanced.rs

This is a synchronous variant of the pusher that does not rely on the RPC for getting the mediantime.

Architecture (Synchronous)

  • ZMQ: It uses zmq::Context with a SUB socket but does not use zmq in an async context. It calls recv_multipart(0) in a blocking loop (std::thread::sleep).
  • Topic: rawblock (not hashblock).
  • Block Header: It extracts the first 80 bytes (the header) from the raw block. The timestamp field is at byte offset 4 + 32 + 32 = 68, and is 4 bytes long (little-endian). It uses byteorder to read this. This avoids the getblockchaininfo RPC call.
  • Block Median Time: It computes the rolling median time from the timestamps of the last 1000 blocks.
  • Preload: It fetches and sorts the pending transactions from the database at startup, keeping them in memory. This reduces the database round trip.
  • RPC: sendrawtransaction is used for the pending transactions, but not for getblockchaininfo.

Design Notes

  • The ZMQ socket is blocking and has no timeout. If the Bitcoin node stops sending, the thread will hang indefinitely. The sleep between attempts is std::thread::sleep(Duration::from_secs(1)), but this happens after a successful recv_multipart, not if recv blocks. This is a potential DoS vector if the ZMQ connection goes silent.
  • The main_result function is not async and does not use a tokio runtime.