feat(pusher): add opt-in v2 WeTime ping signing
- Add WELIST_PING_SIG_VERSION (1 legacy, 2 canonical JSON); default 1
- Sign WELIST-PING-V2\n + compact sorted JSON covering url, chain, height,
median_time, last_block_hash, info, base_fee, version and sig_version
- Fetch info/base_fee/version from the local /{chain}/info endpoint and sign them
- Fall back to null metadata on fetch failure; skip the fetch for onion URLs
- Normalize the advertised URL and bound metadata to match the WeTime service
- Activate v2 in bal-pusher.sh and document the TXT record setup
- Add unit tests for canonical payloads, URL normalization, metadata and onion detection
Includes previously uncommitted local deployment hardening:
- pg-server.sh configurable loopback bind address, volume and recreate handling
- .env loading in bal-server.sh and bal-pusher.sh
- second bal-server/bal-pusher development scripts
This commit is contained in:
@@ -40,7 +40,7 @@ User
|
||||
4. **Monitoring**: The `bal-pusher` listens to the ZMQ `hashblock` topic with a 5-second receive timeout. When a new block is detected, it fetches blockchain info via RPC.
|
||||
5. **Evaluation**: The pusher queries the database for transactions with `status=0` and compares their locktime against the blockchain's best block height or median time (for timestamp-based locktimes above `LOCKTIME_THRESHOLD`).
|
||||
6. **Broadcast**: If the locktime is satisfied, the pusher sends the transaction via `sendrawtransaction` and updates the status to `1` (sent) or `2` (failed with error stored in `push_err`).
|
||||
7. **Statistics**: The pusher periodically calculates statistics and sends them to a remote `welist` server using an Ed25519-signed POST request. The server also exposes stats via the `GET /:network/stats` endpoint if `expose_stats` is enabled.
|
||||
7. **Statistics**: The pusher periodically calculates statistics and sends them to a remote `welist` server using an Ed25519-signed POST request. The signature scheme is selected by `WELIST_PING_SIG_VERSION`: legacy v1, or v2 over a canonical JSON payload whose `info`, `base_fee` and `version` come from the local `/{chain}/info` endpoint. The server also exposes stats via the `GET /:network/stats` endpoint if `expose_stats` is enabled.
|
||||
|
||||
## State Machine
|
||||
|
||||
|
||||
@@ -193,6 +193,23 @@ This is the async daemon that monitors the blockchain and pushes pending transac
|
||||
- Stats are inserted into `tbl_stats` with `ON CONFLICT(chain) DO UPDATE`.
|
||||
- The stats payload is signed with Ed25519 and POSTed to `{welist_url}/ping`.
|
||||
- The `WELIST_SERVER_URL` is validated via `is_valid_welist_url()` before sending (can be bypassed with `WELIST_SKIP_URL_VALIDATION=true`).
|
||||
- `WELIST_PING_SIG_VERSION` selects the signature scheme:
|
||||
- `1` (default): legacy concatenation of URL, chain, height, median time and
|
||||
block hash. The WeTime service verifies it against `/.pub_key.pem`.
|
||||
- `2`: canonical JSON signed with the `WELIST-PING-V2\n` prefix. The pusher
|
||||
fetches `info`, `base_fee` and `version` from the local
|
||||
`/{chain}/info` endpoint and signs them together with the block metadata.
|
||||
The WeTime service discovers the public key from a DNSSEC-validated
|
||||
`_welist-key.<host>` TXT record or from a v3 onion address, so no HTTP
|
||||
callback is needed. The `url` is trimmed of trailing slashes to match the
|
||||
service-side normalization; absent metadata is signed as `null`.
|
||||
- `bal-pusher.sh` sets `WELIST_PING_SIG_VERSION=2` for the development setup;
|
||||
remove that line to fall back to the legacy scheme.
|
||||
- To publish the v2 public key, derive the base64 SubjectPublicKeyInfo DER:
|
||||
`openssl pkey -in private_key.pem -pubout -outform DER | base64 -w0` and
|
||||
publish it in a DNSSEC-signed TXT record named `_welist-key.<host>` with value
|
||||
`v=we1; alg=ed25519; key=<base64>`. A v3 onion URL needs no DNS record: its
|
||||
Ed25519 identity key is embedded in the address.
|
||||
|
||||
### ZMQ Timeout Handling
|
||||
- Uses `set_rcvtimeo(5000)` (5-second timeout).
|
||||
@@ -208,5 +225,6 @@ All configuration is via environment variables (no config files):
|
||||
- `SSL_KEY_PATH`: Path to Ed25519 private key for signing stats.
|
||||
- `WELIST_SERVER_URL`: URL to POST stats to (validated against SSRF).
|
||||
- `WELIST_SKIP_URL_VALIDATION`: Bypass URL validation (for testing).
|
||||
- `WELIST_PING_SIG_VERSION`: WeTime ping signature scheme (`1` legacy, `2` canonical JSON).
|
||||
- `BAL_PUSHER_PREFER_IPV6`: Pin HTTP connection to IPv6 address.
|
||||
- Per-network: `BAL_PUSHER_{NETWORK}_HOST`, `_PORT`, `_DIR_PATH`, `_DB_FIELD`, `_COOKIE_FILE`, `_RPC_USER`, `_RPC_PASSWORD`, `_ZMQ_HASHBLOCK`.
|
||||
|
||||
@@ -147,11 +147,34 @@ The client tries username/password auth first, then falls back to cookie file au
|
||||
|
||||
---
|
||||
|
||||
## Ed25519 Stats Signing
|
||||
## Ed25519 WeTime Ping Signing
|
||||
|
||||
The pusher signs the statistics payload before sending it to the `welist` server:
|
||||
1. Collects statistics from the database.
|
||||
2. Serializes the stats as JSON.
|
||||
3. Signs the JSON payload with the Ed25519 private key (`privkey.pem`).
|
||||
4. Sends the payload with the base64-encoded signature in the `X-Signature` header.
|
||||
5. The `welist` server can verify the signature using the public key served at `GET /.pub_key.pem`.
|
||||
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.
|
||||
|
||||
The `url` is trimmed of surrounding whitespace and trailing slashes, and the
|
||||
`chain` is lowercase, matching the service-side canonicalization.
|
||||
|
||||
### Version 1 (legacy)
|
||||
|
||||
1. Concatenates `url + chain + height + median_time + last_block_hash`.
|
||||
2. Signs that string with the Ed25519 private key.
|
||||
3. POSTs the fields plus the base64 signature to `{WELIST_SERVER_URL}/ping`.
|
||||
4. The `welist` server verifies the signature with the public key served at
|
||||
`GET /.pub_key.pem`; this scheme only refreshes `last_update`.
|
||||
|
||||
@@ -69,6 +69,7 @@ the handler (including request body reading) exceeds the limit.
|
||||
| `BAL_PUSHER_PREFER_IPV6` | `false` | Pin HTTP connection to first IPv6 address (for broken IPv4 routes) |
|
||||
| `WELIST_SERVER_URL` | `"https://welist.bitcoin-after.life"` | URL to POST signed stats to (validated against SSRF) |
|
||||
| `WELIST_SKIP_URL_VALIDATION` | `false` | Bypass SSRF URL validation (for testing only) |
|
||||
| `WELIST_PING_SIG_VERSION` | `1` | WeTime ping signature scheme: `1` (legacy) or `2` (canonical JSON) |
|
||||
|
||||
#### Per-Network Settings
|
||||
|
||||
@@ -200,6 +201,19 @@ export $(grep -v '^#' bal-server.env | xargs)
|
||||
RUST_LOG=info cargo run --bin=bal-server 2>&1
|
||||
```
|
||||
|
||||
### `pg-server.sh` (Local PostgreSQL)
|
||||
Starts the `bal-pg` Docker container used when `BAL_SERVER_DB_BACKEND=postgresql`.
|
||||
|
||||
- Requires `BAL_PG_PASSWORD` (loaded from `.env` by the startup scripts).
|
||||
- Publishes port `5432` on `127.0.0.1` only by default. Set
|
||||
`BAL_PG_BIND_ADDR=0.0.0.0` only if remote access is really required; the
|
||||
script prints a warning when the database is not bound to loopback.
|
||||
- Stores data in the Docker volume `BAL_PG_VOLUME` (default `bal-pg-data`), so
|
||||
the container can be recreated without losing data.
|
||||
- Fails if the existing container publishes the port on a different interface.
|
||||
Run `./pg-server.sh --recreate` to recreate it; the data volume is preserved
|
||||
(and migrated to `BAL_PG_VOLUME` if it changed).
|
||||
|
||||
### `bal-pusher.sh` (Development Pusher Startup)
|
||||
Sources `bal-pusher.env` and runs the pusher with a network argument:
|
||||
```bash
|
||||
@@ -282,6 +296,7 @@ Key points:
|
||||
- [ ] Database file owned by `bal` user (`chown bal:bal /var/bal/bal.db`).
|
||||
- [ ] Database file permissions `600` (`chmod 600 /var/bal/bal.db`).
|
||||
- [ ] `.env` files in `.gitignore` and not committed.
|
||||
- [ ] PostgreSQL (if used) published on `127.0.0.1` only (`BAL_PG_BIND_ADDR`).
|
||||
- [ ] `private_key.pem` / `privkey.pem` not in the repository.
|
||||
- [ ] `public_key.pem` readable by Nginx if served directly.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user